diff --git a/datamodel/high/base/discriminator.go b/datamodel/high/base/discriminator.go index 06c3d4b..c689559 100644 --- a/datamodel/high/base/discriminator.go +++ b/datamodel/high/base/discriminator.go @@ -20,8 +20,8 @@ import ( // // v3 - https://spec.openapis.org/oas/v3.1.0#discriminator-object type Discriminator struct { - PropertyName string `json:"propertyName,omitempty" yaml:"propertyName,omitempty"` - Mapping orderedmap.Map[string, string] `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName,omitempty" yaml:"propertyName,omitempty"` + Mapping *orderedmap.Map[string, string] `json:"mapping,omitempty" yaml:"mapping,omitempty"` low *low.Discriminator } diff --git a/datamodel/high/base/dynamic_value_test.go b/datamodel/high/base/dynamic_value_test.go index f2cad77..9e97514 100644 --- a/datamodel/high/base/dynamic_value_test.go +++ b/datamodel/high/base/dynamic_value_test.go @@ -127,7 +127,7 @@ func TestDynamicValue_MarshalYAMLInline(t *testing.T) { // convert node into yaml bits, _ := yaml.Marshal(rend) - assert.Equal(t, "properties:\n rice:\n $ref: '#/components/schemas/rice'", strings.TrimSpace(string(bits))) + assert.Equal(t, "properties:\n rice:\n type: array\n items:\n type: string", strings.TrimSpace(string(bits))) } func TestDynamicValue_MarshalYAMLInline_Error(t *testing.T) { diff --git a/datamodel/high/base/example.go b/datamodel/high/base/example.go index 6f5a8c9..2d2ec44 100644 --- a/datamodel/high/base/example.go +++ b/datamodel/high/base/example.go @@ -15,11 +15,11 @@ import ( // // v3 - https://spec.openapis.org/oas/v3.1.0#example-object type Example struct { - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Value any `json:"value,omitempty" yaml:"value,omitempty"` - ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Value *yaml.Node `json:"value,omitempty" yaml:"value,omitempty"` + ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Example } @@ -58,7 +58,7 @@ func (e *Example) MarshalYAML() (interface{}, error) { // ExtractExamples will convert a low-level example map, into a high level one that is simple to navigate. // no fidelity is lost, everything is still available via GoLow() -func ExtractExamples(elements orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Example]]) orderedmap.Map[string, *Example] { +func ExtractExamples(elements *orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Example]]) *orderedmap.Map[string, *Example] { extracted := orderedmap.New[string, *Example]() for pair := orderedmap.First(elements); pair != nil; pair = pair.Next() { extracted.Set(pair.Key().Value, NewExample(pair.Value().Value)) diff --git a/datamodel/high/base/example_test.go b/datamodel/high/base/example_test.go index c7149bd..ca09223 100644 --- a/datamodel/high/base/example_test.go +++ b/datamodel/high/base/example_test.go @@ -17,7 +17,6 @@ import ( ) func TestNewExample(t *testing.T) { - var cNode yaml.Node yml := `summary: an example @@ -37,18 +36,23 @@ x-hack: code` // build high highExample := NewExample(&lowExample) + var xHack string + _ = highExample.Extensions.GetOrZero("x-hack").Decode(&xHack) + + var example string + _ = highExample.Value.Decode(&example) + assert.Equal(t, "an example", highExample.Summary) assert.Equal(t, "something more", highExample.Description) assert.Equal(t, "https://pb33f.io", highExample.ExternalValue) - assert.Equal(t, "code", highExample.Extensions["x-hack"]) - assert.Equal(t, "a thing", highExample.Value) + assert.Equal(t, "code", xHack) + assert.Equal(t, "a thing", example) assert.Equal(t, 4, highExample.GoLow().ExternalValue.ValueNode.Line) assert.NotNil(t, highExample.GoLowUntyped()) // render the example as YAML rendered, _ := highExample.Render() - assert.Equal(t, strings.TrimSpace(string(rendered)), yml) - + assert.Equal(t, yml, strings.TrimSpace(string(rendered))) } func TestExtractExamples(t *testing.T) { @@ -71,11 +75,9 @@ func TestExtractExamples(t *testing.T) { ) assert.Equal(t, "herbs", ExtractExamples(examplesMap).GetOrZero("green").Summary) - } func ExampleNewExample() { - // create some example yaml (or can be JSON, it does not matter) yml := `summary: something interesting description: something more interesting with detail @@ -98,5 +100,4 @@ x-hack: code` fmt.Print(highExample.ExternalValue) // Output: https://pb33f.io - } diff --git a/datamodel/high/base/external_doc.go b/datamodel/high/base/external_doc.go index 98c7b9f..3aed32a 100644 --- a/datamodel/high/base/external_doc.go +++ b/datamodel/high/base/external_doc.go @@ -6,6 +6,7 @@ package base import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" ) @@ -16,9 +17,9 @@ import ( // v2 - https://swagger.io/specification/v2/#externalDocumentationObject // v3 - https://spec.openapis.org/oas/v3.1.0#external-documentation-object type ExternalDoc struct { - Description string `json:"description,omitempty" yaml:"description,omitempty"` - URL string `json:"url,omitempty" yaml:"url,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.ExternalDoc } @@ -46,7 +47,7 @@ func (e *ExternalDoc) GoLowUntyped() any { return e.low } -func (e *ExternalDoc) GetExtensions() map[string]any { +func (e *ExternalDoc) GetExtensions() *orderedmap.Map[string, *yaml.Node] { return e.Extensions } diff --git a/datamodel/high/base/external_doc_test.go b/datamodel/high/base/external_doc_test.go index 224e2a9..5be670b 100644 --- a/datamodel/high/base/external_doc_test.go +++ b/datamodel/high/base/external_doc_test.go @@ -5,17 +5,17 @@ package base import ( "context" - "fmt" - lowmodel "github.com/pb33f/libopenapi/datamodel/low" - lowbase "github.com/pb33f/libopenapi/datamodel/low/base" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" "strings" "testing" + + lowmodel "github.com/pb33f/libopenapi/datamodel/low" + lowbase "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestNewExternalDoc(t *testing.T) { - var cNode yaml.Node yml := `description: hack code @@ -31,22 +31,23 @@ x-hack: code` highExt := NewExternalDoc(&lowExt) + var xHack string + _ = highExt.Extensions.GetOrZero("x-hack").Decode(&xHack) + assert.Equal(t, "hack code", highExt.Description) assert.Equal(t, "https://pb33f.io", highExt.URL) - assert.Equal(t, "code", highExt.Extensions["x-hack"]) + assert.Equal(t, "code", xHack) wentLow := highExt.GoLow() assert.Equal(t, 2, wentLow.URL.ValueNode.Line) - assert.Len(t, highExt.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(highExt.GetExtensions())) // render the high-level object as YAML rendered, _ := highExt.Render() assert.Equal(t, strings.TrimSpace(string(rendered)), yml) - } -func ExampleNewExternalDoc() { - +func TestExampleNewExternalDoc(t *testing.T) { // create a new external documentation spec reference // this can be YAML or JSON. yml := `description: hack code docs @@ -67,7 +68,8 @@ x-hack: code` // create new high-level ExternalDoc highExt := NewExternalDoc(&lowExt) - // print out a extension - fmt.Print(highExt.Extensions["x-hack"]) - // Output: code + var xHack string + _ = highExt.Extensions.GetOrZero("x-hack").Decode(&xHack) + + assert.Equal(t, "code", xHack) } diff --git a/datamodel/high/base/info.go b/datamodel/high/base/info.go index 8c623fb..aaf863b 100644 --- a/datamodel/high/base/info.go +++ b/datamodel/high/base/info.go @@ -6,6 +6,7 @@ package base import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" ) @@ -17,14 +18,14 @@ import ( // v2 - https://swagger.io/specification/v2/#infoObject // v3 - https://spec.openapis.org/oas/v3.1.0#info-object type Info struct { - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` - Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` - License *License `json:"license,omitempty" yaml:"license,omitempty"` - Version string `json:"version,omitempty" yaml:"version,omitempty"` - Extensions map[string]any + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` + Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` + License *License `json:"license,omitempty" yaml:"license,omitempty"` + Version string `json:"version,omitempty" yaml:"version,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Info } @@ -53,7 +54,7 @@ func NewInfo(info *low.Info) *Info { if !info.Version.IsEmpty() { i.Version = info.Version.Value } - if len(info.Extensions) > 0 { + if orderedmap.Len(info.Extensions) > 0 { i.Extensions = high.ExtractExtensions(info.Extensions) } return i diff --git a/datamodel/high/base/info_test.go b/datamodel/high/base/info_test.go index c510fd3..d34839d 100644 --- a/datamodel/high/base/info_test.go +++ b/datamodel/high/base/info_test.go @@ -10,6 +10,8 @@ import ( lowmodel "github.com/pb33f/libopenapi/datamodel/low" lowbase "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -37,6 +39,9 @@ x-cli-name: chicken cli` highInfo := NewInfo(&lowInfo) + var xCliName string + _ = highInfo.Extensions.GetOrZero("x-cli-name").Decode(&xCliName) + assert.Equal(t, "chicken", highInfo.Title) assert.Equal(t, "a chicken nugget", highInfo.Summary) assert.Equal(t, "nugget", highInfo.Description) @@ -45,7 +50,7 @@ x-cli-name: chicken cli` assert.Equal(t, "pb33f", highInfo.License.Name) assert.Equal(t, "https://pb33f.io", highInfo.License.URL) assert.Equal(t, "99.99", highInfo.Version) - assert.Equal(t, "chicken cli", highInfo.Extensions["x-cli-name"]) + assert.Equal(t, "chicken cli", xCliName) wentLow := highInfo.GoLow() assert.Equal(t, 10, wentLow.Version.ValueNode.Line) @@ -109,13 +114,12 @@ url: https://opensource.org/licenses/MIT` } func TestInfo_Render(t *testing.T) { - - ext := make(map[string]any) - ext["x-pizza"] = "pepperoni" - ext["x-cake"] = &License{ + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-pizza", utils.CreateStringNode("pepperoni")) + ext.Set("x-cake", utils.CreateYamlNode(&License{ Name: "someone", URL: "nowhere", - } + })) highI := &Info{ Title: "hey", Description: "there you", @@ -146,6 +150,9 @@ func TestInfo_Render(t *testing.T) { // build high highInfo := NewInfo(&lowInfo) + var xPizza string + _ = highInfo.Extensions.GetOrZero("x-pizza").Decode(&xPizza) + assert.Equal(t, "hey", highInfo.Title) assert.Equal(t, "there you", highInfo.Description) assert.Equal(t, "have you got any money", highInfo.TermsOfService) @@ -154,12 +161,11 @@ func TestInfo_Render(t *testing.T) { assert.Equal(t, "MIT", highInfo.License.Name) assert.Equal(t, "https://opensource.org/licenses/MIT", highInfo.License.URL) assert.Equal(t, "1.2.3", highInfo.Version) - assert.Equal(t, "pepperoni", highInfo.Extensions["x-pizza"]) + assert.Equal(t, "pepperoni", xPizza) assert.NotNil(t, highInfo.GoLowUntyped()) } func TestInfo_RenderOrder(t *testing.T) { - yml := `title: hey description: there you termsOfService: have you got any money @@ -187,6 +193,9 @@ x-cake: // build high highInfo := NewInfo(&lowInfo) + var xPizza string + _ = highInfo.Extensions.GetOrZero("x-pizza").Decode(&xPizza) + assert.Equal(t, "hey", highInfo.Title) assert.Equal(t, "there you", highInfo.Description) assert.Equal(t, "have you got any money", highInfo.TermsOfService) @@ -195,7 +204,7 @@ x-cake: assert.Equal(t, "MIT", highInfo.License.Name) assert.Equal(t, "https://opensource.org/licenses/MIT", highInfo.License.URL) assert.Equal(t, "1.2.3", highInfo.Version) - assert.Equal(t, "pepperoni", highInfo.Extensions["x-pizza"]) + assert.Equal(t, "pepperoni", xPizza) // marshal high back to yaml, should be the same as the original, in same order. bytes, _ := highInfo.Render() diff --git a/datamodel/high/base/schema.go b/datamodel/high/base/schema.go index 7c6e137..8dc22da 100644 --- a/datamodel/high/base/schema.go +++ b/datamodel/high/base/schema.go @@ -45,22 +45,22 @@ type Schema struct { Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` // in 3.1 examples can be an array (which is recommended) - Examples []any `json:"examples,omitempty" yaml:"examples,omitempty"` + Examples []*yaml.Node `json:"examples,omitempty" yaml:"examples,omitempty"` // in 3.1 prefixItems provides tuple validation support. PrefixItems []*SchemaProxy `json:"prefixItems,omitempty" yaml:"prefixItems,omitempty"` // 3.1 Specific properties - Contains *SchemaProxy `json:"contains,omitempty" yaml:"contains,omitempty"` - MinContains *int64 `json:"minContains,omitempty" yaml:"minContains,omitempty"` - MaxContains *int64 `json:"maxContains,omitempty" yaml:"maxContains,omitempty"` - If *SchemaProxy `json:"if,omitempty" yaml:"if,omitempty"` - Else *SchemaProxy `json:"else,omitempty" yaml:"else,omitempty"` - Then *SchemaProxy `json:"then,omitempty" yaml:"then,omitempty"` - DependentSchemas orderedmap.Map[string, *SchemaProxy] `json:"dependentSchemas,omitempty" yaml:"dependentSchemas,omitempty"` - PatternProperties orderedmap.Map[string, *SchemaProxy] `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"` - PropertyNames *SchemaProxy `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"` - UnevaluatedItems *SchemaProxy `json:"unevaluatedItems,omitempty" yaml:"unevaluatedItems,omitempty"` + Contains *SchemaProxy `json:"contains,omitempty" yaml:"contains,omitempty"` + MinContains *int64 `json:"minContains,omitempty" yaml:"minContains,omitempty"` + MaxContains *int64 `json:"maxContains,omitempty" yaml:"maxContains,omitempty"` + If *SchemaProxy `json:"if,omitempty" yaml:"if,omitempty"` + Else *SchemaProxy `json:"else,omitempty" yaml:"else,omitempty"` + Then *SchemaProxy `json:"then,omitempty" yaml:"then,omitempty"` + DependentSchemas *orderedmap.Map[string, *SchemaProxy] `json:"dependentSchemas,omitempty" yaml:"dependentSchemas,omitempty"` + PatternProperties *orderedmap.Map[string, *SchemaProxy] `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"` + PropertyNames *SchemaProxy `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"` + UnevaluatedItems *SchemaProxy `json:"unevaluatedItems,omitempty" yaml:"unevaluatedItems,omitempty"` // in 3.1 UnevaluatedProperties can be a Schema or a boolean // https://github.com/pb33f/libopenapi/issues/118 @@ -73,35 +73,35 @@ type Schema struct { Anchor string `json:"$anchor,omitempty" yaml:"$anchor,omitempty"` // Compatible with all versions - Not *SchemaProxy `json:"not,omitempty" yaml:"not,omitempty"` - Properties orderedmap.Map[string, *SchemaProxy] `json:"properties,omitempty" yaml:"properties,omitempty"` - Title string `json:"title,omitempty" yaml:"title,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` - Maximum *float64 `json:"maximum,renderZero,omitempty" yaml:"maximum,renderZero,omitempty"` - Minimum *float64 `json:"minimum,renderZero,omitempty," yaml:"minimum,renderZero,omitempty"` - MaxLength *int64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` - MinLength *int64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` - Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` - Format string `json:"format,omitempty" yaml:"format,omitempty"` - MaxItems *int64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` - MinItems *int64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` - UniqueItems *bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` - MaxProperties *int64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - MinProperties *int64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` - Required []string `json:"required,omitempty" yaml:"required,omitempty"` - Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` - AdditionalProperties *DynamicValue[*SchemaProxy, bool] `json:"additionalProperties,renderZero,omitempty" yaml:"additionalProperties,renderZero,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Default any `json:"default,omitempty" yaml:"default,renderZero,omitempty"` - Const any `json:"const,omitempty" yaml:"const,renderZero,omitempty"` - Nullable *bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` - ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` // https://github.com/pb33f/libopenapi/issues/30 - WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` // https://github.com/pb33f/libopenapi/issues/30 - XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"` - ExternalDocs *ExternalDoc `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - Example any `json:"example,omitempty" yaml:"example,omitempty"` - Deprecated *bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Not *SchemaProxy `json:"not,omitempty" yaml:"not,omitempty"` + Properties *orderedmap.Map[string, *SchemaProxy] `json:"properties,omitempty" yaml:"properties,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Maximum *float64 `json:"maximum,renderZero,omitempty" yaml:"maximum,renderZero,omitempty"` + Minimum *float64 `json:"minimum,renderZero,omitempty," yaml:"minimum,renderZero,omitempty"` + MaxLength *int64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MinLength *int64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + MaxItems *int64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + MinItems *int64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + UniqueItems *bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + MaxProperties *int64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + MinProperties *int64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Enum []*yaml.Node `json:"enum,omitempty" yaml:"enum,omitempty"` + AdditionalProperties *DynamicValue[*SchemaProxy, bool] `json:"additionalProperties,renderZero,omitempty" yaml:"additionalProperties,renderZero,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Default *yaml.Node `json:"default,omitempty" yaml:"default,renderZero,omitempty"` + Const *yaml.Node `json:"const,omitempty" yaml:"const,renderZero,omitempty"` + Nullable *bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` + ReadOnly *bool `json:"readOnly,renderZero,omitempty" yaml:"readOnly,renderZero,omitempty"` // https://github.com/pb33f/libopenapi/issues/30 + WriteOnly *bool `json:"writeOnly,renderZero,omitempty" yaml:"writeOnly,renderZero,omitempty"` // https://github.com/pb33f/libopenapi/issues/30 + XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"` + ExternalDocs *ExternalDoc `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Example *yaml.Node `json:"example,omitempty" yaml:"example,omitempty"` + Deprecated *bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *base.Schema // Parent Proxy refers back to the low level SchemaProxy that is proxying this schema. @@ -266,17 +266,17 @@ func NewSchema(schema *base.Schema) *Schema { s.Nullable = &schema.Nullable.Value } if !schema.ReadOnly.IsEmpty() { - s.ReadOnly = schema.ReadOnly.Value + s.ReadOnly = &schema.ReadOnly.Value } if !schema.WriteOnly.IsEmpty() { - s.WriteOnly = schema.WriteOnly.Value + s.WriteOnly = &schema.WriteOnly.Value } if !schema.Deprecated.IsEmpty() { s.Deprecated = &schema.Deprecated.Value } s.Example = schema.Example.Value if len(schema.Examples.Value) > 0 { - examples := make([]any, len(schema.Examples.Value)) + examples := make([]*yaml.Node, len(schema.Examples.Value)) for i := 0; i < len(schema.Examples.Value); i++ { examples[i] = schema.Examples.Value[i].Value } @@ -298,11 +298,11 @@ func NewSchema(schema *base.Schema) *Schema { } s.Required = req - var enum []any if !schema.Anchor.IsEmpty() { s.Anchor = schema.Anchor.Value } + var enum []*yaml.Node for i := range schema.Enum.Value { enum = append(enum, schema.Enum.Value[i].Value) } @@ -322,11 +322,13 @@ func NewSchema(schema *base.Schema) *Schema { // for every item, build schema async buildSchema := func(sch lowmodel.ValueReference[*base.SchemaProxy], idx int, bChan chan buildResult) { - p := NewSchemaProxy(&lowmodel.NodeReference[*base.SchemaProxy]{ - ValueNode: sch.ValueNode, + n := &lowmodel.NodeReference[*base.SchemaProxy]{ + ValueNode: sch.GetValueNode(), Value: sch.Value, - Reference: sch.GetReference(), - }) + } + n.SetReference(sch.GetReference(), sch.GetValueNode()) + + p := NewSchemaProxy(n) bChan <- buildResult{idx: idx, s: p} } @@ -353,7 +355,7 @@ func NewSchema(schema *base.Schema) *Schema { // props async buildProps := func(k lowmodel.KeyReference[string], v lowmodel.ValueReference[*base.SchemaProxy], - props orderedmap.Map[string, *SchemaProxy], sw int, + props *orderedmap.Map[string, *SchemaProxy], sw int, ) { props.Set(k.Value, NewSchemaProxy(&lowmodel.NodeReference[*base.SchemaProxy]{ Value: v.Value, diff --git a/datamodel/high/base/schema_proxy.go b/datamodel/high/base/schema_proxy.go index 78a7c9c..4961993 100644 --- a/datamodel/high/base/schema_proxy.go +++ b/datamodel/high/base/schema_proxy.go @@ -4,13 +4,14 @@ package base import ( + "sync" + "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sync" ) // SchemaProxy exists as a stub that will create a Schema once (and only once) the Schema() method is called. An @@ -98,11 +99,15 @@ func (sp *SchemaProxy) Schema() *Schema { // IsReference returns true if the SchemaProxy is a reference to another Schema. func (sp *SchemaProxy) IsReference() bool { + if sp == nil { + return false + } + if sp.refStr != "" { return true } if sp.schema != nil { - return sp.schema.Value.IsSchemaReference() + return sp.schema.Value.IsReference() } return false } @@ -112,7 +117,14 @@ func (sp *SchemaProxy) GetReference() string { if sp.refStr != "" { return sp.refStr } - return sp.schema.Value.GetSchemaReference() + return sp.schema.GetValue().GetReference() +} + +func (sp *SchemaProxy) GetReferenceNode() *yaml.Node { + if sp.refStr != "" { + return nil + } + return sp.schema.GetValue().GetReferenceNode() } // GetReferenceOrigin returns a pointer to the index.NodeOrigin of the $ref if this SchemaProxy is a reference to another Schema. @@ -171,12 +183,13 @@ func (sp *SchemaProxy) MarshalYAML() (interface{}, error) { nb := high.NewNodeBuilder(s, s.low) return nb.Render(), nil } else { + refNode := sp.GetReferenceNode() + if refNode != nil { + return refNode, nil + } + // do not build out a reference, just marshal the reference. - mp := utils.CreateEmptyMapNode() - mp.Content = append(mp.Content, - utils.CreateStringNode("$ref"), - utils.CreateStringNode(sp.GetReference())) - return mp, nil + return utils.CreateRefNode(sp.GetReference()), nil } } diff --git a/datamodel/high/base/schema_test.go b/datamodel/high/base/schema_test.go index f11b0e3..09e1356 100644 --- a/datamodel/high/base/schema_test.go +++ b/datamodel/high/base/schema_test.go @@ -306,8 +306,8 @@ $anchor: anchor` assert.Equal(t, "string", compiled.PropertyNames.Schema().Type[0]) assert.Equal(t, "boolean", compiled.UnevaluatedItems.Schema().Type[0]) assert.Equal(t, "integer", compiled.UnevaluatedProperties.A.Schema().Type[0]) - assert.True(t, compiled.ReadOnly) - assert.True(t, compiled.WriteOnly) + assert.True(t, *compiled.ReadOnly) + assert.True(t, *compiled.WriteOnly) assert.True(t, *compiled.Deprecated) assert.True(t, *compiled.Nullable) assert.Equal(t, "anchor", compiled.Anchor) @@ -548,7 +548,7 @@ func TestSchemaProxy_GoLow(t *testing.T) { sp := NewSchemaProxy(&lowRef) assert.Equal(t, lowProxy, sp.GoLow()) - assert.Equal(t, ref, sp.GoLow().GetSchemaReference()) + assert.Equal(t, ref, sp.GoLow().GetReference()) assert.Equal(t, ref, sp.GoLow().GetReference()) spNil := NewSchemaProxy(nil) @@ -703,7 +703,14 @@ examples: ` highSchema := getHighSchema(t, yml) - assert.Equal(t, []any{int64(5), int64(10)}, highSchema.Examples) + examples := []any{} + for _, ex := range highSchema.Examples { + var v int64 + assert.NoError(t, ex.Decode(&v)) + examples = append(examples, v) + } + + assert.Equal(t, []any{int64(5), int64(10)}, examples) } func ExampleNewSchema() { @@ -1123,7 +1130,7 @@ components: // now render it out, it should be identical. schemaBytes, _ := compiled.RenderInline() - assert.Len(t, schemaBytes, 585) + assert.Equal(t, "properties:\n bigBank:\n type: object\n properties:\n failure_balance_transaction:\n allOf:\n - type: object\n properties:\n name:\n type: string\n price:\n type: number\n anyOf:\n - description: A balance transaction\n anyOf:\n - maxLength: 5000\n type: string\n - description: A balance transaction\n", string(schemaBytes)) } func TestUnevaluatedPropertiesBoolean_True(t *testing.T) { diff --git a/datamodel/high/base/security_requirement.go b/datamodel/high/base/security_requirement.go index a4564df..579d337 100644 --- a/datamodel/high/base/security_requirement.go +++ b/datamodel/high/base/security_requirement.go @@ -21,7 +21,7 @@ import ( // The name used for each property MUST correspond to a security scheme declared in the Security Definitions // - https://swagger.io/specification/v2/#securityDefinitionsObject type SecurityRequirement struct { - Requirements orderedmap.Map[string, []string] `json:"-" yaml:"-"` + Requirements *orderedmap.Map[string, []string] `json:"-" yaml:"-"` low *base.SecurityRequirement } @@ -59,7 +59,6 @@ func (s *SecurityRequirement) Render() ([]byte, error) { // MarshalYAML will create a ready to render YAML representation of the SecurityRequirement object. func (s *SecurityRequirement) MarshalYAML() (interface{}, error) { - type req struct { line int key string diff --git a/datamodel/high/base/tag.go b/datamodel/high/base/tag.go index 0b6bfce..fb405a1 100644 --- a/datamodel/high/base/tag.go +++ b/datamodel/high/base/tag.go @@ -6,6 +6,7 @@ package base import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" ) @@ -19,7 +20,7 @@ type Tag struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` ExternalDocs *ExternalDoc `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.Tag } diff --git a/datamodel/high/base/tag_test.go b/datamodel/high/base/tag_test.go index 44ee50a..2159dc6 100644 --- a/datamodel/high/base/tag_test.go +++ b/datamodel/high/base/tag_test.go @@ -16,7 +16,6 @@ import ( ) func TestNewTag(t *testing.T) { - var cNode yaml.Node yml := `name: chicken @@ -33,10 +32,13 @@ x-hack: code` highTag := NewTag(&lowTag) + var xHack string + _ = highTag.Extensions.GetOrZero("x-hack").Decode(&xHack) + assert.Equal(t, "chicken", highTag.Name) assert.Equal(t, "nuggets", highTag.Description) assert.Equal(t, "https://pb33f.io", highTag.ExternalDocs.URL) - assert.Equal(t, "code", highTag.Extensions["x-hack"]) + assert.Equal(t, "code", xHack) wentLow := highTag.GoLow() assert.Equal(t, 5, wentLow.FindExtension("x-hack").ValueNode.Line) @@ -45,11 +47,9 @@ x-hack: code` // render the tag as YAML highTagBytes, _ := highTag.Render() assert.Equal(t, strings.TrimSpace(string(highTagBytes)), yml) - } func TestTag_RenderInline(t *testing.T) { - tag := &Tag{ Name: "cake", } diff --git a/datamodel/high/base/xml.go b/datamodel/high/base/xml.go index 63d132c..688961e 100644 --- a/datamodel/high/base/xml.go +++ b/datamodel/high/base/xml.go @@ -6,6 +6,7 @@ package base import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" ) @@ -25,7 +26,7 @@ type XML struct { Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"` Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"` - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.XML } diff --git a/datamodel/high/node_builder.go b/datamodel/high/node_builder.go index 6479c33..e49520b 100644 --- a/datamodel/high/node_builder.go +++ b/datamodel/high/node_builder.go @@ -11,27 +11,18 @@ import ( "strings" "unicode" + "github.com/pb33f/libopenapi/datamodel/high/nodes" "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) -// NodeEntry represents a single node used by NodeBuilder. -type NodeEntry struct { - Tag string - Key string - Value any - StringValue string - Line int - Style yaml.Style - RenderZero bool -} - // NodeBuilder is a structure used by libopenapi high-level objects, to render themselves back to YAML. // this allows high-level objects to be 'mutable' because all changes will be rendered out. type NodeBuilder struct { Version float32 - Nodes []*NodeEntry + Nodes []*nodes.NodeEntry High any Low any Resolve bool // If set to true, all references will be rendered inline @@ -62,7 +53,6 @@ func NewNodeBuilder(high any, low any) *NodeBuilder { } func (n *NodeBuilder) add(key string, i int) { - // only operate on exported fields. if unicode.IsLower(rune(key[0])) { return @@ -71,38 +61,38 @@ func (n *NodeBuilder) add(key string, i int) { // if the key is 'Extensions' then we need to extract the keys from the map // and add them to the node builder. if key == "Extensions" { - extensions := reflect.ValueOf(n.High).Elem().FieldByName(key) - for b, e := range extensions.MapKeys() { - v := extensions.MapIndex(e) + ev := reflect.ValueOf(n.High).Elem().FieldByName(key).Interface() + var extensions *orderedmap.Map[string, *yaml.Node] + if ev != nil { + extensions = ev.(*orderedmap.Map[string, *yaml.Node]) + } - extKey := e.String() - extValue := v.Interface() - nodeEntry := &NodeEntry{Tag: extKey, Key: extKey, Value: extValue, Line: 9999 + b} + var lowExtensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] + if n.Low != nil && !reflect.ValueOf(n.Low).IsZero() { + if j, ok := n.Low.(low.HasExtensionsUntyped); ok { + lowExtensions = j.GetExtensions() + } + } - if n.Low != nil && !reflect.ValueOf(n.Low).IsZero() { - fieldValue := reflect.ValueOf(n.Low).Elem().FieldByName("Extensions") - f := fieldValue.Interface() - value := reflect.ValueOf(f) - switch value.Kind() { - case reflect.Map: - if j, ok := n.Low.(low.HasExtensionsUntyped); ok { - originalExtensions := j.GetExtensions() - u := 0 - for k := range originalExtensions { - if k.Value == extKey { - if originalExtensions[k].ValueNode.Line != 0 { - nodeEntry.Style = originalExtensions[k].ValueNode.Style - nodeEntry.Line = originalExtensions[k].ValueNode.Line + u - } else { - nodeEntry.Line = 999999 + b + u - } - } - u++ - } - } + j := 0 + if lowExtensions != nil { + // If we have low extensions get the original lowest line number so we end up in the same place + for pair := orderedmap.First(lowExtensions); pair != nil; pair = pair.Next() { + if j == 0 || pair.Key().KeyNode.Line < j { + j = pair.Key().KeyNode.Line } } + } + + for pair := orderedmap.First(extensions); pair != nil; pair = pair.Next() { + nodeEntry := &nodes.NodeEntry{Tag: pair.Key(), Key: pair.Key(), Value: pair.Value(), Line: j} + + if lowExtensions != nil { + lowItem := low.FindItemInOrderedMap(pair.Key(), lowExtensions) + nodeEntry.LowValue = lowItem + } n.Nodes = append(n.Nodes, nodeEntry) + j++ } // done, extensions are handled separately. return @@ -119,11 +109,11 @@ func (n *NodeBuilder) add(key string, i int) { var renderZeroFlag, omitemptyFlag bool tagParts := strings.Split(tag, ",") - for i = 1; i < len(tagParts); i++ { - if tagParts[i] == renderZero { + for _, part := range tagParts { + if part == renderZero { renderZeroFlag = true } - if tagParts[i] == "omitempty" { + if part == "omitempty" { omitemptyFlag = true } } @@ -133,7 +123,9 @@ func (n *NodeBuilder) add(key string, i int) { f := fieldValue.Interface() value := reflect.ValueOf(f) var isZero bool - if zeroer, ok := f.(yaml.IsZeroer); ok && zeroer.IsZero() { + if (value.Kind() == reflect.Interface || value.Kind() == reflect.Ptr) && value.IsNil() { + isZero = true + } else if zeroer, ok := f.(yaml.IsZeroer); ok && zeroer.IsZero() { isZero = true } else if f == nil || value.IsZero() { isZero = true @@ -146,7 +138,7 @@ func (n *NodeBuilder) add(key string, i int) { } // create a new node entry - nodeEntry := &NodeEntry{Tag: tagName, Key: key} + nodeEntry := &nodes.NodeEntry{Tag: tagName, Key: key} nodeEntry.RenderZero = renderZeroFlag switch value.Kind() { case reflect.Float64, reflect.Float32: @@ -192,39 +184,24 @@ func (n *NodeBuilder) add(key string, i int) { fLow := lowFieldValue.Interface() value = reflect.ValueOf(fLow) - type lineStyle struct { - line int - style yaml.Style - } - + nodeEntry.LowValue = fLow switch value.Kind() { case reflect.Slice: l := value.Len() - lines := make([]lineStyle, l) + lines := make([]int, l) for g := 0; g < l; g++ { qw := value.Index(g).Interface() if we, wok := qw.(low.HasKeyNode); wok { - lines[g] = lineStyle{we.GetKeyNode().Line, we.GetKeyNode().Style} + lines[g] = we.GetKeyNode().Line } } sort.Slice(lines, func(i, j int) bool { - return lines[i].line < lines[j].line + return lines[i] < lines[j] }) - nodeEntry.Line = lines[0].line // pick the lowest line number so this key is sorted in order. - nodeEntry.Style = lines[0].style - break + nodeEntry.Line = lines[0] case reflect.Map: - l := value.Len() - line := make([]int, l) - for q, ky := range value.MapKeys() { - if we, wok := ky.Interface().(low.HasKeyNode); wok { - line[q] = we.GetKeyNode().Line - } - } - sort.Ints(line) - nodeEntry.Line = line[0] - + panic("only ordered maps are supported") case reflect.Struct: y := value.Interface() nodeEntry.Line = 9999 + i @@ -232,13 +209,11 @@ func (n *NodeBuilder) add(key string, i int) { if nb.IsReference() { if jk, kj := y.(low.HasKeyNode); kj { nodeEntry.Line = jk.GetKeyNode().Line - nodeEntry.Style = jk.GetKeyNode().Style break } } if nb.GetValueNode() != nil { nodeEntry.Line = nb.GetValueNode().Line - nodeEntry.Style = nb.GetValueNode().Style } } default: @@ -252,12 +227,13 @@ func (n *NodeBuilder) add(key string, i int) { } } -func (n *NodeBuilder) renderReference() []*yaml.Node { - fg := n.Low.(low.IsReferenced) - nodes := make([]*yaml.Node, 2) - nodes[0] = utils.CreateStringNode("$ref") - nodes[1] = utils.CreateStringNode(fg.GetReference()) - return nodes +func (n *NodeBuilder) renderReference(fg low.IsReferenced) *yaml.Node { + origNode := fg.GetReferenceNode() + if origNode == nil { + return utils.CreateRefNode(fg.GetReference()) + } + + return origNode } // Render will render the NodeBuilder back to a YAML node, iterating over every NodeEntry defined @@ -272,8 +248,7 @@ func (n *NodeBuilder) Render() *yaml.Node { g := reflect.ValueOf(fg) if !g.IsNil() { if fg.IsReference() && !n.Resolve { - m.Content = append(m.Content, n.renderReference()...) - return m + return n.renderReference(n.Low.(low.IsReferenced)) } } } @@ -295,7 +270,7 @@ func (n *NodeBuilder) Render() *yaml.Node { // AddYAMLNode will add a new *yaml.Node to the parent node, using the tag, key and value provided. // If the value is nil, then the node will not be added. This method is recursive, so it will dig down // into any non-scalar types. -func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Node { +func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *nodes.NodeEntry) *yaml.Node { if entry.Value == nil { return parent } @@ -305,11 +280,11 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod var l *yaml.Node if entry.Tag != "" { l = utils.CreateStringNode(entry.Tag) + l.Style = entry.KeyStyle } value := entry.Value line := entry.Line - key := entry.Key var valueNode *yaml.Node switch t.Kind() { @@ -318,9 +293,15 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod val := value.(string) valueNode = utils.CreateStringNode(val) valueNode.Line = line - valueNode.Style = entry.Style - break + if entry.LowValue != nil { + if vnut, ok := entry.LowValue.(low.HasValueNodeUntyped); ok { + vn := vnut.GetValueNode() + if vn != nil { + valueNode.Style = vn.Style + } + } + } case reflect.Bool: val := value.(bool) if !val { @@ -329,26 +310,18 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod valueNode = utils.CreateBoolNode("true") } valueNode.Line = line - break - case reflect.Int: val := strconv.Itoa(value.(int)) valueNode = utils.CreateIntNode(val) valueNode.Line = line - break - case reflect.Int64: val := strconv.FormatInt(value.(int64), 10) valueNode = utils.CreateIntNode(val) valueNode.Line = line - break - case reflect.Float32: val := strconv.FormatFloat(float64(value.(float32)), 'f', 2, 64) valueNode = utils.CreateFloatNode(val) valueNode.Line = line - break - case reflect.Float64: precision := -1 if entry.StringValue != "" && strings.Contains(entry.StringValue, ".") { @@ -357,87 +330,8 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod val := strconv.FormatFloat(value.(float64), 'f', precision, 64) valueNode = utils.CreateFloatNode(val) valueNode.Line = line - break - case reflect.Map: - - // the keys will be rendered randomly, if we don't find out the original line - // number of the tag. - - var orderedCollection []*NodeEntry - m := reflect.ValueOf(value) - for g, k := range m.MapKeys() { - var x string - // extract key - yu := k.Interface() - if o, ok := yu.(low.HasKeyNode); ok { - x = o.GetKeyNode().Value - } else { - x = k.String() - } - - // go low and pull out the line number. - lowProps := reflect.ValueOf(n.Low) - if n.Low != nil && !lowProps.IsZero() && !lowProps.IsNil() { - gu := lowProps.Elem() - gi := gu.FieldByName(key) - jl := reflect.ValueOf(gi) - if !jl.IsZero() && gi.Interface() != nil { - gh := gi.Interface() - // extract low level key line number - if pr, ok := gh.(low.HasValueUnTyped); ok { - fg := reflect.ValueOf(pr.GetValueUntyped()) - found := false - found, orderedCollection = n.extractLowMapKeys(fg, x, found, orderedCollection, m, k) - if found != true { - // this is something new, add it. - orderedCollection = append(orderedCollection, &NodeEntry{ - Tag: x, - Key: x, - Line: 9999 + g, - Value: m.MapIndex(k).Interface(), - }) - } - } else { - // this is a map, but it may be wrapped still. - bj := reflect.ValueOf(gh) - orderedCollection = n.extractLowMapKeysWrapped(bj, x, orderedCollection, g) - } - } else { - // this is a map, without any low level details available (probably an extension map). - orderedCollection = append(orderedCollection, &NodeEntry{ - Tag: x, - Key: x, - Line: 9999 + g, - Value: m.MapIndex(k).Interface(), - }) - } - } else { - // this is a map, without any low level details available (probably an extension map). - orderedCollection = append(orderedCollection, &NodeEntry{ - Tag: x, - Key: x, - Line: 9999 + g, - Value: m.MapIndex(k).Interface(), - }) - } - } - - // sort the slice by line number to ensure everything is rendered in order. - sort.Slice(orderedCollection, func(i, j int) bool { - return orderedCollection[i].Line < orderedCollection[j].Line - }) - - // create an empty map. - p := utils.CreateEmptyMapNode() - - // build out each map node in original order. - for _, cv := range orderedCollection { - n.AddYAMLNode(p, cv) - } - if len(p.Content) > 0 { - valueNode = p - } + panic("only ordered maps are supported") case reflect.Slice: @@ -456,8 +350,7 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod if ut != nil && r.GetReference() != "" && ut.(low.IsReferenced).IsReference() { if !n.Resolve { - refNode := utils.CreateRefNode(glu.GoLowUntyped().(low.IsReferenced).GetReference()) - sl.Content = append(sl.Content, refNode) + sl.Content = append(sl.Content, n.renderReference(glu.GoLowUntyped().(low.IsReferenced))) skip = true } else { skip = false @@ -472,13 +365,13 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod if er, ko := sqi.(Renderable); ko { var rend interface{} if !n.Resolve { - rend, _ = er.(Renderable).MarshalYAML() + rend, _ = er.MarshalYAML() } else { // try and render inline, if we can, otherwise treat as normal. if _, ko := er.(RenderableInline); ko { rend, _ = er.(RenderableInline).MarshalYAMLInline() } else { - rend, _ = er.(Renderable).MarshalYAML() + rend, _ = er.MarshalYAML() } } // check if this is a pointer or not. @@ -505,6 +398,17 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod if err != nil { return parent } else { + if entry.LowValue != nil { + if vnut, ok := entry.LowValue.(low.HasValueNodeUntyped); ok { + vn := vnut.GetValueNode() + if vn.Kind == yaml.SequenceNode { + for i := range vn.Content { + rawNode.Content[i].Style = vn.Content[i].Style + } + } + } + } + valueNode = &rawNode } @@ -524,18 +428,29 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod return parent case reflect.Ptr: - if r, ok := value.(Renderable); ok { + if m, ok := value.(orderedmap.MapToYamlNoder); ok { + l := entry.LowValue + + if l == nil { + if gl, ok := value.(GoesLowUntyped); ok && gl.GoLowUntyped() != nil { + l = gl.GoLowUntyped() + } + } + + p := m.ToYamlNode(n, l) + if len(p.Content) > 0 { + valueNode = p + } + } else if r, ok := value.(Renderable); ok { if gl, lg := value.(GoesLowUntyped); lg { - if gl.GoLowUntyped() != nil { - ut := reflect.ValueOf(gl.GoLowUntyped()) + lut := gl.GoLowUntyped() + if lut != nil { + lr := lut.(low.IsReferenced) + ut := reflect.ValueOf(lr) if !ut.IsNil() { - if gl.GoLowUntyped().(low.IsReferenced).IsReference() { + if lut.(low.IsReferenced).IsReference() { if !n.Resolve { - // TODO: use renderReference here. - rvn := utils.CreateEmptyMapNode() - rvn.Content = append(rvn.Content, utils.CreateStringNode("$ref")) - rvn.Content = append(rvn.Content, utils.CreateStringNode(gl.GoLowUntyped().(low.IsReferenced).GetReference())) - valueNode = rvn + valueNode = n.renderReference(lut.(low.IsReferenced)) break } } @@ -621,64 +536,6 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod return parent } -func (n *NodeBuilder) extractLowMapKeysWrapped(iu reflect.Value, x string, orderedCollection []*NodeEntry, g int) []*NodeEntry { - for _, ky := range iu.MapKeys() { - ty := ky.Interface() - if ere, eok := ty.(low.HasKeyNode); eok { - er := ere.GetKeyNode().Value - if er == x { - orderedCollection = append(orderedCollection, &NodeEntry{ - Tag: x, - Key: x, - Line: ky.Interface().(low.HasKeyNode).GetKeyNode().Line, - Value: iu.MapIndex(ky).Interface(), - }) - } - } else { - orderedCollection = append(orderedCollection, &NodeEntry{ - Tag: x, - Key: x, - Line: 9999 + g, - Value: iu.MapIndex(ky).Interface(), - }) - } - } - return orderedCollection -} - -func (n *NodeBuilder) extractLowMapKeys(fg reflect.Value, x string, found bool, orderedCollection []*NodeEntry, m reflect.Value, k reflect.Value) (bool, []*NodeEntry) { - if fg.IsValid() && !fg.IsZero() { - for j, ky := range fg.MapKeys() { - hu := ky.Interface() - if we, wok := hu.(low.HasKeyNode); wok { - er := we.GetKeyNode().Value - if er == x { - found = true - orderedCollection = append(orderedCollection, &NodeEntry{ - Tag: x, - Key: x, - Line: we.GetKeyNode().Line, - Value: m.MapIndex(k).Interface(), - }) - } - } else { - uu := ky.Interface() - if uu == x { - // this is a map, without any low level details available - found = true - orderedCollection = append(orderedCollection, &NodeEntry{ - Tag: uu.(string), - Key: uu.(string), - Line: 9999 + j, - Value: m.MapIndex(k).Interface(), - }) - } - } - } - } - return found, orderedCollection -} - // Renderable is an interface that can be implemented by types that provide a custom MarshalYAML method. type Renderable interface { MarshalYAML() (interface{}, error) diff --git a/datamodel/high/node_builder_test.go b/datamodel/high/node_builder_test.go index 9383e72..3074387 100644 --- a/datamodel/high/node_builder_test.go +++ b/datamodel/high/node_builder_test.go @@ -4,169 +4,147 @@ package high import ( + "strings" + "testing" + + "github.com/pb33f/libopenapi/datamodel/high/nodes" "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "reflect" - "strings" - "testing" ) -type key struct { - Name string `yaml:"name,omitempty"` - ref bool - refStr string - ln int - nilval bool - v any - kn *yaml.Node - low.IsReferenced `yaml:"-"` +type valueReferenceStruct struct { + ref bool + refStr string + Value string `yaml:"value,omitempty"` } -func (k key) GetKeyNode() *yaml.Node { - if k.kn != nil { - return k.kn - } - kn := utils.CreateStringNode("meddy") - kn.Line = k.ln - return kn +func (r valueReferenceStruct) IsReference() bool { + return r.ref } -func (k key) GetValueUntyped() any { - return k.v +func (r valueReferenceStruct) GetReference() string { + return r.refStr } -func (k key) GetValueNode() *yaml.Node { - return k.GetValueNodeUntyped() +func (r *valueReferenceStruct) SetReference(ref string, _ *yaml.Node) { + r.refStr = ref } -func (k key) GetValueNodeUntyped() *yaml.Node { - if k.nilval { - return nil - } - kn := utils.CreateStringNode("maddy") - kn.Line = k.ln - return kn +func (r *valueReferenceStruct) GetReferenceNode() *yaml.Node { + return nil } -func (k key) IsReference() bool { - return k.ref -} - -func (k key) GetReference() string { - return k.refStr -} - -func (k key) SetReference(ref string) { - k.refStr = ref -} - -func (k key) GoLowUntyped() any { - return &k -} - -func (k key) MarshalYAML() (interface{}, error) { +func (r valueReferenceStruct) MarshalYAML() (interface{}, error) { return utils.CreateStringNode("pizza"), nil } -func (k key) MarshalYAMLInline() (interface{}, error) { +func (r valueReferenceStruct) MarshalYAMLInline() (interface{}, error) { return utils.CreateStringNode("pizza-inline!"), nil } +func (r valueReferenceStruct) GoLowUntyped() any { + return &r +} + type plug struct { Name []string `yaml:"name,omitempty"` } type test1 struct { - Thrig map[string]*plug `yaml:"thrig,omitempty"` - Thing string `yaml:"thing,omitempty"` - Thong int `yaml:"thong,omitempty"` - Thrum int64 `yaml:"thrum,omitempty"` - Thang float32 `yaml:"thang,omitempty"` - Thung float64 `yaml:"thung,omitempty"` - Thyme bool `yaml:"thyme,omitempty"` - Thurm any `yaml:"thurm,omitempty"` - Thugg *bool `yaml:"thugg,renderZero"` - Thurr *int64 `yaml:"thurr,omitempty"` - Thral *float64 `yaml:"thral,omitempty"` - Throo *float64 `yaml:"throo,renderZero,omitempty"` - Tharg []string `yaml:"tharg,omitempty"` - Type []string `yaml:"type,omitempty"` - Throg []*key `yaml:"throg,omitempty"` - Thrat []interface{} `yaml:"thrat,omitempty"` - Thrag []map[string][]string `yaml:"thrag,omitempty"` - Thrug map[string]string `yaml:"thrug,omitempty"` - Thoom []map[string]string `yaml:"thoom,omitempty"` - Thomp map[key]string `yaml:"thomp,omitempty"` - Thump key `yaml:"thump,omitempty"` - Thane key `yaml:"thane,omitempty"` - Thunk key `yaml:"thunk,omitempty"` - Thrim *key `yaml:"thrim,omitempty"` - Thril map[string]*key `yaml:"thril,omitempty"` - Extensions map[string]any `yaml:"-"` - ignoreMe string `yaml:"-"` - IgnoreMe string `yaml:"-"` + Thrig *orderedmap.Map[string, *plug] `yaml:"thrig,omitempty"` + Thing string `yaml:"thing,omitempty"` + Thong int `yaml:"thong,omitempty"` + Thrum int64 `yaml:"thrum,omitempty"` + Thang float32 `yaml:"thang,omitempty"` + Thung float64 `yaml:"thung,omitempty"` + Thyme bool `yaml:"thyme,omitempty"` + Thurm any `yaml:"thurm,omitempty"` + Thugg *bool `yaml:"thugg,renderZero"` + Thurr *int64 `yaml:"thurr,omitempty"` + Thral *float64 `yaml:"thral,omitempty"` + Throo *float64 `yaml:"throo,renderZero,omitempty"` + Tharg []string `yaml:"tharg,omitempty"` + Type []string `yaml:"type,omitempty"` + Throg []*valueReferenceStruct `yaml:"throg,omitempty"` + Thrat []interface{} `yaml:"thrat,omitempty"` + Thrag []*orderedmap.Map[string, []string] `yaml:"thrag,omitempty"` + Thrug *orderedmap.Map[string, string] `yaml:"thrug,omitempty"` + Thoom []*orderedmap.Map[string, string] `yaml:"thoom,omitempty"` + Thomp *orderedmap.Map[low.KeyReference[string], string] `yaml:"thomp,omitempty"` + Thump valueReferenceStruct `yaml:"thump,omitempty"` + Thane valueReferenceStruct `yaml:"thane,omitempty"` + Thunk valueReferenceStruct `yaml:"thunk,omitempty"` + Thrim *valueReferenceStruct `yaml:"thrim,omitempty"` + Thril *orderedmap.Map[string, *valueReferenceStruct] `yaml:"thril,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `yaml:"-"` + ignoreMe string `yaml:"-"` + IgnoreMe string `yaml:"-"` } -func (te *test1) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (te *test1) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { + g := orderedmap.New[low.KeyReference[string], low.ValueReference[*yaml.Node]]() - g := make(map[low.KeyReference[string]]low.ValueReference[any]) - - for i := range te.Extensions { - - f := reflect.TypeOf(te.Extensions[i]) - switch f.Kind() { - case reflect.String: - vn := utils.CreateStringNode(te.Extensions[i].(string)) - vn.Line = 999999 // weighted to the bottom. - g[low.KeyReference[string]{ - Value: i, - KeyNode: vn, - }] = low.ValueReference[any]{ - ValueNode: vn, - Value: te.Extensions[i].(string), - } - case reflect.Map: - kn := utils.CreateStringNode(i) - var vn yaml.Node - _ = vn.Decode(te.Extensions[i]) - - kn.Line = 999999 // weighted to the bottom. - g[low.KeyReference[string]{ - Value: i, - KeyNode: kn, - }] = low.ValueReference[any]{ - ValueNode: &vn, - Value: te.Extensions[i], - } - } + i := 0 + for pair := orderedmap.First(te.Extensions); pair != nil; pair = pair.Next() { + kn := utils.CreateStringNode(pair.Key()) + kn.Line = 999999 + i // weighted to the bottom. + g.Set(low.KeyReference[string]{ + Value: pair.Key(), + KeyNode: kn, + }, low.ValueReference[*yaml.Node]{ + ValueNode: pair.Value(), + Value: pair.Value(), + }) + i++ } return g } func (te *test1) MarshalYAML() (interface{}, error) { + panic("MarshalYAML") nb := NewNodeBuilder(te, te) return nb.Render(), nil } func (te *test1) GetKeyNode() *yaml.Node { + panic("GetKeyNode") kn := utils.CreateStringNode("meddy") kn.Line = 20 return kn } func (te *test1) GoesLowUntyped() any { + panic("GoesLowUntyped") return te } func TestNewNodeBuilder(t *testing.T) { - b := true c := int64(12345) d := 1234.1234 + thoom1 := orderedmap.New[string, string]() + thoom1.Set("maddy", "champion") + + thoom2 := orderedmap.New[string, string]() + thoom2.Set("ember", "naughty") + + thomp := orderedmap.New[low.KeyReference[string], string]() + thomp.Set(low.KeyReference[string]{ + Value: "meddy", + KeyNode: utils.CreateStringNode("meddy"), + }, "princess") + + thrug := orderedmap.New[string, string]() + thrug.Set("chicken", "nuggets") + + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-pizza", utils.CreateStringNode("time")) + t1 := test1{ ignoreMe: "I should never be seen!", Thing: "ding", @@ -181,30 +159,18 @@ func TestNewNodeBuilder(t *testing.T) { Thral: &d, Tharg: []string{"chicken", "nuggets"}, Type: []string{"chicken"}, - Thoom: []map[string]string{ - { - "maddy": "champion", - }, - { - "ember": "naughty", - }, + Thoom: []*orderedmap.Map[string, string]{ + thoom1, + thoom2, }, - Thomp: map[key]string{ - {ln: 1}: "princess", - }, - Thane: key{ // this is going to be ignored, needs to be a ValueReference - ln: 2, - ref: true, - refStr: "ripples", - }, - Thrug: map[string]string{ - "chicken": "nuggets", - }, - Thump: key{Name: "I will be ignored", ln: 3}, - Thunk: key{ln: 4, nilval: true}, - Extensions: map[string]any{ - "x-pizza": "time", + Thomp: thomp, + Thane: valueReferenceStruct{ // this is going to be ignored, needs to be a ValueReference + Value: "ripples", }, + Thrug: thrug, + Thump: valueReferenceStruct{Value: "I will be ignored"}, + Thunk: valueReferenceStruct{}, + Extensions: ext, } nb := NewNodeBuilder(&t1, nil) @@ -235,11 +201,9 @@ thomp: x-pizza: time` assert.Equal(t, desired, strings.TrimSpace(string(data))) - } func TestNewNodeBuilder_Type(t *testing.T) { - t1 := test1{ Type: []string{"chicken", "soup"}, } @@ -257,15 +221,12 @@ func TestNewNodeBuilder_Type(t *testing.T) { } func TestNewNodeBuilder_IsReferenced(t *testing.T) { - - t1 := key{ - Name: "cotton", - ref: true, - refStr: "#/my/heart", - ln: 2, + t1 := &low.ValueReference[string]{ + Value: "cotton", } + t1.SetReference("#/my/heart", nil) - nb := NewNodeBuilder(&t1, &t1) + nb := NewNodeBuilder(t1, t1) node := nb.Render() data, _ := yaml.Marshal(node) @@ -276,14 +237,14 @@ func TestNewNodeBuilder_IsReferenced(t *testing.T) { } func TestNewNodeBuilder_Extensions(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-pizza", utils.CreateStringNode("time")) + ext.Set("x-money", utils.CreateStringNode("time")) t1 := test1{ - Thing: "ding", - Extensions: map[string]any{ - "x-pizza": "time", - "x-money": "time", - }, - Thong: 1, + Thing: "ding", + Extensions: ext, + Thong: 1, } nb := NewNodeBuilder(&t1, &t1) @@ -294,14 +255,14 @@ func TestNewNodeBuilder_Extensions(t *testing.T) { } func TestNewNodeBuilder_LowValueNode(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-pizza", utils.CreateStringNode("time")) + ext.Set("x-money", utils.CreateStringNode("time")) t1 := test1{ - Thing: "ding", - Extensions: map[string]any{ - "x-pizza": "time", - "x-money": "time", - }, - Thong: 1, + Thing: "ding", + Extensions: ext, + Thong: 1, } nb := NewNodeBuilder(&t1, &t1) @@ -313,12 +274,11 @@ func TestNewNodeBuilder_LowValueNode(t *testing.T) { } func TestNewNodeBuilder_NoValue(t *testing.T) { - t1 := test1{ Thing: "", } - nodeEnty := NodeEntry{} + nodeEnty := nodes.NodeEntry{} nb := NewNodeBuilder(&t1, &t1) node := nb.AddYAMLNode(nil, &nodeEnty) assert.Nil(t, node) @@ -326,7 +286,7 @@ func TestNewNodeBuilder_NoValue(t *testing.T) { func TestNewNodeBuilder_EmptyString(t *testing.T) { t1 := new(test1) - nodeEnty := NodeEntry{} + nodeEnty := nodes.NodeEntry{} nb := NewNodeBuilder(t1, t1) node := nb.AddYAMLNode(nil, &nodeEnty) assert.Nil(t, node) @@ -334,7 +294,7 @@ func TestNewNodeBuilder_EmptyString(t *testing.T) { func TestNewNodeBuilder_EmptyStringRenderZero(t *testing.T) { t1 := new(test1) - nodeEnty := NodeEntry{RenderZero: true, Value: ""} + nodeEnty := nodes.NodeEntry{RenderZero: true, Value: ""} nb := NewNodeBuilder(t1, t1) m := utils.CreateEmptyMapNode() node := nb.AddYAMLNode(m, &nodeEnty) @@ -344,7 +304,7 @@ func TestNewNodeBuilder_EmptyStringRenderZero(t *testing.T) { func TestNewNodeBuilder_Bool(t *testing.T) { t1 := new(test1) nb := NewNodeBuilder(t1, t1) - nodeEnty := NodeEntry{} + nodeEnty := nodes.NodeEntry{} node := nb.AddYAMLNode(nil, &nodeEnty) assert.Nil(t, node) } @@ -364,7 +324,7 @@ func TestNewNodeBuilder_Int(t *testing.T) { t1 := new(test1) nb := NewNodeBuilder(t1, t1) p := utils.CreateEmptyMapNode() - nodeEnty := NodeEntry{Tag: "p", Value: 12, Key: "p"} + nodeEnty := nodes.NodeEntry{Tag: "p", Value: 12, Key: "p"} node := nb.AddYAMLNode(p, &nodeEnty) assert.NotNil(t, node) assert.Len(t, node.Content, 2) @@ -375,7 +335,7 @@ func TestNewNodeBuilder_Int64(t *testing.T) { t1 := new(test1) nb := NewNodeBuilder(t1, t1) p := utils.CreateEmptyMapNode() - nodeEnty := NodeEntry{Tag: "p", Value: int64(234556), Key: "p"} + nodeEnty := nodes.NodeEntry{Tag: "p", Value: int64(234556), Key: "p"} node := nb.AddYAMLNode(p, &nodeEnty) assert.NotNil(t, node) assert.Len(t, node.Content, 2) @@ -386,7 +346,7 @@ func TestNewNodeBuilder_Float32(t *testing.T) { t1 := new(test1) nb := NewNodeBuilder(t1, t1) p := utils.CreateEmptyMapNode() - nodeEnty := NodeEntry{Tag: "p", Value: float32(1234.23), Key: "p"} + nodeEnty := nodes.NodeEntry{Tag: "p", Value: float32(1234.23), Key: "p"} node := nb.AddYAMLNode(p, &nodeEnty) assert.NotNil(t, node) assert.Len(t, node.Content, 2) @@ -397,7 +357,7 @@ func TestNewNodeBuilder_Float64(t *testing.T) { t1 := new(test1) nb := NewNodeBuilder(t1, t1) p := utils.CreateEmptyMapNode() - nodeEnty := NodeEntry{Tag: "p", Value: 1234.232323, Key: "p", StringValue: "1234.232323"} + nodeEnty := nodes.NodeEntry{Tag: "p", Value: 1234.232323, Key: "p", StringValue: "1234.232323"} node := nb.AddYAMLNode(p, &nodeEnty) assert.NotNil(t, node) assert.Len(t, node.Content, 2) @@ -410,30 +370,33 @@ func TestNewNodeBuilder_EmptyNode(t *testing.T) { nb.Nodes = nil m := nb.Render() assert.Len(t, m.Content, 0) - } func TestNewNodeBuilder_MapKeyHasValue(t *testing.T) { + thrug := orderedmap.New[string, string]() + thrug.Set("dump", "trump") t1 := test1{ - Thrug: map[string]string{ - "dump": "trump", - }, + Thrug: thrug, } type test1low struct { - Thrug key `yaml:"thrug"` - Thugg *bool `yaml:"thugg"` - Throo *float32 `yaml:"throo"` + Thrug *orderedmap.Map[*low.KeyReference[string], *low.ValueReference[string]] `yaml:"thrug"` + Thugg *bool `yaml:"thugg"` + Throo *float32 `yaml:"throo"` } + thrugLow := orderedmap.New[*low.KeyReference[string], *low.ValueReference[string]]() + thrugLow.Set(&low.KeyReference[string]{ + Value: "dump", + KeyNode: utils.CreateStringNode("dump"), + }, &low.ValueReference[string]{ + Value: "trump", + ValueNode: utils.CreateStringNode("trump"), + }) + t2 := test1low{ - Thrug: key{ - v: map[string]string{ - "dump": "trump", - }, - ln: 2, - }, + Thrug: thrugLow, } nb := NewNodeBuilder(&t1, &t2) @@ -448,31 +411,30 @@ func TestNewNodeBuilder_MapKeyHasValue(t *testing.T) { } func TestNewNodeBuilder_MapKeyHasValueThatHasValue(t *testing.T) { + thomp := orderedmap.New[low.KeyReference[string], string]() + thomp.Set(low.KeyReference[string]{Value: "meddy", KeyNode: utils.CreateStringNode("meddy")}, "princess") t1 := test1{ - Thomp: map[key]string{ - {v: "who"}: "princess", - }, + Thomp: thomp, } type test1low struct { - Thomp key `yaml:"thomp"` - Thugg *bool `yaml:"thugg"` - Throo *float32 `yaml:"throo"` + Thomp low.ValueReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] `yaml:"thomp"` + Thugg *bool `yaml:"thugg"` + Throo *float32 `yaml:"throo"` } + valueMap := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]() + valueMap.Set(low.KeyReference[string]{ + Value: "ice", + KeyNode: utils.CreateStringNode("ice"), + }, low.ValueReference[string]{ + Value: "princess", + }) + t2 := test1low{ - Thomp: key{ - v: map[key]string{ - { - v: key{ - v: "ice", - kn: utils.CreateStringNode("limes"), - }, - kn: utils.CreateStringNode("chimes"), - ln: 6}: "princess", - }, - ln: 2, + Thomp: low.ValueReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]]{ + Value: valueMap, }, } @@ -488,23 +450,27 @@ func TestNewNodeBuilder_MapKeyHasValueThatHasValue(t *testing.T) { } func TestNewNodeBuilder_MapKeyHasValueThatHasValueMatch(t *testing.T) { + thomp := orderedmap.New[low.KeyReference[string], string]() + thomp.Set(low.KeyReference[string]{Value: "meddy", KeyNode: utils.CreateStringNode("meddy")}, "princess") t1 := test1{ - Thomp: map[key]string{ - {v: "who"}: "princess", - }, + Thomp: thomp, } type test1low struct { - Thomp low.NodeReference[map[key]string] `yaml:"thomp"` - Thugg *bool `yaml:"thugg"` - Throo *float32 `yaml:"throo"` + Thomp low.NodeReference[*orderedmap.Map[low.KeyReference[string], string]] `yaml:"thomp"` + Thugg *bool `yaml:"thugg"` + Throo *float32 `yaml:"throo"` } - g := low.NodeReference[map[key]string]{ - Value: map[key]string{ - {v: "my", kn: utils.CreateStringNode("limes")}: "princess", - }, + valMap := orderedmap.New[low.KeyReference[string], string]() + valMap.Set(low.KeyReference[string]{ + Value: "meddy", + KeyNode: utils.CreateStringNode("meddy"), + }, "princess") + + g := low.NodeReference[*orderedmap.Map[low.KeyReference[string], string]]{ + Value: valMap, } t2 := test1low{ @@ -522,94 +488,27 @@ func TestNewNodeBuilder_MapKeyHasValueThatHasValueMatch(t *testing.T) { assert.Equal(t, desired, strings.TrimSpace(string(data))) } -func TestNewNodeBuilder_MapKeyHasValueThatHasValueMatchKeyNode(t *testing.T) { - - t1 := test1{ - Thomp: map[key]string{ - {v: "who"}: "princess", - }, - } - - type test1low struct { - Thomp low.NodeReference[map[key]string] `yaml:"thomp"` - Thugg *bool `yaml:"thugg"` - Throo *float32 `yaml:"throo"` - } - - g := low.NodeReference[map[key]string]{ - Value: map[key]string{ - {v: "my", kn: utils.CreateStringNode("limes")}: "princess", - }, - } - - t2 := test1low{ - Thomp: g, - } - - nb := NewNodeBuilder(&t1, &t2) - node := nb.Render() - - data, _ := yaml.Marshal(node) - - desired := `thomp: - meddy: princess` - - assert.Equal(t, desired, strings.TrimSpace(string(data))) -} - -func TestNewNodeBuilder_MapKeyHasValueThatHasValueMatch_NoWrap(t *testing.T) { - - t1 := test1{ - Thomp: map[key]string{ - {v: "who"}: "princess", - }, - } - - type test1low struct { - Thomp map[key]string `yaml:"thomp"` - Thugg *bool `yaml:"thugg"` - Throo *float32 `yaml:"throo"` - } - - t2 := test1low{ - Thomp: map[key]string{ - {v: "my", kn: utils.CreateStringNode("meddy")}: "princess", - }, - } - - nb := NewNodeBuilder(&t1, &t2) - node := nb.Render() - - data, _ := yaml.Marshal(node) - - desired := `thomp: - meddy: princess` - - assert.Equal(t, desired, strings.TrimSpace(string(data))) -} - func TestNewNodeBuilder_MissingLabel(t *testing.T) { - t1 := new(test1) nb := NewNodeBuilder(t1, t1) p := utils.CreateEmptyMapNode() - nodeEnty := NodeEntry{Value: 1234.232323, Key: "p"} + nodeEnty := nodes.NodeEntry{Value: 1234.232323, Key: "p"} node := nb.AddYAMLNode(p, &nodeEnty) assert.NotNil(t, node) assert.Len(t, node.Content, 0) } func TestNewNodeBuilder_ExtensionMap(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + pizza := orderedmap.New[string, string]() + pizza.Set("dump", "trump") + ext.Set("x-pizza", utils.CreateYamlNode(pizza)) + ext.Set("x-money", utils.CreateStringNode("time")) t1 := test1{ - Thing: "ding", - Extensions: map[string]any{ - "x-pizza": map[string]string{ - "dump": "trump", - }, - "x-money": "time", - }, - Thong: 1, + Thing: "ding", + Extensions: ext, + Thong: 1, } nb := NewNodeBuilder(&t1, &t1) @@ -621,20 +520,21 @@ func TestNewNodeBuilder_ExtensionMap(t *testing.T) { } func TestNewNodeBuilder_MapKeyHasValueThatHasValueMismatch(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + pizza := orderedmap.New[string, string]() + pizza.Set("dump", "trump") + ext.Set("x-pizza", utils.CreateYamlNode(pizza)) + cake := orderedmap.New[string, string]() + cake.Set("maga", "nomore") + ext.Set("x-cake", utils.CreateYamlNode(cake)) + + thril := orderedmap.New[string, *valueReferenceStruct]() + thril.Set("princess", &valueReferenceStruct{Value: "who"}) + thril.Set("heavy", &valueReferenceStruct{Value: "who"}) t1 := test1{ - Extensions: map[string]any{ - "x-pizza": map[string]string{ - "dump": "trump", - }, - "x-cake": map[string]string{ - "maga": "nomore", - }, - }, - Thril: map[string]*key{ - "princess": {v: "who", Name: "beef", ln: 2}, - "heavy": {v: "who", Name: "industries", ln: 3}, - }, + Extensions: ext, + Thril: thril, } nb := NewNodeBuilder(&t1, nil) @@ -642,13 +542,18 @@ func TestNewNodeBuilder_MapKeyHasValueThatHasValueMismatch(t *testing.T) { data, _ := yaml.Marshal(node) - assert.Len(t, data, 94) + assert.Equal(t, `thril: + princess: pizza + heavy: pizza +x-pizza: + dump: trump +x-cake: + maga: nomore`, strings.TrimSpace(string(data))) } func TestNewNodeBuilder_SliceRef(t *testing.T) { - - c := key{ref: true, refStr: "#/red/robin/yummmmm", Name: "milky"} - ty := []*key{&c} + c := valueReferenceStruct{ref: true, refStr: "#/red/robin/yummmmm", Value: "milky"} + ty := []*valueReferenceStruct{&c} t1 := test1{ Throg: ty, } @@ -665,9 +570,8 @@ func TestNewNodeBuilder_SliceRef(t *testing.T) { } func TestNewNodeBuilder_SliceRef_Inline(t *testing.T) { - - c := key{ref: true, refStr: "#/red/robin/yummmmm", Name: "milky"} - ty := []*key{&c} + c := valueReferenceStruct{Value: "milky"} + ty := []*valueReferenceStruct{&c} t1 := test1{ Throg: ty, } @@ -684,22 +588,19 @@ func TestNewNodeBuilder_SliceRef_Inline(t *testing.T) { assert.Equal(t, desired, strings.TrimSpace(string(data))) } -type testRender struct { -} +type testRender struct{} func (t testRender) MarshalYAML() (interface{}, error) { return utils.CreateStringNode("testy!"), nil } -type testRenderRawNode struct { -} +type testRenderRawNode struct{} func (t testRenderRawNode) MarshalYAML() (interface{}, error) { return yaml.Node{Kind: yaml.ScalarNode, Value: "zesty!"}, nil } func TestNewNodeBuilder_SliceRef_Inline_NotCompatible(t *testing.T) { - ty := []interface{}{testRender{}} t1 := test1{ Thrat: ty, @@ -718,7 +619,6 @@ func TestNewNodeBuilder_SliceRef_Inline_NotCompatible(t *testing.T) { } func TestNewNodeBuilder_SliceRef_Inline_NotCompatible_NotPointer(t *testing.T) { - ty := []interface{}{testRenderRawNode{}} t1 := test1{ Thrat: ty, @@ -737,7 +637,6 @@ func TestNewNodeBuilder_SliceRef_Inline_NotCompatible_NotPointer(t *testing.T) { } func TestNewNodeBuilder_PointerRef_Inline_NotCompatible_RawNode(t *testing.T) { - ty := testRenderRawNode{} t1 := test1{ Thurm: &ty, @@ -755,8 +654,7 @@ func TestNewNodeBuilder_PointerRef_Inline_NotCompatible_RawNode(t *testing.T) { } func TestNewNodeBuilder_PointerRef_Inline_NotCompatible(t *testing.T) { - - ty := key{} + ty := valueReferenceStruct{} t1 := test1{ Thurm: &ty, } @@ -773,9 +671,8 @@ func TestNewNodeBuilder_PointerRef_Inline_NotCompatible(t *testing.T) { } func TestNewNodeBuilder_SliceNoRef(t *testing.T) { - - c := key{ref: false, Name: "milky"} - ty := []*key{&c} + c := valueReferenceStruct{Value: "milky"} + ty := []*valueReferenceStruct{&c} t1 := test1{ Throg: ty, } @@ -792,7 +689,6 @@ func TestNewNodeBuilder_SliceNoRef(t *testing.T) { } func TestNewNodeBuilder_TestStructAny(t *testing.T) { - t1 := test1{ Thurm: low.ValueReference[any]{ ValueNode: utils.CreateStringNode("beer"), @@ -808,8 +704,8 @@ func TestNewNodeBuilder_TestStructAny(t *testing.T) { assert.Equal(t, desired, strings.TrimSpace(string(data))) } -func TestNewNodeBuilder_TestStructString(t *testing.T) { +func TestNewNodeBuilder_TestStructString(t *testing.T) { t1 := test1{ Thurm: low.ValueReference[string]{ ValueNode: utils.CreateStringNode("beer"), @@ -827,12 +723,11 @@ func TestNewNodeBuilder_TestStructString(t *testing.T) { } func TestNewNodeBuilder_TestStructPointer(t *testing.T) { - t1 := test1{ - Thrim: &key{ + Thrim: &valueReferenceStruct{ ref: true, refStr: "#/cash/money", - Name: "pizza", + Value: "pizza", }, } @@ -848,17 +743,17 @@ func TestNewNodeBuilder_TestStructPointer(t *testing.T) { } func TestNewNodeBuilder_TestStructRef(t *testing.T) { - fkn := utils.CreateStringNode("pizzaBurgers") fkn.Line = 22 + thurm := low.NodeReference[string]{ + KeyNode: fkn, + ValueNode: fkn, + } + thurm.SetReference("#/cash/money", nil) + t1 := test1{ - Thurm: low.NodeReference[string]{ - Reference: "#/cash/money", - ReferenceNode: true, - KeyNode: fkn, - ValueNode: fkn, - }, + Thurm: thurm, } nb := NewNodeBuilder(&t1, &t1) @@ -872,7 +767,6 @@ func TestNewNodeBuilder_TestStructRef(t *testing.T) { } func TestNewNodeBuilder_TestStructDefaultEncode(t *testing.T) { - f := 1 t1 := test1{ Thurm: &f, @@ -889,9 +783,10 @@ func TestNewNodeBuilder_TestStructDefaultEncode(t *testing.T) { } func TestNewNodeBuilder_TestSliceMapSliceStruct(t *testing.T) { - - a := []map[string][]string{ - {"pizza": {"beer", "wine"}}, + pizza := orderedmap.New[string, []string]() + pizza.Set("pizza", []string{"beer", "wine"}) + a := []*orderedmap.Map[string, []string]{ + pizza, } t1 := test1{ @@ -912,7 +807,6 @@ func TestNewNodeBuilder_TestSliceMapSliceStruct(t *testing.T) { } func TestNewNodeBuilder_TestRenderZero(t *testing.T) { - f := false t1 := test1{ Thugg: &f, @@ -929,7 +823,6 @@ func TestNewNodeBuilder_TestRenderZero(t *testing.T) { } func TestNewNodeBuilder_TestRenderZero_Float(t *testing.T) { - f := 0.0 t1 := test1{ Throo: &f, @@ -946,7 +839,6 @@ func TestNewNodeBuilder_TestRenderZero_Float(t *testing.T) { } func TestNewNodeBuilder_TestRenderZero_Float_NotZero(t *testing.T) { - f := 0.12 t1 := test1{ Throo: &f, @@ -963,11 +855,11 @@ func TestNewNodeBuilder_TestRenderZero_Float_NotZero(t *testing.T) { } func TestNewNodeBuilder_TestRenderServerVariableSimulation(t *testing.T) { + thrig := orderedmap.New[string, *plug]() + thrig.Set("pork", &plug{Name: []string{"gammon", "bacon"}}) t1 := test1{ - Thrig: map[string]*plug{ - "pork": {Name: []string{"gammon", "bacon"}}, - }, + Thrig: thrig, } nb := NewNodeBuilder(&t1, &t1) @@ -985,22 +877,21 @@ func TestNewNodeBuilder_TestRenderServerVariableSimulation(t *testing.T) { } func TestNewNodeBuilder_ShouldHaveNotDoneTestsLikeThisOhWell(t *testing.T) { + m := orderedmap.New[low.KeyReference[string], low.ValueReference[*valueReferenceStruct]]() - m := make(map[low.KeyReference[string]]low.ValueReference[*key]) - - m[low.KeyReference[string]{ + m.Set(low.KeyReference[string]{ KeyNode: utils.CreateStringNode("pizza"), Value: "pizza", - }] = low.ValueReference[*key]{ + }, low.ValueReference[*valueReferenceStruct]{ ValueNode: utils.CreateStringNode("beer"), - Value: &key{}, - } + Value: &valueReferenceStruct{}, + }) - d := make(map[string]*key) - d["pizza"] = &key{} + d := orderedmap.New[string, *valueReferenceStruct]() + d.Set("pizza", &valueReferenceStruct{}) type t1low struct { - Thril low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*key]] + Thril low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*valueReferenceStruct]]] Thugg *bool `yaml:"thugg"` Throo *float32 `yaml:"throo"` } @@ -1010,7 +901,7 @@ func TestNewNodeBuilder_ShouldHaveNotDoneTestsLikeThisOhWell(t *testing.T) { } t2 := t1low{ - Thril: low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*key]]{ + Thril: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*valueReferenceStruct]]]{ Value: m, ValueNode: utils.CreateStringNode("beer"), }, diff --git a/datamodel/high/nodes/nodeentry.go b/datamodel/high/nodes/nodeentry.go new file mode 100644 index 0000000..5c4fdfe --- /dev/null +++ b/datamodel/high/nodes/nodeentry.go @@ -0,0 +1,16 @@ +package nodes + +import "gopkg.in/yaml.v3" + +// NodeEntry represents a single node used by NodeBuilder. +type NodeEntry struct { + Tag string + Key string + Value any + StringValue string + Line int + KeyStyle yaml.Style + // ValueStyle yaml.Style + RenderZero bool + LowValue any +} diff --git a/datamodel/high/shared.go b/datamodel/high/shared.go index 6c4b6e8..7c01aad 100644 --- a/datamodel/high/shared.go +++ b/datamodel/high/shared.go @@ -15,12 +15,13 @@ package high import ( "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // GoesLow is used to represent any high-level model. All high level models meet this interface and can be used to // extract low-level models from any high-level model. type GoesLow[T any] interface { - // GoLow returns the low-level object that was used to create the high-level object. This allows consumers // to dive-down into the plumbing API at any point in the model. GoLow() T @@ -29,18 +30,17 @@ type GoesLow[T any] interface { // GoesLowUntyped is used to represent any high-level model. All high level models meet this interface and can be used to // extract low-level models from any high-level model. type GoesLowUntyped interface { - // GoLowUntyped returns the low-level object that was used to create the high-level object. This allows consumers // to dive-down into the plumbing API at any point in the model. GoLowUntyped() any } -// ExtractExtensions is a convenience method for converting low-level extension definitions, to a high level map[string]any +// ExtractExtensions is a convenience method for converting low-level extension definitions, to a high level *orderedmap.Map[string, *yaml.Node] // definition that is easier to consume in applications. -func ExtractExtensions(extensions map[low.KeyReference[string]]low.ValueReference[any]) map[string]any { - extracted := make(map[string]any) - for k, v := range extensions { - extracted[k.Value] = v.Value +func ExtractExtensions(extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]]) *orderedmap.Map[string, *yaml.Node] { + extracted := orderedmap.New[string, *yaml.Node]() + for pair := orderedmap.First(extensions); pair != nil; pair = pair.Next() { + extracted.Set(pair.Key().Value, pair.Value().Value) } return extracted } @@ -61,18 +61,18 @@ func ExtractExtensions(extensions map[low.KeyReference[string]]low.ValueReferenc // // schema := schemaProxy.Schema() // any high-level object that has // extensions, err := UnpackExtensions[MyComplexType, low.Schema](schema) -func UnpackExtensions[T any, R low.HasExtensions[T]](low GoesLow[R]) (map[string]*T, error) { - m := make(map[string]*T) +func UnpackExtensions[T any, R low.HasExtensions[T]](low GoesLow[R]) (*orderedmap.Map[string, *T], error) { + m := orderedmap.New[string, *T]() ext := low.GoLow().GetExtensions() - for i := range ext { - key := i.Value + for pair := orderedmap.First(ext); pair != nil; pair = pair.Next() { + key := pair.Key().Value g := new(T) - valueNode := ext[i].ValueNode + valueNode := pair.Value().ValueNode err := valueNode.Decode(g) if err != nil { return nil, err } - m[key] = g + m.Set(key, g) } return m, nil } diff --git a/datamodel/high/shared_test.go b/datamodel/high/shared_test.go index cf03e54..c4300fa 100644 --- a/datamodel/high/shared_test.go +++ b/datamodel/high/shared_test.go @@ -4,21 +4,30 @@ package high import ( - "github.com/pb33f/libopenapi/datamodel/low" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" "testing" + + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestExtractExtensions(t *testing.T) { - n := make(map[low.KeyReference[string]]low.ValueReference[any]) - n[low.KeyReference[string]{ + n := orderedmap.New[low.KeyReference[string], low.ValueReference[*yaml.Node]]() + n.Set(low.KeyReference[string]{ Value: "pb33f", - }] = low.ValueReference[any]{ - Value: "new cowboy in town", - } + }, low.ValueReference[*yaml.Node]{ + Value: utils.CreateStringNode("new cowboy in town"), + }) ext := ExtractExtensions(n) - assert.Equal(t, "new cowboy in town", ext["pb33f"]) + + var pb33f string + err := ext.GetOrZero("pb33f").Decode(&pb33f) + require.NoError(t, err) + + assert.Equal(t, "new cowboy in town", pb33f) } type textExtension struct { @@ -35,15 +44,14 @@ func (p *parent) GoLow() *child { } type child struct { - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } -func (c *child) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (c *child) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return c.Extensions } func TestUnpackExtensions(t *testing.T) { - var resultA, resultB yaml.Node ymlA := ` @@ -59,18 +67,18 @@ power: 2` err = yaml.Unmarshal([]byte(ymlB), &resultB) assert.NoError(t, err) - n := make(map[low.KeyReference[string]]low.ValueReference[any]) - n[low.KeyReference[string]{ + n := orderedmap.New[low.KeyReference[string], low.ValueReference[*yaml.Node]]() + n.Set(low.KeyReference[string]{ Value: "x-rancher-a", - }] = low.ValueReference[any]{ + }, low.ValueReference[*yaml.Node]{ ValueNode: resultA.Content[0], - } + }) - n[low.KeyReference[string]{ + n.Set(low.KeyReference[string]{ Value: "x-rancher-b", - }] = low.ValueReference[any]{ + }, low.ValueReference[*yaml.Node]{ ValueNode: resultB.Content[0], - } + }) c := new(child) c.Extensions = n @@ -81,14 +89,13 @@ power: 2` res, err := UnpackExtensions[textExtension, *child](p) assert.NoError(t, err) assert.NotEmpty(t, res) - assert.Equal(t, "buckaroo", res["x-rancher-a"].Cowboy) - assert.Equal(t, 100, res["x-rancher-a"].Power) - assert.Equal(t, "frogman", res["x-rancher-b"].Cowboy) - assert.Equal(t, 2, res["x-rancher-b"].Power) + assert.Equal(t, "buckaroo", res.GetOrZero("x-rancher-a").Cowboy) + assert.Equal(t, 100, res.GetOrZero("x-rancher-a").Power) + assert.Equal(t, "frogman", res.GetOrZero("x-rancher-b").Cowboy) + assert.Equal(t, 2, res.GetOrZero("x-rancher-b").Power) } func TestUnpackExtensions_Fail(t *testing.T) { - var resultA, resultB yaml.Node ymlA := ` @@ -105,18 +112,18 @@ power: hello` err = yaml.Unmarshal([]byte(ymlB), &resultB) assert.NoError(t, err) - n := make(map[low.KeyReference[string]]low.ValueReference[any]) - n[low.KeyReference[string]{ + n := orderedmap.New[low.KeyReference[string], low.ValueReference[*yaml.Node]]() + n.Set(low.KeyReference[string]{ Value: "x-rancher-a", - }] = low.ValueReference[any]{ + }, low.ValueReference[*yaml.Node]{ ValueNode: resultA.Content[0], - } + }) - n[low.KeyReference[string]{ + n.Set(low.KeyReference[string]{ Value: "x-rancher-b", - }] = low.ValueReference[any]{ + }, low.ValueReference[*yaml.Node]{ ValueNode: resultB.Content[0], - } + }) c := new(child) c.Extensions = n diff --git a/datamodel/high/v2/definitions.go b/datamodel/high/v2/definitions.go index 138ec5a..081b778 100644 --- a/datamodel/high/v2/definitions.go +++ b/datamodel/high/v2/definitions.go @@ -4,6 +4,7 @@ package v2 import ( + "github.com/pb33f/libopenapi/datamodel" highbase "github.com/pb33f/libopenapi/datamodel/high/base" lowmodel "github.com/pb33f/libopenapi/datamodel/low" lowbase "github.com/pb33f/libopenapi/datamodel/low/base" @@ -17,7 +18,7 @@ import ( // arrays or models. // - https://swagger.io/specification/v2/#definitionsObject type Definitions struct { - Definitions orderedmap.Map[string, *highbase.SchemaProxy] + Definitions *orderedmap.Map[string, *highbase.SchemaProxy] low *low.Definitions } @@ -28,7 +29,7 @@ func NewDefinitions(definitions *low.Definitions) *Definitions { defs := orderedmap.New[string, *highbase.SchemaProxy]() translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*lowbase.SchemaProxy]]) (asyncResult[*highbase.SchemaProxy], error) { return asyncResult[*highbase.SchemaProxy]{ - key: pair.Key().Value, + key: pair.Key().Value, result: highbase.NewSchemaProxy(&lowmodel.NodeReference[*lowbase.SchemaProxy]{ Value: pair.Value().Value, }), @@ -38,7 +39,7 @@ func NewDefinitions(definitions *low.Definitions) *Definitions { defs.Set(value.key, value.result) return nil } - _ = orderedmap.TranslateMapParallel(definitions.Schemas, translateFunc, resultFunc) + _ = datamodel.TranslateMapParallel(definitions.Schemas, translateFunc, resultFunc) rd.Definitions = defs return rd } diff --git a/datamodel/high/v2/examples.go b/datamodel/high/v2/examples.go index 55580b6..702f193 100644 --- a/datamodel/high/v2/examples.go +++ b/datamodel/high/v2/examples.go @@ -6,13 +6,14 @@ package v2 import ( low "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // Example represents a high-level Swagger / OpenAPI 2 Example object, backed by a low level one. // Allows sharing examples for operation responses // - https://swagger.io/specification/v2/#exampleObject type Example struct { - Values orderedmap.Map[string, any] + Values *orderedmap.Map[string, *yaml.Node] low *low.Examples } @@ -21,7 +22,7 @@ func NewExample(examples *low.Examples) *Example { e := new(Example) e.low = examples if orderedmap.Len(examples.Values) > 0 { - values := orderedmap.New[string, any]() + values := orderedmap.New[string, *yaml.Node]() for pair := orderedmap.First(examples.Values); pair != nil; pair = pair.Next() { values.Set(pair.Key().Value, pair.Value().Value) } diff --git a/datamodel/high/v2/header.go b/datamodel/high/v2/header.go index 5a197f5..a912434 100644 --- a/datamodel/high/v2/header.go +++ b/datamodel/high/v2/header.go @@ -6,6 +6,8 @@ package v2 import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // Header Represents a high-level Swagger / OpenAPI 2 Header object, backed by a low-level one. @@ -30,7 +32,7 @@ type Header struct { UniqueItems bool Enum []any MultipleOf int - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.Header } diff --git a/datamodel/high/v2/items.go b/datamodel/high/v2/items.go index 4749ef7..03bf009 100644 --- a/datamodel/high/v2/items.go +++ b/datamodel/high/v2/items.go @@ -5,6 +5,7 @@ package v2 import ( low "github.com/pb33f/libopenapi/datamodel/low/v2" + "gopkg.in/yaml.v3" ) // Items is a high-level representation of a Swagger / OpenAPI 2 Items object, backed by a low level one. @@ -16,7 +17,7 @@ type Items struct { Format string CollectionFormat string Items *Items - Default any + Default *yaml.Node Maximum int ExclusiveMaximum bool Minimum int @@ -27,7 +28,7 @@ type Items struct { MaxItems int MinItems int UniqueItems bool - Enum []any + Enum []*yaml.Node MultipleOf int low *low.Items } @@ -82,7 +83,7 @@ func NewItems(items *low.Items) *Items { i.UniqueItems = items.UniqueItems.Value } if !items.Enum.IsEmpty() { - var enums []any + var enums []*yaml.Node for e := range items.Enum.Value { enums = append(enums, items.Enum.Value[e].Value) } diff --git a/datamodel/high/v2/operation.go b/datamodel/high/v2/operation.go index 710f586..27dd4d5 100644 --- a/datamodel/high/v2/operation.go +++ b/datamodel/high/v2/operation.go @@ -7,6 +7,8 @@ import ( "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/high/base" low "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // Operation represents a high-level Swagger / OpenAPI 2 Operation object, backed by a low-level one. @@ -25,7 +27,7 @@ type Operation struct { Schemes []string Deprecated bool Security []*base.SecurityRequirement - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.Operation } diff --git a/datamodel/high/v2/parameter.go b/datamodel/high/v2/parameter.go index 3012a47..2cc0529 100644 --- a/datamodel/high/v2/parameter.go +++ b/datamodel/high/v2/parameter.go @@ -7,6 +7,8 @@ import ( "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/high/base" low "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // Parameter represents a high-level Swagger / OpenAPI 2 Parameter object, backed by a low-level one. @@ -61,7 +63,7 @@ type Parameter struct { Schema *base.SchemaProxy Items *Items CollectionFormat string - Default any + Default *yaml.Node Maximum *int ExclusiveMaximum *bool Minimum *int @@ -72,9 +74,9 @@ type Parameter struct { MaxItems *int MinItems *int UniqueItems *bool - Enum []any + Enum []*yaml.Node MultipleOf *int - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.Parameter } @@ -147,7 +149,7 @@ func NewParameter(parameter *low.Parameter) *Parameter { p.UniqueItems = ¶meter.UniqueItems.Value } if !parameter.Enum.IsEmpty() { - var enums []any + var enums []*yaml.Node for e := range parameter.Enum.Value { enums = append(enums, parameter.Enum.Value[e].Value) } diff --git a/datamodel/high/v2/parameter_definitions.go b/datamodel/high/v2/parameter_definitions.go index cf3e86e..7fede38 100644 --- a/datamodel/high/v2/parameter_definitions.go +++ b/datamodel/high/v2/parameter_definitions.go @@ -4,6 +4,7 @@ package v2 import ( + "github.com/pb33f/libopenapi/datamodel" lowmodel "github.com/pb33f/libopenapi/datamodel/low" low "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" @@ -16,7 +17,7 @@ import ( // referenced to the ones defined here. It does not define global operation parameters // - https://swagger.io/specification/v2/#parametersDefinitionsObject type ParameterDefinitions struct { - Definitions orderedmap.Map[string, *Parameter] + Definitions *orderedmap.Map[string, *Parameter] low *low.ParameterDefinitions } @@ -36,7 +37,7 @@ func NewParametersDefinitions(parametersDefinitions *low.ParameterDefinitions) * params.Set(value.key, value.result) return nil } - _ = orderedmap.TranslateMapParallel(parametersDefinitions.Definitions, translateFunc, resultFunc) + _ = datamodel.TranslateMapParallel(parametersDefinitions.Definitions, translateFunc, resultFunc) pd.Definitions = params return pd } diff --git a/datamodel/high/v2/path_item.go b/datamodel/high/v2/path_item.go index ec5fe25..627b4cf 100644 --- a/datamodel/high/v2/path_item.go +++ b/datamodel/high/v2/path_item.go @@ -4,10 +4,15 @@ package v2 import ( + "reflect" + "slices" "sync" "github.com/pb33f/libopenapi/datamodel/high" - low "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/datamodel/low" + lowV2 "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // PathItem represents a high-level Swagger / OpenAPI 2 PathItem object backed by a low-level one. @@ -26,12 +31,12 @@ type PathItem struct { Head *Operation Patch *Operation Parameters []*Parameter - Extensions map[string]any - low *low.PathItem + Extensions *orderedmap.Map[string, *yaml.Node] + low *lowV2.PathItem } // NewPathItem will create a new high-level PathItem from a low-level one. All paths are built out asynchronously. -func NewPathItem(pathItem *low.PathItem) *PathItem { +func NewPathItem(pathItem *lowV2.PathItem) *PathItem { p := new(PathItem) p.low = pathItem p.Extensions = high.ExtractExtensions(pathItem.Extensions) @@ -42,7 +47,7 @@ func NewPathItem(pathItem *low.PathItem) *PathItem { } p.Parameters = params } - var buildOperation = func(method string, op *low.Operation) *Operation { + buildOperation := func(method string, op *lowV2.Operation) *Operation { return NewOperation(op) } @@ -50,49 +55,49 @@ func NewPathItem(pathItem *low.PathItem) *PathItem { if !pathItem.Get.IsEmpty() { wg.Add(1) go func() { - p.Get = buildOperation(low.GetLabel, pathItem.Get.Value) + p.Get = buildOperation(lowV2.GetLabel, pathItem.Get.Value) wg.Done() }() } if !pathItem.Put.IsEmpty() { wg.Add(1) go func() { - p.Put = buildOperation(low.PutLabel, pathItem.Put.Value) + p.Put = buildOperation(lowV2.PutLabel, pathItem.Put.Value) wg.Done() }() } if !pathItem.Post.IsEmpty() { wg.Add(1) go func() { - p.Post = buildOperation(low.PostLabel, pathItem.Post.Value) + p.Post = buildOperation(lowV2.PostLabel, pathItem.Post.Value) wg.Done() }() } if !pathItem.Patch.IsEmpty() { wg.Add(1) go func() { - p.Patch = buildOperation(low.PatchLabel, pathItem.Patch.Value) + p.Patch = buildOperation(lowV2.PatchLabel, pathItem.Patch.Value) wg.Done() }() } if !pathItem.Delete.IsEmpty() { wg.Add(1) go func() { - p.Delete = buildOperation(low.DeleteLabel, pathItem.Delete.Value) + p.Delete = buildOperation(lowV2.DeleteLabel, pathItem.Delete.Value) wg.Done() }() } if !pathItem.Head.IsEmpty() { wg.Add(1) go func() { - p.Head = buildOperation(low.HeadLabel, pathItem.Head.Value) + p.Head = buildOperation(lowV2.HeadLabel, pathItem.Head.Value) wg.Done() }() } if !pathItem.Options.IsEmpty() { wg.Add(1) go func() { - p.Options = buildOperation(low.OptionsLabel, pathItem.Options.Value) + p.Options = buildOperation(lowV2.OptionsLabel, pathItem.Options.Value) wg.Done() }() } @@ -101,32 +106,65 @@ func NewPathItem(pathItem *low.PathItem) *PathItem { } // GoLow returns the low-level PathItem used to create the high-level one. -func (p *PathItem) GoLow() *low.PathItem { +func (p *PathItem) GoLow() *lowV2.PathItem { return p.low } -func (p *PathItem) GetOperations() map[string]*Operation { - o := make(map[string]*Operation) +func (p *PathItem) GetOperations() *orderedmap.Map[string, *Operation] { + o := orderedmap.New[string, *Operation]() + + // TODO: this is a bit of a hack, but it works for now. We might just want to actually pull the data out of the document as a map and split it into the individual operations + + type op struct { + name string + op *Operation + line int + } + + getLine := func(field string, idx int) int { + if p.GoLow() == nil { + return idx + } + + l, ok := reflect.ValueOf(p.GoLow()).Elem().FieldByName(field).Interface().(low.NodeReference[*lowV2.Operation]) + if !ok || l.GetKeyNode() == nil { + return idx + } + + return l.GetKeyNode().Line + } + + ops := []op{} + if p.Get != nil { - o[low.GetLabel] = p.Get + ops = append(ops, op{name: lowV2.GetLabel, op: p.Get, line: getLine("Get", -7)}) } if p.Put != nil { - o[low.PutLabel] = p.Put + ops = append(ops, op{name: lowV2.PutLabel, op: p.Put, line: getLine("Put", -6)}) } if p.Post != nil { - o[low.PostLabel] = p.Post + ops = append(ops, op{name: lowV2.PostLabel, op: p.Post, line: getLine("Post", -5)}) } if p.Delete != nil { - o[low.DeleteLabel] = p.Delete + ops = append(ops, op{name: lowV2.DeleteLabel, op: p.Delete, line: getLine("Delete", -4)}) } if p.Options != nil { - o[low.OptionsLabel] = p.Options + ops = append(ops, op{name: lowV2.OptionsLabel, op: p.Options, line: getLine("Options", -3)}) } if p.Head != nil { - o[low.HeadLabel] = p.Head + ops = append(ops, op{name: lowV2.HeadLabel, op: p.Head, line: getLine("Head", -2)}) } if p.Patch != nil { - o[low.PatchLabel] = p.Patch + ops = append(ops, op{name: lowV2.PatchLabel, op: p.Patch, line: getLine("Patch", -1)}) } + + slices.SortStableFunc(ops, func(a op, b op) int { + return a.line - b.line + }) + + for _, op := range ops { + o.Set(op.name, op.op) + } + return o } diff --git a/datamodel/high/v2/path_item_test.go b/datamodel/high/v2/path_item_test.go index acb53c2..2455c33 100644 --- a/datamodel/high/v2/path_item_test.go +++ b/datamodel/high/v2/path_item_test.go @@ -5,16 +5,17 @@ package v2 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" v2 "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestPathItem_GetOperations(t *testing.T) { - yml := `get: description: get put: @@ -41,5 +42,5 @@ options: r := NewPathItem(&n) - assert.Len(t, r.GetOperations(), 7) + assert.Equal(t, 7, orderedmap.Len(r.GetOperations())) } diff --git a/datamodel/high/v2/paths.go b/datamodel/high/v2/paths.go index ad9d68b..3016b09 100644 --- a/datamodel/high/v2/paths.go +++ b/datamodel/high/v2/paths.go @@ -9,12 +9,13 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" v2low "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // Paths represents a high-level Swagger / OpenAPI Paths object, backed by a low-level one. type Paths struct { - PathItems orderedmap.Map[string, *PathItem] - Extensions map[string]any + PathItems *orderedmap.Map[string, *PathItem] + Extensions *orderedmap.Map[string, *yaml.Node] low *v2low.Paths } diff --git a/datamodel/high/v2/response.go b/datamodel/high/v2/response.go index 5e2d4bc..595af99 100644 --- a/datamodel/high/v2/response.go +++ b/datamodel/high/v2/response.go @@ -8,6 +8,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/high/base" low "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // Response is a representation of a high-level Swagger / OpenAPI 2 Response object, backed by a low-level one. @@ -16,9 +17,9 @@ import ( type Response struct { Description string Schema *base.SchemaProxy - Headers orderedmap.Map[string, *Header] + Headers *orderedmap.Map[string, *Header] Examples *Example - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.Response } diff --git a/datamodel/high/v2/responses.go b/datamodel/high/v2/responses.go index b6cc9ec..6bfeabf 100644 --- a/datamodel/high/v2/responses.go +++ b/datamodel/high/v2/responses.go @@ -4,17 +4,19 @@ package v2 import ( + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high" lowmodel "github.com/pb33f/libopenapi/datamodel/low" low "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // Responses is a high-level representation of a Swagger / OpenAPI 2 Responses object, backed by a low level one. type Responses struct { - Codes orderedmap.Map[string, *Response] + Codes *orderedmap.Map[string, *Response] Default *Response - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.Responses } @@ -32,7 +34,7 @@ func NewResponses(responses *low.Responses) *Responses { resp := orderedmap.New[string, *Response]() translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Response]]) (asyncResult[*Response], error) { return asyncResult[*Response]{ - key: pair.Key().Value, + key: pair.Key().Value, result: NewResponse(pair.Value().Value), }, nil } @@ -40,7 +42,7 @@ func NewResponses(responses *low.Responses) *Responses { resp.Set(value.key, value.result) return nil } - _ = orderedmap.TranslateMapParallel(responses.Codes, translateFunc, resultFunc) + _ = datamodel.TranslateMapParallel(responses.Codes, translateFunc, resultFunc) r.Codes = resp } diff --git a/datamodel/high/v2/responses_definitions.go b/datamodel/high/v2/responses_definitions.go index fd8ff63..141b7be 100644 --- a/datamodel/high/v2/responses_definitions.go +++ b/datamodel/high/v2/responses_definitions.go @@ -4,6 +4,7 @@ package v2 import ( + "github.com/pb33f/libopenapi/datamodel" lowmodel "github.com/pb33f/libopenapi/datamodel/low" low "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" @@ -16,7 +17,7 @@ import ( // referenced to the ones defined here. It does not define global operation responses // - https://swagger.io/specification/v2/#responsesDefinitionsObject type ResponsesDefinitions struct { - Definitions orderedmap.Map[string, *Response] + Definitions *orderedmap.Map[string, *Response] low *low.ResponsesDefinitions } @@ -36,7 +37,7 @@ func NewResponsesDefinitions(responsesDefinitions *low.ResponsesDefinitions) *Re return nil } - _ = orderedmap.TranslateMapParallel(responsesDefinitions.Definitions, translateFunc, resultFunc) + _ = datamodel.TranslateMapParallel(responsesDefinitions.Definitions, translateFunc, resultFunc) rd.Definitions = responses return rd } diff --git a/datamodel/high/v2/scopes.go b/datamodel/high/v2/scopes.go index 1197afb..a580d94 100644 --- a/datamodel/high/v2/scopes.go +++ b/datamodel/high/v2/scopes.go @@ -13,7 +13,7 @@ import ( // Scopes lists the available scopes for an OAuth2 security scheme. // - https://swagger.io/specification/v2/#scopesObject type Scopes struct { - Values orderedmap.Map[string, string] + Values *orderedmap.Map[string, string] low *low.Scopes } diff --git a/datamodel/high/v2/security_definitions.go b/datamodel/high/v2/security_definitions.go index feba1c8..67ad069 100644 --- a/datamodel/high/v2/security_definitions.go +++ b/datamodel/high/v2/security_definitions.go @@ -4,6 +4,7 @@ package v2 import ( + "github.com/pb33f/libopenapi/datamodel" lowmodel "github.com/pb33f/libopenapi/datamodel/low" low "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" @@ -16,7 +17,7 @@ import ( // schemes on the operations and only serves to provide the relevant details for each scheme // - https://swagger.io/specification/v2/#securityDefinitionsObject type SecurityDefinitions struct { - Definitions orderedmap.Map[string, *SecurityScheme] + Definitions *orderedmap.Map[string, *SecurityScheme] low *low.SecurityDefinitions } @@ -35,12 +36,8 @@ func NewSecurityDefinitions(definitions *low.SecurityDefinitions) *SecurityDefin schemes.Set(value.key, value.result) return nil } - _ = orderedmap.TranslateMapParallel(definitions.Definitions, translateFunc, resultFunc) + _ = datamodel.TranslateMapParallel(definitions.Definitions, translateFunc, resultFunc) - // schemes := make(map[string]*SecurityScheme) - // for k := range definitions.Definitions { - // schemes[k.Value] = NewSecurityScheme(definitions.Definitions[k].Value) - // } sd.Definitions = schemes return sd } diff --git a/datamodel/high/v2/security_scheme.go b/datamodel/high/v2/security_scheme.go index 774dfed..52c8e80 100644 --- a/datamodel/high/v2/security_scheme.go +++ b/datamodel/high/v2/security_scheme.go @@ -6,6 +6,8 @@ package v2 import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // SecurityScheme is a high-level representation of a Swagger / OpenAPI 2 SecurityScheme object @@ -24,7 +26,7 @@ type SecurityScheme struct { AuthorizationUrl string TokenUrl string Scopes *Scopes - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.SecurityScheme } diff --git a/datamodel/high/v2/swagger.go b/datamodel/high/v2/swagger.go index 65ad816..142d04c 100644 --- a/datamodel/high/v2/swagger.go +++ b/datamodel/high/v2/swagger.go @@ -15,11 +15,12 @@ import ( "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/high/base" low "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // Swagger represents a high-level Swagger / OpenAPI 2 document. An instance of Swagger is the root of the specification. type Swagger struct { - // Swagger is the version of Swagger / OpenAPI being used, extracted from the 'swagger: 2.x' definition. Swagger string @@ -90,7 +91,7 @@ type Swagger struct { ExternalDocs *base.ExternalDoc // Extensions contains all custom extensions defined for the top-level document. - Extensions map[string]any + Extensions *orderedmap.Map[string, *yaml.Node] low *low.Swagger } diff --git a/datamodel/high/v2/swagger_test.go b/datamodel/high/v2/swagger_test.go index 935a8aa..ac1eb91 100644 --- a/datamodel/high/v2/swagger_test.go +++ b/datamodel/high/v2/swagger_test.go @@ -5,13 +5,12 @@ package v2 import ( "os" + "testing" "github.com/pb33f/libopenapi/datamodel" v2 "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" - - "testing" ) var doc *v2.Swagger @@ -42,8 +41,12 @@ func BenchmarkNewDocument(b *testing.B) { func TestNewSwaggerDocument_Base(t *testing.T) { initTest() highDoc := NewSwaggerDocument(doc) + + var xPet bool + _ = highDoc.Extensions.GetOrZero("x-pet").Decode(&xPet) + assert.Equal(t, "2.0", highDoc.Swagger) - assert.True(t, highDoc.Extensions["x-pet"].(bool)) + assert.True(t, xPet) assert.Equal(t, "petstore.swagger.io", highDoc.Host) assert.Equal(t, "/v2", highDoc.BasePath) assert.Len(t, highDoc.Schemes, 2) @@ -56,7 +59,6 @@ func TestNewSwaggerDocument_Base(t *testing.T) { wentLow := highDoc.GoLow() assert.Equal(t, 16, wentLow.Host.ValueNode.Line) assert.Equal(t, 7, wentLow.Host.ValueNode.Column) - } func TestNewSwaggerDocument_Info(t *testing.T) { @@ -82,11 +84,15 @@ func TestNewSwaggerDocument_Parameters(t *testing.T) { initTest() highDoc := NewSwaggerDocument(doc) params := highDoc.Parameters + + var xChicken string + _ = params.Definitions.GetOrZero("simpleParam").Extensions.GetOrZero("x-chicken").Decode(&xChicken) + assert.Equal(t, 1, orderedmap.Len(params.Definitions)) assert.Equal(t, "query", params.Definitions.GetOrZero("simpleParam").In) assert.Equal(t, "simple", params.Definitions.GetOrZero("simpleParam").Name) assert.Equal(t, "string", params.Definitions.GetOrZero("simpleParam").Type) - assert.Equal(t, "nuggets", params.Definitions.GetOrZero("simpleParam").Extensions["x-chicken"]) + assert.Equal(t, "nuggets", xChicken) wentLow := params.GoLow() assert.Equal(t, 20, wentLow.FindParameter("simpleParam").ValueNode.Line) @@ -95,7 +101,6 @@ func TestNewSwaggerDocument_Parameters(t *testing.T) { wentLower := params.Definitions.GetOrZero("simpleParam").GoLow() assert.Equal(t, 21, wentLower.Name.ValueNode.Line) assert.Equal(t, 11, wentLower.Name.ValueNode.Column) - } func TestNewSwaggerDocument_Security(t *testing.T) { @@ -107,7 +112,6 @@ func TestNewSwaggerDocument_Security(t *testing.T) { wentLow := highDoc.Security[0].GoLow() assert.Equal(t, 25, wentLow.Requirements.ValueNode.Line) assert.Equal(t, 5, wentLow.Requirements.ValueNode.Column) - } func TestNewSwaggerDocument_Definitions_Security(t *testing.T) { @@ -140,22 +144,32 @@ func TestNewSwaggerDocument_Definitions_Responses(t *testing.T) { assert.Equal(t, 2, orderedmap.Len(highDoc.Responses.Definitions)) defs := highDoc.Responses.Definitions - assert.Equal(t, "morning", defs.GetOrZero("200").Extensions["x-coffee"]) + + var xCoffee string + _ = defs.GetOrZero("200").Extensions.GetOrZero("x-coffee").Decode(&xCoffee) + + assert.Equal(t, "morning", xCoffee) assert.Equal(t, "OK", defs.GetOrZero("200").Description) assert.Equal(t, "a generic API response object", defs.GetOrZero("200").Schema.Schema().Description) assert.Equal(t, 3, orderedmap.Len(defs.GetOrZero("200").Examples.Values)) - exp := defs.GetOrZero("200").Examples.Values.GetOrZero("application/json") - assert.Len(t, exp.(map[string]interface{}), 2) - assert.Equal(t, "two", exp.(map[string]interface{})["one"]) + var appJson map[string]interface{} + _ = defs.GetOrZero("200").Examples.Values.GetOrZero("application/json").Decode(&appJson) - exp = defs.GetOrZero("200").Examples.Values.GetOrZero("text/xml") - assert.Len(t, exp.([]interface{}), 3) - assert.Equal(t, "two", exp.([]interface{})[1]) + assert.Len(t, appJson, 2) + assert.Equal(t, "two", appJson["one"]) - exp = defs.GetOrZero("200").Examples.Values.GetOrZero("text/plain") - assert.Equal(t, "something else.", exp) + var textXml []interface{} + _ = defs.GetOrZero("200").Examples.Values.GetOrZero("text/xml").Decode(&textXml) + + assert.Len(t, textXml, 3) + assert.Equal(t, "two", textXml[1]) + + var textPlain string + _ = defs.GetOrZero("200").Examples.Values.GetOrZero("text/plain").Decode(&textPlain) + + assert.Equal(t, "something else.", textPlain) expWentLow := defs.GetOrZero("200").Examples.GoLow() assert.Equal(t, 702, expWentLow.FindExample("application/json").ValueNode.Line) @@ -168,10 +182,13 @@ func TestNewSwaggerDocument_Definitions_Responses(t *testing.T) { assert.Len(t, y.Enum, 2) x := y.Items + var def string + _ = x.Default.Decode(&def) + assert.Equal(t, "something", x.Format) assert.Equal(t, "array", x.Type) assert.Equal(t, "csv", x.CollectionFormat) - assert.Equal(t, "cake", x.Default) + assert.Equal(t, "cake", def) assert.Equal(t, 10, x.Maximum) assert.Equal(t, 1, x.Minimum) assert.True(t, x.ExclusiveMaximum) @@ -198,7 +215,6 @@ func TestNewSwaggerDocument_Definitions(t *testing.T) { wentLow := highDoc.Definitions.GoLow() assert.Equal(t, 848, wentLow.FindSchema("User").ValueNode.Line) - } func TestNewSwaggerDocument_Paths(t *testing.T) { @@ -207,7 +223,14 @@ func TestNewSwaggerDocument_Paths(t *testing.T) { assert.Equal(t, 15, orderedmap.Len(highDoc.Paths.PathItems)) upload := highDoc.Paths.PathItems.GetOrZero("/pet/{petId}/uploadImage") - assert.Equal(t, "man", upload.Extensions["x-potato"]) + + var xPotato string + _ = upload.Extensions.GetOrZero("x-potato").Decode(&xPotato) + + var paramEnum0 string + _ = upload.Post.Parameters[0].Enum[0].Decode(¶mEnum0) + + assert.Equal(t, "man", xPotato) assert.Nil(t, upload.Get) assert.Nil(t, upload.Put) assert.Nil(t, upload.Patch) @@ -238,8 +261,11 @@ func TestNewSwaggerDocument_Paths(t *testing.T) { assert.Equal(t, 20, *upload.Post.Parameters[0].MaxItems) assert.True(t, *upload.Post.Parameters[0].UniqueItems) assert.Len(t, upload.Post.Parameters[0].Enum, 2) - assert.Equal(t, "hello", upload.Post.Parameters[0].Enum[0]) - def := upload.Post.Parameters[0].Default.(map[string]interface{}) + assert.Equal(t, "hello", paramEnum0) + + var def map[string]any + _ = upload.Post.Parameters[0].Default.Decode(&def) + assert.Equal(t, "here", def["something"]) assert.Equal(t, "https://pb33f.io", upload.Post.ExternalDocs.URL) @@ -257,11 +283,9 @@ func TestNewSwaggerDocument_Paths(t *testing.T) { wentLowest := upload.Post.GoLow() assert.Equal(t, 55, wentLowest.Tags.KeyNode.Line) - } func TestNewSwaggerDocument_Responses(t *testing.T) { - initTest() highDoc := NewSwaggerDocument(doc) upload := highDoc.Paths.PathItems.GetOrZero("/pet/{petId}/uploadImage").Post @@ -278,5 +302,4 @@ func TestNewSwaggerDocument_Responses(t *testing.T) { wentLower := OK.GoLow() assert.Equal(t, 107, wentLower.Schema.KeyNode.Line) assert.Equal(t, 11, wentLower.Schema.KeyNode.Column) - } diff --git a/datamodel/high/v3/callback.go b/datamodel/high/v3/callback.go index 7bacd31..db88da1 100644 --- a/datamodel/high/v3/callback.go +++ b/datamodel/high/v3/callback.go @@ -21,8 +21,8 @@ import ( // that identifies a URL to use for the callback operation. // - https://spec.openapis.org/oas/v3.1.0#callback-object type Callback struct { - Expression orderedmap.Map[string, *PathItem] `json:"-" yaml:"-"` - Extensions map[string]any `json:"-" yaml:"-"` + Expression *orderedmap.Map[string, *PathItem] `json:"-" yaml:"-"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Callback } @@ -31,13 +31,11 @@ func NewCallback(lowCallback *low.Callback) *Callback { n := new(Callback) n.low = lowCallback n.Expression = orderedmap.New[string, *PathItem]() - for pair := orderedmap.First(lowCallback.Expression.Value); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(lowCallback.Expression); pair != nil; pair = pair.Next() { n.Expression.Set(pair.Key().Value, NewPathItem(pair.Value().Value)) } - n.Extensions = make(map[string]any) - for k, v := range lowCallback.Extensions { - n.Extensions[k.Value] = v.Value - } + + n.Extensions = high.ExtractExtensions(lowCallback.Extensions) return n } @@ -51,36 +49,50 @@ func (c *Callback) GoLowUntyped() any { return c.low } -// Render will return a YAML representation of the Callback object as a byte slice. +// Render will return a YAML representation of the Paths object as a byte slice. func (c *Callback) Render() ([]byte, error) { return yaml.Marshal(c) } -// MarshalYAML will create a ready to render YAML representation of the Callback object. +func (c *Callback) RenderInline() ([]byte, error) { + d, _ := c.MarshalYAMLInline() + return yaml.Marshal(d) +} + +// MarshalYAML will create a ready to render YAML representation of the Paths object. func (c *Callback) MarshalYAML() (interface{}, error) { // map keys correctly. m := utils.CreateEmptyMapNode() - type cbItem struct { - cb *PathItem - exp string - line int - ext *yaml.Node + type pathItem struct { + pi *PathItem + path string + line int + style yaml.Style + rendered *yaml.Node } - var mapped []*cbItem + var mapped []*pathItem for pair := orderedmap.First(c.Expression); pair != nil; pair = pair.Next() { - ln := 999 // default to a high value to weight new content to the bottom. + k := pair.Key() + pi := pair.Value() + ln := 9999 // default to a high value to weight new content to the bottom. + var style yaml.Style if c.low != nil { - for lPair := orderedmap.First(c.low.Expression.Value); lPair != nil; lPair = lPair.Next() { - if lPair.Key().Value == pair.Key() { - ln = lPair.Key().KeyNode.Line + lpi := c.low.FindExpression(k) + if lpi != nil { + ln = lpi.ValueNode.Line + } + + for pair := orderedmap.First(c.low.Expression); pair != nil; pair = pair.Next() { + if pair.Key().Value == k { + style = pair.Key().KeyNode.Style + break } } } - mapped = append(mapped, &cbItem{pair.Value(), pair.Key(), ln, nil}) + mapped = append(mapped, &pathItem{pi, k, ln, style, nil}) } - // extract extensions nb := high.NewNodeBuilder(c, c.low) extNode := nb.Render() if extNode != nil && extNode.Content != nil { @@ -90,23 +102,101 @@ func (c *Callback) MarshalYAML() (interface{}, error) { label = extNode.Content[u].Value continue } - mapped = append(mapped, &cbItem{nil, label, - extNode.Content[u].Line, extNode.Content[u]}) + mapped = append(mapped, &pathItem{ + nil, label, + extNode.Content[u].Line, 0, extNode.Content[u], + }) } } sort.Slice(mapped, func(i, j int) bool { return mapped[i].line < mapped[j].line }) - for j := range mapped { - if mapped[j].cb != nil { - rendered, _ := mapped[j].cb.MarshalYAML() - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].exp)) + for _, mp := range mapped { + if mp.pi != nil { + rendered, _ := mp.pi.MarshalYAML() + + kn := utils.CreateStringNode(mp.path) + kn.Style = mp.style + + m.Content = append(m.Content, kn) m.Content = append(m.Content, rendered.(*yaml.Node)) } - if mapped[j].ext != nil { - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].exp)) - m.Content = append(m.Content, mapped[j].ext) + if mp.rendered != nil { + m.Content = append(m.Content, utils.CreateStringNode(mp.path)) + m.Content = append(m.Content, mp.rendered) + } + } + + return m, nil +} + +func (c *Callback) MarshalYAMLInline() (interface{}, error) { + // map keys correctly. + m := utils.CreateEmptyMapNode() + type pathItem struct { + pi *PathItem + path string + line int + style yaml.Style + rendered *yaml.Node + } + var mapped []*pathItem + + for pair := orderedmap.First(c.Expression); pair != nil; pair = pair.Next() { + k := pair.Key() + pi := pair.Value() + ln := 9999 // default to a high value to weight new content to the bottom. + var style yaml.Style + if c.low != nil { + lpi := c.low.FindExpression(k) + if lpi != nil { + ln = lpi.ValueNode.Line + } + + for pair := orderedmap.First(c.low.Expression); pair != nil; pair = pair.Next() { + if pair.Key().Value == k { + style = pair.Key().KeyNode.Style + break + } + } + } + mapped = append(mapped, &pathItem{pi, k, ln, style, nil}) + } + + nb := high.NewNodeBuilder(c, c.low) + nb.Resolve = true + extNode := nb.Render() + if extNode != nil && extNode.Content != nil { + var label string + for u := range extNode.Content { + if u%2 == 0 { + label = extNode.Content[u].Value + continue + } + mapped = append(mapped, &pathItem{ + nil, label, + extNode.Content[u].Line, 0, extNode.Content[u], + }) + } + } + + sort.Slice(mapped, func(i, j int) bool { + return mapped[i].line < mapped[j].line + }) + for _, mp := range mapped { + if mp.pi != nil { + rendered, _ := mp.pi.MarshalYAMLInline() + + kn := utils.CreateStringNode(mp.path) + kn.Style = mp.style + + m.Content = append(m.Content, kn) + m.Content = append(m.Content, rendered.(*yaml.Node)) + } + if mp.rendered != nil { + m.Content = append(m.Content, utils.CreateStringNode(mp.path)) + m.Content = append(m.Content, mp.rendered) } } diff --git a/datamodel/high/v3/callback_test.go b/datamodel/high/v3/callback_test.go index 64976e3..8e078ce 100644 --- a/datamodel/high/v3/callback_test.go +++ b/datamodel/high/v3/callback_test.go @@ -12,11 +12,14 @@ import ( v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) func TestCallback_MarshalYAML(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-burgers", utils.CreateStringNode("why not?")) cb := &Callback{ Expression: orderedmap.ToOrderedMap(map[string]*PathItem{ @@ -31,9 +34,7 @@ func TestCallback_MarshalYAML(t *testing.T) { }, }, }), - Extensions: map[string]any{ - "x-burgers": "why not?", - }, + Extensions: ext, } rend, _ := cb.Render() @@ -43,7 +44,10 @@ func TestCallback_MarshalYAML(t *testing.T) { // mutate cb.Expression.GetOrZero("https://pb33f.io").Get.OperationId = "blim-blam" - cb.Extensions = map[string]interface{}{"x-burgers": "yes please!"} + + ext = orderedmap.New[string, *yaml.Node]() + ext.Set("x-burgers", utils.CreateStringNode("yes please!")) + cb.Extensions = ext rend, _ = cb.Render() // there is no way to determine order in brand new maps, so we have to check length. @@ -72,7 +76,10 @@ func TestCallback_MarshalYAML(t *testing.T) { r := NewCallback(&n) - assert.Equal(t, "please", r.Extensions["x-break-everything"]) + var xBreakEverything string + _ = r.Extensions.GetOrZero("x-break-everything").Decode(&xBreakEverything) + + assert.Equal(t, "please", xBreakEverything) rend, _ = r.Render() assert.Equal(t, k, strings.TrimSpace(string(rend))) diff --git a/datamodel/high/v3/components.go b/datamodel/high/v3/components.go index 044e443..db155b0 100644 --- a/datamodel/high/v3/components.go +++ b/datamodel/high/v3/components.go @@ -22,16 +22,16 @@ import ( // will have no effect on the API unless they are explicitly referenced from properties outside the components object. // - https://spec.openapis.org/oas/v3.1.0#components-object type Components struct { - Schemas orderedmap.Map[string, *highbase.SchemaProxy] `json:"schemas,omitempty" yaml:"schemas,omitempty"` - Responses orderedmap.Map[string, *Response] `json:"responses,omitempty" yaml:"responses,omitempty"` - Parameters orderedmap.Map[string, *Parameter] `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Examples orderedmap.Map[string, *highbase.Example] `json:"examples,omitempty" yaml:"examples,omitempty"` - RequestBodies orderedmap.Map[string, *RequestBody] `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` - Headers orderedmap.Map[string, *Header] `json:"headers,omitempty" yaml:"headers,omitempty"` - SecuritySchemes orderedmap.Map[string, *SecurityScheme] `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` - Links orderedmap.Map[string, *Link] `json:"links,omitempty" yaml:"links,omitempty"` - Callbacks orderedmap.Map[string, *Callback] `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Schemas *orderedmap.Map[string, *highbase.SchemaProxy] `json:"schemas,omitempty" yaml:"schemas,omitempty"` + Responses *orderedmap.Map[string, *Response] `json:"responses,omitempty" yaml:"responses,omitempty"` + Parameters *orderedmap.Map[string, *Parameter] `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Examples *orderedmap.Map[string, *highbase.Example] `json:"examples,omitempty" yaml:"examples,omitempty"` + RequestBodies *orderedmap.Map[string, *RequestBody] `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` + Headers *orderedmap.Map[string, *Header] `json:"headers,omitempty" yaml:"headers,omitempty"` + SecuritySchemes *orderedmap.Map[string, *SecurityScheme] `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` + Links *orderedmap.Map[string, *Link] `json:"links,omitempty" yaml:"links,omitempty"` + Callbacks *orderedmap.Map[string, *Callback] `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Components } @@ -41,7 +41,7 @@ type Components struct { func NewComponents(comp *low.Components) *Components { c := new(Components) c.low = comp - if len(comp.Extensions) > 0 { + if orderedmap.Len(comp.Extensions) > 0 { c.Extensions = high.ExtractExtensions(comp.Extensions) } cbMap := orderedmap.New[string, *Callback]() @@ -109,12 +109,12 @@ func NewComponents(comp *low.Components) *Components { // contains a component build result. type componentResult[T any] struct { - res T - key string + res T + key string } // buildComponent builds component structs from low level structs. -func buildComponent[IN any, OUT any](inMap orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[IN]], outMap orderedmap.Map[string, OUT], translateItem func(IN) OUT) { +func buildComponent[IN any, OUT any](inMap *orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[IN]], outMap *orderedmap.Map[string, OUT], translateItem func(IN) OUT) { translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[IN]]) (componentResult[OUT], error) { return componentResult[OUT]{key: pair.Key().Value, res: translateItem(pair.Value().Value)}, nil } @@ -126,7 +126,7 @@ func buildComponent[IN any, OUT any](inMap orderedmap.Map[lowmodel.KeyReference[ } // buildSchema builds a schema from low level structs. -func buildSchema(inMap orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*base.SchemaProxy]], outMap orderedmap.Map[string, *highbase.SchemaProxy]) { +func buildSchema(inMap *orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*base.SchemaProxy]], outMap *orderedmap.Map[string, *highbase.SchemaProxy]) { translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*base.SchemaProxy]]) (componentResult[*highbase.SchemaProxy], error) { value := pair.Value() var sch *highbase.SchemaProxy diff --git a/datamodel/high/v3/document.go b/datamodel/high/v3/document.go index 30a0e42..73823d8 100644 --- a/datamodel/high/v3/document.go +++ b/datamodel/high/v3/document.go @@ -23,7 +23,6 @@ import ( // Document represents a high-level OpenAPI 3 document (both 3.0 & 3.1). A Document is the root of the specification. type Document struct { - // Version is the version of OpenAPI being used, extracted from the 'openapi: x.x.x' definition. // This is not a standard property of the OpenAPI model, it's a convenience mechanism only. Version string `json:"openapi,omitempty" yaml:"openapi,omitempty"` @@ -54,7 +53,7 @@ type Document struct { // an empty security requirement ({}) can be included in the array. // - https://spec.openapis.org/oas/v3.1.0#security-requirement-object Security []*base.SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"` - //Security []*base.SecurityRequirement `json:"-" yaml:"-"` + // Security []*base.SecurityRequirement `json:"-" yaml:"-"` // Tags is a slice of base.Tag instances defined by the specification // A list of tags used by the document with additional metadata. The order of the tags can be used to reflect on @@ -69,7 +68,7 @@ type Document struct { ExternalDocs *base.ExternalDoc `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` // Extensions contains all custom extensions defined for the top-level document. - Extensions map[string]any `json:"-" yaml:"-"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` // JsonSchemaDialect is a 3.1+ property that sets the dialect to use for validating *base.Schema definitions // The default value for the $schema keyword within Schema Objects contained within this OAS document. @@ -83,7 +82,7 @@ type Document struct { // for example by an out-of-band registration. The key name is a unique string to refer to each webhook, // while the (optionally referenced) Path Item Object describes a request that may be initiated by the API provider // and the expected responses. An example is available. - Webhooks orderedmap.Map[string, *PathItem] `json:"webhooks,omitempty" yaml:"webhooks,omitempty"` + Webhooks *orderedmap.Map[string, *PathItem] `json:"webhooks,omitempty" yaml:"webhooks,omitempty"` // Index is a reference to the *index.SpecIndex that was created for the document and used // as a guide when building out the Document. Ideal if further processing is required on the model and @@ -122,7 +121,7 @@ func NewDocument(document *low.Document) *Document { if !document.ExternalDocs.IsEmpty() { d.ExternalDocs = base.NewExternalDoc(document.ExternalDocs.Value) } - if len(document.Extensions) > 0 { + if orderedmap.Len(document.Extensions) > 0 { d.Extensions = high.ExtractExtensions(document.Extensions) } if !document.Components.IsEmpty() { diff --git a/datamodel/high/v3/document_test.go b/datamodel/high/v3/document_test.go index 6f5dc3e..7a28c59 100644 --- a/datamodel/high/v3/document_test.go +++ b/datamodel/high/v3/document_test.go @@ -51,7 +51,11 @@ func BenchmarkNewDocument(b *testing.B) { func TestNewDocument_Extensions(t *testing.T) { initTest() h := NewDocument(lowDoc) - assert.Equal(t, "darkside", h.Extensions["x-something-something"]) + + var xSomethingSomething string + _ = h.Extensions.GetOrZero("x-something-something").Decode(&xSomethingSomething) + + assert.Equal(t, "darkside", xSomethingSomething) } func TestNewDocument_ExternalDocs(t *testing.T) { @@ -133,15 +137,28 @@ func TestNewDocument_Servers(t *testing.T) { func TestNewDocument_Tags(t *testing.T) { initTest() h := NewDocument(lowDoc) + + var xInternalTing string + _ = h.Tags[0].Extensions.GetOrZero("x-internal-ting").Decode(&xInternalTing) + + var xInternalTong int64 + _ = h.Tags[0].Extensions.GetOrZero("x-internal-tong").Decode(&xInternalTong) + + var xInternalTang float64 + _ = h.Tags[0].Extensions.GetOrZero("x-internal-tang").Decode(&xInternalTang) + assert.Len(t, h.Tags, 2) assert.Equal(t, "Burgers", h.Tags[0].Name) assert.Equal(t, "All kinds of yummy burgers.", h.Tags[0].Description) assert.Equal(t, "Find out more", h.Tags[0].ExternalDocs.Description) assert.Equal(t, "https://pb33f.io", h.Tags[0].ExternalDocs.URL) - assert.Equal(t, "somethingSpecial", h.Tags[0].Extensions["x-internal-ting"]) - assert.Equal(t, int64(1), h.Tags[0].Extensions["x-internal-tong"]) - assert.Equal(t, 1.2, h.Tags[0].Extensions["x-internal-tang"]) - assert.True(t, h.Tags[0].Extensions["x-internal-tung"].(bool)) + assert.Equal(t, "somethingSpecial", xInternalTing) + assert.Equal(t, int64(1), xInternalTong) + assert.Equal(t, 1.2, xInternalTang) + + var tung bool + _ = h.Tags[0].Extensions.GetOrZero("x-internal-tung").Decode(&tung) + assert.True(t, tung) wentLow := h.Tags[1].GoLow() assert.Equal(t, 39, wentLow.Description.KeyNode.Line) @@ -191,7 +208,10 @@ func TestNewDocument_Components_Callbacks(t *testing.T) { h.Components.Callbacks.GetOrZero("BurgerCallback").GoLow().FindExpression("{$request.query.queryUrl}").ValueNode.Column, ) - assert.Equal(t, "please", h.Components.Callbacks.GetOrZero("BurgerCallback").Extensions["x-break-everything"]) + var xBreakEverything string + _ = h.Components.Callbacks.GetOrZero("BurgerCallback").Extensions.GetOrZero("x-break-everything").Decode(&xBreakEverything) + + assert.Equal(t, "please", xBreakEverything) for pair := orderedmap.First(h.Components.GoLow().Callbacks.Value); pair != nil; pair = pair.Next() { if pair.Key().Value == "BurgerCallback" { @@ -209,7 +229,9 @@ func TestNewDocument_Components_Schemas(t *testing.T) { goLow := h.Components.GoLow() a := h.Components.Schemas.GetOrZero("Error") - abcd := a.Schema().Properties.GetOrZero("message").Schema().Example + + var abcd string + _ = a.Schema().Properties.GetOrZero("message").Schema().Example.Decode(&abcd) assert.Equal(t, "No such burger as 'Big-Whopper'", abcd) assert.Equal(t, 433, goLow.Schemas.KeyNode.Line) assert.Equal(t, 3, goLow.Schemas.KeyNode.Column) @@ -218,13 +240,21 @@ func TestNewDocument_Components_Schemas(t *testing.T) { b := h.Components.Schemas.GetOrZero("Burger") assert.Len(t, b.Schema().Required, 2) assert.Equal(t, "golden slices of happy fun joy", b.Schema().Properties.GetOrZero("fries").Schema().Description) - assert.Equal(t, int64(2), b.Schema().Properties.GetOrZero("numPatties").Schema().Example) + + var numPattiesExample int64 + _ = b.Schema().Properties.GetOrZero("numPatties").Schema().Example.Decode(&numPattiesExample) + + assert.Equal(t, int64(2), numPattiesExample) assert.Equal(t, 448, goLow.FindSchema("Burger").Value.Schema().Properties.KeyNode.Line) assert.Equal(t, 7, goLow.FindSchema("Burger").Value.Schema().Properties.KeyNode.Column) assert.Equal(t, 450, b.Schema().GoLow().FindProperty("name").ValueNode.Line) f := h.Components.Schemas.GetOrZero("Fries") - assert.Equal(t, "salt", f.Schema().Properties.GetOrZero("seasoning").Schema().Items.A.Schema().Example) + + var seasoningExample string + _ = f.Schema().Properties.GetOrZero("seasoning").Schema().Items.A.Schema().Example.Decode(&seasoningExample) + + assert.Equal(t, "salt", seasoningExample) assert.Len(t, f.Schema().Properties.GetOrZero("favoriteDrink").Schema().Properties.GetOrZero("drinkType").Schema().Enum, 1) d := h.Components.Schemas.GetOrZero("Drink") @@ -240,7 +270,11 @@ func TestNewDocument_Components_Schemas(t *testing.T) { assert.Equal(t, 523, pl.Schema().XML.GoLow().Name.ValueNode.Line) ext := h.Components.Extensions - assert.Equal(t, "loud", ext["x-screaming-baby"]) + + var xScreamingBaby string + _ = ext.GetOrZero("x-screaming-baby").Decode(&xScreamingBaby) + + assert.Equal(t, "loud", xScreamingBaby) } func TestNewDocument_Components_Headers(t *testing.T) { @@ -319,8 +353,12 @@ func TestNewDocument_Components_Parameters(t *testing.T) { assert.Equal(t, "burgerHeader", bh.Name) assert.Equal(t, 392, bh.GoLow().Name.KeyNode.Line) assert.Equal(t, 2, orderedmap.Len(bh.Schema.Schema().Properties)) - assert.Equal(t, "big-mac", bh.Example) - assert.True(t, bh.Required) + + var example string + _ = bh.Example.Decode(&example) + + assert.Equal(t, "big-mac", example) + assert.True(t, *bh.Required) assert.Equal( t, "this is a header", @@ -341,8 +379,12 @@ func TestNewDocument_Paths(t *testing.T) { func testBurgerShop(t *testing.T, h *Document, checkLines bool) { burgersOp := h.Paths.PathItems.GetOrZero("/burgers") - assert.Len(t, burgersOp.GetOperations(), 1) - assert.Equal(t, "meaty", burgersOp.Extensions["x-burger-meta"]) + assert.Equal(t, 1, burgersOp.GetOperations().Len()) + + var xBurgerMeta string + _ = burgersOp.Extensions.GetOrZero("x-burger-meta").Decode(&xBurgerMeta) + + assert.Equal(t, "meaty", xBurgerMeta) assert.Nil(t, burgersOp.Get) assert.Nil(t, burgersOp.Put) assert.Nil(t, burgersOp.Patch) diff --git a/datamodel/high/v3/encoding.go b/datamodel/high/v3/encoding.go index fd8d792..f7c417b 100644 --- a/datamodel/high/v3/encoding.go +++ b/datamodel/high/v3/encoding.go @@ -14,11 +14,11 @@ import ( // Encoding represents an OpenAPI 3+ Encoding object // - https://spec.openapis.org/oas/v3.1.0#encoding-object type Encoding struct { - ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` - Headers orderedmap.Map[string, *Header] `json:"headers,omitempty" yaml:"headers,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` + Headers *orderedmap.Map[string, *Header] `json:"headers,omitempty" yaml:"headers,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` low *low.Encoding } @@ -28,7 +28,9 @@ func NewEncoding(encoding *low.Encoding) *Encoding { e.low = encoding e.ContentType = encoding.ContentType.Value e.Style = encoding.Style.Value - e.Explode = &encoding.Explode.Value + if !encoding.Explode.IsEmpty() { + e.Explode = &encoding.Explode.Value + } e.AllowReserved = encoding.AllowReserved.Value e.Headers = ExtractHeaders(encoding.Headers.Value) return e @@ -56,7 +58,7 @@ func (e *Encoding) MarshalYAML() (interface{}, error) { } // ExtractEncoding converts hard to navigate low-level plumbing Encoding definitions, into a high-level simple map -func ExtractEncoding(elements orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Encoding]]) orderedmap.Map[string, *Encoding] { +func ExtractEncoding(elements *orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Encoding]]) *orderedmap.Map[string, *Encoding] { extracted := orderedmap.New[string, *Encoding]() for pair := orderedmap.First(elements); pair != nil; pair = pair.Next() { extracted.Set(pair.Key().Value, NewEncoding(pair.Value().Value)) diff --git a/datamodel/high/v3/header.go b/datamodel/high/v3/header.go index 39057c2..abe9381 100644 --- a/datamodel/high/v3/header.go +++ b/datamodel/high/v3/header.go @@ -16,18 +16,18 @@ import ( // Header represents a high-level OpenAPI 3+ Header object that is backed by a low-level one. // - https://spec.openapis.org/oas/v3.1.0#header-object type Header struct { - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` - Schema *highbase.SchemaProxy `json:"schema,omitempty" yaml:"schema,omitempty"` - Example any `json:"example,omitempty" yaml:"example,omitempty"` - Examples orderedmap.Map[string, *highbase.Example] `json:"examples,omitempty" yaml:"examples,omitempty"` - Content orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Schema *highbase.SchemaProxy `json:"schema,omitempty" yaml:"schema,omitempty"` + Example *yaml.Node `json:"example,omitempty" yaml:"example,omitempty"` + Examples *orderedmap.Map[string, *highbase.Example] `json:"examples,omitempty" yaml:"examples,omitempty"` + Content *orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Header } @@ -52,6 +52,7 @@ func NewHeader(header *low.Header) *Header { h.Content = ExtractContent(header.Content.Value) h.Example = header.Example.Value h.Examples = highbase.ExtractExamples(header.Examples.Value) + h.Extensions = high.ExtractExtensions(header.Extensions) return h } @@ -66,7 +67,7 @@ func (h *Header) GoLowUntyped() any { } // ExtractHeaders will extract a hard to navigate low-level Header map, into simple high-level one. -func ExtractHeaders(elements orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Header]]) orderedmap.Map[string, *Header] { +func ExtractHeaders(elements *orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Header]]) *orderedmap.Map[string, *Header] { extracted := orderedmap.New[string, *Header]() for pair := orderedmap.First(elements); pair != nil; pair = pair.Next() { extracted.Set(pair.Key().Value, NewHeader(pair.Value().Value)) diff --git a/datamodel/high/v3/header_test.go b/datamodel/high/v3/header_test.go index 13ff895..3d28471 100644 --- a/datamodel/high/v3/header_test.go +++ b/datamodel/high/v3/header_test.go @@ -9,10 +9,14 @@ import ( "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestHeader_MarshalYAML(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-burgers", utils.CreateStringNode("why not?")) header := &Header{ Description: "A header", @@ -22,11 +26,11 @@ func TestHeader_MarshalYAML(t *testing.T) { Style: "simple", Explode: true, AllowReserved: true, - Example: "example", + Example: utils.CreateStringNode("example"), Examples: orderedmap.ToOrderedMap(map[string]*base.Example{ - "example": {Value: "example"}, + "example": {Value: utils.CreateStringNode("example")}, }), - Extensions: map[string]interface{}{"x-burgers": "why not?"}, + Extensions: ext, } rend, _ := header.Render() @@ -45,5 +49,4 @@ examples: x-burgers: why not?` assert.Equal(t, desired, strings.TrimSpace(string(rend))) - } diff --git a/datamodel/high/v3/link.go b/datamodel/high/v3/link.go index 07917d7..48f4a85 100644 --- a/datamodel/high/v3/link.go +++ b/datamodel/high/v3/link.go @@ -23,13 +23,13 @@ import ( // in an operation and using them as parameters while invoking the linked operation. // - https://spec.openapis.org/oas/v3.1.0#link-object type Link struct { - OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` - OperationId string `json:"operationId,omitempty" yaml:"operationId,omitempty"` - Parameters orderedmap.Map[string, string] `json:"parameters,omitempty" yaml:"parameters,omitempty"` - RequestBody string `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Server *Server `json:"server,omitempty" yaml:"server,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` + OperationId string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Parameters *orderedmap.Map[string, string] `json:"parameters,omitempty" yaml:"parameters,omitempty"` + RequestBody string `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Server *Server `json:"server,omitempty" yaml:"server,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Link } diff --git a/datamodel/high/v3/media_type.go b/datamodel/high/v3/media_type.go index c55af36..9ac4599 100644 --- a/datamodel/high/v3/media_type.go +++ b/datamodel/high/v3/media_type.go @@ -4,6 +4,7 @@ package v3 import ( + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/high/base" lowmodel "github.com/pb33f/libopenapi/datamodel/low" @@ -17,11 +18,11 @@ import ( // Each Media Type Object provides schema and examples for the media type identified by its key. // - https://spec.openapis.org/oas/v3.1.0#media-type-object type MediaType struct { - Schema *base.SchemaProxy `json:"schema,omitempty" yaml:"schema,omitempty"` - Example any `json:"example,omitempty" yaml:"example,omitempty"` - Examples orderedmap.Map[string, *base.Example] `json:"examples,omitempty" yaml:"examples,omitempty"` - Encoding orderedmap.Map[string, *Encoding] `json:"encoding,omitempty" yaml:"encoding,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Schema *base.SchemaProxy `json:"schema,omitempty" yaml:"schema,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + Examples *orderedmap.Map[string, *base.Example] `json:"examples,omitempty" yaml:"examples,omitempty"` + Encoding *orderedmap.Map[string, *Encoding] `json:"encoding,omitempty" yaml:"encoding,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.MediaType } @@ -73,7 +74,7 @@ func (m *MediaType) MarshalYAMLInline() (interface{}, error) { // ExtractContent takes in a complex and hard to navigate low-level content map, and converts it in to a much simpler // and easier to navigate high-level one. -func ExtractContent(elements orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.MediaType]]) orderedmap.Map[string, *MediaType] { +func ExtractContent(elements *orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.MediaType]]) *orderedmap.Map[string, *MediaType] { extracted := orderedmap.New[string, *MediaType]() translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.MediaType]]) (asyncResult[*MediaType], error) { return asyncResult[*MediaType]{ @@ -85,6 +86,6 @@ func ExtractContent(elements orderedmap.Map[lowmodel.KeyReference[string], lowmo extracted.Set(value.key, value.result) return nil } - _ = orderedmap.TranslateMapParallel(elements, translateFunc, resultFunc) + _ = datamodel.TranslateMapParallel(elements, translateFunc, resultFunc) return extracted } diff --git a/datamodel/high/v3/media_type_test.go b/datamodel/high/v3/media_type_test.go index 3e8f91c..15ca0f6 100644 --- a/datamodel/high/v3/media_type_test.go +++ b/datamodel/high/v3/media_type_test.go @@ -35,8 +35,7 @@ func TestMediaType_MarshalYAMLInline(t *testing.T) { yml, _ := mt.Render() // the rendered output should be a ref to the media type. - op := `schema: - $ref: '#/components/schemas/Pet'` + op := `schema: {"$ref": "#/components/schemas/Pet"}` assert.Equal(t, op, strings.TrimSpace(string(yml))) @@ -45,30 +44,30 @@ func TestMediaType_MarshalYAMLInline(t *testing.T) { op = `schema: required: - - name - - photoUrls + - "name" + - "photoUrls" type: "object" properties: - id: + "id": type: "integer" format: "int64" example: 10 - name: + "name": type: "string" example: "doggie" - category: + "category": type: "object" properties: - id: + "id": type: "integer" format: "int64" example: 1 - name: + "name": type: "string" example: "Dogs" xml: name: "category" - photoUrls: + "photoUrls": type: "array" xml: wrapped: true @@ -76,27 +75,27 @@ func TestMediaType_MarshalYAMLInline(t *testing.T) { type: "string" xml: name: "photoUrl" - tags: + "tags": type: "array" xml: wrapped: true items: type: "object" properties: - id: + "id": type: "integer" format: "int64" - name: + "name": type: "string" xml: name: "tag" - status: + "status": type: "string" description: "pet status in the store" enum: - - available - - pending - - sold + - "available" + - "pending" + - "sold" xml: name: "pet" example: testing a nice mutation` @@ -104,7 +103,6 @@ example: testing a nice mutation` yml, _ = mt.RenderInline() assert.Equal(t, op, strings.TrimSpace(string(yml))) - } func TestMediaType_MarshalYAML(t *testing.T) { @@ -125,22 +123,19 @@ func TestMediaType_MarshalYAML(t *testing.T) { yml, _ := mt.Render() // the rendered output should be a ref to the media type. - op := `schema: - $ref: '#/components/schemas/Pet'` + op := `schema: {"$ref": "#/components/schemas/Pet"}` assert.Equal(t, op, strings.TrimSpace(string(yml))) // modify the media type to have an example mt.Example = "testing a nice mutation" - op = `schema: - $ref: '#/components/schemas/Pet' + op = `schema: {"$ref": "#/components/schemas/Pet"} example: testing a nice mutation` yml, _ = mt.Render() assert.Equal(t, op, strings.TrimSpace(string(yml))) - } func TestMediaType_Examples(t *testing.T) { diff --git a/datamodel/high/v3/oauth_flow.go b/datamodel/high/v3/oauth_flow.go index 432700c..ab0233e 100644 --- a/datamodel/high/v3/oauth_flow.go +++ b/datamodel/high/v3/oauth_flow.go @@ -13,11 +13,11 @@ import ( // OAuthFlow represents a high-level OpenAPI 3+ OAuthFlow object that is backed by a low-level one. // - https://spec.openapis.org/oas/v3.1.0#oauth-flow-object type OAuthFlow struct { - AuthorizationUrl string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenUrl string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshUrl string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes orderedmap.Map[string, string] `json:"scopes,omitempty" yaml:"scopes,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + AuthorizationUrl string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenUrl string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + RefreshUrl string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes *orderedmap.Map[string, string] `json:"scopes,omitempty" yaml:"scopes,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.OAuthFlow } @@ -29,7 +29,7 @@ func NewOAuthFlow(flow *low.OAuthFlow) *OAuthFlow { o.AuthorizationUrl = flow.AuthorizationUrl.Value o.RefreshUrl = flow.RefreshUrl.Value scopes := orderedmap.New[string, string]() - for pair := flow.Scopes.Value.First(); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(flow.Scopes.Value); pair != nil; pair = pair.Next() { scopes.Set(pair.Key().Value, pair.Value().Value) } o.Scopes = scopes diff --git a/datamodel/high/v3/oauth_flow_test.go b/datamodel/high/v3/oauth_flow_test.go index 36ed971..ea112ba 100644 --- a/datamodel/high/v3/oauth_flow_test.go +++ b/datamodel/high/v3/oauth_flow_test.go @@ -8,7 +8,9 @@ import ( "testing" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestOAuthFlow_MarshalYAML(t *testing.T) { @@ -37,7 +39,9 @@ scopes: // mutate oflow.Scopes = nil - oflow.Extensions = map[string]interface{}{"x-burgers": "why not?"} + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-burgers", utils.CreateStringNode("why not?")) + oflow.Extensions = ext desired = `authorizationUrl: https://pb33f.io tokenUrl: https://pb33f.io/token diff --git a/datamodel/high/v3/oauth_flows.go b/datamodel/high/v3/oauth_flows.go index f86ac35..f9b4864 100644 --- a/datamodel/high/v3/oauth_flows.go +++ b/datamodel/high/v3/oauth_flows.go @@ -6,17 +6,18 @@ package v3 import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" ) // OAuthFlows represents a high-level OpenAPI 3+ OAuthFlows object that is backed by a low-level one. // - https://spec.openapis.org/oas/v3.1.0#oauth-flows-object type OAuthFlows struct { - Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` - Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` - ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` - AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` + Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` + ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` + AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.OAuthFlows } diff --git a/datamodel/high/v3/operation.go b/datamodel/high/v3/operation.go index b12738e..0bc76ce 100644 --- a/datamodel/high/v3/operation.go +++ b/datamodel/high/v3/operation.go @@ -17,19 +17,19 @@ import ( // happens here. The entire being for existence of this library and the specification, is this Operation. // - https://spec.openapis.org/oas/v3.1.0#operation-object type Operation struct { - Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - ExternalDocs *base.ExternalDoc `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - OperationId string `json:"operationId,omitempty" yaml:"operationId,omitempty"` - Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` - RequestBody *RequestBody `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` - Responses *Responses `json:"responses,omitempty" yaml:"responses,omitempty"` - Callbacks orderedmap.Map[string, *Callback] `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` - Deprecated *bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Security []*base.SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"` - Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + ExternalDocs *base.ExternalDoc `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + OperationId string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + RequestBody *RequestBody `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` + Responses *Responses `json:"responses,omitempty" yaml:"responses,omitempty"` + Callbacks *orderedmap.Map[string, *Callback] `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Deprecated *bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Security []*base.SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"` + Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Operation } @@ -45,7 +45,9 @@ func NewOperation(operation *low.Operation) *Operation { } o.Tags = tags o.Summary = operation.Summary.Value - o.Deprecated = &operation.Deprecated.Value + if !operation.Deprecated.IsEmpty() { + o.Deprecated = &operation.Deprecated.Value + } o.Description = operation.Description.Value if !operation.ExternalDocs.IsEmpty() { o.ExternalDocs = base.NewExternalDoc(operation.ExternalDocs.Value) diff --git a/datamodel/high/v3/operation_test.go b/datamodel/high/v3/operation_test.go index 13c98db..7230846 100644 --- a/datamodel/high/v3/operation_test.go +++ b/datamodel/high/v3/operation_test.go @@ -50,13 +50,13 @@ callbacks: assert.Equal(t, "https://pb33f.io", r.ExternalDocs.URL) assert.Equal(t, 1, r.GoLow().ExternalDocs.KeyNode.Line) - assert.Contains(t, r.Callbacks, "testCallback") - assert.Contains(t, r.Callbacks.GetOrZero("testCallback").Expression, "{$request.body#/callbackUrl}") + + assert.NotNil(t, r.Callbacks.GetOrZero("testCallback")) + assert.NotNil(t, r.Callbacks.GetOrZero("testCallback").Expression.GetOrZero("{$request.body#/callbackUrl}")) assert.Equal(t, 3, r.GoLow().Callbacks.KeyNode.Line) } func TestOperation_MarshalYAML(t *testing.T) { - op := &Operation{ Tags: []string{"test"}, Summary: "nice", @@ -90,11 +90,9 @@ requestBody: description: dice` assert.Equal(t, desired, strings.TrimSpace(string(rend))) - } func TestOperation_MarshalYAMLInline(t *testing.T) { - op := &Operation{ Tags: []string{"test"}, Summary: "nice", @@ -128,7 +126,6 @@ requestBody: description: dice` assert.Equal(t, desired, strings.TrimSpace(string(rend))) - } func TestOperation_EmptySecurity(t *testing.T) { @@ -147,7 +144,6 @@ security: []` assert.NotNil(t, r.Security) assert.Len(t, r.Security, 0) - } func TestOperation_NoSecurity(t *testing.T) { @@ -164,5 +160,4 @@ func TestOperation_NoSecurity(t *testing.T) { r := NewOperation(&n) assert.Nil(t, r.Security) - } diff --git a/datamodel/high/v3/parameter.go b/datamodel/high/v3/parameter.go index ed85fbf..fd863a7 100644 --- a/datamodel/high/v3/parameter.go +++ b/datamodel/high/v3/parameter.go @@ -16,20 +16,20 @@ import ( // A unique parameter is defined by a combination of a name and location. // - https://spec.openapis.org/oas/v3.1.0#parameter-object type Parameter struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` - Schema *base.SchemaProxy `json:"schema,omitempty" yaml:"schema,omitempty"` - Example any `json:"example,omitempty" yaml:"example,omitempty"` - Examples orderedmap.Map[string, *base.Example] `json:"examples,omitempty" yaml:"examples,omitempty"` - Content orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Required *bool `json:"required,renderZero,omitempty" yaml:"required,renderZero,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode *bool `json:"explode,renderZero,omitempty" yaml:"explode,renderZero,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Schema *base.SchemaProxy `json:"schema,omitempty" yaml:"schema,omitempty"` + Example *yaml.Node `json:"example,omitempty" yaml:"example,omitempty"` + Examples *orderedmap.Map[string, *base.Example] `json:"examples,omitempty" yaml:"examples,omitempty"` + Content *orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Parameter } @@ -50,7 +50,9 @@ func NewParameter(param *low.Parameter) *Parameter { if !param.Schema.IsEmpty() { p.Schema = base.NewSchemaProxy(¶m.Schema) } - p.Required = param.Required.Value + if !param.Required.IsEmpty() { + p.Required = ¶m.Required.Value + } p.Example = param.Example.Value p.Examples = base.ExtractExamples(param.Examples.Value) p.Content = ExtractContent(param.Content.Value) diff --git a/datamodel/high/v3/parameter_test.go b/datamodel/high/v3/parameter_test.go index 6fe1145..df7b51f 100644 --- a/datamodel/high/v3/parameter_test.go +++ b/datamodel/high/v3/parameter_test.go @@ -9,10 +9,14 @@ import ( "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestParameter_MarshalYAML(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-burgers", utils.CreateStringNode("why not?")) explode := true param := Parameter{ @@ -23,11 +27,11 @@ func TestParameter_MarshalYAML(t *testing.T) { Style: "simple", Explode: &explode, AllowReserved: true, - Example: "example", + Example: utils.CreateStringNode("example"), Examples: orderedmap.ToOrderedMap(map[string]*base.Example{ - "example": {Value: "example"}, + "example": {Value: utils.CreateStringNode("example")}, }), - Extensions: map[string]interface{}{"x-burgers": "why not?"}, + Extensions: ext, } rend, _ := param.Render() @@ -49,6 +53,8 @@ x-burgers: why not?` } func TestParameter_MarshalYAMLInline(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-burgers", utils.CreateStringNode("why not?")) explode := true param := Parameter{ @@ -59,11 +65,11 @@ func TestParameter_MarshalYAMLInline(t *testing.T) { Style: "simple", Explode: &explode, AllowReserved: true, - Example: "example", + Example: utils.CreateStringNode("example"), Examples: orderedmap.ToOrderedMap(map[string]*base.Example{ - "example": {Value: "example"}, + "example": {Value: utils.CreateStringNode("example")}, }), - Extensions: map[string]interface{}{"x-burgers": "why not?"}, + Extensions: ext, } rend, _ := param.RenderInline() @@ -85,7 +91,6 @@ x-burgers: why not?` } func TestParameter_IsExploded(t *testing.T) { - explode := true param := Parameter{ Explode: &explode, @@ -106,7 +111,6 @@ func TestParameter_IsExploded(t *testing.T) { } func TestParameter_IsDefaultFormEncoding(t *testing.T) { - param := Parameter{} assert.True(t, param.IsDefaultFormEncoding()) @@ -133,7 +137,6 @@ func TestParameter_IsDefaultFormEncoding(t *testing.T) { } func TestParameter_IsDefaultHeaderEncoding(t *testing.T) { - param := Parameter{} assert.True(t, param.IsDefaultHeaderEncoding()) @@ -163,8 +166,6 @@ func TestParameter_IsDefaultHeaderEncoding(t *testing.T) { } func TestParameter_IsDefaultPathEncoding(t *testing.T) { - param := Parameter{} assert.True(t, param.IsDefaultPathEncoding()) - } diff --git a/datamodel/high/v3/path_item.go b/datamodel/high/v3/path_item.go index bd51cae..df8b4df 100644 --- a/datamodel/high/v3/path_item.go +++ b/datamodel/high/v3/path_item.go @@ -4,8 +4,13 @@ package v3 import ( + "reflect" + "slices" + "github.com/pb33f/libopenapi/datamodel/high" - low "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/datamodel/low" + lowV3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" ) @@ -27,24 +32,24 @@ const ( // are available. // - https://spec.openapis.org/oas/v3.1.0#path-item-object type PathItem struct { - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` - Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` - Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` - Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` - Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` - Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` - Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` - Trace *Operation `json:"trace,omitempty" yaml:"trace,omitempty"` - Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` - Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` - low *low.PathItem + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` + Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` + Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` + Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` + Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` + Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` + Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` + Trace *Operation `json:"trace,omitempty" yaml:"trace,omitempty"` + Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"` + Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` + low *lowV3.PathItem } // NewPathItem creates a new high-level PathItem instance from a low-level one. -func NewPathItem(pathItem *low.PathItem) *PathItem { +func NewPathItem(pathItem *lowV3.PathItem) *PathItem { pi := new(PathItem) pi.low = pathItem pi.Description = pathItem.Description.Value @@ -62,7 +67,7 @@ func NewPathItem(pathItem *low.PathItem) *PathItem { op *Operation } opChan := make(chan opResult) - var buildOperation = func(method int, op *low.Operation, c chan opResult) { + buildOperation := func(method int, op *lowV3.Operation, c chan opResult) { if op == nil { c <- opResult{method: method, op: nil} return @@ -120,7 +125,7 @@ func NewPathItem(pathItem *low.PathItem) *PathItem { } // GoLow returns the low level instance of PathItem, used to build the high-level one. -func (p *PathItem) GoLow() *low.PathItem { +func (p *PathItem) GoLow() *lowV3.PathItem { return p.low } @@ -129,32 +134,65 @@ func (p *PathItem) GoLowUntyped() any { return p.low } -func (p *PathItem) GetOperations() map[string]*Operation { - o := make(map[string]*Operation) +func (p *PathItem) GetOperations() *orderedmap.Map[string, *Operation] { + o := orderedmap.New[string, *Operation]() + + // TODO: this is a bit of a hack, but it works for now. We might just want to actually pull the data out of the document as a map and split it into the individual operations + + type op struct { + name string + op *Operation + line int + } + + getLine := func(field string, idx int) int { + if p.GoLow() == nil { + return idx + } + + l, ok := reflect.ValueOf(p.GoLow()).Elem().FieldByName(field).Interface().(low.NodeReference[*lowV3.Operation]) + if !ok || l.GetKeyNode() == nil { + return idx + } + + return l.GetKeyNode().Line + } + + ops := []op{} + if p.Get != nil { - o[low.GetLabel] = p.Get + ops = append(ops, op{name: lowV3.GetLabel, op: p.Get, line: getLine("Get", -8)}) } if p.Put != nil { - o[low.PutLabel] = p.Put + ops = append(ops, op{name: lowV3.PutLabel, op: p.Put, line: getLine("Put", -7)}) } if p.Post != nil { - o[low.PostLabel] = p.Post + ops = append(ops, op{name: lowV3.PostLabel, op: p.Post, line: getLine("Post", -6)}) } if p.Delete != nil { - o[low.DeleteLabel] = p.Delete + ops = append(ops, op{name: lowV3.DeleteLabel, op: p.Delete, line: getLine("Delete", -5)}) } if p.Options != nil { - o[low.OptionsLabel] = p.Options + ops = append(ops, op{name: lowV3.OptionsLabel, op: p.Options, line: getLine("Options", -4)}) } if p.Head != nil { - o[low.HeadLabel] = p.Head + ops = append(ops, op{name: lowV3.HeadLabel, op: p.Head, line: getLine("Head", -3)}) } if p.Patch != nil { - o[low.PatchLabel] = p.Patch + ops = append(ops, op{name: lowV3.PatchLabel, op: p.Patch, line: getLine("Patch", -2)}) } if p.Trace != nil { - o[low.TraceLabel] = p.Trace + ops = append(ops, op{name: lowV3.TraceLabel, op: p.Trace, line: getLine("Trace", -1)}) } + + slices.SortStableFunc(ops, func(a op, b op) int { + return a.line - b.line + }) + + for _, op := range ops { + o.Set(op.name, op.op) + } + return o } diff --git a/datamodel/high/v3/path_item_test.go b/datamodel/high/v3/path_item_test.go index d70a3ba..af4cd28 100644 --- a/datamodel/high/v3/path_item_test.go +++ b/datamodel/high/v3/path_item_test.go @@ -67,11 +67,19 @@ trace: r := NewPathItem(&n) - assert.Len(t, r.GetOperations(), 8) + assert.Equal(t, 8, r.GetOperations().Len()) + + // test that the operations are in the correct order + expectedOrder := []string{"get", "put", "post", "patch", "delete", "head", "options", "trace"} + + i := 0 + for pair := r.GetOperations().First(); pair != nil; pair = pair.Next() { + assert.Equal(t, expectedOrder[i], pair.Value().Description) + i++ + } } func TestPathItem_MarshalYAML(t *testing.T) { - pi := &PathItem{ Description: "a path item", Summary: "It's a test, don't worry about it, Jim", @@ -112,7 +120,6 @@ parameters: } func TestPathItem_MarshalYAMLInline(t *testing.T) { - pi := &PathItem{ Description: "a path item", Summary: "It's a test, don't worry about it, Jim", diff --git a/datamodel/high/v3/paths.go b/datamodel/high/v3/paths.go index 08156cc..460723e 100644 --- a/datamodel/high/v3/paths.go +++ b/datamodel/high/v3/paths.go @@ -22,8 +22,8 @@ import ( // constraints. // - https://spec.openapis.org/oas/v3.1.0#paths-object type Paths struct { - PathItems orderedmap.Map[string, *PathItem] `json:"-" yaml:"-"` - Extensions map[string]any `json:"-" yaml:"-"` + PathItems *orderedmap.Map[string, *PathItem] `json:"-" yaml:"-"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *v3low.Paths } @@ -81,6 +81,7 @@ func (p *Paths) MarshalYAML() (interface{}, error) { pi *PathItem path string line int + style yaml.Style rendered *yaml.Node } var mapped []*pathItem @@ -89,13 +90,21 @@ func (p *Paths) MarshalYAML() (interface{}, error) { k := pair.Key() pi := pair.Value() ln := 9999 // default to a high value to weight new content to the bottom. + var style yaml.Style if p.low != nil { lpi := p.low.FindPath(k) if lpi != nil { ln = lpi.ValueNode.Line } + + for pair := orderedmap.First(p.low.PathItems); pair != nil; pair = pair.Next() { + if pair.Key().Value == k { + style = pair.Key().KeyNode.Style + break + } + } } - mapped = append(mapped, &pathItem{pi, k, ln, nil}) + mapped = append(mapped, &pathItem{pi, k, ln, style, nil}) } nb := high.NewNodeBuilder(p, p.low) @@ -107,23 +116,29 @@ func (p *Paths) MarshalYAML() (interface{}, error) { label = extNode.Content[u].Value continue } - mapped = append(mapped, &pathItem{nil, label, - extNode.Content[u].Line, extNode.Content[u]}) + mapped = append(mapped, &pathItem{ + nil, label, + extNode.Content[u].Line, 0, extNode.Content[u], + }) } } sort.Slice(mapped, func(i, j int) bool { return mapped[i].line < mapped[j].line }) - for j := range mapped { - if mapped[j].pi != nil { - rendered, _ := mapped[j].pi.MarshalYAML() - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].path)) + for _, mp := range mapped { + if mp.pi != nil { + rendered, _ := mp.pi.MarshalYAML() + + kn := utils.CreateStringNode(mp.path) + kn.Style = mp.style + + m.Content = append(m.Content, kn) m.Content = append(m.Content, rendered.(*yaml.Node)) } - if mapped[j].rendered != nil { - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].path)) - m.Content = append(m.Content, mapped[j].rendered) + if mp.rendered != nil { + m.Content = append(m.Content, utils.CreateStringNode(mp.path)) + m.Content = append(m.Content, mp.rendered) } } @@ -137,6 +152,7 @@ func (p *Paths) MarshalYAMLInline() (interface{}, error) { pi *PathItem path string line int + style yaml.Style rendered *yaml.Node } var mapped []*pathItem @@ -145,13 +161,21 @@ func (p *Paths) MarshalYAMLInline() (interface{}, error) { k := pair.Key() pi := pair.Value() ln := 9999 // default to a high value to weight new content to the bottom. + var style yaml.Style if p.low != nil { lpi := p.low.FindPath(k) if lpi != nil { ln = lpi.ValueNode.Line } + + for pair := orderedmap.First(p.low.PathItems); pair != nil; pair = pair.Next() { + if pair.Key().Value == k { + style = pair.Key().KeyNode.Style + break + } + } } - mapped = append(mapped, &pathItem{pi, k, ln, nil}) + mapped = append(mapped, &pathItem{pi, k, ln, style, nil}) } nb := high.NewNodeBuilder(p, p.low) @@ -164,23 +188,29 @@ func (p *Paths) MarshalYAMLInline() (interface{}, error) { label = extNode.Content[u].Value continue } - mapped = append(mapped, &pathItem{nil, label, - extNode.Content[u].Line, extNode.Content[u]}) + mapped = append(mapped, &pathItem{ + nil, label, + extNode.Content[u].Line, 0, extNode.Content[u], + }) } } sort.Slice(mapped, func(i, j int) bool { return mapped[i].line < mapped[j].line }) - for j := range mapped { - if mapped[j].pi != nil { - rendered, _ := mapped[j].pi.MarshalYAMLInline() - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].path)) + for _, mp := range mapped { + if mp.pi != nil { + rendered, _ := mp.pi.MarshalYAMLInline() + + kn := utils.CreateStringNode(mp.path) + kn.Style = mp.style + + m.Content = append(m.Content, kn) m.Content = append(m.Content, rendered.(*yaml.Node)) } - if mapped[j].rendered != nil { - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].path)) - m.Content = append(m.Content, mapped[j].rendered) + if mp.rendered != nil { + m.Content = append(m.Content, utils.CreateStringNode(mp.path)) + m.Content = append(m.Content, mp.rendered) } } diff --git a/datamodel/high/v3/request_body.go b/datamodel/high/v3/request_body.go index 85c7421..7bc4d89 100644 --- a/datamodel/high/v3/request_body.go +++ b/datamodel/high/v3/request_body.go @@ -13,10 +13,10 @@ import ( // RequestBody represents a high-level OpenAPI 3+ RequestBody object, backed by a low-level one. // - https://spec.openapis.org/oas/v3.1.0#request-body-object type RequestBody struct { - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Content orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` - Required *bool `json:"required,omitempty" yaml:"required,renderZero,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Content *orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` + Required *bool `json:"required,omitempty" yaml:"required,renderZero,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.RequestBody } @@ -25,7 +25,7 @@ func NewRequestBody(rb *low.RequestBody) *RequestBody { r := new(RequestBody) r.low = rb r.Description = rb.Description.Value - if rb.Required.ValueNode != nil { + if !rb.Required.IsEmpty() { r.Required = &rb.Required.Value } r.Extensions = high.ExtractExtensions(rb.Extensions) diff --git a/datamodel/high/v3/request_body_test.go b/datamodel/high/v3/request_body_test.go index d9fb8f4..628ebdf 100644 --- a/datamodel/high/v3/request_body_test.go +++ b/datamodel/high/v3/request_body_test.go @@ -7,16 +7,21 @@ import ( "strings" "testing" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestRequestBody_MarshalYAML(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-high-gravity", utils.CreateStringNode("why not?")) rb := true req := &RequestBody{ Description: "beer", Required: &rb, - Extensions: map[string]interface{}{"x-high-gravity": "why not?"}, + Extensions: ext, } rend, _ := req.Render() @@ -26,16 +31,17 @@ required: true x-high-gravity: why not?` assert.Equal(t, desired, strings.TrimSpace(string(rend))) - } func TestRequestBody_MarshalYAMLInline(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-high-gravity", utils.CreateStringNode("why not?")) rb := true req := &RequestBody{ Description: "beer", Required: &rb, - Extensions: map[string]interface{}{"x-high-gravity": "why not?"}, + Extensions: ext, } rend, _ := req.RenderInline() @@ -45,15 +51,17 @@ required: true x-high-gravity: why not?` assert.Equal(t, desired, strings.TrimSpace(string(rend))) - } func TestRequestBody_MarshalNoRequired(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-high-gravity", utils.CreateStringNode("why not?")) + rb := false req := &RequestBody{ Description: "beer", Required: &rb, - Extensions: map[string]interface{}{"x-high-gravity": "why not?"}, + Extensions: ext, } rend, _ := req.Render() @@ -63,14 +71,15 @@ required: false x-high-gravity: why not?` assert.Equal(t, desired, strings.TrimSpace(string(rend))) - } func TestRequestBody_MarshalRequiredNil(t *testing.T) { + ext := orderedmap.New[string, *yaml.Node]() + ext.Set("x-high-gravity", utils.CreateStringNode("why not?")) req := &RequestBody{ Description: "beer", - Extensions: map[string]interface{}{"x-high-gravity": "why not?"}, + Extensions: ext, } rend, _ := req.Render() @@ -79,5 +88,4 @@ func TestRequestBody_MarshalRequiredNil(t *testing.T) { x-high-gravity: why not?` assert.Equal(t, desired, strings.TrimSpace(string(rend))) - } diff --git a/datamodel/high/v3/response.go b/datamodel/high/v3/response.go index 0cb890a..49b7da3 100644 --- a/datamodel/high/v3/response.go +++ b/datamodel/high/v3/response.go @@ -16,11 +16,11 @@ import ( // operations based on the response. // - https://spec.openapis.org/oas/v3.1.0#response-object type Response struct { - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Headers orderedmap.Map[string, *Header] `json:"headers,omitempty" yaml:"headers,omitempty"` - Content orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` - Links orderedmap.Map[string, *Link] `json:"links,omitempty" yaml:"links,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Headers *orderedmap.Map[string, *Header] `json:"headers,omitempty" yaml:"headers,omitempty"` + Content *orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` + Links *orderedmap.Map[string, *Link] `json:"links,omitempty" yaml:"links,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Response } diff --git a/datamodel/high/v3/response_test.go b/datamodel/high/v3/response_test.go index 42e9950..823d6ed 100644 --- a/datamodel/high/v3/response_test.go +++ b/datamodel/high/v3/response_test.go @@ -21,7 +21,6 @@ import ( // with hard coded line and column numbers in them, changing the spec above the bottom will // create pointless test changes. So here is a standalone test. you know... for science. func TestNewResponse(t *testing.T) { - yml := `description: this is a response headers: someHeader: @@ -46,14 +45,16 @@ links: assert.Equal(t, 1, orderedmap.Len(r.Headers)) assert.Equal(t, 1, orderedmap.Len(r.Content)) - assert.Equal(t, "pizza!", r.Extensions["x-pizza-man"]) + + var xPizzaMan string + _ = r.Extensions.GetOrZero("x-pizza-man").Decode(&xPizzaMan) + + assert.Equal(t, "pizza!", xPizzaMan) assert.Equal(t, 1, orderedmap.Len(r.Links)) assert.Equal(t, 1, r.GoLow().Description.KeyNode.Line) - } func TestResponse_MarshalYAML(t *testing.T) { - yml := `description: this is a response headers: someHeader: @@ -77,11 +78,9 @@ links: rend, _ := r.Render() assert.Equal(t, yml, strings.TrimSpace(string(rend))) - } func TestResponse_MarshalYAMLInline(t *testing.T) { - yml := `description: this is a response headers: someHeader: @@ -105,5 +104,4 @@ links: rend, _ := r.RenderInline() assert.Equal(t, yml, strings.TrimSpace(string(rend))) - } diff --git a/datamodel/high/v3/responses.go b/datamodel/high/v3/responses.go index e6bc695..0953949 100644 --- a/datamodel/high/v3/responses.go +++ b/datamodel/high/v3/responses.go @@ -31,9 +31,9 @@ import ( // be the response for a successful operation call. // - https://spec.openapis.org/oas/v3.1.0#responses-object type Responses struct { - Codes orderedmap.Map[string, *Response] `json:"-" yaml:"-"` - Default *Response `json:"default,omitempty" yaml:"default,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Codes *orderedmap.Map[string, *Response] `json:"-" yaml:"-"` + Default *Response `json:"default,omitempty" yaml:"default,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Responses } @@ -93,23 +93,26 @@ func (r *Responses) MarshalYAML() (interface{}, error) { // map keys correctly. m := utils.CreateEmptyMapNode() type responseItem struct { - resp *Response - code string - line int - ext *yaml.Node + resp *Response + code string + line int + ext *yaml.Node + style yaml.Style } var mapped []*responseItem for pair := orderedmap.First(r.Codes); pair != nil; pair = pair.Next() { ln := 9999 // default to a high value to weight new content to the bottom. + var style yaml.Style if r.low != nil { for lPair := orderedmap.First(r.low.Codes); lPair != nil; lPair = lPair.Next() { if lPair.Key().Value == pair.Key() { ln = lPair.Key().KeyNode.Line + style = lPair.Key().KeyNode.Style } } } - mapped = append(mapped, &responseItem{pair.Value(), pair.Key(), ln, nil}) + mapped = append(mapped, &responseItem{pair.Value(), pair.Key(), ln, nil, style}) } // extract extensions @@ -124,7 +127,7 @@ func (r *Responses) MarshalYAML() (interface{}, error) { } mapped = append(mapped, &responseItem{ nil, label, - extNode.Content[u].Line, extNode.Content[u], + extNode.Content[u].Line, extNode.Content[u], 0, }) } } @@ -132,15 +135,19 @@ func (r *Responses) MarshalYAML() (interface{}, error) { sort.Slice(mapped, func(i, j int) bool { return mapped[i].line < mapped[j].line }) - for j := range mapped { - if mapped[j].resp != nil { - rendered, _ := mapped[j].resp.MarshalYAML() - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].code)) + for _, mp := range mapped { + if mp.resp != nil { + rendered, _ := mp.resp.MarshalYAML() + + kn := utils.CreateStringNode(mp.code) + kn.Style = mp.style + + m.Content = append(m.Content, kn) m.Content = append(m.Content, rendered.(*yaml.Node)) } - if mapped[j].ext != nil { - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].code)) - m.Content = append(m.Content, mapped[j].ext) + if mp.ext != nil { + m.Content = append(m.Content, utils.CreateStringNode(mp.code)) + m.Content = append(m.Content, mp.ext) } } @@ -151,23 +158,26 @@ func (r *Responses) MarshalYAMLInline() (interface{}, error) { // map keys correctly. m := utils.CreateEmptyMapNode() type responseItem struct { - resp *Response - code string - line int - ext *yaml.Node + resp *Response + code string + line int + ext *yaml.Node + style yaml.Style } var mapped []*responseItem for pair := orderedmap.First(r.Codes); pair != nil; pair = pair.Next() { ln := 9999 // default to a high value to weight new content to the bottom. + var style yaml.Style if r.low != nil { for lPair := orderedmap.First(r.low.Codes); lPair != nil; lPair = lPair.Next() { if lPair.Key().Value == pair.Key() { ln = lPair.Key().KeyNode.Line + style = lPair.Key().KeyNode.Style } } } - mapped = append(mapped, &responseItem{pair.Value(), pair.Key(), ln, nil}) + mapped = append(mapped, &responseItem{pair.Value(), pair.Key(), ln, nil, style}) } // extract extensions @@ -183,7 +193,7 @@ func (r *Responses) MarshalYAMLInline() (interface{}, error) { } mapped = append(mapped, &responseItem{ nil, label, - extNode.Content[u].Line, extNode.Content[u], + extNode.Content[u].Line, extNode.Content[u], 0, }) } } @@ -191,16 +201,20 @@ func (r *Responses) MarshalYAMLInline() (interface{}, error) { sort.Slice(mapped, func(i, j int) bool { return mapped[i].line < mapped[j].line }) - for j := range mapped { - if mapped[j].resp != nil { - rendered, _ := mapped[j].resp.MarshalYAMLInline() - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].code)) + for _, mp := range mapped { + if mp.resp != nil { + rendered, _ := mp.resp.MarshalYAMLInline() + + kn := utils.CreateStringNode(mp.code) + kn.Style = mp.style + + m.Content = append(m.Content, kn) m.Content = append(m.Content, rendered.(*yaml.Node)) } - if mapped[j].ext != nil { - m.Content = append(m.Content, utils.CreateStringNode(mapped[j].code)) - m.Content = append(m.Content, mapped[j].ext) + if mp.ext != nil { + m.Content = append(m.Content, utils.CreateStringNode(mp.code)) + m.Content = append(m.Content, mp.ext) } } diff --git a/datamodel/high/v3/security_scheme.go b/datamodel/high/v3/security_scheme.go index 60fbfd6..a289a9b 100644 --- a/datamodel/high/v3/security_scheme.go +++ b/datamodel/high/v3/security_scheme.go @@ -6,6 +6,7 @@ package v3 import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" ) @@ -20,15 +21,15 @@ import ( // Recommended for most use case is Authorization Code Grant flow with PKCE. // - https://spec.openapis.org/oas/v3.1.0#security-scheme-object type SecurityScheme struct { - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` - BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` - Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` - OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` + Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` + OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.SecurityScheme } diff --git a/datamodel/high/v3/server.go b/datamodel/high/v3/server.go index 36c1db5..2fbf06d 100644 --- a/datamodel/high/v3/server.go +++ b/datamodel/high/v3/server.go @@ -13,10 +13,10 @@ import ( // Server represents a high-level OpenAPI 3+ Server object, that is backed by a low level one. // - https://spec.openapis.org/oas/v3.1.0#server-object type Server struct { - URL string `json:"url,omitempty" yaml:"url,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Variables orderedmap.Map[string, *ServerVariable] `json:"variables,omitempty" yaml:"variables,omitempty"` - Extensions map[string]any `json:"-" yaml:"-"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Variables *orderedmap.Map[string, *ServerVariable] `json:"variables,omitempty" yaml:"variables,omitempty"` + Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` low *low.Server } diff --git a/datamodel/low/base/discriminator.go b/datamodel/low/base/discriminator.go index b83caad..533a2db 100644 --- a/datamodel/low/base/discriminator.go +++ b/datamodel/low/base/discriminator.go @@ -22,13 +22,13 @@ import ( // v3 - https://spec.openapis.org/oas/v3.1.0#discriminator-object type Discriminator struct { PropertyName low.NodeReference[string] - Mapping low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] + Mapping low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] low.Reference } // FindMappingValue will return a ValueReference containing the string mapping value func (d *Discriminator) FindMappingValue(key string) *low.ValueReference[string] { - for pair := d.Mapping.Value.First(); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(d.Mapping.Value); pair != nil; pair = pair.Next() { if pair.Key().Value == key { v := pair.Value() return &v @@ -45,7 +45,7 @@ func (d *Discriminator) Hash() [32]byte { f = append(f, d.PropertyName.Value) } - for pair := orderedmap.First(d.Mapping.Value); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(orderedmap.SortAlpha(d.Mapping.Value)); pair != nil; pair = pair.Next() { f = append(f, pair.Value().Value) } diff --git a/datamodel/low/base/example.go b/datamodel/low/base/example.go index 4adfcd6..cf26d53 100644 --- a/datamodel/low/base/example.go +++ b/datamodel/low/base/example.go @@ -7,12 +7,11 @@ import ( "context" "crypto/sha256" "fmt" - "sort" - "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -23,15 +22,15 @@ import ( type Example struct { Summary low.NodeReference[string] Description low.NodeReference[string] - Value low.NodeReference[any] + Value low.NodeReference[*yaml.Node] ExternalValue low.NodeReference[string] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // FindExtension returns a ValueReference containing the extension value, if found. -func (ex *Example) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, ex.Extensions) +func (ex *Example) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap[*yaml.Node](ext, ex.Extensions) } // Hash will return a consistent SHA256 Hash of the Discriminator object @@ -43,21 +42,15 @@ func (ex *Example) Hash() [32]byte { if ex.Description.Value != "" { f = append(f, ex.Description.Value) } - if ex.Value.Value != "" { + if ex.Value.Value != nil && !ex.Value.Value.IsZero() { // this could be anything! - f = append(f, fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(ex.Value.Value))))) + b, _ := yaml.Marshal(ex.Value.Value) + f = append(f, fmt.Sprintf("%x", sha256.Sum256(b))) } if ex.ExternalValue.Value != "" { f = append(f, ex.ExternalValue.Value) } - keys := make([]string, len(ex.Extensions)) - z := 0 - for k := range ex.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(ex.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(ex.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -70,32 +63,8 @@ func (ex *Example) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecInd _, ln, vn := utils.FindKeyNodeFull(ValueLabel, root.Content) if vn != nil { - var n map[string]interface{} - err := vn.Decode(&n) - if err != nil { - // if not a map, then try an array - var k []interface{} - err = vn.Decode(&k) - if err != nil { - // lets just default to interface - var j interface{} - _ = vn.Decode(&j) - ex.Value = low.NodeReference[any]{ - Value: j, - KeyNode: ln, - ValueNode: vn, - } - return nil - } - ex.Value = low.NodeReference[any]{ - Value: k, - KeyNode: ln, - ValueNode: vn, - } - return nil - } - ex.Value = low.NodeReference[any]{ - Value: n, + ex.Value = low.NodeReference[*yaml.Node]{ + Value: vn, KeyNode: ln, ValueNode: vn, } @@ -105,33 +74,6 @@ func (ex *Example) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecInd } // GetExtensions will return Example extensions to satisfy the HasExtensions interface. -func (ex *Example) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (ex *Example) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return ex.Extensions } - -// ExtractExampleValue will extract a primitive example value (if possible), or just the raw Value property if not. -func ExtractExampleValue(exp *yaml.Node) any { - if utils.IsNodeBoolValue(exp) { - v, _ := strconv.ParseBool(exp.Value) - return v - } - if utils.IsNodeIntValue(exp) { - v, _ := strconv.ParseInt(exp.Value, 10, 64) - return v - } - if utils.IsNodeFloatValue(exp) { - v, _ := strconv.ParseFloat(exp.Value, 64) - return v - } - if utils.IsNodeMap(exp) { - var m map[string]interface{} - _ = exp.Decode(&m) - return m - } - if utils.IsNodeArray(exp) { - var m []interface{} - _ = exp.Decode(&m) - return m - } - return exp.Value -} diff --git a/datamodel/low/base/example_test.go b/datamodel/low/base/example_test.go index 912172f..d7ba2e1 100644 --- a/datamodel/low/base/example_test.go +++ b/datamodel/low/base/example_test.go @@ -5,15 +5,17 @@ package base import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" - "testing" ) func TestExample_Build_Success_NoValue(t *testing.T) { - yml := `summary: hot description: cakes x-cake: hot` @@ -32,13 +34,13 @@ x-cake: hot` assert.Equal(t, "hot", n.Summary.Value) assert.Equal(t, "cakes", n.Description.Value) assert.Nil(t, n.Value.Value) - ext := n.FindExtension("x-cake") - assert.NotNil(t, ext) - assert.Equal(t, "hot", ext.Value) + + var xCake string + _ = n.FindExtension("x-cake").Value.Decode(&xCake) + assert.Equal(t, "hot", xCake) } func TestExample_Build_Success_Simple(t *testing.T) { - yml := `summary: hot description: cakes value: a string example @@ -57,14 +59,17 @@ x-cake: hot` assert.NoError(t, err) assert.Equal(t, "hot", n.Summary.Value) assert.Equal(t, "cakes", n.Description.Value) - assert.Equal(t, "a string example", n.Value.Value) - ext := n.FindExtension("x-cake") - assert.NotNil(t, ext) - assert.Equal(t, "hot", ext.Value) + + var example string + err = n.Value.Value.Decode(&example) + assert.Equal(t, "a string example", example) + + var xCake string + _ = n.FindExtension("x-cake").Value.Decode(&xCake) + assert.Equal(t, "hot", xCake) } func TestExample_Build_Success_Object(t *testing.T) { - yml := `summary: hot description: cakes value: @@ -85,17 +90,15 @@ value: assert.Equal(t, "hot", n.Summary.Value) assert.Equal(t, "cakes", n.Description.Value) - if v, ok := n.Value.Value.(map[string]interface{}); ok { - assert.Equal(t, "oven", v["pizza"]) - assert.Equal(t, "pizza", v["yummy"]) - } else { - assert.Fail(t, "failed to decode correctly.") - } + var m map[string]interface{} + err = n.Value.Value.Decode(&m) + require.NoError(t, err) + assert.Equal(t, "oven", m["pizza"]) + assert.Equal(t, "pizza", m["yummy"]) } func TestExample_Build_Success_Array(t *testing.T) { - yml := `summary: hot description: cakes value: @@ -116,16 +119,15 @@ value: assert.Equal(t, "hot", n.Summary.Value) assert.Equal(t, "cakes", n.Description.Value) - if v, ok := n.Value.Value.([]interface{}); ok { - assert.Equal(t, "wow", v[0]) - assert.Equal(t, "such array", v[1]) - } else { - assert.Fail(t, "failed to decode correctly.") - } + var a []any + err = n.Value.Value.Decode(&a) + require.NoError(t, err) + + assert.Equal(t, "wow", a[0]) + assert.Equal(t, "such array", a[1]) } func TestExample_Build_Success_MergeNode(t *testing.T) { - yml := `x-things: &things summary: hot description: cakes @@ -148,71 +150,15 @@ func TestExample_Build_Success_MergeNode(t *testing.T) { assert.Equal(t, "hot", n.Summary.Value) assert.Equal(t, "cakes", n.Description.Value) - if v, ok := n.Value.Value.([]interface{}); ok { - assert.Equal(t, "wow", v[0]) - assert.Equal(t, "such array", v[1]) - } else { - assert.Fail(t, "failed to decode correctly.") - } + var a []any + err = n.Value.GetValue().Decode(&a) + require.NoError(t, err) -} - -func TestExample_ExtractExampleValue_Map(t *testing.T) { - - yml := `hot: - summer: nights - pizza: oven` - - var idxNode yaml.Node - _ = yaml.Unmarshal([]byte(yml), &idxNode) - - val := ExtractExampleValue(idxNode.Content[0]) - if v, ok := val.(map[string]interface{}); ok { - if r, rok := v["hot"].(map[string]interface{}); rok { - assert.Equal(t, "nights", r["summer"]) - assert.Equal(t, "oven", r["pizza"]) - } else { - assert.Fail(t, "failed to decode correctly.") - } - } else { - assert.Fail(t, "failed to decode correctly.") - } -} - -func TestExample_ExtractExampleValue_Slice(t *testing.T) { - - yml := `- hot: - summer: nights -- hotter: - pizza: oven` - - var idxNode yaml.Node - _ = yaml.Unmarshal([]byte(yml), &idxNode) - - val := ExtractExampleValue(idxNode.Content[0]) - if v, ok := val.([]interface{}); ok { - for w := range v { - if r, rok := v[w].(map[string]interface{}); rok { - for k := range r { - if k == "hotter" { - assert.Equal(t, "oven", r[k].(map[string]interface{})["pizza"]) - } - if k == "hot" { - assert.Equal(t, "nights", r[k].(map[string]interface{})["summer"]) - } - } - } else { - assert.Fail(t, "failed to decode correctly.") - } - } - - } else { - assert.Fail(t, "failed to decode correctly.") - } + assert.Equal(t, "wow", a[0]) + assert.Equal(t, "such array", a[1]) } func TestExample_Hash(t *testing.T) { - left := `summary: hot description: cakes x-burger: nice @@ -242,13 +188,5 @@ x-burger: nice` _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) assert.Equal(t, lDoc.Hash(), rDoc.Hash()) - assert.Len(t, lDoc.GetExtensions(), 1) -} - -func TestExtractExampleValue(t *testing.T) { - assert.True(t, ExtractExampleValue(&yaml.Node{Tag: "!!bool", Value: "true"}).(bool)) - assert.Equal(t, int64(10), ExtractExampleValue(&yaml.Node{Tag: "!!int", Value: "10"}).(int64)) - assert.Equal(t, 33.2, ExtractExampleValue(&yaml.Node{Tag: "!!float", Value: "33.2"}).(float64)) - assert.Equal(t, "WHAT A NICE COW", ExtractExampleValue(&yaml.Node{Tag: "!!str", Value: "WHAT A NICE COW"})) - + assert.Equal(t, 1, orderedmap.Len(lDoc.GetExtensions())) } diff --git a/datamodel/low/base/external_doc.go b/datamodel/low/base/external_doc.go index a4056a7..a746e6c 100644 --- a/datamodel/low/base/external_doc.go +++ b/datamodel/low/base/external_doc.go @@ -6,13 +6,13 @@ package base import ( "context" "crypto/sha256" - "fmt" + "strings" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sort" - "strings" ) // ExternalDoc represents a low-level External Documentation object as defined by OpenAPI 2 and 3 @@ -24,13 +24,13 @@ import ( type ExternalDoc struct { Description low.NodeReference[string] URL low.NodeReference[string] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // FindExtension returns a ValueReference containing the extension value, if found. -func (ex *ExternalDoc) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, ex.Extensions) +func (ex *ExternalDoc) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap[*yaml.Node](ext, ex.Extensions) } // Build will extract extensions from the ExternalDoc instance. @@ -43,7 +43,7 @@ func (ex *ExternalDoc) Build(_ context.Context, _, root *yaml.Node, idx *index.S } // GetExtensions returns all ExternalDoc extensions and satisfies the low.HasExtensions interface. -func (ex *ExternalDoc) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (ex *ExternalDoc) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return ex.Extensions } @@ -53,13 +53,6 @@ func (ex *ExternalDoc) Hash() [32]byte { ex.Description.Value, ex.URL.Value, } - keys := make([]string, len(ex.Extensions)) - z := 0 - for k := range ex.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(ex.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(ex.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/base/external_doc_test.go b/datamodel/low/base/external_doc_test.go index 1f48896..d3f71c8 100644 --- a/datamodel/low/base/external_doc_test.go +++ b/datamodel/low/base/external_doc_test.go @@ -5,15 +5,16 @@ package base import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestExternalDoc_FindExtension(t *testing.T) { - yml := `x-fish: cake` var idxNode yaml.Node @@ -26,12 +27,14 @@ func TestExternalDoc_FindExtension(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "cake", n.FindExtension("x-fish").Value) + var xFish string + _ = n.FindExtension("x-fish").Value.Decode(&xFish) + + assert.Equal(t, "cake", xFish) } func TestExternalDoc_Build(t *testing.T) { - yml := `url: https://pb33f.io description: the ranch x-b33f: princess` @@ -49,14 +52,13 @@ x-b33f: princess` assert.NoError(t, err) assert.Equal(t, "https://pb33f.io", n.URL.Value) assert.Equal(t, "the ranch", n.Description.Value) - ext := n.FindExtension("x-b33f") - assert.NotNil(t, ext) - assert.Equal(t, "princess", ext.Value) + var xB33f string + _ = n.FindExtension("x-b33f").Value.Decode(&xB33f) + assert.Equal(t, "princess", xB33f) } func TestExternalDoc_Hash(t *testing.T) { - left := `url: https://pb33f.io description: the ranch x-b33f: princess` @@ -78,5 +80,5 @@ description: the ranch` _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) assert.Equal(t, lDoc.Hash(), rDoc.Hash()) - assert.Len(t, lDoc.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(lDoc.GetExtensions())) } diff --git a/datamodel/low/base/info.go b/datamodel/low/base/info.go index f7440d6..fb75b9a 100644 --- a/datamodel/low/base/info.go +++ b/datamodel/low/base/info.go @@ -6,11 +6,11 @@ package base import ( "context" "crypto/sha256" - "fmt" - "github.com/pb33f/libopenapi/utils" - "sort" "strings" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" @@ -31,17 +31,17 @@ type Info struct { Contact low.NodeReference[*Contact] License low.NodeReference[*License] Version low.NodeReference[string] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // FindExtension attempts to locate an extension with the supplied key -func (i *Info) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap(ext, i.Extensions) +func (i *Info) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, i.Extensions) } // GetExtensions returns all extensions for Info -func (i *Info) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (i *Info) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return i.Extensions } @@ -87,13 +87,6 @@ func (i *Info) Hash() [32]byte { if !i.Version.IsEmpty() { f = append(f, i.Version.Value) } - keys := make([]string, len(i.Extensions)) - z := 0 - for k := range i.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(i.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(i.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/base/info_test.go b/datamodel/low/base/info_test.go index c469a9f..cf63f89 100644 --- a/datamodel/low/base/info_test.go +++ b/datamodel/low/base/info_test.go @@ -9,6 +9,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -54,10 +55,11 @@ x-cli-name: pizza cli` assert.Equal(t, "magic", lic.Name.Value) assert.Equal(t, "https://pb33f.io/license", lic.URL.Value) - cliName := n.FindExtension("x-cli-name") - assert.NotNil(t, cliName) - assert.Equal(t, "pizza cli", cliName.Value) - assert.Len(t, n.GetExtensions(), 1) + var xCliName string + _ = n.FindExtension("x-cli-name").Value.Decode(&xCliName) + + assert.Equal(t, "pizza cli", xCliName) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestContact_Build(t *testing.T) { diff --git a/datamodel/low/base/schema.go b/datamodel/low/base/schema.go index 6c464d7..8fe8b03 100644 --- a/datamodel/low/base/schema.go +++ b/datamodel/low/base/schema.go @@ -82,7 +82,7 @@ type Schema struct { Discriminator low.NodeReference[*Discriminator] // in 3.1 examples can be an array (which is recommended) - Examples low.NodeReference[[]low.ValueReference[any]] + Examples low.NodeReference[[]low.ValueReference[*yaml.Node]] // in 3.1 PrefixItems provides tuple validation using prefixItems. PrefixItems low.NodeReference[[]low.ValueReference[*SchemaProxy]] // in 3.1 Contains is used by arrays and points to a Schema. @@ -97,8 +97,8 @@ type Schema struct { If low.NodeReference[*SchemaProxy] Else low.NodeReference[*SchemaProxy] Then low.NodeReference[*SchemaProxy] - DependentSchemas low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] - PatternProperties low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] + DependentSchemas low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] + PatternProperties low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] PropertyNames low.NodeReference[*SchemaProxy] UnevaluatedItems low.NodeReference[*SchemaProxy] UnevaluatedProperties low.NodeReference[*SchemaDynamicValue[*SchemaProxy, bool]] @@ -119,23 +119,23 @@ type Schema struct { MaxProperties low.NodeReference[int64] MinProperties low.NodeReference[int64] Required low.NodeReference[[]low.ValueReference[string]] - Enum low.NodeReference[[]low.ValueReference[any]] + Enum low.NodeReference[[]low.ValueReference[*yaml.Node]] Not low.NodeReference[*SchemaProxy] - Properties low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] + Properties low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]] AdditionalProperties low.NodeReference[*SchemaDynamicValue[*SchemaProxy, bool]] Description low.NodeReference[string] ContentEncoding low.NodeReference[string] ContentMediaType low.NodeReference[string] - Default low.NodeReference[any] - Const low.NodeReference[any] + Default low.NodeReference[*yaml.Node] + Const low.NodeReference[*yaml.Node] Nullable low.NodeReference[bool] ReadOnly low.NodeReference[bool] WriteOnly low.NodeReference[bool] XML low.NodeReference[*XML] ExternalDocs low.NodeReference[*ExternalDoc] - Example low.NodeReference[any] + Example low.NodeReference[*yaml.Node] Deprecated low.NodeReference[bool] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] // Parent Proxy refers back to the low level SchemaProxy that is proxying this schema. ParentProxy *SchemaProxy @@ -255,23 +255,13 @@ func (s *Schema) Hash() [32]byte { keys = make([]string, len(s.Enum.Value)) for i := range s.Enum.Value { - keys[i] = fmt.Sprint(s.Enum.Value[i].Value) + keys[i] = low.ValueToString(s.Enum.Value[i].Value) } sort.Strings(keys) d = append(d, keys...) - for i := range s.Enum.Value { - d = append(d, fmt.Sprint(s.Enum.Value[i].Value)) - } - propKeys := make([]string, orderedmap.Len(s.Properties.Value)) - z := 0 - for pair := orderedmap.First(s.Properties.Value); pair != nil; pair = pair.Next() { - propKeys[z] = pair.Key().Value - z++ - } - sort.Strings(propKeys) - for k := range propKeys { - d = append(d, low.GenerateHashString(s.FindProperty(propKeys[k]).Value)) + for pair := orderedmap.First(orderedmap.SortAlpha(s.Properties.Value)); pair != nil; pair = pair.Next() { + d = append(d, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) } if s.XML.Value != nil { d = append(d, low.GenerateHashString(s.XML.Value)) @@ -287,7 +277,7 @@ func (s *Schema) Hash() [32]byte { if len(s.OneOf.Value) > 0 { oneOfKeys := make([]string, len(s.OneOf.Value)) oneOfEntities := make(map[string]*SchemaProxy) - z = 0 + z := 0 for i := range s.OneOf.Value { g := s.OneOf.Value[i].Value r := low.GenerateHashString(g) @@ -305,7 +295,7 @@ func (s *Schema) Hash() [32]byte { if len(s.AllOf.Value) > 0 { allOfKeys := make([]string, len(s.AllOf.Value)) allOfEntities := make(map[string]*SchemaProxy) - z = 0 + z := 0 for i := range s.AllOf.Value { g := s.AllOf.Value[i].Value r := low.GenerateHashString(g) @@ -323,7 +313,7 @@ func (s *Schema) Hash() [32]byte { if len(s.AnyOf.Value) > 0 { anyOfKeys := make([]string, len(s.AnyOf.Value)) anyOfEntities := make(map[string]*SchemaProxy) - z = 0 + z := 0 for i := range s.AnyOf.Value { g := s.AnyOf.Value[i].Value r := low.GenerateHashString(g) @@ -372,32 +362,18 @@ func (s *Schema) Hash() [32]byte { d = append(d, fmt.Sprint(s.Anchor.Value)) } - depSchemasKeys := make([]string, orderedmap.Len(s.DependentSchemas.Value)) - z = 0 - for pair := orderedmap.First(s.DependentSchemas.Value); pair != nil; pair = pair.Next() { - depSchemasKeys[z] = pair.Key().Value - z++ - } - sort.Strings(depSchemasKeys) - for k := range depSchemasKeys { - d = append(d, low.GenerateHashString(s.FindDependentSchema(depSchemasKeys[k]).Value)) + for pair := orderedmap.First(orderedmap.SortAlpha(s.DependentSchemas.Value)); pair != nil; pair = pair.Next() { + d = append(d, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) } - patternPropsKeys := make([]string, orderedmap.Len(s.PatternProperties.Value)) - z = 0 - for pair := orderedmap.First(s.PatternProperties.Value); pair != nil; pair = pair.Next() { - patternPropsKeys[z] = pair.Key().Value - z++ - } - sort.Strings(patternPropsKeys) - for k := range patternPropsKeys { - d = append(d, low.GenerateHashString(s.FindPatternProperty(patternPropsKeys[k]).Value)) + for pair := orderedmap.First(orderedmap.SortAlpha(s.PatternProperties.Value)); pair != nil; pair = pair.Next() { + d = append(d, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) } if len(s.PrefixItems.Value) > 0 { itemsKeys := make([]string, len(s.PrefixItems.Value)) itemsEntities := make(map[string]*SchemaProxy) - z = 0 + z := 0 for i := range s.PrefixItems.Value { g := s.PrefixItems.Value[i].Value r := low.GenerateHashString(g) @@ -411,15 +387,7 @@ func (s *Schema) Hash() [32]byte { } } - // add extensions to hash - keys = make([]string, len(s.Extensions)) - z = 0 - for k := range s.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(s.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - d = append(d, keys...) + d = append(d, low.HashExtensions(s.Extensions)...) if s.Example.Value != nil { d = append(d, low.GenerateHashString(s.Example.Value)) } @@ -435,12 +403,9 @@ func (s *Schema) Hash() [32]byte { d = append(d, fmt.Sprint(s.MaxContains.Value)) } if !s.Examples.IsEmpty() { - var xph []string - for w := range s.Examples.Value { - xph = append(xph, low.GenerateHashString(s.Examples.Value[w].Value)) + for _, ex := range s.Examples.Value { + d = append(d, low.GenerateHashString(ex.Value)) } - sort.Strings(xph) - d = append(d, strings.Join(xph, "|")) } return sha256.Sum256([]byte(strings.Join(d, "|"))) } @@ -464,7 +429,7 @@ func (s *Schema) FindPatternProperty(name string) *low.ValueReference[*SchemaPro } // GetExtensions returns all extensions for Schema -func (s *Schema) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (s *Schema) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return s.Extensions } @@ -649,20 +614,20 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde } // handle example if set. (3.0) - _, expLabel, expNode := utils.FindKeyNodeFull(ExampleLabel, root.Content) + _, expLabel, expNode := utils.FindKeyNodeFullTop(ExampleLabel, root.Content) if expNode != nil { - s.Example = low.NodeReference[any]{Value: ExtractExampleValue(expNode), KeyNode: expLabel, ValueNode: expNode} + s.Example = low.NodeReference[*yaml.Node]{Value: expNode, KeyNode: expLabel, ValueNode: expNode} } // handle examples if set.(3.1) _, expArrLabel, expArrNode := utils.FindKeyNodeFullTop(ExamplesLabel, root.Content) if expArrNode != nil { if utils.IsNodeArray(expArrNode) { - var examples []low.ValueReference[any] + var examples []low.ValueReference[*yaml.Node] for i := range expArrNode.Content { - examples = append(examples, low.ValueReference[any]{Value: ExtractExampleValue(expArrNode.Content[i]), ValueNode: expArrNode.Content[i]}) + examples = append(examples, low.ValueReference[*yaml.Node]{Value: expArrNode.Content[i], ValueNode: expArrNode.Content[i]}) } - s.Examples = low.NodeReference[[]low.ValueReference[any]]{ + s.Examples = low.NodeReference[[]low.ValueReference[*yaml.Node]]{ Value: examples, ValueNode: expArrNode, KeyNode: expArrLabel, @@ -1035,7 +1000,7 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde return nil } -func buildPropertyMap(ctx context.Context, root *yaml.Node, idx *index.SpecIndex, label string) (*low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]], error) { +func buildPropertyMap(ctx context.Context, root *yaml.Node, idx *index.SpecIndex, label string) (*low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]], error) { _, propLabel, propsNode := utils.FindKeyNodeFullTop(label, root.Content) if propsNode != nil { propertyMap := orderedmap.New[low.KeyReference[string], low.ValueReference[*SchemaProxy]]() @@ -1047,12 +1012,12 @@ func buildPropertyMap(ctx context.Context, root *yaml.Node, idx *index.SpecIndex } // check our prop isn't reference - isRef := false refString := "" + var refNode *yaml.Node if h, _, l := utils.IsNodeRefValue(prop); h { ref, _, _, _ := low.LocateRefNodeWithContext(ctx, prop, idx) if ref != nil { - isRef = true + refNode = prop prop = ref refString = l } else { @@ -1061,16 +1026,19 @@ func buildPropertyMap(ctx context.Context, root *yaml.Node, idx *index.SpecIndex } } + sp := &SchemaProxy{ctx: ctx, kn: currentProp, vn: prop, idx: idx} + sp.SetReference(refString, refNode) + propertyMap.Set(low.KeyReference[string]{ KeyNode: currentProp, Value: currentProp.Value, }, low.ValueReference[*SchemaProxy]{ - Value: &SchemaProxy{ctx: ctx, kn: currentProp, vn: prop, idx: idx, isReference: isRef, referenceLookup: refString}, + Value: sp, ValueNode: prop, }) } - return &low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]]{ + return &low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SchemaProxy]]]{ Value: propertyMap, KeyNode: propLabel, ValueNode: propsNode, @@ -1112,7 +1080,7 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label syncChan := make(chan buildResult) // build out a SchemaProxy for every sub-schema. - build := func(pctx context.Context, kn *yaml.Node, vn *yaml.Node, schemaIdx int, c chan buildResult, + build := func(pctx context.Context, kn, vn *yaml.Node, rf *yaml.Node, schemaIdx int, c chan buildResult, isRef bool, refLocation string, ) { // a proxy design works best here. polymorphism, pretty much guarantees that a sub-schema can @@ -1127,8 +1095,7 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label sp.idx = idx sp.ctx = pctx if isRef { - sp.referenceLookup = refLocation - sp.isReference = true + sp.SetReference(refLocation, rf) } res := &low.ValueReference[*SchemaProxy]{ Value: sp, @@ -1142,6 +1109,7 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label isRef := false refLocation := "" + var refNode *yaml.Node foundCtx := ctx if utils.IsNodeMap(valueNode) { h := false @@ -1149,6 +1117,7 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label isRef = true ref, _, _, fctx := low.LocateRefNodeWithContext(ctx, valueNode, idx) if ref != nil { + refNode = valueNode valueNode = ref foundCtx = fctx } else { @@ -1159,7 +1128,7 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label // this only runs once, however to keep things consistent, it makes sense to use the same async method // that arrays will use. - go build(foundCtx, labelNode, valueNode, -1, syncChan, isRef, refLocation) + go build(foundCtx, labelNode, valueNode, refNode, -1, syncChan, isRef, refLocation) select { case r := <-syncChan: schemas <- schemaProxyBuildResult{ @@ -1181,6 +1150,7 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label isRef = true ref, _, _, fctx := low.LocateRefNodeWithContext(ctx, vn, idx) if ref != nil { + refNode = vn vn = ref foundCtx = fctx } else { @@ -1191,7 +1161,7 @@ func buildSchema(ctx context.Context, schemas chan schemaProxyBuildResult, label } } refBuilds++ - go build(foundCtx, vn, vn, i, syncChan, isRef, refLocation) + go build(foundCtx, vn, vn, refNode, i, syncChan, isRef, refLocation) } completedBuilds := 0 @@ -1225,12 +1195,11 @@ func ExtractSchema(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) ( var schLabel, schNode *yaml.Node errStr := "schema build failed: reference '%s' cannot be found at line %d, col %d" - isRef := false refLocation := "" + var refNode *yaml.Node if rf, rl, _ := utils.IsNodeRefValue(root); rf { // locate reference in index. - isRef = true ref, fIdx, _, nCtx := low.LocateRefNodeWithContext(ctx, root, idx) if ref != nil { schNode = ref @@ -1250,9 +1219,9 @@ func ExtractSchema(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) ( if schNode != nil { h := false if h, _, refLocation = utils.IsNodeRefValue(schNode); h { - isRef = true ref, foundIdx, _, nCtx := low.LocateRefNodeWithContext(ctx, schNode, idx) if ref != nil { + refNode = schNode schNode = ref if foundIdx != nil { // TODO: check on this @@ -1273,11 +1242,16 @@ func ExtractSchema(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) ( if schNode != nil { // check if schema has already been built. - schema := &SchemaProxy{kn: schLabel, vn: schNode, idx: idx, ctx: ctx, isReference: isRef, referenceLookup: refLocation} - return &low.NodeReference[*SchemaProxy]{ - Value: schema, KeyNode: schLabel, ValueNode: schNode, ReferenceNode: isRef, - Reference: refLocation, - }, nil + schema := &SchemaProxy{kn: schLabel, vn: schNode, idx: idx, ctx: ctx} + schema.SetReference(refLocation, refNode) + + n := &low.NodeReference[*SchemaProxy]{ + Value: schema, + KeyNode: schLabel, + ValueNode: schNode, + } + n.SetReference(refLocation, refNode) + return n, nil } return nil, nil } diff --git a/datamodel/low/base/schema_proxy.go b/datamodel/low/base/schema_proxy.go index 34737e1..f249800 100644 --- a/datamodel/low/base/schema_proxy.go +++ b/datamodel/low/base/schema_proxy.go @@ -6,6 +6,8 @@ package base import ( "context" "crypto/sha256" + + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" @@ -44,14 +46,13 @@ import ( // it's not actually JSONSchema until 3.1, so lots of times a bad schema will break parsing. Errors are only found // when a schema is needed, so the rest of the document is parsed and ready to use. type SchemaProxy struct { - kn *yaml.Node - vn *yaml.Node - idx *index.SpecIndex - rendered *Schema - buildError error - isReference bool // Is the schema underneath originally a $ref? - referenceLookup string // If the schema is a $ref, what's its name? - ctx context.Context + low.Reference + kn *yaml.Node + vn *yaml.Node + idx *index.SpecIndex + rendered *Schema + buildError error + ctx context.Context } // Build will prepare the SchemaProxy for rendering, it does not build the Schema, only sets up internal state. @@ -62,8 +63,7 @@ func (sp *SchemaProxy) Build(ctx context.Context, key, value *yaml.Node, idx *in sp.idx = idx sp.ctx = ctx if rf, _, r := utils.IsNodeRefValue(value); rf { - sp.isReference = true - sp.referenceLookup = r + sp.SetReference(r, value) } return nil } @@ -101,36 +101,6 @@ func (sp *SchemaProxy) GetBuildError() error { return sp.buildError } -// IsSchemaReference returns true if the Schema that this SchemaProxy represents, is actually a reference to -// a Schema contained within Components or Definitions. There is no difference in the mechanism used to resolve the -// Schema when calling Schema(), however if we want to know if this schema was originally a reference, we won't -// be able to determine that from the model, without this bit. -func (sp *SchemaProxy) IsSchemaReference() bool { - return sp.isReference -} - -// IsReference is an alias for IsSchemaReference() except it's compatible wih the IsReferenced interface type. -func (sp *SchemaProxy) IsReference() bool { - return sp.IsSchemaReference() -} - -// GetReference is an alias for GetSchemaReference() except it's compatible wih the IsReferenced interface type. -func (sp *SchemaProxy) GetReference() string { - return sp.GetSchemaReference() -} - -// SetReference will set the reference lookup for this SchemaProxy. -func (sp *SchemaProxy) SetReference(ref string) { - sp.referenceLookup = ref -} - -// GetSchemaReference will return the lookup defined by the $ref that this schema points to. If the schema -// is inline, and not a reference, then this method returns an empty string. Only useful when combined with -// IsSchemaReference() -func (sp *SchemaProxy) GetSchemaReference() string { - return sp.referenceLookup -} - func (sp *SchemaProxy) GetSchemaReferenceLocation() *index.NodeOrigin { if sp.idx != nil { origin := sp.idx.FindNodeOrigin(sp.vn) @@ -158,11 +128,11 @@ func (sp *SchemaProxy) GetValueNode() *yaml.Node { // Hash will return a consistent SHA256 Hash of the SchemaProxy object (it will resolve it) func (sp *SchemaProxy) Hash() [32]byte { if sp.rendered != nil { - if !sp.isReference { + if !sp.IsReference() { return sp.rendered.Hash() } } else { - if !sp.isReference { + if !sp.IsReference() { // only resolve this proxy if it's not a ref. sch := sp.Schema() sp.rendered = sch @@ -170,5 +140,5 @@ func (sp *SchemaProxy) Hash() [32]byte { } } // hash reference value only, do not resolve! - return sha256.Sum256([]byte(sp.referenceLookup)) + return sha256.Sum256([]byte(sp.GetReference())) } diff --git a/datamodel/low/base/schema_proxy_test.go b/datamodel/low/base/schema_proxy_test.go index feb0232..698d38e 100644 --- a/datamodel/low/base/schema_proxy_test.go +++ b/datamodel/low/base/schema_proxy_test.go @@ -5,15 +5,16 @@ package base import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestSchemaProxy_Build(t *testing.T) { - yml := `x-windows: washed description: something` @@ -24,29 +25,25 @@ description: something` err := sch.Build(context.Background(), &idxNode, idxNode.Content[0], nil) assert.NoError(t, err) - assert.Equal(t, "db2a35dd6fb3d9481d0682571b9d687616bb2a34c1887f7863f0b2e769ca7b23", + assert.Equal(t, "e20c009d370944d177c0b46e8fa29e15fadc3a6f9cca6bb251ff9e120265fc96", low.GenerateHashString(&sch)) - assert.Equal(t, "something", sch.Schema().Description.Value) - assert.Empty(t, sch.GetSchemaReference()) + assert.Equal(t, "something", sch.Schema().Description.GetValue()) + assert.Empty(t, sch.GetReference()) assert.NotNil(t, sch.GetKeyNode()) assert.NotNil(t, sch.GetValueNode()) - assert.False(t, sch.IsSchemaReference()) assert.False(t, sch.IsReference()) - assert.Empty(t, sch.GetReference()) - sch.SetReference("coffee") + sch.SetReference("coffee", nil) assert.Equal(t, "coffee", sch.GetReference()) // already rendered, should spit out the same - assert.Equal(t, "db2a35dd6fb3d9481d0682571b9d687616bb2a34c1887f7863f0b2e769ca7b23", + assert.Equal(t, "37290d74ac4d186e3a8e5785d259d2ec04fac91ae28092e7620ec8bc99e830aa", low.GenerateHashString(&sch)) - assert.Len(t, sch.Schema().GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(sch.Schema().GetExtensions())) } func TestSchemaProxy_Build_CheckRef(t *testing.T) { - yml := `$ref: wat` var sch SchemaProxy @@ -55,14 +52,13 @@ func TestSchemaProxy_Build_CheckRef(t *testing.T) { err := sch.Build(context.Background(), nil, idxNode.Content[0], nil) assert.NoError(t, err) - assert.True(t, sch.IsSchemaReference()) - assert.Equal(t, "wat", sch.GetSchemaReference()) + assert.True(t, sch.IsReference()) + assert.Equal(t, "wat", sch.GetReference()) assert.Equal(t, "f00a787f7492a95e165b470702f4fe9373583fbdc025b2c8bdf0262cc48fcff4", low.GenerateHashString(&sch)) } func TestSchemaProxy_Build_HashInline(t *testing.T) { - yml := `type: int` var sch SchemaProxy @@ -71,14 +67,13 @@ func TestSchemaProxy_Build_HashInline(t *testing.T) { err := sch.Build(context.Background(), nil, idxNode.Content[0], nil) assert.NoError(t, err) - assert.False(t, sch.IsSchemaReference()) + assert.False(t, sch.IsReference()) assert.NotNil(t, sch.Schema()) assert.Equal(t, "6da88c34ba124c41f977db66a4fc5c1a951708d285c81bb0d47c3206f4c27ca8", low.GenerateHashString(&sch)) } func TestSchemaProxy_Build_UsingMergeNodes(t *testing.T) { - yml := ` x-common-definitions: life_cycle_types: &life_cycle_types_def @@ -95,11 +90,9 @@ x-common-definitions: assert.NoError(t, err) assert.Len(t, sch.Schema().Enum.Value, 3) assert.Equal(t, "The type of life cycle", sch.Schema().Description.Value) - } func TestSchemaProxy_GetSchemaReferenceLocation(t *testing.T) { - yml := `type: object properties: name: @@ -159,5 +152,4 @@ properties: err = schC.Build(context.Background(), nil, idxNodeA.Content[0], nil) origin = schC.GetSchemaReferenceLocation() assert.Nil(t, origin) - } diff --git a/datamodel/low/base/schema_test.go b/datamodel/low/base/schema_test.go index 2945d0d..5c2301a 100644 --- a/datamodel/low/base/schema_test.go +++ b/datamodel/low/base/schema_test.go @@ -2,6 +2,8 @@ package base import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" @@ -9,7 +11,6 @@ import ( "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func test_get_schema_blob() string { @@ -192,8 +193,8 @@ func Test_Schema(t *testing.T) { assert.Equal(t, "an xml namespace", j.XML.Value.Namespace.Value) assert.Equal(t, "a prefix", j.XML.Value.Prefix.Value) assert.Equal(t, true, j.XML.Value.Attribute.Value) - assert.Len(t, j.XML.Value.Extensions, 1) - assert.Len(t, j.XML.Value.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(j.XML.Value.Extensions)) + assert.Equal(t, 1, orderedmap.Len(j.XML.Value.GetExtensions())) assert.NotNil(t, v.Value.Schema().AdditionalProperties.Value) @@ -213,12 +214,20 @@ func Test_Schema(t *testing.T) { io := v.Value.Schema() assert.Equal(t, "allOfA description", io.Description.Value) - assert.Equal(t, "allOfAExp", io.Example.Value) + + var ioExample string + _ = io.Example.GetValueNode().Decode(&ioExample) + + assert.Equal(t, "allOfAExp", ioExample) qw := f.FindProperty("allOfB").Value.Schema() assert.NotNil(t, v) assert.Equal(t, "allOfB description", qw.Description.Value) - assert.Equal(t, "allOfBExp", qw.Example.Value) + + var qwExample string + _ = qw.Example.GetValueNode().Decode(&qwExample) + + assert.Equal(t, "allOfBExp", qwExample) // check polymorphic values anyOf assert.Equal(t, "an anyOf thing", sch.AnyOf.Value[0].Value.Schema().Description.Value) @@ -227,12 +236,18 @@ func Test_Schema(t *testing.T) { v = sch.AnyOf.Value[0].Value.Schema().FindProperty("anyOfA") assert.NotNil(t, v) assert.Equal(t, "anyOfA description", v.Value.Schema().Description.Value) - assert.Equal(t, "anyOfAExp", v.Value.Schema().Example.Value) + + var vSchemaExample string + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + + assert.Equal(t, "anyOfAExp", vSchemaExample) v = sch.AnyOf.Value[0].Value.Schema().FindProperty("anyOfB") assert.NotNil(t, v) assert.Equal(t, "anyOfB description", v.Value.Schema().Description.Value) - assert.Equal(t, "anyOfBExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + assert.Equal(t, "anyOfBExp", vSchemaExample) // check polymorphic values oneOf assert.Equal(t, "a oneof thing", sch.OneOf.Value[0].Value.Schema().Description.Value) @@ -241,12 +256,16 @@ func Test_Schema(t *testing.T) { v = sch.OneOf.Value[0].Value.Schema().FindProperty("oneOfA") assert.NotNil(t, v) assert.Equal(t, "oneOfA description", v.Value.Schema().Description.Value) - assert.Equal(t, "oneOfAExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + assert.Equal(t, "oneOfAExp", vSchemaExample) v = sch.OneOf.Value[0].Value.Schema().FindProperty("oneOfB") assert.NotNil(t, v) assert.Equal(t, "oneOfB description", v.Value.Schema().Description.Value) - assert.Equal(t, "oneOfBExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + assert.Equal(t, "oneOfBExp", vSchemaExample) // check values NOT assert.Equal(t, "a not thing", sch.Not.Value.Schema().Description.Value) @@ -255,12 +274,16 @@ func Test_Schema(t *testing.T) { v = sch.Not.Value.Schema().FindProperty("notA") assert.NotNil(t, v) assert.Equal(t, "notA description", v.Value.Schema().Description.Value) - assert.Equal(t, "notAExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + assert.Equal(t, "notAExp", vSchemaExample) v = sch.Not.Value.Schema().FindProperty("notB") assert.NotNil(t, v) assert.Equal(t, "notB description", v.Value.Schema().Description.Value) - assert.Equal(t, "notBExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + assert.Equal(t, "notBExp", vSchemaExample) // check values Items assert.Equal(t, "an items thing", sch.Items.Value.A.Schema().Description.Value) @@ -269,12 +292,16 @@ func Test_Schema(t *testing.T) { v = sch.Items.Value.A.Schema().FindProperty("itemsA") assert.NotNil(t, v) assert.Equal(t, "itemsA description", v.Value.Schema().Description.Value) - assert.Equal(t, "itemsAExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + assert.Equal(t, "itemsAExp", vSchemaExample) v = sch.Items.Value.A.Schema().FindProperty("itemsB") assert.NotNil(t, v) assert.Equal(t, "itemsB description", v.Value.Schema().Description.Value) - assert.Equal(t, "itemsBExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + assert.Equal(t, "itemsBExp", vSchemaExample) // check values PrefixItems assert.Equal(t, "an items thing", sch.PrefixItems.Value[0].Value.Schema().Description.Value) @@ -283,17 +310,21 @@ func Test_Schema(t *testing.T) { v = sch.PrefixItems.Value[0].Value.Schema().FindProperty("itemsA") assert.NotNil(t, v) assert.Equal(t, "itemsA description", v.Value.Schema().Description.Value) - assert.Equal(t, "itemsAExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValueNode().Decode(&vSchemaExample) + assert.Equal(t, "itemsAExp", vSchemaExample) v = sch.PrefixItems.Value[0].Value.Schema().FindProperty("itemsB") assert.NotNil(t, v) assert.Equal(t, "itemsB description", v.Value.Schema().Description.Value) - assert.Equal(t, "itemsBExp", v.Value.Schema().Example.Value) + + _ = v.GetValue().Schema().Example.GetValue().Decode(&vSchemaExample) + assert.Equal(t, "itemsBExp", vSchemaExample) // check discriminator assert.NotNil(t, sch.Discriminator.Value) assert.Equal(t, "athing", sch.Discriminator.Value.PropertyName.Value) - assert.Len(t, sch.Discriminator.Value.Mapping.Value, 2) + assert.Equal(t, 2, sch.Discriminator.GetValue().Mapping.GetValue().Len()) mv := sch.Discriminator.Value.FindMappingValue("log") assert.Equal(t, "cat", mv.Value) mv = sch.Discriminator.Value.FindMappingValue("pizza") @@ -429,12 +460,20 @@ const: tasty` assert.Equal(t, float64(12), sch.ExclusiveMinimum.Value.B) assert.Equal(t, float64(13), sch.ExclusiveMaximum.Value.B) assert.Len(t, sch.Examples.Value, 1) - assert.Equal(t, "testing", sch.Examples.Value[0].Value) + + var example0 string + _ = sch.Examples.GetValue()[0].GetValue().Decode(&example0) + + assert.Equal(t, "testing", example0) assert.Equal(t, "fish64", sch.ContentEncoding.Value) assert.Equal(t, "fish/paste", sch.ContentMediaType.Value) assert.True(t, sch.Items.Value.IsB()) assert.True(t, sch.Items.Value.B) - assert.Equal(t, "tasty", sch.Const.Value) + + var schConst string + _ = sch.Const.GetValue().Decode(&schConst) + + assert.Equal(t, "tasty", schConst) } func TestSchema_Build_PropsLookup(t *testing.T) { @@ -986,7 +1025,11 @@ schema: assert.NoError(t, err) assert.NotNil(t, res.Value) sch := res.Value.Schema() - assert.Equal(t, 5, sch.Default.Value) + + var def int + _ = sch.Default.GetValueNode().Decode(&def) + + assert.Equal(t, 5, def) } func TestExtractSchema_ConstPrimitive(t *testing.T) { @@ -1002,7 +1045,11 @@ schema: assert.NoError(t, err) assert.NotNil(t, res.Value) sch := res.Value.Schema() - assert.Equal(t, 5, sch.Const.Value) + + var cnst int + _ = sch.Const.GetValueNode().Decode(&cnst) + + assert.Equal(t, 5, cnst) } func TestExtractSchema_Ref(t *testing.T) { @@ -1785,7 +1832,6 @@ components: res, e := ExtractSchema(context.Background(), idxNode.Content[0], idx) assert.Nil(t, res) assert.Equal(t, "schema build failed: reference '[empty]' cannot be found at line 2, col 9", e.Error()) - } func TestSchema_EmptyRef(t *testing.T) { @@ -1814,5 +1860,4 @@ components: res, e := ExtractSchema(context.Background(), idxNode.Content[0], idx) assert.Nil(t, res) assert.Equal(t, "schema build failed: reference '[empty]' cannot be found at line 1, col 7", e.Error()) - } diff --git a/datamodel/low/base/security_requirement.go b/datamodel/low/base/security_requirement.go index c621a4b..afb0a17 100644 --- a/datamodel/low/base/security_requirement.go +++ b/datamodel/low/base/security_requirement.go @@ -26,7 +26,7 @@ import ( // - https://swagger.io/specification/v2/#securityDefinitionsObject // - https://swagger.io/specification/#security-requirement-object type SecurityRequirement struct { - Requirements low.ValueReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[[]low.ValueReference[string]]]] + Requirements low.ValueReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[[]low.ValueReference[string]]]] *low.Reference } @@ -61,7 +61,7 @@ func (s *SecurityRequirement) Build(_ context.Context, _, root *yaml.Node, _ *in }, ) } - s.Requirements = low.ValueReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[[]low.ValueReference[string]]]]{ + s.Requirements = low.ValueReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[[]low.ValueReference[string]]]]{ Value: valueMap, ValueNode: root, } @@ -91,22 +91,14 @@ func (s *SecurityRequirement) GetKeys() []string { // Hash will return a consistent SHA256 Hash of the SecurityRequirement object func (s *SecurityRequirement) Hash() [32]byte { var f []string - values := make(map[string][]string, orderedmap.Len(s.Requirements.Value)) - var valKeys []string - for pair := orderedmap.First(s.Requirements.Value); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(orderedmap.SortAlpha(s.Requirements.Value)); pair != nil; pair = pair.Next() { var vals []string for y := range pair.Value().Value { vals = append(vals, pair.Value().Value[y].Value) } sort.Strings(vals) - valKeys = append(valKeys, pair.Key().Value) - if len(vals) > 0 { - values[pair.Key().Value] = vals - } - } - sort.Strings(valKeys) - for val := range valKeys { - f = append(f, fmt.Sprintf("%s-%s", valKeys[val], strings.Join(values[valKeys[val]], "|"))) + + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, strings.Join(vals, "|"))) } return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/base/tag.go b/datamodel/low/base/tag.go index 0cdea0d..0e7bb83 100644 --- a/datamodel/low/base/tag.go +++ b/datamodel/low/base/tag.go @@ -6,13 +6,13 @@ package base import ( "context" "crypto/sha256" - "fmt" + "strings" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sort" - "strings" ) // Tag represents a low-level Tag instance that is backed by a low-level one. @@ -25,13 +25,13 @@ type Tag struct { Name low.NodeReference[string] Description low.NodeReference[string] ExternalDocs low.NodeReference[*ExternalDoc] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // FindExtension returns a ValueReference containing the extension value, if found. -func (t *Tag) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, t.Extensions) +func (t *Tag) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, t.Extensions) } // Build will extract extensions and external docs for the Tag. @@ -48,7 +48,7 @@ func (t *Tag) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecInde } // GetExtensions returns all Tag extensions and satisfies the low.HasExtensions interface. -func (t *Tag) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (t *Tag) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return t.Extensions } @@ -64,13 +64,6 @@ func (t *Tag) Hash() [32]byte { if !t.ExternalDocs.IsEmpty() { f = append(f, low.GenerateHashString(t.ExternalDocs.Value)) } - keys := make([]string, len(t.Extensions)) - z := 0 - for k := range t.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(t.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(t.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/base/tag_test.go b/datamodel/low/base/tag_test.go index a53b628..34fec98 100644 --- a/datamodel/low/base/tag_test.go +++ b/datamodel/low/base/tag_test.go @@ -5,15 +5,16 @@ package base import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestTag_Build(t *testing.T) { - yml := `name: a tag description: a description externalDocs: @@ -33,13 +34,15 @@ x-coffee: tasty` assert.Equal(t, "a tag", n.Name.Value) assert.Equal(t, "a description", n.Description.Value) assert.Equal(t, "https://pb33f.io", n.ExternalDocs.Value.URL.Value) - assert.Equal(t, "tasty", n.FindExtension("x-coffee").Value) - assert.Len(t, n.GetExtensions(), 1) + var xCoffee string + _ = n.FindExtension("x-coffee").GetValue().Decode(&xCoffee) + + assert.Equal(t, "tasty", xCoffee) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestTag_Build_Error(t *testing.T) { - yml := `name: a tag description: a description externalDocs: @@ -58,7 +61,6 @@ externalDocs: } func TestTag_Hash(t *testing.T) { - left := `name: melody description: my princess externalDocs: @@ -84,5 +86,4 @@ x-b33f: princess` _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) assert.Equal(t, lDoc.Hash(), rDoc.Hash()) - } diff --git a/datamodel/low/base/xml.go b/datamodel/low/base/xml.go index d6d9420..5c33e48 100644 --- a/datamodel/low/base/xml.go +++ b/datamodel/low/base/xml.go @@ -3,12 +3,13 @@ package base import ( "crypto/sha256" "fmt" + "strings" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sort" - "strings" ) // XML represents a low-level representation of an XML object defined by all versions of OpenAPI. @@ -26,7 +27,7 @@ type XML struct { Prefix low.NodeReference[string] Attribute low.NodeReference[bool] Wrapped low.NodeReference[bool] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } @@ -40,7 +41,7 @@ func (x *XML) Build(root *yaml.Node, _ *index.SpecIndex) error { } // GetExtensions returns all Tag extensions and satisfies the low.HasExtensions interface. -func (x *XML) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (x *XML) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return x.Extensions } @@ -62,13 +63,6 @@ func (x *XML) Hash() [32]byte { if !x.Wrapped.IsEmpty() { f = append(f, fmt.Sprint(x.Wrapped.Value)) } - keys := make([]string, len(x.Extensions)) - z := 0 - for k := range x.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(x.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(x.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/extraction_functions.go b/datamodel/low/extraction_functions.go index 0c920f4..f634948 100644 --- a/datamodel/low/extraction_functions.go +++ b/datamodel/low/extraction_functions.go @@ -7,37 +7,24 @@ import ( "context" "crypto/sha256" "fmt" + "net/url" + "path/filepath" "reflect" - "strconv" "strings" + "sync" + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" "gopkg.in/yaml.v3" - "net/url" - "path/filepath" ) -// FindItemInMap accepts a string key and a collection of KeyReference[string] and ValueReference[T]. Every -// KeyReference will have its value checked against the string key and if there is a match, it will be returned. -func FindItemInMap[T any](item string, collection map[KeyReference[string]]ValueReference[T]) *ValueReference[T] { - for n, o := range collection { - if n.Value == item { - return &o - } - if strings.EqualFold(item, n.Value) { - return &o - } - } - return nil -} - // FindItemInOrderedMap accepts a string key and a collection of KeyReference[string] and ValueReference[T]. // Every KeyReference will have its value checked against the string key and if there is a match, it will be // returned. -func FindItemInOrderedMap[T any](item string, collection orderedmap.Map[KeyReference[string], ValueReference[T]]) *ValueReference[T] { +func FindItemInOrderedMap[T any](item string, collection *orderedmap.Map[KeyReference[string], ValueReference[T]]) *ValueReference[T] { for pair := orderedmap.First(collection); pair != nil; pair = pair.Next() { n := pair.Key() if n.Value == item { @@ -50,6 +37,18 @@ func FindItemInOrderedMap[T any](item string, collection orderedmap.Map[KeyRefer return nil } +// HashExtensions will generate a hash from the low representation of extensions. +func HashExtensions(ext *orderedmap.Map[KeyReference[string], ValueReference[*yaml.Node]]) []string { + f := []string{} + + for pair := orderedmap.First(orderedmap.SortAlpha(ext)); pair != nil; pair = pair.Next() { + b, _ := yaml.Marshal(pair.Value().GetValue()) + f = append(f, fmt.Sprintf("%s-%x", pair.Key().Value, sha256.Sum256([]byte(b)))) + } + + return f +} + // helper function to generate a list of all the things an index should be searched for. func generateIndexCollection(idx *index.SpecIndex) []func() map[string]*index.Reference { return []func() map[string]*index.Reference{ @@ -68,7 +67,6 @@ func generateIndexCollection(idx *index.SpecIndex) []func() map[string]*index.Re } func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) (*yaml.Node, *index.SpecIndex, error, context.Context) { - if rf, _, rv := utils.IsNodeRefValue(root); rf { if rv == "" { @@ -112,9 +110,7 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S explodedRefValue := strings.Split(rv, "#") if len(explodedRefValue) == 2 { if !strings.HasPrefix(explodedRefValue[0], "http") { - if !filepath.IsAbs(explodedRefValue[0]) { - if strings.HasPrefix(specPath, "http") { u, _ := url.Parse(specPath) p := "" @@ -137,7 +133,6 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S } rv = fmt.Sprintf("%s#%s", abs, explodedRefValue[1]) } else { - // check for a config baseURL and use that if it exists. if idx.GetConfig().BaseURL != nil { @@ -154,11 +149,8 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S } } } else { - if !strings.HasPrefix(explodedRefValue[0], "http") { - if !filepath.IsAbs(explodedRefValue[0]) { - if strings.HasPrefix(specPath, "http") { u, _ := url.Parse(specPath) p := filepath.Dir(u.Path) @@ -173,7 +165,6 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S rv = abs } else { - // check for a config baseURL and use that if it exists. if idx.GetConfig().BaseURL != nil { u := *idx.GetConfig().BaseURL @@ -211,7 +202,6 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S rv, root.Line, root.Column), ctx } return nil, idx, nil, ctx - } // LocateRefNode will perform a complete lookup for a $ref node. This function searches the entire index for @@ -227,10 +217,12 @@ func ExtractObjectRaw[T Buildable[N], N any](ctx context.Context, key, root *yam var circError error var isReference bool var referenceValue string + var refNode *yaml.Node root = utils.NodeAlias(root) if h, _, rv := utils.IsNodeRefValue(root); h { ref, fIdx, err, nCtx := LocateRefNodeWithContext(ctx, root, idx) if ref != nil { + refNode = root root = ref isReference = true referenceValue = rv @@ -257,7 +249,7 @@ func ExtractObjectRaw[T Buildable[N], N any](ctx context.Context, key, root *yam // if this is a reference, keep track of the reference in the value if isReference { - SetReference(n, referenceValue) + SetReference(n, referenceValue, refNode) } // do we want to throw an error as well if circular error reporting is on? @@ -274,10 +266,12 @@ func ExtractObject[T Buildable[N], N any](ctx context.Context, label string, roo var circError error var isReference bool var referenceValue string + var refNode *yaml.Node root = utils.NodeAlias(root) if rf, rl, refVal := utils.IsNodeRefValue(root); rf { ref, fIdx, err, nCtx := LocateRefNodeWithContext(ctx, root, idx) if ref != nil { + refNode = root vn = ref ln = rl isReference = true @@ -298,6 +292,7 @@ func ExtractObject[T Buildable[N], N any](ctx context.Context, label string, roo if h, _, rVal := utils.IsNodeRefValue(vn); h { ref, fIdx, lerr, nCtx := LocateRefNodeWithContext(ctx, vn, idx) if ref != nil { + refNode = vn vn = ref if fIdx != nil { idx = fIdx @@ -331,16 +326,15 @@ func ExtractObject[T Buildable[N], N any](ctx context.Context, label string, roo // if this is a reference, keep track of the reference in the value if isReference { - SetReference(n, referenceValue) + SetReference(n, referenceValue, refNode) } res := NodeReference[T]{ - Value: n, - KeyNode: ln, - ValueNode: vn, - ReferenceNode: isReference, - Reference: referenceValue, + Value: n, + KeyNode: ln, + ValueNode: vn, } + res.SetReference(referenceValue, refNode) // do we want to throw an error as well if circular error reporting is on? if circError != nil && !idx.AllowCircularReferenceResolving() { @@ -349,12 +343,13 @@ func ExtractObject[T Buildable[N], N any](ctx context.Context, label string, roo return res, nil } -func SetReference(obj any, ref string) { +func SetReference(obj any, ref string, refNode *yaml.Node) { if obj == nil { return } - if r, ok := obj.(IsReferenced); ok { - r.SetReference(ref) + + if r, ok := obj.(SetReferencer); ok { + r.SetReference(ref, refNode) } } @@ -429,9 +424,12 @@ func ExtractArray[T Buildable[N], N any](ctx context.Context, label string, root foundCtx := ctx foundIndex := idx + var refNode *yaml.Node + if rf, _, rv := utils.IsNodeRefValue(node); rf { refg, fIdx, err, nCtx := LocateRefEnd(ctx, node, idx, 0) if refg != nil { + refNode = node node = refg localReferenceValue = rv foundIndex = fIdx @@ -457,15 +455,16 @@ func ExtractArray[T Buildable[N], N any](ctx context.Context, label string, root } if localReferenceValue != "" { - SetReference(n, localReferenceValue) + SetReference(n, localReferenceValue, refNode) } - items = append(items, ValueReference[T]{ - Value: n, - ValueNode: node, - ReferenceNode: localReferenceValue != "", - Reference: localReferenceValue, - }) + v := ValueReference[T]{ + Value: n, + ValueNode: node, + } + v.SetReference(localReferenceValue, refNode) + + items = append(items, v) } } // include circular errors? @@ -475,23 +474,6 @@ func ExtractArray[T Buildable[N], N any](ctx context.Context, label string, root return items, ln, vn, nil } -// ExtractExample will extract a value supplied as an example into a NodeReference. Value can be anything. -// the node value is untyped, so casting will be required when trying to use it. -func ExtractExample(expNode, expLabel *yaml.Node) NodeReference[any] { - ref := NodeReference[any]{Value: expNode.Value, KeyNode: expLabel, ValueNode: expNode} - if utils.IsNodeMap(expNode) { - var decoded map[string]interface{} - _ = expNode.Decode(&decoded) - ref.Value = decoded - } - if utils.IsNodeArray(expNode) { - var decoded []interface{} - _ = expNode.Decode(&decoded) - ref.Value = decoded - } - return ref -} - // ExtractMapNoLookupExtensions will extract a map of KeyReference and ValueReference from a root yaml.Node. The 'NoLookup' part // refers to the fact that there is no key supplied as part of the extraction, there is no lookup performed and the // root yaml.Node pointer is used directly. Pass a true bit to includeExtensions to include extension keys in the map. @@ -502,7 +484,7 @@ func ExtractMapNoLookupExtensions[PT Buildable[N], N any]( root *yaml.Node, idx *index.SpecIndex, includeExtensions bool, -) (orderedmap.Map[KeyReference[string], ValueReference[PT]], error) { +) (*orderedmap.Map[KeyReference[string], ValueReference[PT]], error) { valueMap := orderedmap.New[KeyReference[string], ValueReference[PT]]() var circError error if utils.IsNodeMap(root) { @@ -540,10 +522,12 @@ func ExtractMapNoLookupExtensions[PT Buildable[N], N any]( var isReference bool var referenceValue string + var refNode *yaml.Node // if value is a reference, we have to look it up in the index! if h, _, rv := utils.IsNodeRefValue(node); h { ref, fIdx, err, nCtx := LocateRefNodeWithContext(ctx, node, idx) if ref != nil { + refNode = node node = ref isReference = true referenceValue = rv @@ -570,19 +554,21 @@ func ExtractMapNoLookupExtensions[PT Buildable[N], N any]( return nil, berr } if isReference { - SetReference(n, referenceValue) + SetReference(n, referenceValue, refNode) } if currentKey != nil { + v := ValueReference[PT]{ + Value: n, + ValueNode: node, + } + v.SetReference(referenceValue, refNode) + valueMap.Set( KeyReference[string]{ Value: currentKey.Value, KeyNode: currentKey, }, - ValueReference[PT]{ - Value: n, - ValueNode: node, - Reference: referenceValue, - }, + v, ) } } @@ -591,7 +577,6 @@ func ExtractMapNoLookupExtensions[PT Buildable[N], N any]( return valueMap, circError } return valueMap, nil - } // ExtractMapNoLookup will extract a map of KeyReference and ValueReference from a root yaml.Node. The 'NoLookup' part @@ -603,7 +588,7 @@ func ExtractMapNoLookup[PT Buildable[N], N any]( ctx context.Context, root *yaml.Node, idx *index.SpecIndex, -) (orderedmap.Map[KeyReference[string], ValueReference[PT]], error) { +) (*orderedmap.Map[KeyReference[string], ValueReference[PT]], error) { return ExtractMapNoLookupExtensions[PT, N](ctx, root, idx, false) } @@ -624,7 +609,186 @@ func ExtractMapExtensions[PT Buildable[N], N any]( root *yaml.Node, idx *index.SpecIndex, extensions bool, -) (orderedmap.Map[KeyReference[string], ValueReference[PT]], *yaml.Node, *yaml.Node, error) { +) (*orderedmap.Map[KeyReference[string], ValueReference[PT]], *yaml.Node, *yaml.Node, error) { + var labelNode, valueNode *yaml.Node + var circError error + root = utils.NodeAlias(root) + if rf, rl, _ := utils.IsNodeRefValue(root); rf { + // locate reference in index. + ref, fIdx, err, fCtx := LocateRefNodeWithContext(ctx, root, idx) + if ref != nil { + valueNode = ref + labelNode = rl + ctx = fCtx + idx = fIdx + if err != nil { + circError = err + } + } else { + return nil, labelNode, valueNode, fmt.Errorf("map build failed: reference cannot be found: %s", + root.Content[1].Value) + } + } else { + _, labelNode, valueNode = utils.FindKeyNodeFull(label, root.Content) + valueNode = utils.NodeAlias(valueNode) + if valueNode != nil { + if h, _, _ := utils.IsNodeRefValue(valueNode); h { + ref, fIdx, err, nCtx := LocateRefNodeWithContext(ctx, valueNode, idx) + if ref != nil { + valueNode = ref + idx = fIdx + ctx = nCtx + if err != nil { + circError = err + } + } else { + if err != nil { + return nil, labelNode, valueNode, fmt.Errorf("map build failed: reference cannot be found: %s", + err.Error()) + } + } + } + } + } + if valueNode != nil { + valueMap := orderedmap.New[KeyReference[string], ValueReference[PT]]() + + type buildInput struct { + label *yaml.Node + value *yaml.Node + } + in := make(chan buildInput) + out := make(chan mappingResult[PT]) + done := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(2) // input and output goroutines. + + // TranslatePipeline input. + go func() { + defer func() { + close(in) + wg.Done() + }() + var currentLabelNode *yaml.Node + for i, en := range valueNode.Content { + if !extensions { + if strings.HasPrefix(en.Value, "x-") { + continue // yo, don't pay any attention to extensions, not here anyway. + } + } + + en = utils.NodeAlias(en) + if i%2 == 0 { + currentLabelNode = en + continue + } + + select { + case in <- buildInput{ + label: currentLabelNode, + value: en, + }: + case <-done: + return + } + } + }() + + // TranslatePipeline output. + go func() { + for { + result, ok := <-out + if !ok { + break + } + valueMap.Set(result.k, result.v) + } + close(done) + wg.Done() + }() + + translateFunc := func(input buildInput) (mappingResult[PT], error) { + foundIndex := idx + foundContext := ctx + + en := input.value + + var refNode *yaml.Node + var referenceValue string + // check our valueNode isn't a reference still. + if h, _, refVal := utils.IsNodeRefValue(en); h { + ref, fIdx, err, nCtx := LocateRefNodeWithContext(ctx, en, idx) + if ref != nil { + refNode = en + en = ref + referenceValue = refVal + if fIdx != nil { + foundIndex = fIdx + } + foundContext = nCtx + if err != nil { + circError = err + } + } else { + if err != nil { + return mappingResult[PT]{}, fmt.Errorf("flat map build failed: reference cannot be found: %s", + err.Error()) + } + } + } + + var n PT = new(N) + en = utils.NodeAlias(en) + _ = BuildModel(en, n) + err := n.Build(foundContext, input.label, en, foundIndex) + if err != nil { + return mappingResult[PT]{}, err + } + + if referenceValue != "" { + SetReference(n, referenceValue, refNode) + } + + v := ValueReference[PT]{ + Value: n, + ValueNode: en, + } + v.SetReference(referenceValue, refNode) + + return mappingResult[PT]{ + k: KeyReference[string]{ + KeyNode: input.label, + Value: input.label.Value, + }, + v: v, + }, nil + } + err := datamodel.TranslatePipeline[buildInput, mappingResult[PT]](in, out, translateFunc) + wg.Wait() + if err != nil { + return nil, labelNode, valueNode, err + } + if circError != nil && !idx.AllowCircularReferenceResolving() { + return valueMap, labelNode, valueNode, circError + } + return valueMap, labelNode, valueNode, nil + } + return nil, labelNode, valueNode, nil +} + +// ExtractMapExtensions will extract a map of KeyReference and ValueReference from a root yaml.Node. The 'label' is +// used to locate the node to be extracted from the root node supplied. Supply a bit to decide if extensions should +// be included or not. required in some use cases. +// +// The second return value is the yaml.Node found for the 'label' and the third return value is the yaml.Node +// found for the value extracted from the label node. +func ExtractMapExtensionsOld[PT Buildable[N], N any]( + ctx context.Context, + label string, + root *yaml.Node, + idx *index.SpecIndex, + extensions bool, +) (*orderedmap.Map[KeyReference[string], ValueReference[PT]], *yaml.Node, *yaml.Node, error) { var referenceValue string var labelNode, valueNode *yaml.Node var circError error @@ -687,7 +851,7 @@ func ExtractMapExtensions[PT Buildable[N], N any]( } if ref != "" { - SetReference(n, ref) + SetReference(n, ref, nil) } c <- mappingResult[PT]{ @@ -698,7 +862,7 @@ func ExtractMapExtensions[PT Buildable[N], N any]( v: ValueReference[PT]{ Value: n, ValueNode: value, - Reference: ref, + // Reference: ref, }, } } @@ -773,7 +937,7 @@ func ExtractMap[PT Buildable[N], N any]( label string, root *yaml.Node, idx *index.SpecIndex, -) (orderedmap.Map[KeyReference[string], ValueReference[PT]], *yaml.Node, *yaml.Node, error) { +) (*orderedmap.Map[KeyReference[string], ValueReference[PT]], *yaml.Node, *yaml.Node, error) { return ExtractMapExtensions[PT, N](ctx, label, root, idx, false) } @@ -781,7 +945,7 @@ func ExtractMap[PT Buildable[N], N any]( // // Maps // -// map[string]interface{} for maps +// *orderedmap.Map[string, *yaml.Node] for maps // // Slices // @@ -790,54 +954,15 @@ func ExtractMap[PT Buildable[N], N any]( // int, float, bool, string // // int64, float64, bool, string -func ExtractExtensions(root *yaml.Node) map[KeyReference[string]]ValueReference[any] { +func ExtractExtensions(root *yaml.Node) *orderedmap.Map[KeyReference[string], ValueReference[*yaml.Node]] { root = utils.NodeAlias(root) extensions := utils.FindExtensionNodes(root.Content) - extensionMap := make(map[KeyReference[string]]ValueReference[any]) + extensionMap := orderedmap.New[KeyReference[string], ValueReference[*yaml.Node]]() for _, ext := range extensions { - if utils.IsNodeMap(ext.Value) { - var v interface{} - _ = ext.Value.Decode(&v) - extensionMap[KeyReference[string]{ - Value: ext.Key.Value, - KeyNode: ext.Key, - }] = ValueReference[any]{Value: v, ValueNode: ext.Value} - } - if utils.IsNodeStringValue(ext.Value) { - extensionMap[KeyReference[string]{ - Value: ext.Key.Value, - KeyNode: ext.Key, - }] = ValueReference[any]{Value: ext.Value.Value, ValueNode: ext.Value} - } - if utils.IsNodeFloatValue(ext.Value) { - fv, _ := strconv.ParseFloat(ext.Value.Value, 64) - extensionMap[KeyReference[string]{ - Value: ext.Key.Value, - KeyNode: ext.Key, - }] = ValueReference[any]{Value: fv, ValueNode: ext.Value} - } - if utils.IsNodeIntValue(ext.Value) { - iv, _ := strconv.ParseInt(ext.Value.Value, 10, 64) - extensionMap[KeyReference[string]{ - Value: ext.Key.Value, - KeyNode: ext.Key, - }] = ValueReference[any]{Value: iv, ValueNode: ext.Value} - } - if utils.IsNodeBoolValue(ext.Value) { - bv, _ := strconv.ParseBool(ext.Value.Value) - extensionMap[KeyReference[string]{ - Value: ext.Key.Value, - KeyNode: ext.Key, - }] = ValueReference[any]{Value: bv, ValueNode: ext.Value} - } - if utils.IsNodeArray(ext.Value) { - var v []interface{} - _ = ext.Value.Decode(&v) - extensionMap[KeyReference[string]{ - Value: ext.Key.Value, - KeyNode: ext.Key, - }] = ValueReference[any]{Value: v, ValueNode: ext.Value} - } + extensionMap.Set(KeyReference[string]{ + Value: ext.Key.Value, + KeyNode: ext.Key, + }, ValueReference[*yaml.Node]{Value: ext.Value, ValueNode: ext.Value}) } return extensionMap } @@ -869,6 +994,10 @@ func GenerateHashString(v any) string { return fmt.Sprintf(HASH, h.Hash()) } } + if n, ok := v.(*yaml.Node); ok { + b, _ := yaml.Marshal(n) + return fmt.Sprintf(HASH, sha256.Sum256(b)) + } // if we get here, we're a primitive, check if we're a pointer and de-point if reflect.TypeOf(v).Kind() == reflect.Ptr { v = reflect.ValueOf(v).Elem().Interface() @@ -876,6 +1005,15 @@ func GenerateHashString(v any) string { return fmt.Sprintf(HASH, sha256.Sum256([]byte(fmt.Sprint(v)))) } +func ValueToString(v any) string { + if n, ok := v.(*yaml.Node); ok { + b, _ := yaml.Marshal(n) + return string(b) + } + + return fmt.Sprint(v) +} + // LocateRefEnd will perform a complete lookup for a $ref node. This function searches the entire index for // the reference being supplied. If there is a match found, the reference *yaml.Node is returned. // the function operates recursively and will keep iterating through references until it finds a non-reference diff --git a/datamodel/low/extraction_functions_test.go b/datamodel/low/extraction_functions_test.go index 344c31e..7e42f22 100644 --- a/datamodel/low/extraction_functions_test.go +++ b/datamodel/low/extraction_functions_test.go @@ -7,51 +7,52 @@ import ( "context" "crypto/sha256" "fmt" - "golang.org/x/sync/syncmap" - "gopkg.in/yaml.v3" "net/url" "os" "path/filepath" "strings" "testing" + "golang.org/x/sync/syncmap" + "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestFindItemInMap(t *testing.T) { - v := make(map[KeyReference[string]]ValueReference[string]) - v[KeyReference[string]{ +func TestFindItemInOrderedMap(t *testing.T) { + v := orderedmap.New[KeyReference[string], ValueReference[string]]() + v.Set(KeyReference[string]{ Value: "pizza", - }] = ValueReference[string]{ + }, ValueReference[string]{ Value: "pie", - } - assert.Equal(t, "pie", FindItemInMap("pizza", v).Value) + }) + assert.Equal(t, "pie", FindItemInOrderedMap("pizza", v).Value) } -func TestFindItemInMap_WrongCase(t *testing.T) { - v := make(map[KeyReference[string]]ValueReference[string]) - v[KeyReference[string]{ +func TestFindItemInOrderedMap_WrongCase(t *testing.T) { + v := orderedmap.New[KeyReference[string], ValueReference[string]]() + v.Set(KeyReference[string]{ Value: "pizza", - }] = ValueReference[string]{ + }, ValueReference[string]{ Value: "pie", - } - assert.Equal(t, "pie", FindItemInMap("PIZZA", v).Value) + }) + assert.Equal(t, "pie", FindItemInOrderedMap("PIZZA", v).Value) } -func TestFindItemInMap_Error(t *testing.T) { - v := make(map[KeyReference[string]]ValueReference[string]) - v[KeyReference[string]{ +func TestFindItemInOrderedMap_Error(t *testing.T) { + v := orderedmap.New[KeyReference[string], ValueReference[string]]() + v.Set(KeyReference[string]{ Value: "pizza", - }] = ValueReference[string]{ + }, ValueReference[string]{ Value: "pie", - } - assert.Nil(t, FindItemInMap("nuggets", v)) + }) + assert.Nil(t, FindItemInOrderedMap("nuggets", v)) } func TestLocateRefNode(t *testing.T) { - yml := `components: schemas: cake: @@ -69,11 +70,9 @@ func TestLocateRefNode(t *testing.T) { located, _, _ := LocateRefNode(cNode.Content[0], idx) assert.NotNil(t, located) - } func TestLocateRefNode_BadNode(t *testing.T) { - yml := `components: schemas: cake: @@ -94,11 +93,9 @@ func TestLocateRefNode_BadNode(t *testing.T) { // should both be empty. assert.Nil(t, located) assert.Nil(t, err) - } func TestLocateRefNode_Path(t *testing.T) { - yml := `paths: /burger/time: description: hello` @@ -115,11 +112,9 @@ func TestLocateRefNode_Path(t *testing.T) { located, _, _ := LocateRefNode(cNode.Content[0], idx) assert.NotNil(t, located) - } func TestLocateRefNode_Path_NotFound(t *testing.T) { - yml := `paths: /burger/time: description: hello` @@ -137,7 +132,6 @@ func TestLocateRefNode_Path_NotFound(t *testing.T) { located, _, err := LocateRefNode(cNode.Content[0], idx) assert.Nil(t, located) assert.Error(t, err) - } type pizza struct { @@ -149,7 +143,6 @@ func (p *pizza) Build(_ context.Context, _, _ *yaml.Node, _ *index.SpecIndex) er } func TestExtractObject(t *testing.T) { - yml := `components: schemas: pizza: @@ -173,7 +166,6 @@ func TestExtractObject(t *testing.T) { } func TestExtractObject_Ref(t *testing.T) { - yml := `components: schemas: pizza: @@ -197,7 +189,6 @@ func TestExtractObject_Ref(t *testing.T) { } func TestExtractObject_DoubleRef(t *testing.T) { - yml := `components: schemas: cake: @@ -282,7 +273,6 @@ func TestExtractObject_DoubleRef_Circular_Fail(t *testing.T) { } func TestExtractObject_DoubleRef_Circular_Direct(t *testing.T) { - yml := `components: schemas: loopy: @@ -312,7 +302,6 @@ func TestExtractObject_DoubleRef_Circular_Direct(t *testing.T) { } func TestExtractObject_DoubleRef_Circular_Direct_Fail(t *testing.T) { - yml := `components: schemas: loopy: @@ -338,7 +327,6 @@ func TestExtractObject_DoubleRef_Circular_Direct_Fail(t *testing.T) { _, err := ExtractObject[*pizza](context.Background(), "tags", cNode.Content[0], idx) assert.Error(t, err) - } type test_borked struct { @@ -374,7 +362,6 @@ func (t *test_Good) Build(_ context.Context, _, root *yaml.Node, idx *index.Spec } func TestExtractObject_BadLowLevelModel(t *testing.T) { - yml := `components: schemas: hey:` @@ -391,11 +378,9 @@ func TestExtractObject_BadLowLevelModel(t *testing.T) { _, err := ExtractObject[*test_noGood](context.Background(), "thing", &cNode, idx) assert.Error(t, err) - } func TestExtractObject_BadBuild(t *testing.T) { - yml := `components: schemas: hey:` @@ -412,11 +397,9 @@ func TestExtractObject_BadBuild(t *testing.T) { _, err := ExtractObject[*test_almostGood](context.Background(), "thing", &cNode, idx) assert.Error(t, err) - } func TestExtractObject_BadLabel(t *testing.T) { - yml := `components: schemas: hey:` @@ -434,11 +417,9 @@ func TestExtractObject_BadLabel(t *testing.T) { res, err := ExtractObject[*test_almostGood](context.Background(), "ding", &cNode, idx) assert.Nil(t, res.Value) assert.NoError(t, err) - } func TestExtractObject_PathIsCircular(t *testing.T) { - // first we need an index. yml := `paths: '/something/here': @@ -467,11 +448,9 @@ func TestExtractObject_PathIsCircular(t *testing.T) { res, err := ExtractObject[*test_Good](context.Background(), "thing", &rootNode, idx) assert.NotNil(t, res.Value) assert.Error(t, err) // circular error would have been thrown. - } func TestExtractObject_PathIsCircular_IgnoreErrors(t *testing.T) { - // first we need an index. yml := `paths: '/something/here': @@ -503,11 +482,9 @@ func TestExtractObject_PathIsCircular_IgnoreErrors(t *testing.T) { res, err := ExtractObject[*test_Good](context.Background(), "thing", &rootNode, idx) assert.NotNil(t, res.Value) assert.NoError(t, err) // circular error would have been thrown, but we're ignoring them. - } func TestExtractObjectRaw(t *testing.T) { - yml := `components: schemas: pizza: @@ -530,7 +507,6 @@ func TestExtractObjectRaw(t *testing.T) { } func TestExtractObjectRaw_With_Ref(t *testing.T) { - yml := `components: schemas: pizza: @@ -555,7 +531,6 @@ func TestExtractObjectRaw_With_Ref(t *testing.T) { } func TestExtractObjectRaw_Ref_Circular(t *testing.T) { - yml := `components: schemas: pizza: @@ -579,11 +554,9 @@ func TestExtractObjectRaw_Ref_Circular(t *testing.T) { tag, err, _, _ := ExtractObjectRaw[*pizza](context.Background(), nil, cNode.Content[0], idx) assert.Error(t, err) assert.NotNil(t, tag) - } func TestExtractObjectRaw_RefBroken(t *testing.T) { - yml := `components: schemas: pizza: @@ -601,11 +574,9 @@ func TestExtractObjectRaw_RefBroken(t *testing.T) { tag, err, _, _ := ExtractObjectRaw[*pizza](context.Background(), nil, cNode.Content[0], idx) assert.Error(t, err) assert.Nil(t, tag) - } func TestExtractObjectRaw_Ref_NonBuildable(t *testing.T) { - yml := `components: schemas: pizza: @@ -622,11 +593,9 @@ func TestExtractObjectRaw_Ref_NonBuildable(t *testing.T) { _, err, _, _ := ExtractObjectRaw[*test_noGood](context.Background(), nil, cNode.Content[0], idx) assert.Error(t, err) - } func TestExtractObjectRaw_Ref_AlmostBuildable(t *testing.T) { - yml := `components: schemas: pizza: @@ -643,11 +612,9 @@ func TestExtractObjectRaw_Ref_AlmostBuildable(t *testing.T) { _, err, _, _ := ExtractObjectRaw[*test_almostGood](context.Background(), nil, cNode.Content[0], idx) assert.Error(t, err) - } func TestExtractArray(t *testing.T) { - yml := `components: schemas: pizza: @@ -675,7 +642,6 @@ func TestExtractArray(t *testing.T) { } func TestExtractArray_Ref(t *testing.T) { - yml := `components: schemas: things: @@ -702,7 +668,6 @@ func TestExtractArray_Ref(t *testing.T) { } func TestExtractArray_Ref_Unbuildable(t *testing.T) { - yml := `components: schemas: things: @@ -726,7 +691,6 @@ func TestExtractArray_Ref_Unbuildable(t *testing.T) { } func TestExtractArray_Ref_Circular(t *testing.T) { - yml := `components: schemas: thongs: @@ -754,7 +718,6 @@ func TestExtractArray_Ref_Circular(t *testing.T) { } func TestExtractArray_Ref_Bad(t *testing.T) { - yml := `components: schemas: thongs: @@ -782,7 +745,6 @@ func TestExtractArray_Ref_Bad(t *testing.T) { } func TestExtractArray_Ref_Nested(t *testing.T) { - yml := `components: schemas: thongs: @@ -811,7 +773,6 @@ func TestExtractArray_Ref_Nested(t *testing.T) { } func TestExtractArray_Ref_Nested_Circular(t *testing.T) { - yml := `components: schemas: thongs: @@ -840,7 +801,6 @@ func TestExtractArray_Ref_Nested_Circular(t *testing.T) { } func TestExtractArray_Ref_Nested_BadRef(t *testing.T) { - yml := `components: schemas: thongs: @@ -867,7 +827,6 @@ func TestExtractArray_Ref_Nested_BadRef(t *testing.T) { } func TestExtractArray_Ref_Nested_CircularFlat(t *testing.T) { - yml := `components: schemas: thongs: @@ -896,7 +855,6 @@ func TestExtractArray_Ref_Nested_CircularFlat(t *testing.T) { } func TestExtractArray_BadBuild(t *testing.T) { - yml := `components: schemas: thongs:` @@ -918,7 +876,6 @@ func TestExtractArray_BadBuild(t *testing.T) { } func TestExtractArray_BadRefPropsTupe(t *testing.T) { - yml := `components: parameters: cakes: @@ -940,45 +897,7 @@ func TestExtractArray_BadRefPropsTupe(t *testing.T) { assert.Len(t, things, 0) } -func TestExtractExample_String(t *testing.T) { - yml := `hi` - var e yaml.Node - _ = yaml.Unmarshal([]byte(yml), &e) - - exp := ExtractExample(e.Content[0], e.Content[0]) - assert.NotNil(t, exp.Value) - assert.Equal(t, "hi", exp.Value) -} -func TestExtractExample_Map(t *testing.T) { - yml := `one: two` - var e yaml.Node - _ = yaml.Unmarshal([]byte(yml), &e) - - exp := ExtractExample(e.Content[0], e.Content[0]) - assert.NotNil(t, exp.Value) - if n, ok := exp.Value.(map[string]interface{}); ok { - assert.Equal(t, "two", n["one"]) - } else { - panic("example unpacked incorrectly.") - } -} - -func TestExtractExample_Array(t *testing.T) { - yml := `- hello` - var e yaml.Node - _ = yaml.Unmarshal([]byte(yml), &e) - - exp := ExtractExample(e.Content[0], e.Content[0]) - assert.NotNil(t, exp.Value) - if n, ok := exp.Value.([]interface{}); ok { - assert.Equal(t, "hello", n[0]) - } else { - panic("example unpacked incorrectly.") - } -} - func TestExtractMapFlatNoLookup(t *testing.T) { - yml := `components:` var idxNode yaml.Node @@ -997,11 +916,9 @@ one: things, err := ExtractMapNoLookup[*test_Good](context.Background(), cNode.Content[0], idx) assert.NoError(t, err) assert.Equal(t, 1, orderedmap.Len(things)) - } func TestExtractMap_NoLookupWithExtensions(t *testing.T) { - yml := `components:` var idxNode yaml.Node @@ -1031,7 +948,6 @@ one: } func TestExtractMap_NoLookupWithExtensions_UsingMerge(t *testing.T) { - yml := `components:` var idxNode yaml.Node @@ -1053,11 +969,9 @@ one: things, err := ExtractMapNoLookupExtensions[*test_Good](context.Background(), cNode.Content[0], idx, true) assert.NoError(t, err) assert.Equal(t, 4, orderedmap.Len(things)) - } func TestExtractMap_NoLookupWithoutExtensions(t *testing.T) { - yml := `components:` var idxNode yaml.Node @@ -1083,7 +997,6 @@ one: } func TestExtractMap_WithExtensions(t *testing.T) { - yml := `components:` var idxNode yaml.Node @@ -1105,7 +1018,6 @@ one: } func TestExtractMap_WithoutExtensions(t *testing.T) { - yml := `components:` var idxNode yaml.Node @@ -1127,7 +1039,6 @@ one: } func TestExtractMapFlatNoLookup_Ref(t *testing.T) { - yml := `components: schemas: pizza: @@ -1149,11 +1060,9 @@ one: things, err := ExtractMapNoLookup[*test_Good](context.Background(), cNode.Content[0], idx) assert.NoError(t, err) assert.Equal(t, 1, orderedmap.Len(things)) - } func TestExtractMapFlatNoLookup_Ref_Bad(t *testing.T) { - yml := `components: schemas: pizza: @@ -1175,11 +1084,9 @@ one: things, err := ExtractMapNoLookup[*test_Good](context.Background(), cNode.Content[0], idx) assert.Error(t, err) assert.Zero(t, orderedmap.Len(things)) - } func TestExtractMapFlatNoLookup_Ref_Circular(t *testing.T) { - yml := `components: schemas: thongs: @@ -1207,11 +1114,9 @@ one: things, err := ExtractMapNoLookup[*test_Good](context.Background(), cNode.Content[0], idx) assert.Error(t, err) assert.Equal(t, 1, orderedmap.Len(things)) - } func TestExtractMapFlatNoLookup_Ref_BadBuild(t *testing.T) { - yml := `components: schemas: pizza: @@ -1233,11 +1138,9 @@ hello: things, err := ExtractMapNoLookup[*test_noGood](context.Background(), cNode.Content[0], idx) assert.Error(t, err) assert.Zero(t, orderedmap.Len(things)) - } func TestExtractMapFlatNoLookup_Ref_AlmostBuild(t *testing.T) { - yml := `components: schemas: pizza: @@ -1259,11 +1162,9 @@ one: things, err := ExtractMapNoLookup[*test_almostGood](context.Background(), cNode.Content[0], idx) assert.Error(t, err) assert.Zero(t, orderedmap.Len(things)) - } func TestExtractMapFlat(t *testing.T) { - yml := `components:` var idxNode yaml.Node @@ -1282,11 +1183,9 @@ one: things, _, _, err := ExtractMap[*test_Good](context.Background(), "one", cNode.Content[0], idx) assert.NoError(t, err) assert.Equal(t, 1, orderedmap.Len(things)) - } func TestExtractMapFlat_Ref(t *testing.T) { - yml := `components: schemas: stank: @@ -1313,11 +1212,9 @@ one: for pair := orderedmap.First(things); pair != nil; pair = pair.Next() { assert.Equal(t, 99, pair.Value().Value.AlmostWork.Value) } - } func TestExtractMapFlat_DoubleRef(t *testing.T) { - yml := `components: schemas: stank: @@ -1346,7 +1243,6 @@ func TestExtractMapFlat_DoubleRef(t *testing.T) { } func TestExtractMapFlat_DoubleRef_Error(t *testing.T) { - yml := `components: schemas: stank: @@ -1369,11 +1265,9 @@ func TestExtractMapFlat_DoubleRef_Error(t *testing.T) { things, _, _, err := ExtractMap[*test_almostGood](context.Background(), "one", cNode.Content[0], idx) assert.Error(t, err) assert.Zero(t, orderedmap.Len(things)) - } func TestExtractMapFlat_DoubleRef_Error_NotFound(t *testing.T) { - yml := `components: schemas: stank: @@ -1396,11 +1290,9 @@ func TestExtractMapFlat_DoubleRef_Error_NotFound(t *testing.T) { things, _, _, err := ExtractMap[*test_almostGood](context.Background(), "one", cNode.Content[0], idx) assert.Error(t, err) assert.Zero(t, orderedmap.Len(things)) - } func TestExtractMapFlat_DoubleRef_Circles(t *testing.T) { - yml := `components: schemas: stonk: @@ -1428,11 +1320,9 @@ func TestExtractMapFlat_DoubleRef_Circles(t *testing.T) { things, _, _, err := ExtractMap[*test_Good](context.Background(), "one", cNode.Content[0], idx) assert.Error(t, err) assert.Equal(t, 1, orderedmap.Len(things)) - } func TestExtractMapFlat_Ref_Error(t *testing.T) { - yml := `components: schemas: stank: @@ -1455,11 +1345,9 @@ func TestExtractMapFlat_Ref_Error(t *testing.T) { things, _, _, err := ExtractMap[*test_almostGood](context.Background(), "one", cNode.Content[0], idx) assert.Error(t, err) assert.Zero(t, orderedmap.Len(things)) - } func TestExtractMapFlat_Ref_Circ_Error(t *testing.T) { - yml := `components: schemas: stink: @@ -1488,7 +1376,6 @@ func TestExtractMapFlat_Ref_Circ_Error(t *testing.T) { } func TestExtractMapFlat_Ref_Nested_Circ_Error(t *testing.T) { - yml := `components: schemas: stink: @@ -1518,7 +1405,6 @@ func TestExtractMapFlat_Ref_Nested_Circ_Error(t *testing.T) { } func TestExtractMapFlat_Ref_Nested_Error(t *testing.T) { - yml := `components: schemas: stink: @@ -1544,7 +1430,6 @@ func TestExtractMapFlat_Ref_Nested_Error(t *testing.T) { } func TestExtractMapFlat_BadKey_Ref_Nested_Error(t *testing.T) { - yml := `components: schemas: stink: @@ -1570,7 +1455,6 @@ func TestExtractMapFlat_BadKey_Ref_Nested_Error(t *testing.T) { } func TestExtractMapFlat_Ref_Bad(t *testing.T) { - yml := `components: schemas: stink: @@ -1599,7 +1483,6 @@ func TestExtractMapFlat_Ref_Bad(t *testing.T) { } func TestExtractExtensions(t *testing.T) { - yml := `x-bing: ding x-bong: 1 x-ling: true @@ -1612,25 +1495,27 @@ x-tacos: [1,2,3]` _ = yaml.Unmarshal([]byte(yml), &idxNode) r := ExtractExtensions(idxNode.Content[0]) - assert.Len(t, r, 6) - for i := range r { - switch i.Value { + assert.Equal(t, 6, orderedmap.Len(r)) + for pair := orderedmap.First(r); pair != nil; pair = pair.Next() { + var v any + _ = pair.Value().Value.Decode(&v) + + switch pair.Key().Value { case "x-bing": - assert.Equal(t, "ding", r[i].Value) + assert.Equal(t, "ding", v) case "x-bong": - assert.Equal(t, int64(1), r[i].Value) + assert.Equal(t, 1, v) case "x-ling": - assert.Equal(t, true, r[i].Value) + assert.Equal(t, true, v) case "x-long": - assert.Equal(t, 0.99, r[i].Value) + assert.Equal(t, 0.99, v) case "x-fish": - if a, ok := r[i].Value.(map[string]interface{}); ok { - assert.Equal(t, "yeah", a["woo"]) - } else { - panic("should not fail casting") - } + var m map[string]any + err := pair.Value().Value.Decode(&m) + require.NoError(t, err) + assert.Equal(t, "yeah", m["woo"]) case "x-tacos": - assert.Len(t, r[i].Value, 3) + assert.Len(t, v, 3) } } } @@ -1650,8 +1535,8 @@ func (f test_fresh) Hash() [32]byte { } return sha256.Sum256([]byte(strings.Join(data, "|"))) } -func TestAreEqual(t *testing.T) { +func TestAreEqual(t *testing.T) { var hey *test_fresh assert.True(t, AreEqual(test_fresh{val: "hello"}, test_fresh{val: "hello"})) @@ -1664,7 +1549,6 @@ func TestAreEqual(t *testing.T) { } func TestGenerateHashString(t *testing.T) { - assert.Equal(t, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", GenerateHashString(test_fresh{val: "hello"})) @@ -1676,46 +1560,39 @@ func TestGenerateHashString(t *testing.T) { assert.Equal(t, "", GenerateHashString(nil)) - } func TestGenerateHashString_Pointer(t *testing.T) { - val := true assert.Equal(t, "b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b", GenerateHashString(test_fresh{thang: &val})) assert.Equal(t, "b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b", GenerateHashString(&val)) - } func TestSetReference(t *testing.T) { - type testObj struct { *Reference } n := testObj{Reference: &Reference{}} - SetReference(&n, "#/pigeon/street") + SetReference(&n, "#/pigeon/street", nil) assert.Equal(t, "#/pigeon/street", n.GetReference()) - } func TestSetReference_nil(t *testing.T) { - type testObj struct { *Reference } n := testObj{Reference: &Reference{}} - SetReference(nil, "#/pigeon/street") + SetReference(nil, "#/pigeon/street", nil) assert.NotEqual(t, "#/pigeon/street", n.GetReference()) } func TestLocateRefNode_CurrentPathKey_HttpLink(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1741,7 +1618,6 @@ func TestLocateRefNode_CurrentPathKey_HttpLink(t *testing.T) { } func TestLocateRefNode_CurrentPathKey_HttpLink_Local(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1767,7 +1643,6 @@ func TestLocateRefNode_CurrentPathKey_HttpLink_Local(t *testing.T) { } func TestLocateRefNode_CurrentPathKey_HttpLink_RemoteCtx(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1792,7 +1667,6 @@ func TestLocateRefNode_CurrentPathKey_HttpLink_RemoteCtx(t *testing.T) { } func TestLocateRefNode_CurrentPathKey_HttpLink_RemoteCtx_WithPath(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1817,7 +1691,6 @@ func TestLocateRefNode_CurrentPathKey_HttpLink_RemoteCtx_WithPath(t *testing.T) } func TestLocateRefNode_CurrentPathKey_Path_Link(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1842,7 +1715,6 @@ func TestLocateRefNode_CurrentPathKey_Path_Link(t *testing.T) { } func TestLocateRefNode_CurrentPathKey_Path_URL(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1869,7 +1741,6 @@ func TestLocateRefNode_CurrentPathKey_Path_URL(t *testing.T) { } func TestLocateRefNode_CurrentPathKey_DeeperPath_URL(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1896,7 +1767,6 @@ func TestLocateRefNode_CurrentPathKey_DeeperPath_URL(t *testing.T) { } func TestLocateRefNode_NoExplode(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1923,7 +1793,6 @@ func TestLocateRefNode_NoExplode(t *testing.T) { } func TestLocateRefNode_NoExplode_HTTP(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1951,7 +1820,6 @@ func TestLocateRefNode_NoExplode_HTTP(t *testing.T) { } func TestLocateRefNode_NoExplode_NoSpecPath(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -1979,7 +1847,6 @@ func TestLocateRefNode_NoExplode_NoSpecPath(t *testing.T) { } func TestLocateRefNode_DoARealLookup(t *testing.T) { - no := yaml.Node{ Kind: yaml.MappingNode, Content: []*yaml.Node{ @@ -2020,7 +1887,6 @@ func TestLocateRefNode_DoARealLookup(t *testing.T) { } func TestLocateRefEndNoRef_NoName(t *testing.T) { - r := &yaml.Node{Content: []*yaml.Node{{Kind: yaml.ScalarNode, Value: "$ref"}, {Kind: yaml.ScalarNode, Value: ""}}} n, i, e, c := LocateRefEnd(nil, r, nil, 0) assert.Nil(t, n) @@ -2030,7 +1896,6 @@ func TestLocateRefEndNoRef_NoName(t *testing.T) { } func TestLocateRefEndNoRef(t *testing.T) { - r := &yaml.Node{Content: []*yaml.Node{{Kind: yaml.ScalarNode, Value: "$ref"}, {Kind: yaml.ScalarNode, Value: "cake"}}} n, i, e, c := LocateRefEnd(context.Background(), r, index.NewSpecIndexWithConfig(r, index.CreateClosedAPIIndexConfig()), 0) assert.Nil(t, n) @@ -2049,7 +1914,6 @@ func TestLocateRefEnd_TooDeep(t *testing.T) { } func TestLocateRefEnd_Loop(t *testing.T) { - yml, _ := os.ReadFile("../../test_specs/first.yaml") var bsn yaml.Node _ = yaml.Unmarshal(yml, &bsn) @@ -2094,7 +1958,6 @@ func TestLocateRefEnd_Loop(t *testing.T) { } func TestLocateRefEnd_Loop_WithResolve(t *testing.T) { - yml, _ := os.ReadFile("../../test_specs/first.yaml") var bsn yaml.Node _ = yaml.Unmarshal(yml, &bsn) @@ -2139,7 +2002,6 @@ func TestLocateRefEnd_Loop_WithResolve(t *testing.T) { } func TestLocateRefEnd_Empty(t *testing.T) { - yml, _ := os.ReadFile("../../test_specs/first.yaml") var bsn yaml.Node _ = yaml.Unmarshal(yml, &bsn) @@ -2184,7 +2046,6 @@ func TestLocateRefEnd_Empty(t *testing.T) { } func TestArray_NotRefNotArray(t *testing.T) { - yml := `` var idxNode yaml.Node mErr := yaml.Unmarshal([]byte(yml), &idxNode) @@ -2201,5 +2062,4 @@ func TestArray_NotRefNotArray(t *testing.T) { assert.Error(t, err) assert.Equal(t, err.Error(), "array build failed, input is not an array, line 2, column 3") assert.Len(t, things, 0) - } diff --git a/datamodel/low/model_builder.go b/datamodel/low/model_builder.go index 8e55411..a396365 100644 --- a/datamodel/low/model_builder.go +++ b/datamodel/low/model_builder.go @@ -78,72 +78,63 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) er switch field.Type() { - case reflect.TypeOf(map[string]NodeReference[any]{}): + case reflect.TypeOf(orderedmap.New[string, NodeReference[*yaml.Node]]()): + if utils.IsNodeMap(valueNode) { if field.CanSet() { - items := make(map[string]NodeReference[any]) + items := orderedmap.New[string, NodeReference[*yaml.Node]]() var currentLabel string for i, sliceItem := range valueNode.Content { if i%2 == 0 { currentLabel = sliceItem.Value continue } - var decoded map[string]interface{} - // I cannot think of a way to make this error out by this point. - _ = sliceItem.Decode(&decoded) - items[currentLabel] = NodeReference[any]{ - Value: decoded, + items.Set(currentLabel, NodeReference[*yaml.Node]{ + Value: sliceItem, ValueNode: sliceItem, KeyNode: valueNode, - } + }) } field.Set(reflect.ValueOf(items)) } } - case reflect.TypeOf(map[string]NodeReference[string]{}): + case reflect.TypeOf(orderedmap.New[string, NodeReference[string]]()): if utils.IsNodeMap(valueNode) { if field.CanSet() { - items := make(map[string]NodeReference[string]) + items := orderedmap.New[string, NodeReference[string]]() var currentLabel string for i, sliceItem := range valueNode.Content { if i%2 == 0 { currentLabel = sliceItem.Value continue } - items[currentLabel] = NodeReference[string]{ + items.Set(currentLabel, NodeReference[string]{ Value: fmt.Sprintf("%v", sliceItem.Value), ValueNode: sliceItem, KeyNode: valueNode, - } + }) } field.Set(reflect.ValueOf(items)) } } - case reflect.TypeOf(NodeReference[any]{}): + case reflect.TypeOf(NodeReference[*yaml.Node]{}): - var decoded interface{} - _ = valueNode.Decode(&decoded) if field.CanSet() { - or := NodeReference[any]{Value: decoded, ValueNode: valueNode, KeyNode: keyNode} + or := NodeReference[*yaml.Node]{Value: valueNode, ValueNode: valueNode, KeyNode: keyNode} field.Set(reflect.ValueOf(or)) } - case reflect.TypeOf([]NodeReference[any]{}): + case reflect.TypeOf([]NodeReference[*yaml.Node]{}): if utils.IsNodeArray(valueNode) { if field.CanSet() { - var items []NodeReference[any] + var items []NodeReference[*yaml.Node] for _, sliceItem := range valueNode.Content { - var decoded map[string]interface{} - err := sliceItem.Decode(&decoded) - if err != nil { - return err - } - items = append(items, NodeReference[any]{ - Value: decoded, + items = append(items, NodeReference[*yaml.Node]{ + Value: sliceItem, ValueNode: sliceItem, KeyNode: valueNode, }) @@ -340,57 +331,9 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) er } } - // helper for unpacking string maps. - case reflect.TypeOf(map[KeyReference[string]]ValueReference[string]{}): + // helper for unpacking string maps. + case reflect.TypeOf(orderedmap.New[KeyReference[string], ValueReference[string]]()): - if utils.IsNodeMap(valueNode) { - if field.CanSet() { - items := make(map[KeyReference[string]]ValueReference[string]) - var cf *yaml.Node - for i, sliceItem := range valueNode.Content { - if i%2 == 0 { - cf = sliceItem - continue - } - items[KeyReference[string]{ - Value: cf.Value, - KeyNode: cf, - }] = ValueReference[string]{ - Value: sliceItem.Value, - ValueNode: sliceItem, - } - } - field.Set(reflect.ValueOf(items)) - } - } - - case reflect.TypeOf(KeyReference[map[KeyReference[string]]ValueReference[string]]{}): - - if utils.IsNodeMap(valueNode) { - if field.CanSet() { - items := make(map[KeyReference[string]]ValueReference[string]) - var cf *yaml.Node - for i, sliceItem := range valueNode.Content { - if i%2 == 0 { - cf = sliceItem - continue - } - items[KeyReference[string]{ - Value: cf.Value, - KeyNode: cf, - }] = ValueReference[string]{ - Value: sliceItem.Value, - ValueNode: sliceItem, - } - } - ref := KeyReference[map[KeyReference[string]]ValueReference[string]]{ - Value: items, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(ref)) - } - } - case reflect.TypeOf(NodeReference[orderedmap.Map[KeyReference[string], ValueReference[string]]]{}): if utils.IsNodeMap(valueNode) { if field.CanSet() { items := orderedmap.New[KeyReference[string], ValueReference[string]]() @@ -408,7 +351,55 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) er ValueNode: sliceItem, }) } - ref := NodeReference[orderedmap.Map[KeyReference[string], ValueReference[string]]]{ + field.Set(reflect.ValueOf(items)) + } + } + + case reflect.TypeOf(KeyReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]]{}): + + if utils.IsNodeMap(valueNode) { + if field.CanSet() { + items := orderedmap.New[KeyReference[string], ValueReference[string]]() + var cf *yaml.Node + for i, sliceItem := range valueNode.Content { + if i%2 == 0 { + cf = sliceItem + continue + } + items.Set(KeyReference[string]{ + Value: cf.Value, + KeyNode: cf, + }, ValueReference[string]{ + Value: sliceItem.Value, + ValueNode: sliceItem, + }) + } + ref := KeyReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]]{ + Value: items, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(ref)) + } + } + case reflect.TypeOf(NodeReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]]{}): + if utils.IsNodeMap(valueNode) { + if field.CanSet() { + items := orderedmap.New[KeyReference[string], ValueReference[string]]() + var cf *yaml.Node + for i, sliceItem := range valueNode.Content { + if i%2 == 0 { + cf = sliceItem + continue + } + items.Set(KeyReference[string]{ + Value: cf.Value, + KeyNode: cf, + }, ValueReference[string]{ + Value: sliceItem.Value, + ValueNode: sliceItem, + }) + } + ref := NodeReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]]{ Value: items, KeyNode: keyNode, ValueNode: valueNode, @@ -436,34 +427,18 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) er } } - case reflect.TypeOf(NodeReference[[]ValueReference[any]]{}): + case reflect.TypeOf(NodeReference[[]ValueReference[*yaml.Node]]{}): if utils.IsNodeArray(valueNode) { if field.CanSet() { - var items []ValueReference[any] + var items []ValueReference[*yaml.Node] for _, sliceItem := range valueNode.Content { - - var val any - if utils.IsNodeIntValue(sliceItem) || utils.IsNodeFloatValue(sliceItem) { - if utils.IsNodeIntValue(sliceItem) { - val, _ = strconv.ParseInt(sliceItem.Value, 10, 64) - } else { - val, _ = strconv.ParseFloat(sliceItem.Value, 64) - } - } - if utils.IsNodeBoolValue(sliceItem) { - val, _ = strconv.ParseBool(sliceItem.Value) - } - if utils.IsNodeStringValue(sliceItem) { - val = sliceItem.Value - } - - items = append(items, ValueReference[any]{ - Value: val, + items = append(items, ValueReference[*yaml.Node]{ + Value: sliceItem, ValueNode: sliceItem, }) } - n := NodeReference[[]ValueReference[any]]{ + n := NodeReference[[]ValueReference[*yaml.Node]]{ Value: items, KeyNode: keyNode, ValueNode: valueNode, diff --git a/datamodel/low/model_builder_test.go b/datamodel/low/model_builder_test.go index c5e16f4..5447eb4 100644 --- a/datamodel/low/model_builder_test.go +++ b/datamodel/low/model_builder_test.go @@ -25,11 +25,11 @@ type hotdog struct { Temps []NodeReference[int] HighTemps []NodeReference[int64] Buns []NodeReference[bool] - UnknownElements NodeReference[any] - LotsOfUnknowns []NodeReference[any] - Where map[string]NodeReference[any] - There map[string]NodeReference[string] - AllTheThings NodeReference[orderedmap.Map[KeyReference[string], ValueReference[string]]] + UnknownElements NodeReference[*yaml.Node] + LotsOfUnknowns []NodeReference[*yaml.Node] + Where *orderedmap.Map[string, NodeReference[*yaml.Node]] + There *orderedmap.Map[string, NodeReference[string]] + AllTheThings NodeReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]] } func TestBuildModel_Mismatch(t *testing.T) { @@ -126,11 +126,15 @@ allTheThings: assert.Equal(t, int64(7392837462032342), hd.MaxTempHigh.Value) assert.Equal(t, 2, hd.Temps[1].Value) assert.Equal(t, 27, hd.Temps[1].ValueNode.Line) - assert.Len(t, hd.UnknownElements.Value, 2) + + var unknownElements map[string]any + _ = hd.UnknownElements.Value.Decode(&unknownElements) + + assert.Len(t, unknownElements, 2) assert.Len(t, hd.LotsOfUnknowns, 3) - assert.Len(t, hd.Where, 2) - assert.Len(t, hd.There, 2) - assert.Equal(t, "bear", hd.There["care"].Value) + assert.Equal(t, 2, orderedmap.Len(hd.Where)) + assert.Equal(t, 2, orderedmap.Len(hd.There)) + assert.Equal(t, "bear", hd.There.GetOrZero("care").Value) assert.Equal(t, 324938249028.98234892374892374923874823974, hd.Mustard.Value) allTheThings := hd.AllTheThings.Value @@ -201,27 +205,9 @@ thing: yeah` assert.Equal(t, "yeah", ins.Thing.Value) } -func TestSetField_NodeRefAny_Error(t *testing.T) { - type internal struct { - Thing []NodeReference[any] - } - - yml := `thing: - - 999 - - false` - - ins := new(internal) - var rootNode yaml.Node - mErr := yaml.Unmarshal([]byte(yml), &rootNode) - assert.NoError(t, mErr) - - try := BuildModel(rootNode.Content[0], ins) - assert.Error(t, try) -} - func TestSetField_MapHelperWrapped(t *testing.T) { type internal struct { - Thing KeyReference[map[KeyReference[string]]ValueReference[string]] + Thing KeyReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]] } yml := `thing: @@ -236,12 +222,12 @@ func TestSetField_MapHelperWrapped(t *testing.T) { try := BuildModel(rootNode.Content[0], ins) assert.NoError(t, try) - assert.Len(t, ins.Thing.Value, 3) + assert.Equal(t, 3, orderedmap.Len(ins.Thing.Value)) } func TestSetField_MapHelper(t *testing.T) { type internal struct { - Thing map[KeyReference[string]]ValueReference[string] + Thing *orderedmap.Map[KeyReference[string], ValueReference[string]] } yml := `thing: @@ -256,7 +242,7 @@ func TestSetField_MapHelper(t *testing.T) { try := BuildModel(rootNode.Content[0], ins) assert.NoError(t, try) - assert.Len(t, ins.Thing, 3) + assert.Equal(t, 3, orderedmap.Len(ins.Thing)) } func TestSetField_ArrayHelper(t *testing.T) { @@ -281,7 +267,7 @@ func TestSetField_ArrayHelper(t *testing.T) { func TestSetField_Enum_Helper(t *testing.T) { type internal struct { - Thing NodeReference[[]ValueReference[any]] + Thing NodeReference[[]ValueReference[*yaml.Node]] } yml := `thing: @@ -324,7 +310,7 @@ func TestSetField_Default_Helper(t *testing.T) { func TestHandleSlicesOfInts(t *testing.T) { type internal struct { - Thing NodeReference[[]ValueReference[any]] + Thing NodeReference[[]ValueReference[*yaml.Node]] } yml := `thing: @@ -338,13 +324,20 @@ func TestHandleSlicesOfInts(t *testing.T) { try := BuildModel(rootNode.Content[0], ins) assert.NoError(t, try) - assert.Equal(t, int64(5), ins.Thing.Value[0].Value) - assert.Equal(t, 1.234, ins.Thing.Value[1].Value) + + var thing0 int64 + _ = ins.Thing.GetValue()[0].Value.Decode(&thing0) + + var thing1 float64 + _ = ins.Thing.GetValue()[1].Value.Decode(&thing1) + + assert.Equal(t, int64(5), thing0) + assert.Equal(t, 1.234, thing1) } func TestHandleSlicesOfBools(t *testing.T) { type internal struct { - Thing NodeReference[[]ValueReference[any]] + Thing NodeReference[[]ValueReference[*yaml.Node]] } yml := `thing: @@ -357,9 +350,16 @@ func TestHandleSlicesOfBools(t *testing.T) { assert.NoError(t, mErr) try := BuildModel(rootNode.Content[0], ins) + + var thing0 bool + _ = ins.Thing.GetValue()[0].Value.Decode(&thing0) + + var thing1 bool + _ = ins.Thing.GetValue()[1].Value.Decode(&thing1) + assert.NoError(t, try) - assert.Equal(t, true, ins.Thing.Value[0].Value) - assert.Equal(t, false, ins.Thing.Value[1].Value) + assert.Equal(t, true, thing0) + assert.Equal(t, false, thing1) } func TestSetField_Ignore(t *testing.T) { @@ -387,7 +387,7 @@ func TestSetField_Ignore(t *testing.T) { func TestBuildModelAsync(t *testing.T) { type internal struct { - Thing KeyReference[map[KeyReference[string]]ValueReference[string]] + Thing KeyReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]] } yml := `thing: @@ -405,28 +405,5 @@ func TestBuildModelAsync(t *testing.T) { wg.Add(1) BuildModelAsync(rootNode.Content[0], ins, &wg, &errors) wg.Wait() - assert.Len(t, ins.Thing.Value, 3) -} - -func TestBuildModelAsync_Error(t *testing.T) { - type internal struct { - Thing []NodeReference[any] - } - - yml := `thing: - - 999 - - false` - - ins := new(internal) - var rootNode yaml.Node - mErr := yaml.Unmarshal([]byte(yml), &rootNode) - assert.NoError(t, mErr) - - var wg sync.WaitGroup - var errors []error - wg.Add(1) - BuildModelAsync(rootNode.Content[0], ins, &wg, &errors) - wg.Wait() - assert.Len(t, errors, 1) - assert.Len(t, ins.Thing, 0) + assert.Equal(t, 3, orderedmap.Len(ins.Thing.Value)) } diff --git a/datamodel/low/model_interfaces.go b/datamodel/low/model_interfaces.go index 7758da1..6045782 100644 --- a/datamodel/low/model_interfaces.go +++ b/datamodel/low/model_interfaces.go @@ -3,6 +3,11 @@ package low +import ( + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" +) + type SharedParameters interface { HasDescription Hash() [32]byte @@ -30,7 +35,7 @@ type SwaggerParameter interface { GetType() *NodeReference[string] GetFormat() *NodeReference[string] GetCollectionFormat() *NodeReference[string] - GetDefault() *NodeReference[any] + GetDefault() *NodeReference[*yaml.Node] GetMaximum() *NodeReference[int] GetExclusiveMaximum() *NodeReference[bool] GetMinimum() *NodeReference[int] @@ -41,7 +46,7 @@ type SwaggerParameter interface { GetMaxItems() *NodeReference[int] GetMinItems() *NodeReference[int] GetUniqueItems() *NodeReference[bool] - GetEnum() *NodeReference[[]ValueReference[any]] + GetEnum() *NodeReference[[]ValueReference[*yaml.Node]] GetMultipleOf() *NodeReference[int] } @@ -51,7 +56,7 @@ type SwaggerHeader interface { GetType() *NodeReference[string] GetFormat() *NodeReference[string] GetCollectionFormat() *NodeReference[string] - GetDefault() *NodeReference[any] + GetDefault() *NodeReference[*yaml.Node] GetMaximum() *NodeReference[int] GetExclusiveMaximum() *NodeReference[bool] GetMinimum() *NodeReference[int] @@ -62,7 +67,7 @@ type SwaggerHeader interface { GetMaxItems() *NodeReference[int] GetMinItems() *NodeReference[int] GetUniqueItems() *NodeReference[bool] - GetEnum() *NodeReference[[]ValueReference[any]] + GetEnum() *NodeReference[[]ValueReference[*yaml.Node]] GetMultipleOf() *NodeReference[int] GetItems() *NodeReference[any] // requires cast. } @@ -74,7 +79,7 @@ type OpenAPIHeader interface { GetStyle() *NodeReference[string] GetAllowReserved() *NodeReference[bool] GetExplode() *NodeReference[bool] - GetExample() *NodeReference[any] + GetExample() *NodeReference[*yaml.Node] GetRequired() *NodeReference[bool] GetAllowEmptyValue() *NodeReference[bool] GetSchema() *NodeReference[any] // requires cast. @@ -88,13 +93,12 @@ type OpenAPIParameter interface { GetStyle() *NodeReference[string] GetAllowReserved() *NodeReference[bool] GetExplode() *NodeReference[bool] - GetExample() *NodeReference[any] + GetExample() *NodeReference[*yaml.Node] GetExamples() *NodeReference[any] // requires cast. GetContent() *NodeReference[any] // requires cast. } -//TODO: this needs to be fixed, move returns to pointers. - +// TODO: this needs to be fixed, move returns to pointers. type SharedOperations interface { GetOperationId() NodeReference[string] GetExternalDocs() NodeReference[any] @@ -102,7 +106,7 @@ type SharedOperations interface { GetTags() NodeReference[[]ValueReference[string]] GetSummary() NodeReference[string] GetDeprecated() NodeReference[bool] - GetExtensions() map[KeyReference[string]]ValueReference[any] + GetExtensions() *orderedmap.Map[KeyReference[string], ValueReference[*yaml.Node]] GetResponses() NodeReference[any] // requires cast. GetParameters() NodeReference[any] // requires cast. GetSecurity() NodeReference[any] // requires cast. @@ -117,6 +121,6 @@ type SwaggerOperations interface { type OpenAPIOperations interface { SharedOperations - GetCallbacks() NodeReference[map[KeyReference[string]]ValueReference[any]] // requires cast - GetServers() NodeReference[any] // requires cast. + GetCallbacks() NodeReference[*orderedmap.Map[KeyReference[string], ValueReference[any]]] // requires cast + GetServers() NodeReference[any] // requires cast. } diff --git a/datamodel/low/reference.go b/datamodel/low/reference.go index 34b6091..409c0a4 100644 --- a/datamodel/low/reference.go +++ b/datamodel/low/reference.go @@ -3,7 +3,9 @@ package low import ( "context" "fmt" + "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -13,25 +15,35 @@ const ( ) type Reference struct { - Reference string `json:"-" yaml:"-"` + refNode *yaml.Node + reference string } -func (r *Reference) GetReference() string { - return r.Reference +func (r Reference) GetReference() string { + return r.reference } -func (r *Reference) IsReference() bool { - return r.Reference != "" +func (r Reference) IsReference() bool { + return r.reference != "" } -func (r *Reference) SetReference(ref string) { - r.Reference = ref +func (r Reference) GetReferenceNode() *yaml.Node { + return r.refNode +} + +func (r *Reference) SetReference(ref string, node *yaml.Node) { + r.reference = ref + r.refNode = node } type IsReferenced interface { IsReference() bool GetReference() string - SetReference(string) + GetReferenceNode() *yaml.Node +} + +type SetReferencer interface { + SetReference(ref string, node *yaml.Node) } // Buildable is an interface for any struct that can be 'built out'. This means that a struct can accept @@ -63,16 +75,14 @@ type Hashable interface { // HasExtensions is implemented by any object that exposes extensions type HasExtensions[T any] interface { - // GetExtensions returns generic low level extensions - GetExtensions() map[KeyReference[string]]ValueReference[any] + GetExtensions() *orderedmap.Map[KeyReference[string], ValueReference[*yaml.Node]] } // HasExtensionsUntyped is implemented by any object that exposes extensions type HasExtensionsUntyped interface { - // GetExtensions returns generic low level extensions - GetExtensions() map[KeyReference[string]]ValueReference[any] + GetExtensions() *orderedmap.Map[KeyReference[string], ValueReference[*yaml.Node]] } // HasValue is implemented by NodeReference and ValueReference to return the yaml.Node backing the value. @@ -98,6 +108,7 @@ type HasKeyNode interface { // a key yaml.Node that points to the key node that contains the value node, and the value node that contains // the actual value. type NodeReference[T any] struct { + Reference // The value being referenced Value T @@ -108,19 +119,14 @@ type NodeReference[T any] struct { // The yaml.Node that is the key, that contains the value. KeyNode *yaml.Node - // Is this value actually a reference in the original tree? - ReferenceNode bool - - // If HasReference is true, then Reference contains the original $ref value. - Reference string - Context context.Context } +var _ HasValueNodeUntyped = &NodeReference[any]{} + // KeyReference is a low-level container for key nodes holding a Value of type T. A KeyNode is a pointer to the // yaml.Node that holds a key to a value. type KeyReference[T any] struct { - // The value being referenced. Value T @@ -131,18 +137,13 @@ type KeyReference[T any] struct { // ValueReference is a low-level container for value nodes that hold a Value of type T. A ValueNode is a pointer // to the yaml.Node that holds the value. type ValueReference[T any] struct { + Reference // The value being referenced. Value T // The yaml.Node that holds the referenced value ValueNode *yaml.Node - - // Is this value actually a reference in the original tree? - ReferenceNode bool - - // If HasReference is true, then Reference contains the original $ref value. - Reference string } // IsEmpty will return true if this reference has no key or value nodes assigned (it's been ignored) @@ -158,32 +159,6 @@ func (n NodeReference[T]) NodeLineNumber() int { } } -func (n NodeReference[T]) GetReference() string { - return n.Reference -} - -func (n NodeReference[T]) SetReference(ref string) { - n.Reference = ref -} - -// IsReference will return true if the key node contains a $ref key. -func (n NodeReference[T]) IsReference() bool { - if n.ReferenceNode { - return true - } - if n.KeyNode != nil { - for k := range n.KeyNode.Content { - if k%2 == 0 { - if n.KeyNode.Content[k].Value == "$ref" { - n.ReferenceNode = true - return true - } - } - } - } - return false -} - // GenerateMapKey will return a string based on the line and column number of the node, e.g. 33:56 for line 33, col 56. func (n NodeReference[T]) GenerateMapKey() string { return fmt.Sprintf("%d:%d", n.ValueNode.Line, n.ValueNode.Column) @@ -251,36 +226,19 @@ func (n ValueReference[T]) GetValueUntyped() any { return n.Value } -func (n ValueReference[T]) GetReference() string { - return n.Reference -} - -func (n ValueReference[T]) SetReference(ref string) { - n.Reference = ref -} - -// IsReference will return true if the key node contains a $ref -func (n ValueReference[T]) IsReference() bool { - if n.Reference != "" { - return true - } - return false -} - func (n ValueReference[T]) MarshalYAML() (interface{}, error) { if n.IsReference() { - nodes := make([]*yaml.Node, 2) - nodes[0] = utils.CreateStringNode("$ref") - nodes[1] = utils.CreateStringNode(n.Reference) - m := utils.CreateEmptyMapNode() - m.Content = nodes - return m, nil + return n.GetReferenceNode(), nil } var h yaml.Node e := n.ValueNode.Decode(&h) return h, e } +func (n KeyReference[T]) MarshalYAML() (interface{}, error) { + return n.KeyNode, nil +} + // IsEmpty will return true if this reference has no key or value nodes assigned (it's been ignored) func (n KeyReference[T]) IsEmpty() bool { return n.KeyNode == nil diff --git a/datamodel/low/reference_test.go b/datamodel/low/reference_test.go index c1129c7..c4f9223 100644 --- a/datamodel/low/reference_test.go +++ b/datamodel/low/reference_test.go @@ -6,10 +6,11 @@ package low import ( "crypto/sha256" "fmt" - "github.com/pb33f/libopenapi/utils" "strings" "testing" + "github.com/pb33f/libopenapi/utils" + "github.com/pb33f/libopenapi/index" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" @@ -99,7 +100,6 @@ func TestKeyReference_GenerateMapKey(t *testing.T) { } func TestIsCircular_LookupFromJourney(t *testing.T) { - yml := `components: schemas: Something: @@ -136,7 +136,6 @@ func TestIsCircular_LookupFromJourney(t *testing.T) { } func TestIsCircular_LookupFromJourney_Optional(t *testing.T) { - yml := `components: schemas: Something: @@ -237,7 +236,6 @@ func TestIsCircular_LookupFromLoopPoint_Optional(t *testing.T) { } func TestIsCircular_FromRefLookup(t *testing.T) { - yml := `components: schemas: NotCircle: @@ -558,21 +556,17 @@ func TestGetCircularReferenceResult_NothingFound(t *testing.T) { func TestHashToString(t *testing.T) { assert.Equal(t, "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5", HashToString(sha256.Sum256([]byte("12345")))) - } func TestReference_IsReference(t *testing.T) { - ref := Reference{ - Reference: "#/components/schemas/SomeSchema", - } + ref := Reference{} + ref.SetReference("#/components/schemas/SomeSchema", nil) assert.True(t, ref.IsReference()) - } func TestNodeReference_NodeLineNumber(t *testing.T) { - n := utils.CreateStringNode("pizza") - nr := NodeReference[string]{ + nr := &NodeReference[string]{ Value: "pizza", ValueNode: n, } @@ -582,51 +576,36 @@ func TestNodeReference_NodeLineNumber(t *testing.T) { } func TestNodeReference_NodeLineNumberEmpty(t *testing.T) { - - nr := NodeReference[string]{ + nr := &NodeReference[string]{ Value: "pizza", } assert.Equal(t, 0, nr.NodeLineNumber()) } func TestNodeReference_GetReference(t *testing.T) { - - nr := NodeReference[string]{ - Reference: "#/happy/sunday", - } + nr := &NodeReference[string]{} + nr.SetReference("#/happy/sunday", nil) assert.Equal(t, "#/happy/sunday", nr.GetReference()) } func TestNodeReference_SetReference(t *testing.T) { - - nr := NodeReference[string]{} - nr.SetReference("#/happy/sunday") -} - -func TestNodeReference_IsReference(t *testing.T) { - - nr := NodeReference[string]{ - ReferenceNode: true, - } - assert.True(t, nr.IsReference()) + nr := &NodeReference[string]{} + nr.SetReference("#/happy/sunday", nil) } func TestNodeReference_GetKeyNode(t *testing.T) { - - nr := NodeReference[string]{ + nr := &NodeReference[string]{ KeyNode: utils.CreateStringNode("pizza"), } assert.Equal(t, "pizza", nr.GetKeyNode().Value) - } func TestNodeReference_GetValueUntyped(t *testing.T) { - type anything struct { thing string } - nr := NodeReference[any]{ + nr := &NodeReference[any]{ Value: anything{thing: "ding"}, } @@ -634,7 +613,6 @@ func TestNodeReference_GetValueUntyped(t *testing.T) { } func TestValueReference_NodeLineNumber(t *testing.T) { - n := utils.CreateStringNode("pizza") nr := ValueReference[string]{ Value: "pizza", @@ -646,7 +624,6 @@ func TestValueReference_NodeLineNumber(t *testing.T) { } func TestValueReference_NodeLineNumber_Nil(t *testing.T) { - nr := ValueReference[string]{ Value: "pizza", } @@ -655,21 +632,12 @@ func TestValueReference_NodeLineNumber_Nil(t *testing.T) { } func TestValueReference_GetReference(t *testing.T) { - - nr := ValueReference[string]{ - Reference: "#/happy/sunday", - } + nr := ValueReference[string]{} + nr.SetReference("#/happy/sunday", nil) assert.Equal(t, "#/happy/sunday", nr.GetReference()) } -func TestValueReference_SetReference(t *testing.T) { - - nr := ValueReference[string]{} - nr.SetReference("#/happy/sunday") -} - func TestValueReference_GetValueUntyped(t *testing.T) { - type anything struct { thing string } @@ -681,28 +649,15 @@ func TestValueReference_GetValueUntyped(t *testing.T) { assert.Equal(t, "{ding}", fmt.Sprint(nr.GetValueUntyped())) } -func TestValueReference_IsReference(t *testing.T) { - - nr := NodeReference[string]{ - ReferenceNode: true, - } - assert.True(t, nr.IsReference()) -} - func TestValueReference_MarshalYAML_Ref(t *testing.T) { - - nr := ValueReference[string]{ - ReferenceNode: true, - Reference: "#/burgers/beer", - } + nr := ValueReference[string]{} + nr.SetReference("#/burgers/beer", nil) data, _ := yaml.Marshal(nr) assert.Equal(t, `$ref: '#/burgers/beer'`, strings.TrimSpace(string(data))) - } func TestValueReference_MarshalYAML(t *testing.T) { - v := map[string]interface{}{ "beer": "burger", "wine": "cheese", @@ -725,7 +680,6 @@ wine: cheese` } func TestKeyReference_GetValueUntyped(t *testing.T) { - type anything struct { thing string } diff --git a/datamodel/low/v2/definitions.go b/datamodel/low/v2/definitions.go index d56801b..2917a08 100644 --- a/datamodel/low/v2/definitions.go +++ b/datamodel/low/v2/definitions.go @@ -6,9 +6,10 @@ package v2 import ( "context" "crypto/sha256" - "sort" "strings" + "sync" + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" @@ -23,7 +24,7 @@ import ( // referenced to the ones defined here. It does not define global operation parameters // - https://swagger.io/specification/v2/#parametersDefinitionsObject type ParameterDefinitions struct { - Definitions orderedmap.Map[low.KeyReference[string], low.ValueReference[*Parameter]] + Definitions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*Parameter]] } // ResponsesDefinitions is a low-level representation of a Swagger / OpenAPI 2 Responses Definitions object. @@ -32,7 +33,7 @@ type ParameterDefinitions struct { // referenced to the ones defined here. It does not define global operation responses // - https://swagger.io/specification/v2/#responsesDefinitionsObject type ResponsesDefinitions struct { - Definitions orderedmap.Map[low.KeyReference[string], low.ValueReference[*Response]] + Definitions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*Response]] } // SecurityDefinitions is a low-level representation of a Swagger / OpenAPI 2 Security Definitions object. @@ -41,7 +42,7 @@ type ResponsesDefinitions struct { // schemes on the operations and only serves to provide the relevant details for each scheme // - https://swagger.io/specification/v2/#securityDefinitionsObject type SecurityDefinitions struct { - Definitions orderedmap.Map[low.KeyReference[string], low.ValueReference[*SecurityScheme]] + Definitions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*SecurityScheme]] } // Definitions is a low-level representation of a Swagger / OpenAPI 2 Definitions object @@ -50,7 +51,7 @@ type SecurityDefinitions struct { // arrays or models. // - https://swagger.io/specification/v2/#definitionsObject type Definitions struct { - Schemas orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]] + Schemas *orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]] } // FindSchema will attempt to locate a base.SchemaProxy instance using a name. @@ -77,46 +78,79 @@ func (s *SecurityDefinitions) FindSecurityDefinition(securityDef string) *low.Va func (d *Definitions) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { root = utils.NodeAlias(root) utils.CheckForMergeNodes(root) - // TODO: Refactor with orderedmap.TranslatePipeline. - errorChan := make(chan error) - resultChan := make(chan definitionResult[*base.SchemaProxy]) - var defLabel *yaml.Node - totalDefinitions := 0 - var buildFunc = func(label *yaml.Node, value *yaml.Node, idx *index.SpecIndex, - r chan definitionResult[*base.SchemaProxy], e chan error) { - - obj, err, _, rv := low.ExtractObjectRaw[*base.SchemaProxy](ctx, label, value, idx) - if err != nil { - e <- err - } - r <- definitionResult[*base.SchemaProxy]{k: label, v: low.ValueReference[*base.SchemaProxy]{ - Value: obj, ValueNode: value, Reference: rv, - }} + type buildInput struct { + label *yaml.Node + value *yaml.Node } - for i := range root.Content { - if i%2 == 0 { - defLabel = root.Content[i] - continue - } - totalDefinitions++ - go buildFunc(defLabel, root.Content[i], idx, resultChan, errorChan) - } - - completedDefs := 0 results := orderedmap.New[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]]() - for completedDefs < totalDefinitions { - select { - case err := <-errorChan: - return err - case sch := <-resultChan: - completedDefs++ - key := low.KeyReference[string]{ - Value: sch.k.Value, - KeyNode: sch.k, + in := make(chan buildInput) + out := make(chan definitionResult[*base.SchemaProxy]) + done := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(2) // input and output goroutines. + + // TranslatePipeline input. + go func() { + defer func() { + close(in) + wg.Done() + }() + var label *yaml.Node + for i, value := range root.Content { + if i%2 == 0 { + label = value + continue + } + + select { + case in <- buildInput{ + label: label, + value: value, + }: + case <-done: + return } - results.Set(key, sch.v) } + }() + + // TranslatePipeline output. + go func() { + for { + result, ok := <-out + if !ok { + break + } + + key := low.KeyReference[string]{ + Value: result.k.Value, + KeyNode: result.k, + } + results.Set(key, result.v) + } + close(done) + wg.Done() + }() + + translateFunc := func(value buildInput) (definitionResult[*base.SchemaProxy], error) { + obj, err, _, rv := low.ExtractObjectRaw[*base.SchemaProxy](ctx, value.label, value.value, idx) + if err != nil { + return definitionResult[*base.SchemaProxy]{}, err + } + + v := low.ValueReference[*base.SchemaProxy]{ + Value: obj, ValueNode: value.value, + } + v.SetReference(rv, value.value) + + return definitionResult[*base.SchemaProxy]{k: value.label, v: v}, nil } + + err := datamodel.TranslatePipeline[buildInput, definitionResult[*base.SchemaProxy]](in, out, translateFunc) + wg.Wait() + if err != nil { + return err + } + d.Schemas = results return nil } @@ -124,15 +158,8 @@ func (d *Definitions) Build(ctx context.Context, _, root *yaml.Node, idx *index. // Hash will return a consistent SHA256 Hash of the Definitions object func (d *Definitions) Hash() [32]byte { var f []string - keys := make([]string, orderedmap.Len(d.Schemas)) - z := 0 - for pair := orderedmap.First(d.Schemas); pair != nil; pair = pair.Next() { - keys[z] = pair.Key().Value - z++ - } - sort.Strings(keys) - for k := range keys { - f = append(f, low.GenerateHashString(d.FindSchema(keys[k]).Value)) + for pair := orderedmap.First(orderedmap.SortAlpha(d.Schemas)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(d.FindSchema(pair.Key().Value).Value)) } return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -143,15 +170,21 @@ func (pd *ParameterDefinitions) Build(ctx context.Context, _, root *yaml.Node, i resultChan := make(chan definitionResult[*Parameter]) var defLabel *yaml.Node totalDefinitions := 0 - var buildFunc = func(label *yaml.Node, value *yaml.Node, idx *index.SpecIndex, - r chan definitionResult[*Parameter], e chan error) { - + buildFunc := func(label *yaml.Node, value *yaml.Node, idx *index.SpecIndex, + r chan definitionResult[*Parameter], e chan error, + ) { obj, err, _, rv := low.ExtractObjectRaw[*Parameter](ctx, label, value, idx) if err != nil { e <- err } - r <- definitionResult[*Parameter]{k: label, v: low.ValueReference[*Parameter]{Value: obj, - ValueNode: value, Reference: rv}} + + v := low.ValueReference[*Parameter]{ + Value: obj, + ValueNode: value, + } + v.SetReference(rv, value) + + r <- definitionResult[*Parameter]{k: label, v: v} } for i := range root.Content { if i%2 == 0 { @@ -193,15 +226,21 @@ func (r *ResponsesDefinitions) Build(ctx context.Context, _, root *yaml.Node, id resultChan := make(chan definitionResult[*Response]) var defLabel *yaml.Node totalDefinitions := 0 - var buildFunc = func(label *yaml.Node, value *yaml.Node, idx *index.SpecIndex, - r chan definitionResult[*Response], e chan error) { - + buildFunc := func(label *yaml.Node, value *yaml.Node, idx *index.SpecIndex, + r chan definitionResult[*Response], e chan error, + ) { obj, err, _, rv := low.ExtractObjectRaw[*Response](ctx, label, value, idx) if err != nil { e <- err } - r <- definitionResult[*Response]{k: label, v: low.ValueReference[*Response]{Value: obj, - ValueNode: value, Reference: rv}} + + v := low.ValueReference[*Response]{ + Value: obj, + ValueNode: value, + } + v.SetReference(rv, value) + + r <- definitionResult[*Response]{k: label, v: v} } for i := range root.Content { if i%2 == 0 { @@ -238,16 +277,20 @@ func (s *SecurityDefinitions) Build(ctx context.Context, _, root *yaml.Node, idx var defLabel *yaml.Node totalDefinitions := 0 - var buildFunc = func(label *yaml.Node, value *yaml.Node, idx *index.SpecIndex, - r chan definitionResult[*SecurityScheme], e chan error) { - + buildFunc := func(label *yaml.Node, value *yaml.Node, idx *index.SpecIndex, + r chan definitionResult[*SecurityScheme], e chan error, + ) { obj, err, _, rv := low.ExtractObjectRaw[*SecurityScheme](ctx, label, value, idx) if err != nil { e <- err } - r <- definitionResult[*SecurityScheme]{k: label, v: low.ValueReference[*SecurityScheme]{ - Value: obj, ValueNode: value, Reference: rv, - }} + + v := low.ValueReference[*SecurityScheme]{ + Value: obj, ValueNode: value, + } + v.SetReference(rv, value) + + r <- definitionResult[*SecurityScheme]{k: label, v: v} } for i := range root.Content { diff --git a/datamodel/low/v2/examples.go b/datamodel/low/v2/examples.go index bb49a1b..308b28a 100644 --- a/datamodel/low/v2/examples.go +++ b/datamodel/low/v2/examples.go @@ -6,8 +6,6 @@ package v2 import ( "context" "crypto/sha256" - "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -21,12 +19,12 @@ import ( // Allows sharing examples for operation responses // - https://swagger.io/specification/v2/#exampleObject type Examples struct { - Values orderedmap.Map[low.KeyReference[string], low.ValueReference[any]] + Values *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // FindExample attempts to locate an example value, using a key label. -func (e *Examples) FindExample(name string) *low.ValueReference[any] { - return low.FindItemInOrderedMap[any](name, e.Values) +func (e *Examples) FindExample(name string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(name, e.Values) } // Build will extract all examples and will attempt to unmarshal content into a map or slice based on type. @@ -34,54 +32,21 @@ func (e *Examples) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecInd root = utils.NodeAlias(root) utils.CheckForMergeNodes(root) var keyNode, currNode *yaml.Node - var err error - e.Values = orderedmap.New[low.KeyReference[string], low.ValueReference[any]]() + e.Values = orderedmap.New[low.KeyReference[string], low.ValueReference[*yaml.Node]]() for i := range root.Content { if i%2 == 0 { keyNode = root.Content[i] continue } currNode = root.Content[i] - var n map[string]interface{} - err = currNode.Decode(&n) - if err != nil { - var k []interface{} - err = currNode.Decode(&k) - if err != nil { - // lets just default to interface - var j interface{} - _ = currNode.Decode(&j) - e.Values.Set( - low.KeyReference[string]{ - Value: keyNode.Value, - KeyNode: keyNode, - }, - low.ValueReference[any]{ - Value: j, - ValueNode: currNode, - }, - ) - continue - } - e.Values.Set( - low.KeyReference[string]{ - Value: keyNode.Value, - KeyNode: keyNode, - }, - low.ValueReference[any]{ - Value: k, - ValueNode: currNode, - }, - ) - continue - } + e.Values.Set( low.KeyReference[string]{ Value: keyNode.Value, KeyNode: keyNode, }, - low.ValueReference[any]{ - Value: n, + low.ValueReference[*yaml.Node]{ + Value: currNode, ValueNode: currNode, }, ) @@ -92,15 +57,8 @@ func (e *Examples) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecInd // Hash will return a consistent SHA256 Hash of the Examples object func (e *Examples) Hash() [32]byte { var f []string - keys := make([]string, orderedmap.Len(e.Values)) - z := 0 - for pair := orderedmap.First(e.Values); pair != nil; pair = pair.Next() { - keys[z] = pair.Key().Value - z++ - } - sort.Strings(keys) - for k := range keys { - f = append(f, fmt.Sprintf("%v", e.FindExample(keys[k]).Value)) + for pair := orderedmap.First(orderedmap.SortAlpha(e.Values)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v2/header.go b/datamodel/low/v2/header.go index dc493fc..ef70325 100644 --- a/datamodel/low/v2/header.go +++ b/datamodel/low/v2/header.go @@ -7,12 +7,14 @@ import ( "context" "crypto/sha256" "fmt" - "github.com/pb33f/libopenapi/datamodel/low" - "github.com/pb33f/libopenapi/index" - "github.com/pb33f/libopenapi/utils" - "gopkg.in/yaml.v3" "sort" "strings" + + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" ) // Header Represents a low-level Swagger / OpenAPI 2 Header object. @@ -25,7 +27,7 @@ type Header struct { Description low.NodeReference[string] Items low.NodeReference[*Items] CollectionFormat low.NodeReference[string] - Default low.NodeReference[any] + Default low.NodeReference[*yaml.Node] Maximum low.NodeReference[int] ExclusiveMaximum low.NodeReference[bool] Minimum low.NodeReference[int] @@ -36,18 +38,18 @@ type Header struct { MaxItems low.NodeReference[int] MinItems low.NodeReference[int] UniqueItems low.NodeReference[bool] - Enum low.NodeReference[[]low.ValueReference[any]] + Enum low.NodeReference[[]low.ValueReference[*yaml.Node]] MultipleOf low.NodeReference[int] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // FindExtension will attempt to locate an extension value using a name lookup. -func (h *Header) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, h.Extensions) +func (h *Header) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, h.Extensions) } // GetExtensions returns all Header extensions and satisfies the low.HasExtensions interface. -func (h *Header) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (h *Header) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return h.Extensions } @@ -64,37 +66,14 @@ func (h *Header) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecI _, ln, vn := utils.FindKeyNodeFull(DefaultLabel, root.Content) if vn != nil { - var n map[string]interface{} - err = vn.Decode(&n) - if err != nil { - // if not a map, then try an array - var k []interface{} - err = vn.Decode(&k) - if err != nil { - // lets just default to interface - var j interface{} - _ = vn.Decode(&j) - h.Default = low.NodeReference[any]{ - Value: j, - KeyNode: ln, - ValueNode: vn, - } - return nil - } - h.Default = low.NodeReference[any]{ - Value: k, - KeyNode: ln, - ValueNode: vn, - } - return nil - } - h.Default = low.NodeReference[any]{ - Value: n, + h.Default = low.NodeReference[*yaml.Node]{ + Value: vn, KeyNode: ln, ValueNode: vn, } return nil } + return nil } @@ -113,8 +92,8 @@ func (h *Header) Hash() [32]byte { if h.CollectionFormat.Value != "" { f = append(f, h.CollectionFormat.Value) } - if h.Default.Value != "" { - f = append(f, fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(h.Default.Value))))) + if h.Default.Value != nil && !h.Default.Value.IsZero() { + f = append(f, low.GenerateHashString(h.Default.Value)) } f = append(f, fmt.Sprint(h.Maximum.Value)) f = append(f, fmt.Sprint(h.Minimum.Value)) @@ -129,24 +108,17 @@ func (h *Header) Hash() [32]byte { if h.Pattern.Value != "" { f = append(f, fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(h.Pattern.Value))))) } + f = append(f, low.HashExtensions(h.Extensions)...) - keys := make([]string, len(h.Extensions)) + keys := make([]string, len(h.Enum.Value)) z := 0 - for k := range h.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(h.Extensions[k].Value)))) + for k := range h.Enum.Value { + keys[z] = low.ValueToString(h.Enum.Value[k].Value) z++ } sort.Strings(keys) f = append(f, keys...) - keys = make([]string, len(h.Enum.Value)) - z = 0 - for k := range h.Enum.Value { - keys[z] = fmt.Sprint(h.Enum.Value[k].Value) - z++ - } - sort.Strings(keys) - f = append(f, keys...) if h.Items.Value != nil { f = append(f, low.GenerateHashString(h.Items.Value)) } @@ -158,12 +130,15 @@ func (h *Header) Hash() [32]byte { func (h *Header) GetType() *low.NodeReference[string] { return &h.Type } + func (h *Header) GetDescription() *low.NodeReference[string] { return &h.Description } + func (h *Header) GetFormat() *low.NodeReference[string] { return &h.Format } + func (h *Header) GetItems() *low.NodeReference[any] { i := low.NodeReference[any]{ KeyNode: h.Items.KeyNode, @@ -172,45 +147,59 @@ func (h *Header) GetItems() *low.NodeReference[any] { } return &i } + func (h *Header) GetCollectionFormat() *low.NodeReference[string] { return &h.CollectionFormat } -func (h *Header) GetDefault() *low.NodeReference[any] { + +func (h *Header) GetDefault() *low.NodeReference[*yaml.Node] { return &h.Default } + func (h *Header) GetMaximum() *low.NodeReference[int] { return &h.Maximum } + func (h *Header) GetExclusiveMaximum() *low.NodeReference[bool] { return &h.ExclusiveMaximum } + func (h *Header) GetMinimum() *low.NodeReference[int] { return &h.Minimum } + func (h *Header) GetExclusiveMinimum() *low.NodeReference[bool] { return &h.ExclusiveMinimum } + func (h *Header) GetMaxLength() *low.NodeReference[int] { return &h.MaxLength } + func (h *Header) GetMinLength() *low.NodeReference[int] { return &h.MinLength } + func (h *Header) GetPattern() *low.NodeReference[string] { return &h.Pattern } + func (h *Header) GetMaxItems() *low.NodeReference[int] { return &h.MaxItems } + func (h *Header) GetMinItems() *low.NodeReference[int] { return &h.MinItems } + func (h *Header) GetUniqueItems() *low.NodeReference[bool] { return &h.UniqueItems } -func (h *Header) GetEnum() *low.NodeReference[[]low.ValueReference[any]] { + +func (h *Header) GetEnum() *low.NodeReference[[]low.ValueReference[*yaml.Node]] { return &h.Enum } + func (h *Header) GetMultipleOf() *low.NodeReference[int] { return &h.MultipleOf } diff --git a/datamodel/low/v2/header_test.go b/datamodel/low/v2/header_test.go index 9e3f59d..8775604 100644 --- a/datamodel/low/v2/header_test.go +++ b/datamodel/low/v2/header_test.go @@ -5,15 +5,16 @@ package v2 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestHeader_Build(t *testing.T) { - yml := `items: $ref: break` @@ -28,11 +29,9 @@ func TestHeader_Build(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestHeader_DefaultAsSlice(t *testing.T) { - yml := `x-ext: thing default: - why @@ -48,12 +47,15 @@ default: _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.NotNil(t, n.Default.Value) - assert.Len(t, n.Default.Value, 3) - assert.Len(t, n.GetExtensions(), 1) + + var def []string + _ = n.Default.GetValue().Decode(&def) + + assert.Len(t, def, 3) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestHeader_DefaultAsObject(t *testing.T) { - yml := `default: lets: create: @@ -72,7 +74,6 @@ func TestHeader_DefaultAsObject(t *testing.T) { } func TestHeader_NoDefault(t *testing.T) { - yml := `minimum: 12` var idxNode yaml.Node @@ -87,7 +88,6 @@ func TestHeader_NoDefault(t *testing.T) { } func TestHeader_Hash_n_Grab(t *testing.T) { - yml := `description: head type: string format: left @@ -160,7 +160,11 @@ pattern: wow assert.Equal(t, "left", n.GetFormat().Value) assert.Equal(t, "left", n.GetFormat().Value) assert.Equal(t, "nice", n.GetCollectionFormat().Value) - assert.Equal(t, "shut that door!", n.GetDefault().Value) + + var def string + _ = n.GetDefault().Value.Decode(&def) + assert.Equal(t, "shut that door!", def) + assert.Equal(t, 10, n.GetMaximum().Value) assert.Equal(t, 1, n.GetMinimum().Value) assert.True(t, n.GetExclusiveMinimum().Value) @@ -174,6 +178,8 @@ pattern: wow assert.Equal(t, "wow", n.GetPattern().Value) assert.Equal(t, "int", n.GetItems().Value.(*Items).Type.Value) assert.Len(t, n.GetEnum().Value, 2) - assert.Equal(t, "large", n.FindExtension("x-belly").Value) + var xBelly string + _ = n.FindExtension("x-belly").GetValue().Decode(&xBelly) + assert.Equal(t, "large", xBelly) } diff --git a/datamodel/low/v2/items.go b/datamodel/low/v2/items.go index 36036bc..3134688 100644 --- a/datamodel/low/v2/items.go +++ b/datamodel/low/v2/items.go @@ -7,12 +7,14 @@ import ( "context" "crypto/sha256" "fmt" - "github.com/pb33f/libopenapi/datamodel/low" - "github.com/pb33f/libopenapi/index" - "github.com/pb33f/libopenapi/utils" - "gopkg.in/yaml.v3" "sort" "strings" + + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" ) // Items is a low-level representation of a Swagger / OpenAPI 2 Items object. @@ -25,7 +27,7 @@ type Items struct { Format low.NodeReference[string] CollectionFormat low.NodeReference[string] Items low.NodeReference[*Items] - Default low.NodeReference[any] + Default low.NodeReference[*yaml.Node] Maximum low.NodeReference[int] ExclusiveMaximum low.NodeReference[bool] Minimum low.NodeReference[int] @@ -36,18 +38,18 @@ type Items struct { MaxItems low.NodeReference[int] MinItems low.NodeReference[int] UniqueItems low.NodeReference[bool] - Enum low.NodeReference[[]low.ValueReference[any]] + Enum low.NodeReference[[]low.ValueReference[*yaml.Node]] MultipleOf low.NodeReference[int] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // FindExtension will attempt to locate an extension value using a name lookup. -func (i *Items) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, i.Extensions) +func (i *Items) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, i.Extensions) } // GetExtensions returns all Items extensions and satisfies the low.HasExtensions interface. -func (i *Items) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (i *Items) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return i.Extensions } @@ -63,8 +65,8 @@ func (i *Items) Hash() [32]byte { if i.CollectionFormat.Value != "" { f = append(f, i.CollectionFormat.Value) } - if i.Default.Value != "" { - f = append(f, fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(i.Default.Value))))) + if i.Default.Value != nil && !i.Default.Value.IsZero() { + f = append(f, low.GenerateHashString(i.Default.Value)) } f = append(f, fmt.Sprint(i.Maximum.Value)) f = append(f, fmt.Sprint(i.Minimum.Value)) @@ -82,7 +84,7 @@ func (i *Items) Hash() [32]byte { keys := make([]string, len(i.Enum.Value)) z := 0 for k := range i.Enum.Value { - keys[z] = fmt.Sprint(i.Enum.Value[k].Value) + keys[z] = low.ValueToString(i.Enum.Value[k].Value) z++ } sort.Strings(keys) @@ -91,14 +93,7 @@ func (i *Items) Hash() [32]byte { if i.Items.Value != nil { f = append(f, low.GenerateHashString(i.Items.Value)) } - keys = make([]string, len(i.Extensions)) - z = 0 - for k := range i.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(i.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(i.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -115,32 +110,8 @@ func (i *Items) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIn _, ln, vn := utils.FindKeyNodeFull(DefaultLabel, root.Content) if vn != nil { - var n map[string]interface{} - err := vn.Decode(&n) - if err != nil { - // if not a map, then try an array - var k []interface{} - err = vn.Decode(&k) - if err != nil { - // lets just default to interface - var j interface{} - _ = vn.Decode(&j) - i.Default = low.NodeReference[any]{ - Value: j, - KeyNode: ln, - ValueNode: vn, - } - return nil - } - i.Default = low.NodeReference[any]{ - Value: k, - KeyNode: ln, - ValueNode: vn, - } - return nil - } - i.Default = low.NodeReference[any]{ - Value: n, + i.Default = low.NodeReference[*yaml.Node]{ + Value: vn, KeyNode: ln, ValueNode: vn, } @@ -154,9 +125,11 @@ func (i *Items) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIn func (i *Items) GetType() *low.NodeReference[string] { return &i.Type } + func (i *Items) GetFormat() *low.NodeReference[string] { return &i.Format } + func (i *Items) GetItems() *low.NodeReference[any] { k := low.NodeReference[any]{ KeyNode: i.Items.KeyNode, @@ -165,48 +138,63 @@ func (i *Items) GetItems() *low.NodeReference[any] { } return &k } + func (i *Items) GetCollectionFormat() *low.NodeReference[string] { return &i.CollectionFormat } + func (i *Items) GetDescription() *low.NodeReference[string] { return nil // not implemented, but required to align with header contract } -func (i *Items) GetDefault() *low.NodeReference[any] { + +func (i *Items) GetDefault() *low.NodeReference[*yaml.Node] { return &i.Default } + func (i *Items) GetMaximum() *low.NodeReference[int] { return &i.Maximum } + func (i *Items) GetExclusiveMaximum() *low.NodeReference[bool] { return &i.ExclusiveMaximum } + func (i *Items) GetMinimum() *low.NodeReference[int] { return &i.Minimum } + func (i *Items) GetExclusiveMinimum() *low.NodeReference[bool] { return &i.ExclusiveMinimum } + func (i *Items) GetMaxLength() *low.NodeReference[int] { return &i.MaxLength } + func (i *Items) GetMinLength() *low.NodeReference[int] { return &i.MinLength } + func (i *Items) GetPattern() *low.NodeReference[string] { return &i.Pattern } + func (i *Items) GetMaxItems() *low.NodeReference[int] { return &i.MaxItems } + func (i *Items) GetMinItems() *low.NodeReference[int] { return &i.MinItems } + func (i *Items) GetUniqueItems() *low.NodeReference[bool] { return &i.UniqueItems } -func (i *Items) GetEnum() *low.NodeReference[[]low.ValueReference[any]] { + +func (i *Items) GetEnum() *low.NodeReference[[]low.ValueReference[*yaml.Node]] { return &i.Enum } + func (i *Items) GetMultipleOf() *low.NodeReference[int] { return &i.MultipleOf } diff --git a/datamodel/low/v2/items_test.go b/datamodel/low/v2/items_test.go index a5bf4b3..08fad7e 100644 --- a/datamodel/low/v2/items_test.go +++ b/datamodel/low/v2/items_test.go @@ -5,15 +5,16 @@ package v2 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestItems_Build(t *testing.T) { - yml := `items: $ref: break` @@ -31,7 +32,6 @@ func TestItems_Build(t *testing.T) { } func TestItems_DefaultAsSlice(t *testing.T) { - yml := `x-thing: thing default: - pizza @@ -45,12 +45,14 @@ default: _ = low.BuildModel(&idxNode, &n) _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) - assert.Len(t, n.Default.Value, 2) - assert.Len(t, n.GetExtensions(), 1) + var def []string + _ = n.Default.Value.Decode(&def) + + assert.Len(t, def, 2) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestItems_DefaultAsMap(t *testing.T) { - yml := `default: hot: pizza tasty: beer` @@ -63,12 +65,13 @@ func TestItems_DefaultAsMap(t *testing.T) { _ = low.BuildModel(&idxNode, &n) _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) - assert.Len(t, n.Default.Value, 2) + var def map[string]string + _ = n.Default.GetValue().Decode(&def) + assert.Len(t, def, 2) } func TestItems_Hash_n_Grab(t *testing.T) { - yml := `type: string format: left collectionFormat: nice @@ -138,7 +141,10 @@ pattern: wow assert.Equal(t, "left", n.GetFormat().Value) assert.Equal(t, "left", n.GetFormat().Value) assert.Equal(t, "nice", n.GetCollectionFormat().Value) - assert.Equal(t, "shut that door!", n.GetDefault().Value) + + var def string + _ = n.GetDefault().Value.Decode(&def) + assert.Equal(t, "shut that door!", def) assert.Equal(t, 10, n.GetMaximum().Value) assert.Equal(t, 1, n.GetMinimum().Value) assert.True(t, n.GetExclusiveMinimum().Value) @@ -152,7 +158,8 @@ pattern: wow assert.Equal(t, "wow", n.GetPattern().Value) assert.Equal(t, "int", n.GetItems().Value.(*Items).Type.Value) assert.Len(t, n.GetEnum().Value, 2) - assert.Equal(t, "large", n.FindExtension("x-belly").Value) - assert.Nil(t, n.GetDescription()) + var xBelly string + _ = n.FindExtension("x-belly").GetValue().Decode(&xBelly) + assert.Equal(t, "large", xBelly) } diff --git a/datamodel/low/v2/operation.go b/datamodel/low/v2/operation.go index bf4a4c6..a50b42d 100644 --- a/datamodel/low/v2/operation.go +++ b/datamodel/low/v2/operation.go @@ -7,13 +7,15 @@ import ( "context" "crypto/sha256" "fmt" + "sort" + "strings" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sort" - "strings" ) // Operation represents a low-level Swagger / OpenAPI 2 Operation object. @@ -33,7 +35,7 @@ type Operation struct { Schemes low.NodeReference[[]low.ValueReference[string]] Deprecated low.NodeReference[bool] Security low.NodeReference[[]low.ValueReference[*base.SecurityRequirement]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // Build will extract external docs, extensions, parameters, responses and security requirements. @@ -150,14 +152,7 @@ func (o *Operation) Hash() [32]byte { } sort.Strings(keys) f = append(f, keys...) - keys = make([]string, len(o.Extensions)) - z := 0 - for k := range o.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(o.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(o.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -185,7 +180,7 @@ func (o *Operation) GetOperationId() low.NodeReference[string] { func (o *Operation) GetDeprecated() low.NodeReference[bool] { return o.Deprecated } -func (o *Operation) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (o *Operation) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return o.Extensions } func (o *Operation) GetResponses() low.NodeReference[any] { diff --git a/datamodel/low/v2/operation_test.go b/datamodel/low/v2/operation_test.go index 87e015f..cc690ad 100644 --- a/datamodel/low/v2/operation_test.go +++ b/datamodel/low/v2/operation_test.go @@ -16,7 +16,6 @@ import ( ) func TestOperation_Build_ExternalDocs(t *testing.T) { - yml := `externalDocs: $ref: break` @@ -31,11 +30,9 @@ func TestOperation_Build_ExternalDocs(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestOperation_Build_Params(t *testing.T) { - yml := `parameters: $ref: break` @@ -50,11 +47,9 @@ func TestOperation_Build_Params(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestOperation_Build_Responses(t *testing.T) { - yml := `responses: $ref: break` @@ -69,11 +64,9 @@ func TestOperation_Build_Responses(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestOperation_Build_Security(t *testing.T) { - yml := `security: $ref: break` @@ -88,11 +81,9 @@ func TestOperation_Build_Security(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestOperation_Hash_n_Grab(t *testing.T) { - yml := `tags: - nice - hat @@ -185,5 +176,5 @@ security: assert.True(t, n.GetDeprecated().Value) assert.Equal(t, 1, orderedmap.Len(n.GetResponses().Value.(*Responses).Codes)) assert.Len(t, n.GetSecurity().Value, 1) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v2/parameter.go b/datamodel/low/v2/parameter.go index 96514ab..ed1b47f 100644 --- a/datamodel/low/v2/parameter.go +++ b/datamodel/low/v2/parameter.go @@ -7,13 +7,15 @@ import ( "context" "crypto/sha256" "fmt" + "sort" + "strings" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sort" - "strings" ) // Parameter represents a low-level Swagger / OpenAPI 2 Parameter object. @@ -68,7 +70,7 @@ type Parameter struct { Schema low.NodeReference[*base.SchemaProxy] Items low.NodeReference[*Items] CollectionFormat low.NodeReference[string] - Default low.NodeReference[any] + Default low.NodeReference[*yaml.Node] Maximum low.NodeReference[int] ExclusiveMaximum low.NodeReference[bool] Minimum low.NodeReference[int] @@ -79,18 +81,18 @@ type Parameter struct { MaxItems low.NodeReference[int] MinItems low.NodeReference[int] UniqueItems low.NodeReference[bool] - Enum low.NodeReference[[]low.ValueReference[any]] + Enum low.NodeReference[[]low.ValueReference[*yaml.Node]] MultipleOf low.NodeReference[int] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // FindExtension attempts to locate a extension value given a name. -func (p *Parameter) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, p.Extensions) +func (p *Parameter) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, p.Extensions) } // GetExtensions returns all Parameter extensions and satisfies the low.HasExtensions interface. -func (p *Parameter) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (p *Parameter) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return p.Extensions } @@ -114,30 +116,8 @@ func (p *Parameter) Build(ctx context.Context, _, root *yaml.Node, idx *index.Sp _, ln, vn := utils.FindKeyNodeFull(DefaultLabel, root.Content) if vn != nil { - var n map[string]interface{} - err := vn.Decode(&n) - if err != nil { - var k []interface{} - err = vn.Decode(&k) - if err != nil { - var j interface{} - _ = vn.Decode(&j) - p.Default = low.NodeReference[any]{ - Value: j, - KeyNode: ln, - ValueNode: vn, - } - return nil - } - p.Default = low.NodeReference[any]{ - Value: k, - KeyNode: ln, - ValueNode: vn, - } - return nil - } - p.Default = low.NodeReference[any]{ - Value: n, + p.Default = low.NodeReference[*yaml.Node]{ + Value: vn, KeyNode: ln, ValueNode: vn, } @@ -172,8 +152,8 @@ func (p *Parameter) Hash() [32]byte { if p.CollectionFormat.Value != "" { f = append(f, p.CollectionFormat.Value) } - if p.Default.Value != "" { - f = append(f, fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(p.Default.Value))))) + if p.Default.Value != nil && !p.Default.Value.IsZero() { + f = append(f, low.GenerateHashString(p.Default.Value)) } f = append(f, fmt.Sprint(p.Maximum.Value)) f = append(f, fmt.Sprint(p.Minimum.Value)) @@ -192,20 +172,13 @@ func (p *Parameter) Hash() [32]byte { keys := make([]string, len(p.Enum.Value)) z := 0 for k := range p.Enum.Value { - keys[z] = fmt.Sprint(p.Enum.Value[k].Value) + keys[z] = low.ValueToString(p.Enum.Value[k].Value) z++ } sort.Strings(keys) f = append(f, keys...) - keys = make([]string, len(p.Extensions)) - z = 0 - for k := range p.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(p.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(p.Extensions)...) if p.Items.Value != nil { f = append(f, fmt.Sprintf("%x", p.Items.Value.Hash())) } @@ -217,21 +190,27 @@ func (p *Parameter) Hash() [32]byte { func (p *Parameter) GetName() *low.NodeReference[string] { return &p.Name } + func (p *Parameter) GetIn() *low.NodeReference[string] { return &p.In } + func (p *Parameter) GetType() *low.NodeReference[string] { return &p.Type } + func (p *Parameter) GetDescription() *low.NodeReference[string] { return &p.Description } + func (p *Parameter) GetRequired() *low.NodeReference[bool] { return &p.Required } + func (p *Parameter) GetAllowEmptyValue() *low.NodeReference[bool] { return &p.AllowEmptyValue } + func (p *Parameter) GetSchema() *low.NodeReference[any] { i := low.NodeReference[any]{ KeyNode: p.Schema.KeyNode, @@ -240,9 +219,11 @@ func (p *Parameter) GetSchema() *low.NodeReference[any] { } return &i } + func (p *Parameter) GetFormat() *low.NodeReference[string] { return &p.Format } + func (p *Parameter) GetItems() *low.NodeReference[any] { i := low.NodeReference[any]{ KeyNode: p.Items.KeyNode, @@ -251,45 +232,59 @@ func (p *Parameter) GetItems() *low.NodeReference[any] { } return &i } + func (p *Parameter) GetCollectionFormat() *low.NodeReference[string] { return &p.CollectionFormat } -func (p *Parameter) GetDefault() *low.NodeReference[any] { + +func (p *Parameter) GetDefault() *low.NodeReference[*yaml.Node] { return &p.Default } + func (p *Parameter) GetMaximum() *low.NodeReference[int] { return &p.Maximum } + func (p *Parameter) GetExclusiveMaximum() *low.NodeReference[bool] { return &p.ExclusiveMaximum } + func (p *Parameter) GetMinimum() *low.NodeReference[int] { return &p.Minimum } + func (p *Parameter) GetExclusiveMinimum() *low.NodeReference[bool] { return &p.ExclusiveMinimum } + func (p *Parameter) GetMaxLength() *low.NodeReference[int] { return &p.MaxLength } + func (p *Parameter) GetMinLength() *low.NodeReference[int] { return &p.MinLength } + func (p *Parameter) GetPattern() *low.NodeReference[string] { return &p.Pattern } + func (p *Parameter) GetMaxItems() *low.NodeReference[int] { return &p.MaxItems } + func (p *Parameter) GetMinItems() *low.NodeReference[int] { return &p.MinItems } + func (p *Parameter) GetUniqueItems() *low.NodeReference[bool] { return &p.UniqueItems } -func (p *Parameter) GetEnum() *low.NodeReference[[]low.ValueReference[any]] { + +func (p *Parameter) GetEnum() *low.NodeReference[[]low.ValueReference[*yaml.Node]] { return &p.Enum } + func (p *Parameter) GetMultipleOf() *low.NodeReference[int] { return &p.MultipleOf } diff --git a/datamodel/low/v2/parameter_test.go b/datamodel/low/v2/parameter_test.go index ae2cedd..755696c 100644 --- a/datamodel/low/v2/parameter_test.go +++ b/datamodel/low/v2/parameter_test.go @@ -5,16 +5,17 @@ package v2 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestParameter_Build(t *testing.T) { - yml := `$ref: break` var idxNode yaml.Node @@ -28,11 +29,9 @@ func TestParameter_Build(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestParameter_Build_Items(t *testing.T) { - yml := `items: $ref: break` @@ -47,11 +46,9 @@ func TestParameter_Build_Items(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestParameter_DefaultSlice(t *testing.T) { - yml := `default: - things - junk @@ -65,11 +62,14 @@ func TestParameter_DefaultSlice(t *testing.T) { _ = low.BuildModel(&idxNode, &n) _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) - assert.Len(t, n.Default.Value.([]any), 3) + + var a []any + _ = n.Default.Value.Decode(&a) + + assert.Len(t, a, 3) } func TestParameter_DefaultMap(t *testing.T) { - yml := `default: things: junk stuff: more junk` @@ -82,11 +82,14 @@ func TestParameter_DefaultMap(t *testing.T) { _ = low.BuildModel(&idxNode, &n) _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) - assert.Len(t, n.Default.Value.(map[string]any), 2) + + var m map[string]any + _ = n.Default.Value.Decode(&m) + + assert.Len(t, m, 2) } func TestParameter_NoDefaultNoError(t *testing.T) { - yml := `name: pizza-pie` var idxNode yaml.Node @@ -101,7 +104,6 @@ func TestParameter_NoDefaultNoError(t *testing.T) { } func TestParameter_Hash_n_Grab(t *testing.T) { - yml := `name: mcmuffin in: my-belly description: tasty! @@ -185,7 +187,10 @@ allowEmptyValue: true assert.Equal(t, "left", n.GetFormat().Value) assert.Equal(t, "left", n.GetFormat().Value) assert.Equal(t, "nice", n.GetCollectionFormat().Value) - assert.Equal(t, "shut that door!", n.GetDefault().Value) + + var def string + _ = n.GetDefault().Value.Decode(&def) + assert.Equal(t, "shut that door!", def) assert.Equal(t, 10, n.GetMaximum().Value) assert.Equal(t, 1, n.GetMinimum().Value) assert.True(t, n.GetExclusiveMinimum().Value) @@ -199,7 +204,10 @@ allowEmptyValue: true assert.Equal(t, "wow", n.GetPattern().Value) assert.Equal(t, "int", n.GetItems().Value.(*Items).Type.Value) assert.Len(t, n.GetEnum().Value, 2) - assert.Equal(t, "large", n.FindExtension("x-belly").Value) + + var xBelly string + _ = n.FindExtension("x-belly").Value.Decode(&xBelly) + assert.Equal(t, "large", xBelly) assert.Equal(t, "tasty!", n.GetDescription().Value) assert.Equal(t, "mcmuffin", n.GetName().Value) assert.Equal(t, "my-belly", n.GetIn().Value) @@ -208,6 +216,5 @@ allowEmptyValue: true assert.Equal(t, "int", v.Value.A) // A is v2 assert.True(t, n.GetRequired().Value) assert.True(t, n.GetAllowEmptyValue().Value) - assert.Len(t, n.GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v2/path_item.go b/datamodel/low/v2/path_item.go index 944d665..0bc287a 100644 --- a/datamodel/low/v2/path_item.go +++ b/datamodel/low/v2/path_item.go @@ -13,6 +13,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -34,16 +35,16 @@ type PathItem struct { Head low.NodeReference[*Operation] Patch low.NodeReference[*Operation] Parameters low.NodeReference[[]low.ValueReference[*Parameter]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // FindExtension will attempt to locate an extension given a name. -func (p *PathItem) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, p.Extensions) +func (p *PathItem) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, p.Extensions) } // GetExtensions returns all PathItem extensions and satisfies the low.HasExtensions interface. -func (p *PathItem) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (p *PathItem) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return p.Extensions } @@ -153,8 +154,8 @@ func (p *PathItem) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe } } - //all operations have been superficially built, - //now we need to build out the operation, we will do this asynchronously for speed. + // all operations have been superficially built, + // now we need to build out the operation, we will do this asynchronously for speed. opBuildChan := make(chan bool) opErrorChan := make(chan error) @@ -223,13 +224,6 @@ func (p *PathItem) Hash() [32]byte { } sort.Strings(keys) f = append(f, keys...) - keys = make([]string, len(p.Extensions)) - z := 0 - for k := range p.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(p.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(p.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v2/path_item_test.go b/datamodel/low/v2/path_item_test.go index aac313e..89cc063 100644 --- a/datamodel/low/v2/path_item_test.go +++ b/datamodel/low/v2/path_item_test.go @@ -5,15 +5,16 @@ package v2 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestPathItem_Build_Params(t *testing.T) { - yml := `parameters: $ref: break` @@ -28,11 +29,9 @@ func TestPathItem_Build_Params(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestPathItem_Build_MethodFail(t *testing.T) { - yml := `post: $ref: break` @@ -47,11 +46,9 @@ func TestPathItem_Build_MethodFail(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestPathItem_Hash(t *testing.T) { - yml := `get: description: get me up put: @@ -108,6 +105,5 @@ parameters: // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v2/paths.go b/datamodel/low/v2/paths.go index 2655b6e..614f0d8 100644 --- a/datamodel/low/v2/paths.go +++ b/datamodel/low/v2/paths.go @@ -6,8 +6,6 @@ package v2 import ( "context" "crypto/sha256" - "fmt" - "sort" "strings" "sync" @@ -21,12 +19,12 @@ import ( // Paths represents a low-level Swagger / OpenAPI Paths object. type Paths struct { - PathItems orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + PathItems *orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // GetExtensions returns all Paths extensions and satisfies the low.HasExtensions interface. -func (p *Paths) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (p *Paths) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return p.Extensions } @@ -54,8 +52,8 @@ func (p *Paths) FindPathAndKey(path string) (key *low.KeyReference[string], valu } // FindExtension will attempt to locate an extension value given a name. -func (p *Paths) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, p.Extensions) +func (p *Paths) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, p.Extensions) } // Build will extract extensions and paths from node. @@ -159,28 +157,9 @@ func (p *Paths) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIn // Hash will return a consistent SHA256 Hash of the PathItem object func (p *Paths) Hash() [32]byte { var f []string - l := make([]string, orderedmap.Len(p.PathItems)) - keys := make(map[string]low.ValueReference[*PathItem]) - z := 0 - - for pair := orderedmap.First(p.PathItems); pair != nil; pair = pair.Next() { - k := pair.Key().Value - keys[k] = pair.Value() - l[z] = k - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(p.PathItems)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } - - sort.Strings(l) - for k := range l { - f = append(f, low.GenerateHashString(keys[l[k]].Value)) - } - ekeys := make([]string, len(p.Extensions)) - z = 0 - for k := range p.Extensions { - ekeys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(p.Extensions[k].Value)))) - z++ - } - sort.Strings(ekeys) - f = append(f, ekeys...) + f = append(f, low.HashExtensions(p.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v2/paths_test.go b/datamodel/low/v2/paths_test.go index 41d26af..609fde9 100644 --- a/datamodel/low/v2/paths_test.go +++ b/datamodel/low/v2/paths_test.go @@ -10,12 +10,12 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) func TestPaths_Build(t *testing.T) { - yml := `"/fresh/code": $ref: break` @@ -30,11 +30,9 @@ func TestPaths_Build(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestPaths_FindPathAndKey(t *testing.T) { - yml := `/no/sleep: get: description: til brooklyn @@ -57,7 +55,6 @@ func TestPaths_FindPathAndKey(t *testing.T) { } func TestPaths_Hash(t *testing.T) { - yml := `/data/dog: get: description: does data kinda, ish. @@ -99,8 +96,7 @@ x-milk: creamy` // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } // Test parse failure among many paths. diff --git a/datamodel/low/v2/response.go b/datamodel/low/v2/response.go index 08ca388..b2ac4c9 100644 --- a/datamodel/low/v2/response.go +++ b/datamodel/low/v2/response.go @@ -6,8 +6,6 @@ package v2 import ( "context" "crypto/sha256" - "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -25,18 +23,18 @@ import ( type Response struct { Description low.NodeReference[string] Schema low.NodeReference[*base.SchemaProxy] - Headers low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]] + Headers low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]] Examples low.NodeReference[*Examples] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // FindExtension will attempt to locate an extension value given a key to lookup. -func (r *Response) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, r.Extensions) +func (r *Response) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, r.Extensions) } // GetExtensions returns all Response extensions and satisfies the low.HasExtensions interface. -func (r *Response) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (r *Response) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return r.Extensions } @@ -65,13 +63,13 @@ func (r *Response) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe } r.Examples = examples - //extract headers + // extract headers headers, lN, kN, err := low.ExtractMap[*Header](ctx, HeadersLabel, root, idx) if err != nil { return err } if headers != nil { - r.Headers = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]]{ + r.Headers = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]]{ Value: headers, KeyNode: lN, ValueNode: kN, @@ -90,17 +88,10 @@ func (r *Response) Hash() [32]byte { f = append(f, low.GenerateHashString(r.Schema.Value)) } if !r.Examples.IsEmpty() { - for pair := orderedmap.First(r.Examples.Value.Values); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(orderedmap.SortAlpha(r.Examples.Value.Values)); pair != nil; pair = pair.Next() { f = append(f, low.GenerateHashString(pair.Value().Value)) } } - keys := make([]string, len(r.Extensions)) - z := 0 - for k := range r.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(r.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(r.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v2/response_test.go b/datamodel/low/v2/response_test.go index 786932a..514c432 100644 --- a/datamodel/low/v2/response_test.go +++ b/datamodel/low/v2/response_test.go @@ -5,15 +5,16 @@ package v2 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestResponse_Build_Schema(t *testing.T) { - yml := `schema: $ref: break` @@ -28,11 +29,9 @@ func TestResponse_Build_Schema(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponse_Build_Examples(t *testing.T) { - yml := `examples: $ref: break` @@ -47,11 +46,9 @@ func TestResponse_Build_Examples(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponse_Build_Headers(t *testing.T) { - yml := `headers: $ref: break` @@ -66,11 +63,9 @@ func TestResponse_Build_Headers(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponse_Hash(t *testing.T) { - yml := `description: your thing, sir. schema: type: string @@ -111,5 +106,5 @@ headers: // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v2/responses.go b/datamodel/low/v2/responses.go index 4c76b6c..c2564dc 100644 --- a/datamodel/low/v2/responses.go +++ b/datamodel/low/v2/responses.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -19,13 +18,13 @@ import ( // Responses is a low-level representation of a Swagger / OpenAPI 2 Responses object. type Responses struct { - Codes orderedmap.Map[low.KeyReference[string], low.ValueReference[*Response]] + Codes *orderedmap.Map[low.KeyReference[string], low.ValueReference[*Response]] Default low.NodeReference[*Response] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // GetExtensions returns all Responses extensions and satisfies the low.HasExtensions interface. -func (r *Responses) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (r *Responses) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return r.Extensions } @@ -95,29 +94,12 @@ func (r *Responses) FindResponseByCode(code string) *low.ValueReference[*Respons // Hash will return a consistent SHA256 Hash of the Examples object func (r *Responses) Hash() [32]byte { var f []string - var keys []string - keys = make([]string, orderedmap.Len(r.Codes)) - cmap := make(map[string]*Response, len(keys)) - z := 0 - for pair := orderedmap.First(r.Codes); pair != nil; pair = pair.Next() { - keys[z] = pair.Key().Value - cmap[pair.Key().Value] = pair.Value().Value - z++ - } - sort.Strings(keys) - for k := range keys { - f = append(f, fmt.Sprintf("%s-%s", keys[k], low.GenerateHashString(cmap[keys[k]]))) + for pair := orderedmap.First(orderedmap.SortAlpha(r.Codes)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) } if !r.Default.IsEmpty() { f = append(f, low.GenerateHashString(r.Default.Value)) } - keys = make([]string, len(r.Extensions)) - z = 0 - for k := range r.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(r.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(r.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v2/responses_test.go b/datamodel/low/v2/responses_test.go index 5cb3c24..0013969 100644 --- a/datamodel/low/v2/responses_test.go +++ b/datamodel/low/v2/responses_test.go @@ -5,15 +5,16 @@ package v2 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestResponses_Build_Response(t *testing.T) { - yml := `- $ref: break` var idxNode yaml.Node @@ -27,11 +28,9 @@ func TestResponses_Build_Response(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Build_Response_Default(t *testing.T) { - yml := `default: $ref: break` @@ -46,11 +45,9 @@ func TestResponses_Build_Response_Default(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Build_WrongType(t *testing.T) { - yml := `- $ref: break` var idxNode yaml.Node @@ -64,11 +61,9 @@ func TestResponses_Build_WrongType(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Hash(t *testing.T) { - yml := `default: description: I am a potato 200: @@ -115,6 +110,5 @@ x-tea: warm` // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v2/scopes.go b/datamodel/low/v2/scopes.go index e6395af..e9aaa3c 100644 --- a/datamodel/low/v2/scopes.go +++ b/datamodel/low/v2/scopes.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -22,12 +21,12 @@ import ( // Scopes lists the available scopes for an OAuth2 security scheme. // - https://swagger.io/specification/v2/#scopesObject type Scopes struct { - Values orderedmap.Map[low.KeyReference[string], low.ValueReference[string]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Values *orderedmap.Map[low.KeyReference[string], low.ValueReference[string]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // GetExtensions returns all Scopes extensions and satisfies the low.HasExtensions interface. -func (s *Scopes) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (s *Scopes) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return s.Extensions } @@ -68,25 +67,9 @@ func (s *Scopes) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecIndex // Hash will return a consistent SHA256 Hash of the Scopes object func (s *Scopes) Hash() [32]byte { var f []string - vals := make(map[string]low.ValueReference[string], orderedmap.Len(s.Values)) - keys := make([]string, orderedmap.Len(s.Values)) - z := 0 - for pair := orderedmap.First(s.Values); pair != nil; pair = pair.Next() { - keys[z] = pair.Key().Value - vals[pair.Key().Value] = pair.Value() - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(s.Values)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, pair.Value().Value)) } - sort.Strings(keys) - for k := range keys { - f = append(f, fmt.Sprintf("%s-%s", keys[k], vals[keys[k]].Value)) - } - keys = make([]string, len(s.Extensions)) - z = 0 - for k := range s.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(s.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(s.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v2/scopes_test.go b/datamodel/low/v2/scopes_test.go index 7c0b521..1e5df0b 100644 --- a/datamodel/low/v2/scopes_test.go +++ b/datamodel/low/v2/scopes_test.go @@ -5,15 +5,16 @@ package v2 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestScopes_Hash(t *testing.T) { - yml := `burgers: chips pizza: beans x-men: needs a reboot or a refresh` @@ -40,6 +41,5 @@ burgers: chips` // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v2/security_scheme.go b/datamodel/low/v2/security_scheme.go index bdf235d..e4f7a7d 100644 --- a/datamodel/low/v2/security_scheme.go +++ b/datamodel/low/v2/security_scheme.go @@ -6,13 +6,13 @@ package v2 import ( "context" "crypto/sha256" - "fmt" + "strings" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sort" - "strings" ) // SecurityScheme is a low-level representation of a Swagger / OpenAPI 2 SecurityScheme object. @@ -30,11 +30,11 @@ type SecurityScheme struct { AuthorizationUrl low.NodeReference[string] TokenUrl low.NodeReference[string] Scopes low.NodeReference[*Scopes] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] } // GetExtensions returns all SecurityScheme extensions and satisfies the low.HasExtensions interface. -func (ss *SecurityScheme) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (ss *SecurityScheme) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return ss.Extensions } @@ -79,13 +79,6 @@ func (ss *SecurityScheme) Hash() [32]byte { if !ss.Scopes.IsEmpty() { f = append(f, low.GenerateHashString(ss.Scopes.Value)) } - keys := make([]string, len(ss.Extensions)) - z := 0 - for k := range ss.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(ss.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(ss.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v2/security_scheme_test.go b/datamodel/low/v2/security_scheme_test.go index 6312dae..a45a8b4 100644 --- a/datamodel/low/v2/security_scheme_test.go +++ b/datamodel/low/v2/security_scheme_test.go @@ -15,7 +15,6 @@ import ( ) func TestSecurityScheme_Build_Borked(t *testing.T) { - yml := `scopes: $ref: break` @@ -30,11 +29,9 @@ func TestSecurityScheme_Build_Borked(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestSecurityScheme_Build_Scopes(t *testing.T) { - yml := `scopes: some:thing: here something: there` @@ -55,7 +52,6 @@ func TestSecurityScheme_Build_Scopes(t *testing.T) { } func TestSecurityScheme_Hash(t *testing.T) { - yml := `type: secure description: a very secure thing name: securityPerson @@ -97,6 +93,5 @@ authorizationUrl: https://pb33f.io // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v2/swagger.go b/datamodel/low/v2/swagger.go index 244cca0..908255b 100644 --- a/datamodel/low/v2/swagger.go +++ b/datamodel/low/v2/swagger.go @@ -14,12 +14,14 @@ package v2 import ( "context" "errors" + "path/filepath" + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" - "path/filepath" ) // processes a property of a Swagger document asynchronously using bool and error channels for signals. @@ -27,7 +29,6 @@ type documentFunction func(ctx context.Context, root *yaml.Node, doc *Swagger, i // Swagger represents a high-level Swagger / OpenAPI 2 document. An instance of Swagger is the root of the specification. type Swagger struct { - // Swagger is the version of Swagger / OpenAPI being used, extracted from the 'swagger: 2.x' definition. Swagger low.ValueReference[string] @@ -98,7 +99,7 @@ type Swagger struct { ExternalDocs low.NodeReference[*base.ExternalDoc] // Extensions contains all custom extensions defined for the top-level document. - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] // Index is a reference to the index.SpecIndex that was created for the document and used // as a guide when building out the Document. Ideal if further processing is required on the model and @@ -118,12 +119,12 @@ type Swagger struct { } // FindExtension locates an extension from the root of the Swagger document. -func (s *Swagger) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, s.Extensions) +func (s *Swagger) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, s.Extensions) } // GetExtensions returns all Swagger/Top level extensions and satisfies the low.HasExtensions interface. -func (s *Swagger) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (s *Swagger) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return s.Extensions } @@ -279,6 +280,7 @@ func extractPaths(ctx context.Context, root *yaml.Node, doc *Swagger, idx *index doc.Paths = paths c <- true } + func extractDefinitions(ctx context.Context, root *yaml.Node, doc *Swagger, idx *index.SpecIndex, c chan<- bool, e chan<- error) { def, err := low.ExtractObject[*Definitions](ctx, DefinitionsLabel, root, idx) if err != nil { @@ -288,6 +290,7 @@ func extractDefinitions(ctx context.Context, root *yaml.Node, doc *Swagger, idx doc.Definitions = def c <- true } + func extractParamDefinitions(ctx context.Context, root *yaml.Node, doc *Swagger, idx *index.SpecIndex, c chan<- bool, e chan<- error) { param, err := low.ExtractObject[*ParameterDefinitions](ctx, ParametersLabel, root, idx) if err != nil { diff --git a/datamodel/low/v2/swagger_test.go b/datamodel/low/v2/swagger_test.go index c0c5318..395966d 100644 --- a/datamodel/low/v2/swagger_test.go +++ b/datamodel/low/v2/swagger_test.go @@ -5,16 +5,18 @@ package v2 import ( "fmt" - "github.com/pb33f/libopenapi/index" - "github.com/pb33f/libopenapi/utils" "net/http" "net/url" "os" "testing" + "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var doc *Swagger @@ -63,10 +65,13 @@ func TestCreateDocument(t *testing.T) { assert.Equal(t, 15, orderedmap.Len(doc.Paths.Value.PathItems)) assert.Equal(t, 2, orderedmap.Len(doc.Responses.Value.Definitions)) assert.Equal(t, "http://swagger.io", doc.ExternalDocs.Value.URL.Value) - assert.Equal(t, true, doc.FindExtension("x-pet").Value) - assert.Equal(t, true, doc.FindExtension("X-Pet").Value) + + var xPet bool + _ = doc.FindExtension("x-pet").Value.Decode(&xPet) + + assert.Equal(t, true, xPet) assert.NotNil(t, doc.GetExternalDocs()) - assert.Len(t, doc.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(doc.GetExtensions())) } func TestCreateDocument_Info(t *testing.T) { @@ -81,8 +86,11 @@ func TestCreateDocument_Parameters(t *testing.T) { simpleParam := doc.Parameters.Value.FindParameter("simpleParam") assert.NotNil(t, simpleParam) assert.Equal(t, "simple", simpleParam.Value.Name.Value) - assert.Equal(t, "nuggets", simpleParam.Value.FindExtension("x-chicken").Value) + var xChicken string + _ = simpleParam.Value.FindExtension("x-chicken").Value.Decode(&xChicken) + + assert.Equal(t, "nuggets", xChicken) } func TestCreateDocument_Tags(t *testing.T) { @@ -128,30 +136,43 @@ func TestCreateDocument_ResponseDefinitions(t *testing.T) { apiResp := doc.Responses.Value.FindResponse("200") assert.NotNil(t, apiResp) assert.Equal(t, "OK", apiResp.Value.Description.Value) - assert.Equal(t, "morning", apiResp.Value.FindExtension("x-coffee").Value) + + var xCoffee string + _ = apiResp.Value.FindExtension("x-coffee").Value.Decode(&xCoffee) + + assert.Equal(t, "morning", xCoffee) header := apiResp.Value.FindHeader("noHeader") assert.NotNil(t, header) - assert.True(t, header.Value.FindExtension("x-empty").Value.(bool)) + + var xEmpty bool + _ = header.Value.FindExtension("x-empty").Value.Decode(&xEmpty) + + assert.True(t, xEmpty) header = apiResp.Value.FindHeader("myHeader") - if k, ok := header.Value.Items.Value.Default.Value.(map[string]interface{}); ok { - assert.Equal(t, "here", k["something"]) - } else { - panic("should not fail.") - } - if k, ok := header.Value.Items.Value.Items.Value.Default.Value.([]interface{}); ok { - assert.Len(t, k, 2) - assert.Equal(t, "two", k[1]) - } else { - panic("should not fail.") - } + + var m map[string]any + err := header.Value.Items.Value.Default.Value.Decode(&m) + require.NoError(t, err) + + assert.Equal(t, "here", m["something"]) + + var a []any + err = header.Value.Items.Value.Items.Value.Default.Value.Decode(&a) + require.NoError(t, err) + + assert.Len(t, a, 2) + assert.Equal(t, "two", a[1]) header = apiResp.Value.FindHeader("yourHeader") - assert.Equal(t, "somethingSimple", header.Value.Items.Value.Default.Value) + + var def string + _ = header.Value.Items.Value.Default.Value.Decode(&def) + + assert.Equal(t, "somethingSimple", def) assert.NotNil(t, apiResp.Value.Examples.Value.FindExample("application/json").Value) - } func TestCreateDocument_Paths(t *testing.T) { @@ -159,15 +180,21 @@ func TestCreateDocument_Paths(t *testing.T) { uploadImage := doc.Paths.Value.FindPath("/pet/{petId}/uploadImage").Value assert.NotNil(t, uploadImage) assert.Nil(t, doc.Paths.Value.FindPath("/nothing-nowhere-nohow")) - assert.Equal(t, "man", uploadImage.FindExtension("x-potato").Value) - assert.Equal(t, "fresh", doc.Paths.Value.FindExtension("x-minty").Value) + + var xPotato string + _ = uploadImage.FindExtension("x-potato").Value.Decode(&xPotato) + + assert.Equal(t, "man", xPotato) + + var xMinty string + _ = doc.Paths.Value.FindExtension("x-minty").Value.Decode(&xMinty) + + assert.Equal(t, "fresh", xMinty) assert.Equal(t, "successful operation", uploadImage.Post.Value.Responses.Value.FindResponseByCode("200").Value.Description.Value) - } func TestCreateDocument_Bad(t *testing.T) { - yml := `swagger: $ref: bork` @@ -177,7 +204,6 @@ func TestCreateDocument_Bad(t *testing.T) { } func TestCreateDocument_ExternalDocsBad(t *testing.T) { - yml := `externalDocs: $ref: bork` @@ -195,7 +221,6 @@ func TestCreateDocument_ExternalDocsBad(t *testing.T) { } func TestCreateDocument_TagsBad(t *testing.T) { - yml := `tags: $ref: bork` @@ -213,7 +238,6 @@ func TestCreateDocument_TagsBad(t *testing.T) { } func TestCreateDocument_PathsBad(t *testing.T) { - yml := `paths: "/hey": post: @@ -235,7 +259,6 @@ func TestCreateDocument_PathsBad(t *testing.T) { } func TestCreateDocument_SecurityBad(t *testing.T) { - yml := `security: $ref: ` @@ -253,7 +276,6 @@ func TestCreateDocument_SecurityBad(t *testing.T) { } func TestCreateDocument_SecurityDefinitionsBad(t *testing.T) { - yml := `securityDefinitions: $ref: ` @@ -271,7 +293,6 @@ func TestCreateDocument_SecurityDefinitionsBad(t *testing.T) { } func TestCreateDocument_ResponsesBad(t *testing.T) { - yml := `responses: $ref: ` @@ -289,7 +310,6 @@ func TestCreateDocument_ResponsesBad(t *testing.T) { } func TestCreateDocument_ParametersBad(t *testing.T) { - yml := `parameters: $ref: ` @@ -307,7 +327,6 @@ func TestCreateDocument_ParametersBad(t *testing.T) { } func TestCreateDocument_DefinitionsBad(t *testing.T) { - yml := `definitions: $ref: ` @@ -325,7 +344,6 @@ func TestCreateDocument_DefinitionsBad(t *testing.T) { } func TestCreateDocument_InfoBad(t *testing.T) { - yml := `info: $ref: ` @@ -343,13 +361,11 @@ func TestCreateDocument_InfoBad(t *testing.T) { } func TestCircularReferenceError(t *testing.T) { - data, _ := os.ReadFile("../../../test_specs/swagger-circular-tests.yaml") info, _ := datamodel.ExtractSpecInfo(data) circDoc, err := CreateDocumentFromConfig(info, datamodel.NewDocumentConfiguration()) assert.NotNil(t, circDoc) assert.Len(t, utils.UnwrapErrors(err), 3) - } func TestRolodexLocalFileSystem(t *testing.T) { diff --git a/datamodel/low/v3/callback.go b/datamodel/low/v3/callback.go index a3996bf..5547e2a 100644 --- a/datamodel/low/v3/callback.go +++ b/datamodel/low/v3/callback.go @@ -6,8 +6,6 @@ package v3 import ( "context" "crypto/sha256" - "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/orderedmap" @@ -26,19 +24,19 @@ import ( // that identifies a URL to use for the callback operation. // - https://spec.openapis.org/oas/v3.1.0#callback-object type Callback struct { - Expression low.ValueReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Expression *orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // GetExtensions returns all Callback extensions and satisfies the low.HasExtensions interface. -func (cb *Callback) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (cb *Callback) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return cb.Extensions } // FindExpression will locate a string expression and return a ValueReference containing the located PathItem func (cb *Callback) FindExpression(exp string) *low.ValueReference[*PathItem] { - return low.FindItemInOrderedMap[*PathItem](exp, cb.Expression.Value) + return low.FindItemInOrderedMap(exp, cb.Expression) } // Build will extract extensions, expressions and PathItem objects for Callback @@ -48,64 +46,22 @@ func (cb *Callback) Build(ctx context.Context, _, root *yaml.Node, idx *index.Sp cb.Reference = new(low.Reference) cb.Extensions = low.ExtractExtensions(root) - // handle callback - var currentCB *yaml.Node - callbacks := orderedmap.New[low.KeyReference[string], low.ValueReference[*PathItem]]() + expressions, err := extractPathItemsMap(ctx, root, idx) + if err != nil { + return err + } + cb.Expression = expressions - for i, callbackNode := range root.Content { - if i%2 == 0 { - currentCB = callbackNode - continue - } - if strings.HasPrefix(currentCB.Value, "x-") { - continue // ignore extension. - } - callback, eErr, _, rv := low.ExtractObjectRaw[*PathItem](ctx, currentCB, callbackNode, idx) - if eErr != nil { - return eErr - } - callbacks.Set( - low.KeyReference[string]{ - Value: currentCB.Value, - KeyNode: currentCB, - }, - low.ValueReference[*PathItem]{ - Value: callback, - ValueNode: callbackNode, - Reference: rv, - }, - ) - } - if orderedmap.Len(callbacks) > 0 { - cb.Expression = low.ValueReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]]]{ - Value: callbacks, - ValueNode: root, - } - } return nil } // Hash will return a consistent SHA256 Hash of the Callback object func (cb *Callback) Hash() [32]byte { var f []string - var keys []string - keys = make([]string, orderedmap.Len(cb.Expression.Value)) - z := 0 - for pair := orderedmap.First(cb.Expression.Value); pair != nil; pair = pair.Next() { - keys[z] = low.GenerateHashString(pair.Value().Value) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(cb.Expression)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } - sort.Strings(keys) - f = append(f, keys...) - - keys = make([]string, len(cb.Extensions)) - z = 0 - for k := range cb.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(cb.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(cb.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/callback_test.go b/datamodel/low/v3/callback_test.go index f3561c0..3ea04bc 100644 --- a/datamodel/low/v3/callback_test.go +++ b/datamodel/low/v3/callback_test.go @@ -15,7 +15,6 @@ import ( ) func TestCallback_Build_Success(t *testing.T) { - yml := `'{$request.query.queryUrl}': post: requestBody: @@ -39,12 +38,10 @@ func TestCallback_Build_Success(t *testing.T) { err = n.Build(context.Background(), nil, rootNode.Content[0], nil) assert.NoError(t, err) - assert.Equal(t, 1, orderedmap.Len(n.Expression.Value)) - + assert.Equal(t, 1, orderedmap.Len(n.Expression)) } func TestCallback_Build_Error(t *testing.T) { - // first we need an index. doc := `components: schemas: @@ -70,11 +67,9 @@ func TestCallback_Build_Error(t *testing.T) { err = n.Build(context.Background(), nil, rootNode.Content[0], idx) assert.Error(t, err) - } func TestCallback_Build_Using_InlineRef(t *testing.T) { - // first we need an index. doc := `components: schemas: @@ -105,17 +100,15 @@ func TestCallback_Build_Using_InlineRef(t *testing.T) { err = n.Build(context.Background(), nil, rootNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, 1, orderedmap.Len(n.Expression.Value)) + assert.Equal(t, 1, orderedmap.Len(n.Expression)) exp := n.FindExpression("{$request.query.queryUrl}") assert.NotNil(t, exp.Value) assert.NotNil(t, exp.Value.Post.Value) assert.Equal(t, "this is something", exp.Value.Post.Value.RequestBody.Value.Description.Value) - } func TestCallback_Hash(t *testing.T) { - yml := `x-seed: grow pizza: description: cheesy @@ -152,6 +145,5 @@ beer: // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 2) - + assert.Equal(t, 2, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v3/components.go b/datamodel/low/v3/components.go index 447c6d1..a4d8e1e 100644 --- a/datamodel/low/v3/components.go +++ b/datamodel/low/v3/components.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "sync" @@ -26,16 +25,16 @@ import ( // will have no effect on the API unless they are explicitly referenced from properties outside the components object. // - https://spec.openapis.org/oas/v3.1.0#components-object type Components struct { - Schemas low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]]] - Responses low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Response]]] - Parameters low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Parameter]]] - Examples low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]] - RequestBodies low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*RequestBody]]] - Headers low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]] - SecuritySchemes low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*SecurityScheme]]] - Links low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Link]]] - Callbacks low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Callback]]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Schemas low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]]] + Responses low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Response]]] + Parameters low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Parameter]]] + Examples low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]] + RequestBodies low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*RequestBody]]] + Headers low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]] + SecuritySchemes low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*SecurityScheme]]] + Links low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Link]]] + Callbacks low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Callback]]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } @@ -50,7 +49,7 @@ type componentInput struct { } // GetExtensions returns all Components extensions and satisfies the low.HasExtensions interface. -func (co *Components) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (co *Components) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return co.Extensions } @@ -66,38 +65,19 @@ func (co *Components) Hash() [32]byte { generateHashForObjectMap(co.SecuritySchemes.Value, &f) generateHashForObjectMap(co.Links.Value, &f) generateHashForObjectMap(co.Callbacks.Value, &f) - keys := make([]string, len(co.Extensions)) - z := 0 - for k := range co.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(co.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(co.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } -func generateHashForObjectMap[T any](collection orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], hash *[]string) { - if collection == nil { - return - } - l := make([]string, orderedmap.Len(collection)) - keys := make(map[string]low.ValueReference[T]) - z := 0 - for pair := orderedmap.First(collection); pair != nil; pair = pair.Next() { - keys[pair.Key().Value] = pair.Value() - l[z] = pair.Key().Value - z++ - } - sort.Strings(l) - for k := range l { - *hash = append(*hash, low.GenerateHashString(keys[l[k]].Value)) +func generateHashForObjectMap[T any](collection *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], hash *[]string) { + for pair := orderedmap.First(orderedmap.SortAlpha(collection)); pair != nil; pair = pair.Next() { + *hash = append(*hash, low.GenerateHashString(pair.Value().Value)) } } // FindExtension attempts to locate an extension with the supplied key -func (co *Components) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, co.Extensions) +func (co *Components) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, co.Extensions) } // FindSchema attempts to locate a SchemaProxy from 'schemas' with a specific name @@ -224,8 +204,8 @@ func (co *Components) Build(ctx context.Context, root *yaml.Node, idx *index.Spe // extractComponentValues converts all the YAML nodes of a component type to // low level model. // Process each node in parallel. -func extractComponentValues[T low.Buildable[N], N any](ctx context.Context, label string, root *yaml.Node, idx *index.SpecIndex) (low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[T]]], error) { - var emptyResult low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[T]]] +func extractComponentValues[T low.Buildable[N], N any](ctx context.Context, label string, root *yaml.Node, idx *index.SpecIndex) (low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[T]]], error) { + var emptyResult low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[T]]] _, nodeLabel, nodeValue := utils.FindKeyNodeFullTop(label, root.Content) if nodeValue == nil { return emptyResult, nil @@ -319,7 +299,7 @@ func extractComponentValues[T low.Buildable[N], N any](ctx context.Context, labe return emptyResult, err } - results := low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[T]]]{ + results := low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[T]]]{ KeyNode: nodeLabel, ValueNode: nodeValue, Value: componentValues, diff --git a/datamodel/low/v3/components_test.go b/datamodel/low/v3/components_test.go index d69a8e0..f53d994 100644 --- a/datamodel/low/v3/components_test.go +++ b/datamodel/low/v3/components_test.go @@ -10,6 +10,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -100,7 +101,7 @@ func TestComponents_Build_Success(t *testing.T) { assert.Equal(t, "eighteen of many", n.FindCallback("eighteen").Value.FindExpression("{raference}").Value.Post.Value.Description.Value) - assert.Equal(t, "7add1a6c63a354b1a8ffe22552c213fe26d1229beb0b0cbe7c7ca06e63f9a364", + assert.Equal(t, "76328a0e32a9989471d335734af04a37bdfad333cf8cd8aa8065998c3a1489a2", low.GenerateHashString(&n)) } @@ -223,7 +224,11 @@ headers: err = n.Build(context.Background(), idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "seagull", n.FindExtension("x-curry").Value) + + var xCurry string + _ = n.FindExtension("x-curry").Value.Decode(&xCurry) + + assert.Equal(t, "seagull", xCurry) } func TestComponents_Build_HashEmpty(t *testing.T) { @@ -240,8 +245,12 @@ func TestComponents_Build_HashEmpty(t *testing.T) { err = n.Build(context.Background(), idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "seagull", n.FindExtension("x-curry").Value) - assert.Len(t, n.GetExtensions(), 1) - assert.Equal(t, "9cf2c6ab3f9ff7e5231fcb391c8af5c47406711d2ca366533f21a8bb2f67edfe", + + var xCurry string + _ = n.FindExtension("x-curry").Value.Decode(&xCurry) + + assert.Equal(t, "seagull", xCurry) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) + assert.Equal(t, "e45605d7361dbc9d4b9723257701bef1d283f8fe9566b9edda127fc66a6b8fdd", low.GenerateHashString(&n)) } diff --git a/datamodel/low/v3/create_document.go b/datamodel/low/v3/create_document.go index b70a053..0c6149f 100644 --- a/datamodel/low/v3/create_document.go +++ b/datamodel/low/v3/create_document.go @@ -277,7 +277,7 @@ func extractWebhooks(ctx context.Context, info *datamodel.SpecInfo, doc *Documen return eErr } if hooks != nil { - doc.Webhooks = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]]]{ + doc.Webhooks = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]]]{ Value: hooks, KeyNode: hooksL, ValueNode: hooksN, diff --git a/datamodel/low/v3/create_document_test.go b/datamodel/low/v3/create_document_test.go index 10493dd..c8b83a3 100644 --- a/datamodel/low/v3/create_document_test.go +++ b/datamodel/low/v3/create_document_test.go @@ -14,6 +14,7 @@ import ( "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var doc *Document @@ -274,7 +275,7 @@ func TestCreateDocument(t *testing.T) { assert.Equal(t, "Burger Shop", doc.Info.Value.Title.Value) assert.NotEmpty(t, doc.Info.Value.Title.Value) assert.Equal(t, "https://pb33f.io/schema", doc.JsonSchemaDialect.Value) - assert.Len(t, doc.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(doc.GetExtensions())) } //func TestCreateDocumentHash(t *testing.T) { @@ -363,28 +364,45 @@ func TestCreateDocument_Tags(t *testing.T) { assert.NotNil(t, doc.Tags.Value[0].Value.ExternalDocs.Value) assert.Equal(t, "https://pb33f.io", doc.Tags.Value[0].Value.ExternalDocs.Value.URL.Value) assert.NotEmpty(t, doc.Tags.Value[0].Value.ExternalDocs.Value.URL.Value) - assert.Len(t, doc.Tags.Value[0].Value.Extensions, 7) + assert.Equal(t, 7, orderedmap.Len(doc.Tags.Value[0].Value.Extensions)) - for key, extension := range doc.Tags.Value[0].Value.Extensions { + for pair := orderedmap.First(doc.Tags.Value[0].Value.Extensions); pair != nil; pair = pair.Next() { + key := pair.Key() + extension := pair.Value() + + var val any + _ = extension.Value.Decode(&val) switch key.Value { case "x-internal-ting": - assert.Equal(t, "somethingSpecial", extension.Value) + assert.Equal(t, "somethingSpecial", val) case "x-internal-tong": - assert.Equal(t, int64(1), extension.Value) + assert.Equal(t, 1, val) case "x-internal-tang": - assert.Equal(t, 1.2, extension.Value) + assert.Equal(t, 1.2, val) case "x-internal-tung": - assert.Equal(t, true, extension.Value) + assert.Equal(t, true, val) case "x-internal-arr": - assert.Len(t, extension.Value, 2) - assert.Equal(t, "one", extension.Value.([]interface{})[0].(string)) + var a []any + err := extension.Value.Decode(&a) + require.NoError(t, err) + + assert.Len(t, a, 2) + assert.Equal(t, "one", a[0].(string)) case "x-internal-arrmap": - assert.Len(t, extension.Value, 2) - assert.Equal(t, "now", extension.Value.([]interface{})[0].(map[string]interface{})["what"]) + var a []any + err := extension.Value.Decode(&a) + require.NoError(t, err) + + assert.Len(t, a, 2) + assert.Equal(t, "now", a[0].(map[string]interface{})["what"]) case "x-something-else": + var m map[string]any + err := extension.Value.Decode(&m) + require.NoError(t, err) + // crazy times in the upside down. this API should be avoided for the higher up use cases. // this is why we will need a higher level API to this model, this looks cool and all, but dude. - assert.Equal(t, "now?", extension.Value.(map[string]interface{})["ok"].([]interface{})[0].(map[string]interface{})["what"]) + assert.Equal(t, "now?", m["ok"].([]interface{})[0].(map[string]interface{})["what"]) } } @@ -394,7 +412,7 @@ func TestCreateDocument_Tags(t *testing.T) { assert.NotNil(t, doc.Tags.Value[1].Value.ExternalDocs.Value) assert.Equal(t, "https://pb33f.io", doc.Tags.Value[1].Value.ExternalDocs.Value.URL.Value) assert.NotEmpty(t, doc.Tags.Value[1].Value.ExternalDocs.Value.URL.Value) - assert.Len(t, doc.Tags.Value[1].Value.Extensions, 0) + assert.Equal(t, 0, orderedmap.Len(doc.Tags.Value[1].Value.Extensions)) } func TestCreateDocument_Paths(t *testing.T) { @@ -407,11 +425,17 @@ func TestCreateDocument_Paths(t *testing.T) { assert.Equal(t, "burgerHeader", param.Value.Name.Value) prop := param.Value.Schema.Value.Schema().FindProperty("burgerTheme").Value assert.Equal(t, "something about a theme goes in here?", prop.Schema().Description.Value) - assert.Equal(t, "big-mac", param.Value.Example.Value) + + var paramExample string + _ = param.GetValue().Example.Value.Decode(¶mExample) + assert.Equal(t, "big-mac", paramExample) // check content pContent := param.Value.FindContent("application/json") - assert.Equal(t, "somethingNice", pContent.Value.Example.Value) + + var contentExample string + _ = pContent.Value.Example.Value.Decode(&contentExample) + assert.Equal(t, "somethingNice", contentExample) encoding := pContent.Value.FindPropertyEncoding("burgerTheme") assert.NotNil(t, encoding.Value) @@ -445,25 +469,25 @@ func TestCreateDocument_Paths(t *testing.T) { assert.NotEmpty(t, ex.Value.Summary.Value) assert.NotNil(t, ex.Value.Value.Value) - if n, ok := ex.Value.Value.Value.(map[string]interface{}); ok { - assert.Len(t, n, 2) - assert.Equal(t, 3, n["numPatties"]) - } else { - assert.Fail(t, "should easily be convertable. something changed!") - } + var pbjBurgerExample map[string]any + err := ex.Value.Value.Value.Decode(&pbjBurgerExample) + require.NoError(t, err) + + assert.Len(t, pbjBurgerExample, 2) + assert.Equal(t, 3, pbjBurgerExample["numPatties"]) cb := content.FindExample("cakeBurger") assert.NotNil(t, cb.Value) assert.NotEmpty(t, cb.Value.Summary.Value) assert.NotNil(t, cb.Value.Value.Value) - if n, ok := cb.Value.Value.Value.(map[string]interface{}); ok { - assert.Len(t, n, 2) - assert.Equal(t, "Chocolate Cake Burger", n["name"]) - assert.Equal(t, 5, n["numPatties"]) - } else { - assert.Fail(t, "should easily be convertable. something changed!") - } + var cakeBurgerExample map[string]any + err = cb.Value.Value.Value.Decode(&cakeBurgerExample) + require.NoError(t, err) + + assert.Len(t, cakeBurgerExample, 2) + assert.Equal(t, "Chocolate Cake Burger", cakeBurgerExample["name"]) + assert.Equal(t, 5, cakeBurgerExample["numPatties"]) // check responses responses := burgersPost.Responses.Value @@ -490,13 +514,13 @@ func TestCreateDocument_Paths(t *testing.T) { assert.NotNil(t, respExample.Value) assert.NotNil(t, respExample.Value.Value.Value) - if n, ok := respExample.Value.Value.Value.(map[string]interface{}); ok { - assert.Len(t, n, 2) - assert.Equal(t, "Quarter Pounder with Cheese", n["name"]) - assert.Equal(t, 1, n["numPatties"]) - } else { - assert.Fail(t, "should easily be convertable. something changed!") - } + var quarterPounderExample map[string]any + err = respExample.Value.Value.Value.Decode(&quarterPounderExample) + require.NoError(t, err) + + assert.Len(t, quarterPounderExample, 2) + assert.Equal(t, "Quarter Pounder with Cheese", quarterPounderExample["name"]) + assert.Equal(t, 1, quarterPounderExample["numPatties"]) // check links links := okCode.Value.Links diff --git a/datamodel/low/v3/document.go b/datamodel/low/v3/document.go index f7f093a..8aff44b 100644 --- a/datamodel/low/v3/document.go +++ b/datamodel/low/v3/document.go @@ -13,10 +13,10 @@ import ( "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) type Document struct { - // Version is the version of OpenAPI being used, extracted from the 'openapi: x.x.x' definition. // This is not a standard property of the OpenAPI model, it's a convenience mechanism only. Version low.NodeReference[string] @@ -38,7 +38,7 @@ type Document struct { // for example by an out-of-band registration. The key name is a unique string to refer to each webhook, // while the (optionally referenced) Path Item Object describes a request that may be initiated by the API provider // and the expected responses. An example is available. - Webhooks low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]]] // 3.1 + Webhooks low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]]] // 3.1 // Servers is a slice of Server instances which provide connectivity information to a target server. If the servers // property is not provided, or is an empty array, the default value would be a Server Object with an url value of /. @@ -75,7 +75,7 @@ type Document struct { ExternalDocs low.NodeReference[*base.ExternalDoc] // Extensions contains all custom extensions defined for the top-level document. - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] // Index is a reference to the *index.SpecIndex that was created for the document and used // as a guide when building out the Document. Ideal if further processing is required on the model and @@ -102,7 +102,7 @@ func (d *Document) FindSecurityRequirement(name string) []low.ValueReference[str } // GetExtensions returns all Document extensions and satisfies the low.HasExtensions interface. -func (d *Document) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (d *Document) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return d.Extensions } diff --git a/datamodel/low/v3/encoding.go b/datamodel/low/v3/encoding.go index 50643d9..68a5466 100644 --- a/datamodel/low/v3/encoding.go +++ b/datamodel/low/v3/encoding.go @@ -20,7 +20,7 @@ import ( // - https://spec.openapis.org/oas/v3.1.0#encoding-object type Encoding struct { ContentType low.NodeReference[string] - Headers low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]] + Headers low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]] Style low.NodeReference[string] Explode low.NodeReference[bool] AllowReserved low.NodeReference[bool] @@ -38,20 +38,8 @@ func (en *Encoding) Hash() [32]byte { if en.ContentType.Value != "" { f = append(f, en.ContentType.Value) } - if orderedmap.Len(en.Headers.Value) > 0 { - l := make([]string, orderedmap.Len(en.Headers.Value)) - keys := make(map[string]low.ValueReference[*Header]) - z := 0 - for pair := orderedmap.First(en.Headers.Value); pair != nil; pair = pair.Next() { - keys[pair.Key().Value] = pair.Value() - l[z] = pair.Key().Value - z++ - } - - // FIXME: Redundant iteration? - for pair := orderedmap.First(en.Headers.Value); pair != nil; pair = pair.Next() { - f = append(f, fmt.Sprintf("%s-%x", pair.Key().Value, pair.Value().Value.Hash())) - } + for pair := orderedmap.First(orderedmap.SortAlpha(en.Headers.Value)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%x", pair.Key().Value, pair.Value().Value.Hash())) } if en.Style.Value != "" { f = append(f, en.Style.Value) @@ -71,7 +59,7 @@ func (en *Encoding) Build(ctx context.Context, _, root *yaml.Node, idx *index.Sp return err } if headers != nil { - en.Headers = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]]{ + en.Headers = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]]{ Value: headers, KeyNode: hL, ValueNode: hN, diff --git a/datamodel/low/v3/header.go b/datamodel/low/v3/header.go index ca76b8b..8f1c625 100644 --- a/datamodel/low/v3/header.go +++ b/datamodel/low/v3/header.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -29,16 +28,16 @@ type Header struct { Explode low.NodeReference[bool] AllowReserved low.NodeReference[bool] Schema low.NodeReference[*base.SchemaProxy] - Example low.NodeReference[any] - Examples low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]] - Content low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Example low.NodeReference[*yaml.Node] + Examples low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]] + Content low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // FindExtension will attempt to locate an extension with the supplied name -func (h *Header) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, h.Extensions) +func (h *Header) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, h.Extensions) } // FindExample will attempt to locate an Example with a specified name @@ -52,7 +51,7 @@ func (h *Header) FindContent(ext string) *low.ValueReference[*MediaType] { } // GetExtensions returns all Header extensions and satisfies the low.HasExtensions interface. -func (h *Header) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (h *Header) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return h.Extensions } @@ -73,27 +72,16 @@ func (h *Header) Hash() [32]byte { if h.Schema.Value != nil { f = append(f, low.GenerateHashString(h.Schema.Value)) } - if h.Example.Value != nil { - f = append(f, fmt.Sprint(h.Example.Value)) + if h.Example.Value != nil && !h.Example.Value.IsZero() { + f = append(f, low.GenerateHashString(h.Example.Value)) } - if orderedmap.Len(h.Examples.Value) > 0 { - for pair := orderedmap.First(h.Examples.Value); pair != nil; pair = pair.Next() { - f = append(f, fmt.Sprintf("%s-%x", pair.Key().Value, pair.Value().Value.Hash())) - } + for pair := orderedmap.First(orderedmap.SortAlpha(h.Examples.Value)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%x", pair.Key().Value, pair.Value().Value.Hash())) } - if orderedmap.Len(h.Content.Value) > 0 { - for pair := orderedmap.First(h.Content.Value); pair != nil; pair = pair.Next() { - f = append(f, fmt.Sprintf("%s-%x", pair.Key().Value, pair.Value().Value.Hash())) - } + for pair := orderedmap.First(orderedmap.SortAlpha(h.Content.Value)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%x", pair.Key().Value, pair.Value().Value.Hash())) } - keys := make([]string, len(h.Extensions)) - z := 0 - for k := range h.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(h.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(h.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -107,7 +95,11 @@ func (h *Header) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecI // handle example if set. _, expLabel, expNode := utils.FindKeyNodeFull(base.ExampleLabel, root.Content) if expNode != nil { - h.Example = low.ExtractExample(expNode, expLabel) + h.Example = low.NodeReference[*yaml.Node]{ + Value: expNode, + ValueNode: expNode, + KeyNode: expLabel, + } } // handle examples if set. @@ -116,7 +108,7 @@ func (h *Header) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecI return eErr } if exps != nil { - h.Examples = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]]{ + h.Examples = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]]{ Value: exps, KeyNode: expsL, ValueNode: expsN, @@ -137,7 +129,7 @@ func (h *Header) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecI if cErr != nil { return cErr } - h.Content = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]]{ + h.Content = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]]{ Value: con, KeyNode: cL, ValueNode: cN, @@ -176,7 +168,7 @@ func (h *Header) GetAllowReserved() *low.NodeReference[bool] { func (h *Header) GetExplode() *low.NodeReference[bool] { return &h.Explode } -func (h *Header) GetExample() *low.NodeReference[any] { +func (h *Header) GetExample() *low.NodeReference[*yaml.Node] { return &h.Example } func (h *Header) GetExamples() *low.NodeReference[any] { diff --git a/datamodel/low/v3/header_test.go b/datamodel/low/v3/header_test.go index 330a466..59ad558 100644 --- a/datamodel/low/v3/header_test.go +++ b/datamodel/low/v3/header_test.go @@ -12,6 +12,7 @@ import ( "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -70,22 +71,23 @@ content: assert.Equal(t, "she is my song.", n.Schema.Value.Schema().FindProperty("meddy").Value.Schema().Description.Value) assert.Equal(t, "he is my champion.", n.Schema.Value.Schema().FindProperty("maddy").Value.Schema().Description.Value) - if v, ok := n.Example.Value.(map[string]interface{}); ok { - assert.Equal(t, "my love.", v["michelle"]) - assert.Equal(t, "my song.", v["meddy"]) - assert.Equal(t, "my champion.", v["maddy"]) - } else { - assert.Fail(t, "should not fail") - } + var m map[string]any + err = n.Example.Value.Decode(&m) + require.NoError(t, err) + + assert.Equal(t, "my love.", m["michelle"]) + assert.Equal(t, "my song.", m["meddy"]) + assert.Equal(t, "my champion.", m["maddy"]) con := n.FindContent("family/love").Value assert.NotNil(t, con) assert.Equal(t, "family love.", con.Schema.Value.Schema().Description.Value) assert.Nil(t, n.FindContent("unknown")) - ext := n.FindExtension("x-family-love").Value - assert.Equal(t, "strong", ext) - assert.Len(t, n.GetExtensions(), 1) + var xFamilyLove string + _ = n.FindExtension("x-family-love").Value.Decode(&xFamilyLove) + assert.Equal(t, "strong", xFamilyLove) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestHeader_Build_Success_Examples(t *testing.T) { @@ -110,13 +112,13 @@ func TestHeader_Build_Success_Examples(t *testing.T) { exp := n.FindExample("family").Value assert.NotNil(t, exp) - if v, ok := exp.Value.Value.(map[string]interface{}); ok { - assert.Equal(t, "my love.", v["michelle"]) - assert.Equal(t, "my song.", v["meddy"]) - assert.Equal(t, "my champion.", v["maddy"]) - } else { - assert.Fail(t, "should not fail") - } + var m map[string]any + err = exp.Value.GetValue().Decode(&m) + require.NoError(t, err) + + assert.Equal(t, "my love.", m["michelle"]) + assert.Equal(t, "my song.", m["meddy"]) + assert.Equal(t, "my champion.", m["maddy"]) } func TestHeader_Build_Fail_Examples(t *testing.T) { @@ -241,7 +243,10 @@ schema: assert.True(t, n.GetAllowReserved().Value) sch := n.GetSchema().Value.(*base.SchemaProxy).Schema() assert.Len(t, sch.Type.Value.B, 2) // using multiple types for 3.1 testing. - assert.Equal(t, "what a good puppy", n.GetExample().Value) + + var example string + _ = n.GetExample().Value.Decode(&example) + assert.Equal(t, "what a good puppy", example) assert.Equal(t, 1, orderedmap.Cast[low.KeyReference[string], low.ValueReference[*base.Example]](n.GetExamples().Value).Len()) assert.Equal(t, 1, orderedmap.Cast[low.KeyReference[string], low.ValueReference[*MediaType]](n.GetContent().Value).Len()) } diff --git a/datamodel/low/v3/link.go b/datamodel/low/v3/link.go index 05596ba..2fb9021 100644 --- a/datamodel/low/v3/link.go +++ b/datamodel/low/v3/link.go @@ -6,8 +6,6 @@ package v3 import ( "context" "crypto/sha256" - "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -32,16 +30,16 @@ import ( type Link struct { OperationRef low.NodeReference[string] OperationId low.NodeReference[string] - Parameters low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] + Parameters low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] RequestBody low.NodeReference[string] Description low.NodeReference[string] Server low.NodeReference[*Server] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // GetExtensions returns all Link extensions and satisfies the low.HasExtensions interface. -func (l *Link) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (l *Link) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return l.Extensions } @@ -51,8 +49,8 @@ func (l *Link) FindParameter(pName string) *low.ValueReference[string] { } // FindExtension will attempt to locate an extension with a specific key -func (l *Link) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, l.Extensions) +func (l *Link) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, l.Extensions) } // Build will extract extensions and servers from the node. @@ -88,23 +86,9 @@ func (l *Link) Hash() [32]byte { if l.Server.Value != nil { f = append(f, low.GenerateHashString(l.Server.Value)) } - // todo: needs ordering. - - keys := make([]string, orderedmap.Len(l.Parameters.Value)) - z := 0 - for pair := orderedmap.First(l.Parameters.Value); pair != nil; pair = pair.Next() { - keys[z] = pair.Value().Value - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(l.Parameters.Value)); pair != nil; pair = pair.Next() { + f = append(f, pair.Value().Value) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, len(l.Extensions)) - z = 0 - for k := range l.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(l.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(l.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/link_test.go b/datamodel/low/v3/link_test.go index 3a80241..684fa01 100644 --- a/datamodel/low/v3/link_test.go +++ b/datamodel/low/v3/link_test.go @@ -5,15 +5,16 @@ package v3 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestLink_Build(t *testing.T) { - yml := `operationRef: '#/someref' operationId: someId parameters: @@ -41,9 +42,9 @@ x-linky: slinky assert.Equal(t, "someId", n.OperationId.Value) assert.Equal(t, "this is a link object.", n.Description.Value) - ext := n.FindExtension("x-linky") - assert.NotNil(t, ext) - assert.Equal(t, "slinky", ext.Value) + var xLinky string + _ = n.FindExtension("x-linky").Value.Decode(&xLinky) + assert.Equal(t, "slinky", xLinky) param1 := n.FindParameter("param1") assert.Equal(t, "something", param1.Value) @@ -52,12 +53,10 @@ x-linky: slinky assert.NotNil(t, n.Server.Value) assert.Equal(t, "https://pb33f.io", n.Server.Value.URL.Value) - assert.Len(t, n.GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestLink_Build_Fail(t *testing.T) { - yml := `operationRef: '#/someref' operationId: someId parameters: @@ -78,11 +77,9 @@ server: err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestLink_Hash(t *testing.T) { - yml := `operationRef: something operationId: someWhere parameters: @@ -123,5 +120,4 @@ server: // hash assert.Equal(t, n.Hash(), n2.Hash()) - } diff --git a/datamodel/low/v3/media_type.go b/datamodel/low/v3/media_type.go index 5ea4b52..d28754b 100644 --- a/datamodel/low/v3/media_type.go +++ b/datamodel/low/v3/media_type.go @@ -6,8 +6,6 @@ package v3 import ( "context" "crypto/sha256" - "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -24,21 +22,21 @@ import ( // - https://spec.openapis.org/oas/v3.1.0#media-type-object type MediaType struct { Schema low.NodeReference[*base.SchemaProxy] - Example low.NodeReference[any] - Examples low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]] - Encoding low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Encoding]]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Example low.NodeReference[*yaml.Node] + Examples low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]] + Encoding low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Encoding]]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // GetExtensions returns all MediaType extensions and satisfies the low.HasExtensions interface. -func (mt *MediaType) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (mt *MediaType) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return mt.Extensions } // FindExtension will attempt to locate an extension with the supplied name. -func (mt *MediaType) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, mt.Extensions) +func (mt *MediaType) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, mt.Extensions) } // FindPropertyEncoding will attempt to locate an Encoding value with a specific name. @@ -52,7 +50,7 @@ func (mt *MediaType) FindExample(eType string) *low.ValueReference[*base.Example } // GetAllExamples will extract all examples from the MediaType instance. -func (mt *MediaType) GetAllExamples() orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]] { +func (mt *MediaType) GetAllExamples() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]] { return mt.Examples.Value } @@ -66,26 +64,10 @@ func (mt *MediaType) Build(ctx context.Context, _, root *yaml.Node, idx *index.S // handle example if set. _, expLabel, expNode := utils.FindKeyNodeFull(base.ExampleLabel, root.Content) if expNode != nil { - var value any - if utils.IsNodeMap(expNode) { - var h map[string]any - _ = expNode.Decode(&h) - value = h - } - if utils.IsNodeArray(expNode) { - var h []any - _ = expNode.Decode(&h) - value = h - } - if value == nil { - if expNode.Value != "" { - value = expNode.Value - } - } - mt.Example = low.NodeReference[any]{Value: value, KeyNode: expLabel, ValueNode: expNode} + mt.Example = low.NodeReference[*yaml.Node]{Value: expNode, KeyNode: expLabel, ValueNode: expNode} } - //handle schema + // handle schema sch, sErr := base.ExtractSchema(ctx, root, idx) if sErr != nil { return sErr @@ -100,7 +82,7 @@ func (mt *MediaType) Build(ctx context.Context, _, root *yaml.Node, idx *index.S return eErr } if exps != nil { - mt.Examples = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]]{ + mt.Examples = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]]{ Value: exps, KeyNode: expsL, ValueNode: expsN, @@ -113,7 +95,7 @@ func (mt *MediaType) Build(ctx context.Context, _, root *yaml.Node, idx *index.S return encErr } if encs != nil { - mt.Encoding = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Encoding]]]{ + mt.Encoding = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Encoding]]]{ Value: encs, KeyNode: encsL, ValueNode: encsN, @@ -128,31 +110,15 @@ func (mt *MediaType) Hash() [32]byte { if mt.Schema.Value != nil { f = append(f, low.GenerateHashString(mt.Schema.Value)) } - if mt.Example.Value != nil { - f = append(f, fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(mt.Example.Value))))) + if mt.Example.Value != nil && !mt.Example.Value.IsZero() { + f = append(f, low.GenerateHashString(mt.Example.Value)) } - keys := make([]string, orderedmap.Len(mt.Examples.Value)) - z := 0 - for pair := orderedmap.First(mt.Examples.Value); pair != nil; pair = pair.Next() { - keys[z] = low.GenerateHashString(pair.Value().Value) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(mt.Examples.Value)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, orderedmap.Len(mt.Encoding.Value)) - z = 0 - for pair := orderedmap.First(mt.Encoding.Value); pair != nil; pair = pair.Next() { - keys[z] = low.GenerateHashString(pair.Value().Value) + for pair := orderedmap.First(orderedmap.SortAlpha(mt.Encoding.Value)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, len(mt.Extensions)) - z = 0 - for k := range mt.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(mt.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(mt.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/media_type_test.go b/datamodel/low/v3/media_type_test.go index 51ef702..00ce8c2 100644 --- a/datamodel/low/v3/media_type_test.go +++ b/datamodel/low/v3/media_type_test.go @@ -9,6 +9,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -37,11 +38,22 @@ x-rock: and roll` err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "and roll", n.FindExtension("x-rock").Value) + + var xRock string + _ = n.FindExtension("x-rock").Value.Decode(&xRock) + assert.Equal(t, "and roll", xRock) assert.Equal(t, "string", n.Schema.Value.Schema().Type.Value.A) - assert.Equal(t, "hello", n.Example.Value) - assert.Equal(t, "why?", n.FindExample("what").Value.Value.Value) - assert.Equal(t, "there?", n.FindExample("where").Value.Value.Value) + var example string + _ = n.Example.Value.Decode(&example) + assert.Equal(t, "hello", example) + + var whatExample string + _ = n.FindExample("what").Value.Value.Value.Decode(&whatExample) + assert.Equal(t, "why?", whatExample) + + var whereExample string + _ = n.FindExample("where").Value.Value.Value.Decode(&whereExample) + assert.Equal(t, "there?", whereExample) assert.True(t, n.FindPropertyEncoding("chicken").Value.Explode.Value) assert.Equal(t, n.GetAllExamples().Len(), 2) } @@ -141,5 +153,5 @@ example: a thing` // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v3/oauth_flows.go b/datamodel/low/v3/oauth_flows.go index 209d9f5..eb074ac 100644 --- a/datamodel/low/v3/oauth_flows.go +++ b/datamodel/low/v3/oauth_flows.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -24,18 +23,18 @@ type OAuthFlows struct { Password low.NodeReference[*OAuthFlow] ClientCredentials low.NodeReference[*OAuthFlow] AuthorizationCode low.NodeReference[*OAuthFlow] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // GetExtensions returns all OAuthFlows extensions and satisfies the low.HasExtensions interface. -func (o *OAuthFlows) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (o *OAuthFlows) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return o.Extensions } // FindExtension will attempt to locate an extension with the supplied name. -func (o *OAuthFlows) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, o.Extensions) +func (o *OAuthFlows) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, o.Extensions) } // Build will extract extensions and all OAuthFlow types from the supplied node. @@ -86,9 +85,7 @@ func (o *OAuthFlows) Hash() [32]byte { if !o.AuthorizationCode.IsEmpty() { f = append(f, low.GenerateHashString(o.AuthorizationCode.Value)) } - for k := range o.Extensions { - f = append(f, fmt.Sprintf("%s-%v", k.Value, o.Extensions[k].Value)) - } + f = append(f, low.HashExtensions(o.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -98,13 +95,13 @@ type OAuthFlow struct { AuthorizationUrl low.NodeReference[string] TokenUrl low.NodeReference[string] RefreshUrl low.NodeReference[string] - Scopes low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Scopes low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // GetExtensions returns all OAuthFlow extensions and satisfies the low.HasExtensions interface. -func (o *OAuthFlow) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (o *OAuthFlow) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return o.Extensions } @@ -114,8 +111,8 @@ func (o *OAuthFlow) FindScope(scope string) *low.ValueReference[string] { } // FindExtension attempts to locate an extension with a specified key -func (o *OAuthFlow) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, o.Extensions) +func (o *OAuthFlow) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, o.Extensions) } // Build will extract extensions from the node. @@ -137,21 +134,9 @@ func (o *OAuthFlow) Hash() [32]byte { if !o.RefreshUrl.IsEmpty() { f = append(f, o.RefreshUrl.Value) } - keys := make([]string, orderedmap.Len(o.Scopes.Value)) - z := 0 - for pair := orderedmap.First(o.Scopes.Value); pair != nil; pair = pair.Next() { - keys[z] = fmt.Sprintf("%s-%s", pair.Key().Value, sha256.Sum256([]byte(fmt.Sprint(pair.Value().Value)))) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(o.Scopes.Value)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, sha256.Sum256([]byte(fmt.Sprint(pair.Value().Value))))) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, len(o.Extensions)) - z = 0 - for k := range o.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(o.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(o.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/oauth_flows_test.go b/datamodel/low/v3/oauth_flows_test.go index a556a28..0163cae 100644 --- a/datamodel/low/v3/oauth_flows_test.go +++ b/datamodel/low/v3/oauth_flows_test.go @@ -5,15 +5,16 @@ package v3 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestOAuthFlows_Build(t *testing.T) { - yml := `authorizationUrl: https://pb33f.io/auth tokenUrl: https://pb33f.io/token refreshUrl: https://pb33f.io/refresh @@ -33,16 +34,18 @@ x-tasty: herbs err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "herbs", n.FindExtension("x-tasty").Value) + + var xTasty string + _ = n.FindExtension("x-tasty").Value.Decode(&xTasty) + assert.Equal(t, "herbs", xTasty) assert.Equal(t, "https://pb33f.io/auth", n.AuthorizationUrl.Value) assert.Equal(t, "https://pb33f.io/token", n.TokenUrl.Value) assert.Equal(t, "https://pb33f.io/refresh", n.RefreshUrl.Value) assert.Equal(t, "vanilla", n.FindScope("fresh:cake").Value) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestOAuthFlow_Build_Implicit(t *testing.T) { - yml := `implicit: authorizationUrl: https://pb33f.io/auth x-tasty: herbs` @@ -57,13 +60,15 @@ x-tasty: herbs` err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "herbs", n.FindExtension("x-tasty").Value) + + var xTasty string + _ = n.FindExtension("x-tasty").GetValue().Decode(&xTasty) + assert.Equal(t, "herbs", xTasty) assert.Equal(t, "https://pb33f.io/auth", n.Implicit.Value.AuthorizationUrl.Value) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestOAuthFlow_Build_Implicit_Fail(t *testing.T) { - yml := `implicit: $ref: #bork"` @@ -80,7 +85,6 @@ func TestOAuthFlow_Build_Implicit_Fail(t *testing.T) { } func TestOAuthFlow_Build_Password(t *testing.T) { - yml := `password: authorizationUrl: https://pb33f.io/auth` @@ -98,7 +102,6 @@ func TestOAuthFlow_Build_Password(t *testing.T) { } func TestOAuthFlow_Build_Password_Fail(t *testing.T) { - yml := `password: $ref: #bork"` @@ -115,7 +118,6 @@ func TestOAuthFlow_Build_Password_Fail(t *testing.T) { } func TestOAuthFlow_Build_ClientCredentials(t *testing.T) { - yml := `clientCredentials: authorizationUrl: https://pb33f.io/auth` @@ -133,7 +135,6 @@ func TestOAuthFlow_Build_ClientCredentials(t *testing.T) { } func TestOAuthFlow_Build_ClientCredentials_Fail(t *testing.T) { - yml := `clientCredentials: $ref: #bork"` @@ -150,7 +151,6 @@ func TestOAuthFlow_Build_ClientCredentials_Fail(t *testing.T) { } func TestOAuthFlow_Build_AuthCode(t *testing.T) { - yml := `authorizationCode: authorizationUrl: https://pb33f.io/auth` @@ -168,7 +168,6 @@ func TestOAuthFlow_Build_AuthCode(t *testing.T) { } func TestOAuthFlow_Build_AuthCode_Fail(t *testing.T) { - yml := `authorizationCode: $ref: #bork"` @@ -185,7 +184,6 @@ func TestOAuthFlow_Build_AuthCode_Fail(t *testing.T) { } func TestOAuthFlow_Hash(t *testing.T) { - yml := `authorizationUrl: https://pb33f.io/auth tokenUrl: https://pb33f.io/token refreshUrl: https://pb33f.io/refresh @@ -218,11 +216,9 @@ scopes: // hash assert.Equal(t, n.Hash(), n2.Hash()) - } func TestOAuthFlows_Hash(t *testing.T) { - yml := `implicit: authorizationUrl: https://pb33f.io/auth password: diff --git a/datamodel/low/v3/operation.go b/datamodel/low/v3/operation.go index 7e598a6..3109ab3 100644 --- a/datamodel/low/v3/operation.go +++ b/datamodel/low/v3/operation.go @@ -32,17 +32,17 @@ type Operation struct { Parameters low.NodeReference[[]low.ValueReference[*Parameter]] RequestBody low.NodeReference[*RequestBody] Responses low.NodeReference[*Responses] - Callbacks low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Callback]]] + Callbacks low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Callback]]] Deprecated low.NodeReference[bool] Security low.NodeReference[[]low.ValueReference[*base.SecurityRequirement]] Servers low.NodeReference[[]low.ValueReference[*Server]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // FindCallback will attempt to locate a Callback instance by the supplied name. func (o *Operation) FindCallback(callback string) *low.ValueReference[*Callback] { - return low.FindItemInOrderedMap[*Callback](callback, o.Callbacks.Value) + return low.FindItemInOrderedMap(callback, o.Callbacks.GetValue()) } // FindSecurityRequirement will attempt to locate a security requirement string from a supplied name. @@ -105,7 +105,7 @@ func (o *Operation) Build(ctx context.Context, _, root *yaml.Node, idx *index.Sp return cbErr } if callbacks != nil { - o.Callbacks = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Callback]]]{ + o.Callbacks = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Callback]]]{ Value: callbacks, KeyNode: cbL, ValueNode: cbN, @@ -206,23 +206,10 @@ func (o *Operation) Hash() [32]byte { sort.Strings(keys) f = append(f, keys...) - keys = make([]string, orderedmap.Len(o.Callbacks.Value)) - z := 0 - for pair := orderedmap.First(o.Callbacks.Value); pair != nil; pair = pair.Next() { - keys[z] = low.GenerateHashString(pair.Value().Value) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(o.Callbacks.Value)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } - sort.Strings(keys) - f = append(f, keys...) - - keys = make([]string, len(o.Extensions)) - z = 0 - for k := range o.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(o.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(o.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -232,12 +219,15 @@ func (o *Operation) Hash() [32]byte { func (o *Operation) GetTags() low.NodeReference[[]low.ValueReference[string]] { return o.Tags } + func (o *Operation) GetSummary() low.NodeReference[string] { return o.Summary } + func (o *Operation) GetDescription() low.NodeReference[string] { return o.Description } + func (o *Operation) GetExternalDocs() low.NodeReference[any] { return low.NodeReference[any]{ ValueNode: o.ExternalDocs.ValueNode, @@ -245,15 +235,19 @@ func (o *Operation) GetExternalDocs() low.NodeReference[any] { Value: o.ExternalDocs.Value, } } + func (o *Operation) GetOperationId() low.NodeReference[string] { return o.OperationId } + func (o *Operation) GetDeprecated() low.NodeReference[bool] { return o.Deprecated } -func (o *Operation) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { + +func (o *Operation) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return o.Extensions } + func (o *Operation) GetResponses() low.NodeReference[any] { return low.NodeReference[any]{ ValueNode: o.Responses.ValueNode, @@ -261,6 +255,7 @@ func (o *Operation) GetResponses() low.NodeReference[any] { Value: o.Responses.Value, } } + func (o *Operation) GetParameters() low.NodeReference[any] { return low.NodeReference[any]{ ValueNode: o.Parameters.ValueNode, @@ -268,6 +263,7 @@ func (o *Operation) GetParameters() low.NodeReference[any] { Value: o.Parameters.Value, } } + func (o *Operation) GetSecurity() low.NodeReference[any] { return low.NodeReference[any]{ ValueNode: o.Security.ValueNode, @@ -275,6 +271,7 @@ func (o *Operation) GetSecurity() low.NodeReference[any] { Value: o.Security.Value, } } + func (o *Operation) GetServers() low.NodeReference[any] { return low.NodeReference[any]{ ValueNode: o.Servers.ValueNode, @@ -282,6 +279,7 @@ func (o *Operation) GetServers() low.NodeReference[any] { Value: o.Servers.Value, } } -func (o *Operation) GetCallbacks() low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Callback]]] { + +func (o *Operation) GetCallbacks() low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Callback]]] { return o.Callbacks } diff --git a/datamodel/low/v3/operation_test.go b/datamodel/low/v3/operation_test.go index 2caf0d0..7c26651 100644 --- a/datamodel/low/v3/operation_test.go +++ b/datamodel/low/v3/operation_test.go @@ -10,6 +10,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -272,7 +273,7 @@ x-mint: sweet` assert.Len(t, n.GetParameters().Value, 1) assert.Len(t, n.GetSecurity().Value, 1) assert.True(t, n.GetDeprecated().Value) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) assert.Len(t, n.GetServers().Value.([]low.ValueReference[*Server]), 1) assert.Equal(t, 1, n.GetCallbacks().Value.Len()) assert.Equal(t, 1, n.GetResponses().Value.(*Responses).Codes.Len()) diff --git a/datamodel/low/v3/parameter.go b/datamodel/low/v3/parameter.go index 92215a4..4037b67 100644 --- a/datamodel/low/v3/parameter.go +++ b/datamodel/low/v3/parameter.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -33,10 +32,10 @@ type Parameter struct { Explode low.NodeReference[bool] AllowReserved low.NodeReference[bool] Schema low.NodeReference[*base.SchemaProxy] - Example low.NodeReference[any] - Examples low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]] - Content low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Example low.NodeReference[*yaml.Node] + Examples low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]] + Content low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } @@ -51,12 +50,12 @@ func (p *Parameter) FindExample(eType string) *low.ValueReference[*base.Example] } // FindExtension attempts to locate an extension using the specified name. -func (p *Parameter) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, p.Extensions) +func (p *Parameter) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, p.Extensions) } // GetExtensions returns all extensions for Parameter. -func (p *Parameter) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (p *Parameter) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return p.Extensions } @@ -68,9 +67,9 @@ func (p *Parameter) Build(ctx context.Context, _, root *yaml.Node, idx *index.Sp p.Extensions = low.ExtractExtensions(root) // handle example if set. - _, expLabel, expNode := utils.FindKeyNodeFull(base.ExampleLabel, root.Content) + _, expLabel, expNode := utils.FindKeyNodeFullTop(base.ExampleLabel, root.Content) if expNode != nil { - p.Example = low.ExtractExample(expNode, expLabel) + p.Example = low.NodeReference[*yaml.Node]{Value: expNode, KeyNode: expLabel, ValueNode: expNode} } // handle schema @@ -88,7 +87,7 @@ func (p *Parameter) Build(ctx context.Context, _, root *yaml.Node, idx *index.Sp return eErr } if exps != nil { - p.Examples = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]]{ + p.Examples = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.Example]]]{ Value: exps, KeyNode: expsL, ValueNode: expsN, @@ -100,7 +99,7 @@ func (p *Parameter) Build(ctx context.Context, _, root *yaml.Node, idx *index.Sp if cErr != nil { return cErr } - p.Content = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]]{ + p.Content = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]]{ Value: con, KeyNode: cL, ValueNode: cN, @@ -131,36 +130,16 @@ func (p *Parameter) Hash() [32]byte { if p.Schema.Value != nil { f = append(f, fmt.Sprintf("%x", p.Schema.Value.Schema().Hash())) } - if p.Example.Value != nil { - f = append(f, fmt.Sprintf("%x", p.Example.Value)) + if p.Example.Value != nil && !p.Example.Value.IsZero() { + f = append(f, low.GenerateHashString(p.Example.Value)) } - - var keys []string - keys = make([]string, orderedmap.Len(p.Examples.Value)) - z := 0 - for pair := orderedmap.First(p.Examples.Value); pair != nil; pair = pair.Next() { - keys[z] = low.GenerateHashString(pair.Value().Value) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(p.Examples.Value)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, orderedmap.Len(p.Content.Value)) - z = 0 - for pair := orderedmap.First(p.Content.Value); pair != nil; pair = pair.Next() { - keys[z] = low.GenerateHashString(pair.Value().Value) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(p.Content.Value)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, len(p.Extensions)) - z = 0 - for k := range p.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(p.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) - + f = append(f, low.HashExtensions(p.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -169,21 +148,27 @@ func (p *Parameter) Hash() [32]byte { func (p *Parameter) GetName() *low.NodeReference[string] { return &p.Name } + func (p *Parameter) GetIn() *low.NodeReference[string] { return &p.In } + func (p *Parameter) GetDescription() *low.NodeReference[string] { return &p.Description } + func (p *Parameter) GetRequired() *low.NodeReference[bool] { return &p.Required } + func (p *Parameter) GetDeprecated() *low.NodeReference[bool] { return &p.Deprecated } + func (p *Parameter) GetAllowEmptyValue() *low.NodeReference[bool] { return &p.AllowEmptyValue } + func (p *Parameter) GetSchema() *low.NodeReference[any] { i := low.NodeReference[any]{ KeyNode: p.Schema.KeyNode, @@ -192,18 +177,23 @@ func (p *Parameter) GetSchema() *low.NodeReference[any] { } return &i } + func (p *Parameter) GetStyle() *low.NodeReference[string] { return &p.Style } + func (p *Parameter) GetAllowReserved() *low.NodeReference[bool] { return &p.AllowReserved } + func (p *Parameter) GetExplode() *low.NodeReference[bool] { return &p.Explode } -func (p *Parameter) GetExample() *low.NodeReference[any] { + +func (p *Parameter) GetExample() *low.NodeReference[*yaml.Node] { return &p.Example } + func (p *Parameter) GetExamples() *low.NodeReference[any] { i := low.NodeReference[any]{ KeyNode: p.Examples.KeyNode, @@ -212,6 +202,7 @@ func (p *Parameter) GetExamples() *low.NodeReference[any] { } return &i } + func (p *Parameter) GetContent() *low.NodeReference[any] { c := low.NodeReference[any]{ KeyNode: p.Content.KeyNode, diff --git a/datamodel/low/v3/parameter_test.go b/datamodel/low/v3/parameter_test.go index a241a16..f787e78 100644 --- a/datamodel/low/v3/parameter_test.go +++ b/datamodel/low/v3/parameter_test.go @@ -12,6 +12,7 @@ import ( "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -74,22 +75,23 @@ content: assert.Equal(t, "she is my song.", n.Schema.Value.Schema().FindProperty("meddy").Value.Schema().Description.Value) assert.Equal(t, "he is my champion.", n.Schema.Value.Schema().FindProperty("maddy").Value.Schema().Description.Value) - if v, ok := n.Example.Value.(map[string]interface{}); ok { - assert.Equal(t, "my love.", v["michelle"]) - assert.Equal(t, "my song.", v["meddy"]) - assert.Equal(t, "my champion.", v["maddy"]) - } else { - assert.Fail(t, "should not fail") - } + var m map[string]any + err = n.Example.Value.Decode(&m) + require.NoError(t, err) + + assert.Equal(t, "my love.", m["michelle"]) + assert.Equal(t, "my song.", m["meddy"]) + assert.Equal(t, "my champion.", m["maddy"]) con := n.FindContent("family/love").Value assert.NotNil(t, con) assert.Equal(t, "family love.", con.Schema.Value.Schema().Description.Value) assert.Nil(t, n.FindContent("unknown")) - ext := n.FindExtension("x-family-love").Value - assert.Equal(t, "strong", ext) - assert.Len(t, n.GetExtensions(), 1) + var xFamilyLove string + _ = n.FindExtension("x-family-love").Value.Decode(&xFamilyLove) + assert.Equal(t, "strong", xFamilyLove) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestParameter_Build_Success_Examples(t *testing.T) { @@ -114,13 +116,13 @@ func TestParameter_Build_Success_Examples(t *testing.T) { exp := n.FindExample("family").Value assert.NotNil(t, exp) - if v, ok := exp.Value.Value.(map[string]interface{}); ok { - assert.Equal(t, "my love.", v["michelle"]) - assert.Equal(t, "my song.", v["meddy"]) - assert.Equal(t, "my champion.", v["maddy"]) - } else { - assert.Fail(t, "should not fail") - } + var m map[string]any + err = exp.Value.Value.Decode(&m) + require.NoError(t, err) + + assert.Equal(t, "my love.", m["michelle"]) + assert.Equal(t, "my song.", m["meddy"]) + assert.Equal(t, "my champion.", m["maddy"]) } func TestParameter_Build_Fail_Examples(t *testing.T) { diff --git a/datamodel/low/v3/path_item.go b/datamodel/low/v3/path_item.go index 95cb239..7ce3f07 100644 --- a/datamodel/low/v3/path_item.go +++ b/datamodel/low/v3/path_item.go @@ -14,6 +14,7 @@ import ( "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -37,7 +38,7 @@ type PathItem struct { Trace low.NodeReference[*Operation] Servers low.NodeReference[[]low.ValueReference[*Server]] Parameters low.NodeReference[[]low.ValueReference[*Parameter]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } @@ -86,25 +87,17 @@ func (p *PathItem) Hash() [32]byte { } sort.Strings(keys) f = append(f, keys...) - - keys = make([]string, len(p.Extensions)) - z := 0 - for k := range p.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(p.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(p.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } // FindExtension attempts to find an extension -func (p *PathItem) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, p.Extensions) +func (p *PathItem) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, p.Extensions) } // GetExtensions returns all PathItem extensions and satisfies the low.HasExtensions interface. -func (p *PathItem) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (p *PathItem) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return p.Extensions } @@ -203,6 +196,7 @@ func (p *PathItem) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe var op Operation opIsRef := false var opRefVal string + var opRefNode *yaml.Node if ok, _, ref := utils.IsNodeRefValue(pathNode); ok { // According to OpenAPI spec the only valid $ref for paths is // reference for the whole pathItem. Unfortunately, internet is full of invalid specs @@ -215,6 +209,7 @@ func (p *PathItem) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe opIsRef = true opRefVal = ref + opRefNode = pathNode r, newIdx, err, nCtx := low.LocateRefNodeWithContext(ctx, pathNode, idx) if r != nil { if r.Kind == yaml.DocumentNode { @@ -251,8 +246,7 @@ func (p *PathItem) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe Context: foundContext, } if opIsRef { - opRef.Reference = opRefVal - opRef.ReferenceNode = true + opRef.SetReference(opRefVal, opRefNode) } ops = append(ops, opRef) @@ -281,13 +275,15 @@ func (p *PathItem) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe // now we need to build out the operation, we will do this asynchronously for speed. translateFunc := func(_ int, op low.NodeReference[*Operation]) (any, error) { ref := "" - if op.ReferenceNode { - ref = op.Reference + var refNode *yaml.Node + if op.IsReference() { + ref = op.GetReference() + refNode = op.GetReferenceNode() } err := op.Value.Build(op.Context, op.KeyNode, op.ValueNode, op.Context.Value(index.FoundIndexKey).(*index.SpecIndex)) if ref != "" { - op.Value.Reference.Reference = ref + op.Value.Reference.SetReference(ref, refNode) } if err != nil { return nil, err diff --git a/datamodel/low/v3/path_item_test.go b/datamodel/low/v3/path_item_test.go index e83b0d4..c7abf6d 100644 --- a/datamodel/low/v3/path_item_test.go +++ b/datamodel/low/v3/path_item_test.go @@ -5,15 +5,16 @@ package v3 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestPathItem_Hash(t *testing.T) { - yml := `description: a path item summary: it's another path item servers: @@ -80,5 +81,5 @@ summary: it's another path item` // hash assert.Equal(t, n.Hash(), n2.Hash()) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } diff --git a/datamodel/low/v3/paths.go b/datamodel/low/v3/paths.go index 3b0292f..e15c7e5 100644 --- a/datamodel/low/v3/paths.go +++ b/datamodel/low/v3/paths.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "sync" @@ -26,8 +25,8 @@ import ( // constraints. // - https://spec.openapis.org/oas/v3.1.0#paths-object type Paths struct { - PathItems orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + PathItems *orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } @@ -55,12 +54,12 @@ func (p *Paths) FindPathAndKey(path string) (key *low.KeyReference[string], valu } // FindExtension will attempt to locate an extension using the specified string. -func (p *Paths) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, p.Extensions) +func (p *Paths) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, p.Extensions) } // GetExtensions returns all Paths extensions and satisfies the low.HasExtensions interface. -func (p *Paths) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (p *Paths) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return p.Extensions } @@ -71,6 +70,26 @@ func (p *Paths) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIn p.Reference = new(low.Reference) p.Extensions = low.ExtractExtensions(root) + pathsMap, err := extractPathItemsMap(ctx, root, idx) + if err != nil { + return err + } + + p.PathItems = pathsMap + return nil +} + +// Hash will return a consistent SHA256 Hash of the PathItem object +func (p *Paths) Hash() [32]byte { + var f []string + for pair := orderedmap.First(orderedmap.SortAlpha(p.PathItems)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) + } + f = append(f, low.HashExtensions(p.Extensions)...) + return sha256.Sum256([]byte(strings.Join(f, "|"))) +} + +func extractPathItemsMap(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) (*orderedmap.Map[low.KeyReference[string], low.ValueReference[*PathItem]], error) { // Translate YAML nodes to pathsMap using `TranslatePipeline`. type buildResult struct { key low.KeyReference[string] @@ -120,7 +139,6 @@ func (p *Paths) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIn } }() - // TranslatePipeline output. go func() { for { @@ -134,13 +152,11 @@ func (p *Paths) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIn wg.Done() }() - err := datamodel.TranslatePipeline[buildInput, buildResult](in, out, func(value buildInput) (buildResult, error) { pNode := value.pathNode cNode := value.currentNode - if ok, _, _ := utils.IsNodeRefValue(pNode); ok { r, _, err := low.LocateRefNode(pNode, idx) if r != nil { @@ -156,19 +172,16 @@ func (p *Paths) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIn } } - path := new(PathItem) _ = low.BuildModel(pNode, path) err := path.Build(ctx, cNode, pNode, idx) - if err != nil { if idx != nil && idx.GetLogger() != nil { idx.GetLogger().Error(fmt.Sprintf("error building path item '%s'", err.Error())) } - //return buildResult{}, err + // return buildResult{}, err } - return buildResult{ key: low.KeyReference[string]{ Value: cNode.Value, @@ -183,39 +196,7 @@ func (p *Paths) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIn ) wg.Wait() if err != nil { - return err + return nil, err } - - - p.PathItems = pathsMap - return nil -} - -// Hash will return a consistent SHA256 Hash of the PathItem object -func (p *Paths) Hash() [32]byte { - var f []string - l := make([]string, orderedmap.Len(p.PathItems)) - keys := make(map[string]low.ValueReference[*PathItem]) - z := 0 - - for pair := orderedmap.First(p.PathItems); pair != nil; pair = pair.Next() { - k := pair.Key().Value - keys[k] = pair.Value() - l[z] = k - z++ - } - - sort.Strings(l) - for k := range l { - f = append(f, fmt.Sprintf("%s-%s", l[k], low.GenerateHashString(keys[l[k]].Value))) - } - ekeys := make([]string, len(p.Extensions)) - z = 0 - for k := range p.Extensions { - ekeys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(p.Extensions[k].Value)))) - z++ - } - sort.Strings(ekeys) - f = append(f, ekeys...) - return sha256.Sum256([]byte(strings.Join(f, "|"))) + return pathsMap, nil } diff --git a/datamodel/low/v3/paths_test.go b/datamodel/low/v3/paths_test.go index a4d78eb..425b17a 100644 --- a/datamodel/low/v3/paths_test.go +++ b/datamodel/low/v3/paths_test.go @@ -13,12 +13,12 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) func TestPaths_Build(t *testing.T) { - yml := `"/some/path": get: description: get method @@ -57,7 +57,10 @@ x-milk: cold` path := n.FindPath("/some/path").Value assert.NotNil(t, path) assert.Equal(t, "get method", path.Get.Value.Description.Value) - assert.Equal(t, "yummy", path.FindExtension("x-cake").Value) + + var xCake string + _ = path.FindExtension("x-cake").Value.Decode(&xCake) + assert.Equal(t, "yummy", xCake) assert.Equal(t, "post method", path.Post.Value.Description.Value) assert.Equal(t, "put method", path.Put.Value.Description.Value) assert.Equal(t, "patch method", path.Patch.Value.Description.Value) @@ -65,13 +68,15 @@ x-milk: cold` assert.Equal(t, "head method", path.Head.Value.Description.Value) assert.Equal(t, "trace method", path.Trace.Value.Description.Value) assert.Len(t, path.Parameters.Value, 1) - assert.Equal(t, "cold", n.FindExtension("x-milk").Value) + + var xMilk string + _ = n.FindExtension("x-milk").Value.Decode(&xMilk) + assert.Equal(t, "cold", xMilk) assert.Equal(t, "hello", path.Parameters.Value[0].Value.Name.Value) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestPaths_Build_Fail(t *testing.T) { - yml := `"/some/path": $ref: $bork` @@ -85,11 +90,9 @@ func TestPaths_Build_Fail(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestPaths_Build_FailRef(t *testing.T) { - // this is kinda nuts, and, it's completely illegal, but you never know! yml := `"/some/path": description: this is some path @@ -125,7 +128,6 @@ func TestPaths_Build_FailRef(t *testing.T) { } func TestPaths_Build_FailRefDeadEnd(t *testing.T) { - // this is nuts. yml := `"/no/path": get: @@ -160,7 +162,6 @@ func TestPaths_Build_FailRefDeadEnd(t *testing.T) { } func TestPaths_Build_SuccessRef(t *testing.T) { - // this is kinda nuts, it's also not illegal, however the mechanics still need to work. yml := `"/some/path": description: this is some path @@ -198,7 +199,6 @@ func TestPaths_Build_SuccessRef(t *testing.T) { } func TestPaths_Build_BadParams(t *testing.T) { - yml := `"/some/path": parameters: this: shouldFail` @@ -223,11 +223,9 @@ func TestPaths_Build_BadParams(t *testing.T) { _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) er := buf.String() assert.Contains(t, er, "array build failed, input is not an array, line 3, column 5'") - } func TestPaths_Build_BadRef(t *testing.T) { - // this is kinda nuts, it's also not illegal, however the mechanics still need to work. yml := `"/some/path": description: this is some path @@ -262,11 +260,9 @@ func TestPaths_Build_BadRef(t *testing.T) { _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Contains(t, buf.String(), "unable to locate reference anywhere in the rolodex\" reference=#/no-where") assert.Contains(t, buf.String(), "error building path item 'path item build failed: cannot find reference: #/no-where at line 4, col 10'") - } func TestPathItem_Build_GoodRef(t *testing.T) { - // this is kinda nuts, it's also not illegal, however the mechanics still need to work. yml := `"/some/path": description: this is some path @@ -296,7 +292,6 @@ func TestPathItem_Build_GoodRef(t *testing.T) { } func TestPathItem_Build_BadRef(t *testing.T) { - // this is kinda nuts, it's also not illegal, however the mechanics still need to work. yml := `"/some/path": description: this is some path @@ -333,11 +328,9 @@ func TestPathItem_Build_BadRef(t *testing.T) { _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Contains(t, buf.String(), "unable to locate reference anywhere in the rolodex\" reference=#/~1cakes/NotFound") assert.Contains(t, buf.String(), "error building path item 'path item build failed: cannot find reference: #/~1another~1path/get at line 4, col 10") - } func TestPathNoOps(t *testing.T) { - // this is kinda nuts, it's also not illegal, however the mechanics still need to work. yml := `"/some/path": "/cakes":` @@ -355,7 +348,6 @@ func TestPathNoOps(t *testing.T) { } func TestPathItem_Build_Using_Ref(t *testing.T) { - // first we need an index. yml := `paths: '/something/here': @@ -393,7 +385,6 @@ func TestPathItem_Build_Using_Ref(t *testing.T) { } func TestPath_Build_Using_CircularRef(t *testing.T) { - // first we need an index. yml := `paths: '/something/here': @@ -425,11 +416,9 @@ func TestPath_Build_Using_CircularRef(t *testing.T) { err = n.Build(context.Background(), nil, rootNode.Content[0], idx) assert.Error(t, err) - } func TestPath_Build_Using_CircularRefWithOp(t *testing.T) { - // first we need an index. yml := `paths: '/something/here': @@ -471,11 +460,9 @@ func TestPath_Build_Using_CircularRefWithOp(t *testing.T) { _ = n.Build(context.Background(), nil, rootNode.Content[0], idx) assert.Contains(t, buf.String(), "error building path item 'build schema failed: circular reference 'post -> post -> post' found during lookup at line 4, column 7, It cannot be resolved'") - } func TestPaths_Build_BrokenOp(t *testing.T) { - yml := `"/some/path": post: externalDocs: @@ -503,7 +490,6 @@ func TestPaths_Build_BrokenOp(t *testing.T) { } func TestPaths_Hash(t *testing.T) { - yml := `/french/toast: description: toast /french/hen: @@ -545,7 +531,6 @@ x-france: french` a, b = n.FindPathAndKey("I do not exist") assert.Nil(t, a) assert.Nil(t, b) - } // Test parse failure among many paths. diff --git a/datamodel/low/v3/request_body.go b/datamodel/low/v3/request_body.go index 56e3f8b..5478f01 100644 --- a/datamodel/low/v3/request_body.go +++ b/datamodel/low/v3/request_body.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -21,19 +20,19 @@ import ( // - https://spec.openapis.org/oas/v3.1.0#request-body-object type RequestBody struct { Description low.NodeReference[string] - Content low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]] + Content low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]] Required low.NodeReference[bool] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // FindExtension attempts to locate an extension using the provided name. -func (rb *RequestBody) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, rb.Extensions) +func (rb *RequestBody) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, rb.Extensions) } // GetExtensions returns all RequestBody extensions and satisfies the low.HasExtensions interface. -func (rb *RequestBody) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (rb *RequestBody) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return rb.Extensions } @@ -55,7 +54,7 @@ func (rb *RequestBody) Build(ctx context.Context, _, root *yaml.Node, idx *index return cErr } if con != nil { - rb.Content = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]]{ + rb.Content = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]]{ Value: con, KeyNode: cL, ValueNode: cN, @@ -73,18 +72,9 @@ func (rb *RequestBody) Hash() [32]byte { if !rb.Required.IsEmpty() { f = append(f, fmt.Sprint(rb.Required.Value)) } - for pair := orderedmap.First(rb.Content.Value); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(orderedmap.SortAlpha(rb.Content.Value)); pair != nil; pair = pair.Next() { f = append(f, low.GenerateHashString(pair.Value().Value)) } - - keys := make([]string, len(rb.Extensions)) - z := 0 - for k := range rb.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(rb.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) - + f = append(f, low.HashExtensions(rb.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/request_body_test.go b/datamodel/low/v3/request_body_test.go index 39b6605..62658ca 100644 --- a/datamodel/low/v3/request_body_test.go +++ b/datamodel/low/v3/request_body_test.go @@ -5,15 +5,16 @@ package v3 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestRequestBody_Build(t *testing.T) { - yml := `description: a nice request required: true content: @@ -33,14 +34,18 @@ x-requesto: presto` assert.NoError(t, err) assert.Equal(t, "a nice request", n.Description.Value) assert.True(t, n.Required.Value) - assert.Equal(t, "nice.", n.FindContent("fresh/fish").Value.Example.Value) - assert.Equal(t, "presto", n.FindExtension("x-requesto").Value) - assert.Len(t, n.GetExtensions(), 1) + var example string + _ = n.FindContent("fresh/fish").Value.Example.Value.Decode(&example) + assert.Equal(t, "nice.", example) + + var xRequesto string + _ = n.FindExtension("x-requesto").Value.Decode(&xRequesto) + assert.Equal(t, "presto", xRequesto) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestRequestBody_Fail(t *testing.T) { - yml := `content: $ref: #illegal` @@ -57,7 +62,6 @@ func TestRequestBody_Fail(t *testing.T) { } func TestRequestBody_Hash(t *testing.T) { - yml := `description: nice toast content: jammy/toast: @@ -99,5 +103,4 @@ x-toast: nice` // hash assert.Equal(t, n.Hash(), n2.Hash()) - } diff --git a/datamodel/low/v3/response.go b/datamodel/low/v3/response.go index 13ec225..da52a4e 100644 --- a/datamodel/low/v3/response.go +++ b/datamodel/low/v3/response.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -24,20 +23,20 @@ import ( // - https://spec.openapis.org/oas/v3.1.0#response-object type Response struct { Description low.NodeReference[string] - Headers low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]] - Content low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] - Links low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Link]]] + Headers low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]] + Content low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] + Links low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Link]]] *low.Reference } // FindExtension will attempt to locate an extension using the supplied key -func (r *Response) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, r.Extensions) +func (r *Response) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, r.Extensions) } // GetExtensions returns all OAuthFlow extensions and satisfies the low.HasExtensions interface. -func (r *Response) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (r *Response) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return r.Extensions } @@ -63,13 +62,13 @@ func (r *Response) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe r.Reference = new(low.Reference) r.Extensions = low.ExtractExtensions(root) - //extract headers + // extract headers headers, lN, kN, err := low.ExtractMapExtensions[*Header](ctx, HeadersLabel, root, idx, true) if err != nil { return err } if headers != nil { - r.Headers = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]]{ + r.Headers = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Header]]]{ Value: headers, KeyNode: lN, ValueNode: kN, @@ -81,7 +80,7 @@ func (r *Response) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe return cErr } if con != nil { - r.Content = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]]{ + r.Content = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*MediaType]]]{ Value: con, KeyNode: clN, ValueNode: cN, @@ -94,7 +93,7 @@ func (r *Response) Build(ctx context.Context, _, root *yaml.Node, idx *index.Spe return lErr } if links != nil { - r.Links = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*Link]]]{ + r.Links = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*Link]]]{ Value: links, KeyNode: linkLabel, ValueNode: linkValue, @@ -109,37 +108,15 @@ func (r *Response) Hash() [32]byte { if r.Description.Value != "" { f = append(f, r.Description.Value) } - keys := make([]string, orderedmap.Len(r.Headers.Value)) - z := 0 - for pair := orderedmap.First(r.Headers.Value); pair != nil; pair = pair.Next() { - keys[z] = fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value)) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(r.Headers.Value)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, orderedmap.Len(r.Content.Value)) - z = 0 - for pair := orderedmap.First(r.Content.Value); pair != nil; pair = pair.Next() { - keys[z] = fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value)) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(r.Content.Value)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, orderedmap.Len(r.Links.Value)) - z = 0 - for pair := orderedmap.First(r.Links.Value); pair != nil; pair = pair.Next() { - keys[z] = fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value)) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(r.Links.Value)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) } - sort.Strings(keys) - f = append(f, keys...) - keys = make([]string, len(r.Extensions)) - z = 0 - for k := range r.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(r.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(r.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/response_test.go b/datamodel/low/v3/response_test.go index 51d31a2..1fced16 100644 --- a/datamodel/low/v3/response_test.go +++ b/datamodel/low/v3/response_test.go @@ -5,15 +5,16 @@ package v3 import ( "context" + "testing" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "testing" ) func TestResponses_Build(t *testing.T) { - yml := `"200": description: some response headers: @@ -47,7 +48,10 @@ default: ok := n.FindResponseByCode("200") assert.NotNil(t, ok.Value) assert.Equal(t, "some response", ok.Value.Description.Value) - assert.Equal(t, "rot", ok.Value.FindExtension("x-gut").Value) + + var xGut string + _ = ok.Value.FindExtension("x-gut").Value.Decode(&xGut) + assert.Equal(t, "rot", xGut) con := ok.Value.FindContent("nice/rice") assert.NotNil(t, con.Value) @@ -62,13 +66,11 @@ default: assert.Equal(t, "a link", link.Value.Description.Value) // check hash - assert.Equal(t, "c009b2046101bc03df802b4cf23f78176931137e6115bf7b445ca46856c06b51", + assert.Equal(t, "37ae6a91f2260031e22bd6fbf2d286928dd910b14cb75d4239fb80651ac5ecff", low.GenerateHashString(&n)) - } func TestResponses_NoDefault(t *testing.T) { - yml := `"200": description: some response headers: @@ -96,16 +98,14 @@ x-shoes: old` err = n.Build(context.Background(), nil, idxNode.Content[0], idx) // check hash - assert.Equal(t, "54ab66e6cb8bd226940f421c2387e45215b84c946182435dfe2a3036043fa07c", + assert.Equal(t, "3da5051dcd82a06f8e4c7698cdec03550ae1988ee54d96d4c4a90a5c8f9d7b2b", low.GenerateHashString(&n)) - assert.Len(t, n.FindResponseByCode("200").Value.GetExtensions(), 1) - assert.Len(t, n.GetExtensions(), 1) - + assert.Equal(t, 1, orderedmap.Len(n.FindResponseByCode("200").Value.GetExtensions())) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestResponses_Build_FailCodes_WrongType(t *testing.T) { - yml := `- "200": $ref: #bork` @@ -119,11 +119,9 @@ func TestResponses_Build_FailCodes_WrongType(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Build_FailCodes(t *testing.T) { - yml := `"200": $ref: #bork` @@ -137,11 +135,9 @@ func TestResponses_Build_FailCodes(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Build_FailDefault(t *testing.T) { - yml := `- default` var idxNode yaml.Node @@ -154,11 +150,9 @@ func TestResponses_Build_FailDefault(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Build_FailBadHeader(t *testing.T) { - yml := `"200": headers: header1: @@ -174,11 +168,9 @@ func TestResponses_Build_FailBadHeader(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Build_FailBadContent(t *testing.T) { - yml := `"200": content: flim/flam: @@ -194,11 +186,9 @@ func TestResponses_Build_FailBadContent(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Build_FailBadLinks(t *testing.T) { - yml := `"200": links: aLink: @@ -214,11 +204,9 @@ func TestResponses_Build_FailBadLinks(t *testing.T) { err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.Error(t, err) - } func TestResponses_Build_AllowXPrefixHeader(t *testing.T) { - yml := `"200": headers: x-header1: @@ -242,7 +230,6 @@ func TestResponses_Build_AllowXPrefixHeader(t *testing.T) { } func TestResponse_Hash(t *testing.T) { - yml := `description: nice toast headers: heady: @@ -299,7 +286,6 @@ links: // hash assert.Equal(t, n.Hash(), n2.Hash()) - } // diff --git a/datamodel/low/v3/responses.go b/datamodel/low/v3/responses.go index 6adfbac..1904e86 100644 --- a/datamodel/low/v3/responses.go +++ b/datamodel/low/v3/responses.go @@ -7,7 +7,6 @@ import ( "context" "crypto/sha256" "fmt" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -36,14 +35,14 @@ import ( // the duplication. Perhaps in the future we could use generics here, but for now to keep things // simple, they are broken out into individual versions. type Responses struct { - Codes orderedmap.Map[low.KeyReference[string], low.ValueReference[*Response]] + Codes *orderedmap.Map[low.KeyReference[string], low.ValueReference[*Response]] Default low.NodeReference[*Response] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // GetExtensions returns all Responses extensions and satisfies the low.HasExtensions interface. -func (r *Responses) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (r *Responses) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return r.Extensions } @@ -55,7 +54,6 @@ func (r *Responses) Build(ctx context.Context, _, root *yaml.Node, idx *index.Sp utils.CheckForMergeNodes(root) if utils.IsNodeMap(root) { codes, err := low.ExtractMapNoLookup[*Response](ctx, root, idx) - if err != nil { return err } @@ -113,29 +111,12 @@ func (r *Responses) FindResponseByCode(code string) *low.ValueReference[*Respons // Hash will return a consistent SHA256 Hash of the Examples object func (r *Responses) Hash() [32]byte { var f []string - var keys []string - keys = make([]string, orderedmap.Len(r.Codes)) - cMap := make(map[string]*Response, len(keys)) - z := 0 - for pair := orderedmap.First(r.Codes); pair != nil; pair = pair.Next() { - keys[z] = pair.Key().Value - cMap[pair.Key().Value] = pair.Value().Value - z++ - } - sort.Strings(keys) - for k := range keys { - f = append(f, fmt.Sprintf("%s-%s", keys[k], low.GenerateHashString(cMap[keys[k]]))) + for pair := orderedmap.First(orderedmap.SortAlpha(r.Codes)); pair != nil; pair = pair.Next() { + f = append(f, fmt.Sprintf("%s-%s", pair.Key().Value, low.GenerateHashString(pair.Value().Value))) } if !r.Default.IsEmpty() { f = append(f, low.GenerateHashString(r.Default.Value)) } - keys = make([]string, len(r.Extensions)) - z = 0 - for k := range r.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(r.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(r.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/security_scheme.go b/datamodel/low/v3/security_scheme.go index ef5e365..bf6bf36 100644 --- a/datamodel/low/v3/security_scheme.go +++ b/datamodel/low/v3/security_scheme.go @@ -6,13 +6,13 @@ package v3 import ( "context" "crypto/sha256" - "fmt" + "strings" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sort" - "strings" ) // SecurityScheme represents a low-level OpenAPI 3+ SecurityScheme object. @@ -34,17 +34,17 @@ type SecurityScheme struct { BearerFormat low.NodeReference[string] Flows low.NodeReference[*OAuthFlows] OpenIdConnectUrl low.NodeReference[string] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // FindExtension attempts to locate an extension using the supplied key. -func (ss *SecurityScheme) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, ss.Extensions) +func (ss *SecurityScheme) FindExtension(ext string) *low.ValueReference[*yaml.Node] { + return low.FindItemInOrderedMap(ext, ss.Extensions) } // GetExtensions returns all SecurityScheme extensions and satisfies the low.HasExtensions interface. -func (ss *SecurityScheme) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (ss *SecurityScheme) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return ss.Extensions } @@ -92,13 +92,6 @@ func (ss *SecurityScheme) Hash() [32]byte { if !ss.OpenIdConnectUrl.IsEmpty() { f = append(f, ss.OpenIdConnectUrl.Value) } - keys := make([]string, len(ss.Extensions)) - z := 0 - for k := range ss.Extensions { - keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(ss.Extensions[k].Value)))) - z++ - } - sort.Strings(keys) - f = append(f, keys...) + f = append(f, low.HashExtensions(ss.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/security_scheme_test.go b/datamodel/low/v3/security_scheme_test.go index d6728ca..c06a58a 100644 --- a/datamodel/low/v3/security_scheme_test.go +++ b/datamodel/low/v3/security_scheme_test.go @@ -10,6 +10,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -60,7 +61,7 @@ x-milk: please` err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "0b5ee36519fdfc6383c7befd92294d77b5799cd115911ff8c3e194f345a8c103", + assert.Equal(t, "306c5ee231d9854f21f03e909517c1fa8a8cb9431f11e8429a501eafaca31652", low.GenerateHashString(&n)) assert.Equal(t, "tea", n.Type.Value) @@ -70,9 +71,12 @@ x-milk: please` assert.Equal(t, "lovely", n.Scheme.Value) assert.Equal(t, "wow", n.BearerFormat.Value) assert.Equal(t, "https://pb33f.io/openid", n.OpenIdConnectUrl.Value) - assert.Equal(t, "please", n.FindExtension("x-milk").Value) + + var xMilk string + _ = n.FindExtension("x-milk").Value.Decode(&xMilk) + assert.Equal(t, "please", xMilk) assert.Equal(t, "https://pb33f.io", n.Flows.Value.Implicit.Value.TokenUrl.Value) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestSecurityScheme_Build_Fail(t *testing.T) { diff --git a/datamodel/low/v3/server.go b/datamodel/low/v3/server.go index d91fefc..737029b 100644 --- a/datamodel/low/v3/server.go +++ b/datamodel/low/v3/server.go @@ -6,7 +6,6 @@ package v3 import ( "context" "crypto/sha256" - "sort" "strings" "github.com/pb33f/libopenapi/datamodel/low" @@ -21,13 +20,13 @@ import ( type Server struct { URL low.NodeReference[string] Description low.NodeReference[string] - Variables low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*ServerVariable]]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Variables low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*ServerVariable]]] + Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] *low.Reference } // GetExtensions returns all Paths extensions and satisfies the low.HasExtensions interface. -func (s *Server) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { +func (s *Server) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { return s.Extensions } @@ -70,7 +69,7 @@ func (s *Server) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecIndex }, ) } - s.Variables = low.NodeReference[orderedmap.Map[low.KeyReference[string], low.ValueReference[*ServerVariable]]]{ + s.Variables = low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*ServerVariable]]]{ KeyNode: kn, ValueNode: vars, Value: variablesMap, @@ -82,19 +81,15 @@ func (s *Server) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecIndex // Hash will return a consistent SHA256 Hash of the Server object func (s *Server) Hash() [32]byte { var f []string - keys := make([]string, orderedmap.Len(s.Variables.Value)) - z := 0 - for pair := orderedmap.First(s.Variables.Value); pair != nil; pair = pair.Next() { - keys[z] = low.GenerateHashString(pair.Value().Value) - z++ + for pair := orderedmap.First(orderedmap.SortAlpha(s.Variables.Value)); pair != nil; pair = pair.Next() { + f = append(f, low.GenerateHashString(pair.Value().Value)) } - sort.Strings(keys) - f = append(f, keys...) if !s.URL.IsEmpty() { f = append(f, s.URL.Value) } if !s.Description.IsEmpty() { f = append(f, s.Description.Value) } + f = append(f, low.HashExtensions(s.Extensions)...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/server_test.go b/datamodel/low/v3/server_test.go index b10564b..d16c4ee 100644 --- a/datamodel/low/v3/server_test.go +++ b/datamodel/low/v3/server_test.go @@ -35,7 +35,7 @@ variables: err = n.Build(context.Background(), nil, idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "ec69dfcf68ad8988f3804e170ee6c4a7ad2e4ac51084796eea93168820827546", + assert.Equal(t, "25535d0a6dd30c609aeae6e08f9eaa82fef49df540fc048fe4adffbce7841c0b", low.GenerateHashString(&n)) assert.Equal(t, "https://pb33f.io", n.URL.Value) @@ -48,7 +48,7 @@ variables: assert.Equal(t, "00eef99ee4a7b746be7b4ccdece59c5a96222c6206f846fafed782c9f3f9b46b", low.GenerateHashString(s.Value)) - assert.Len(t, n.GetExtensions(), 1) + assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) } func TestServer_Build_NoVars(t *testing.T) { diff --git a/datamodel/translate.go b/datamodel/translate.go index 455ba19..ceb55ca 100644 --- a/datamodel/translate.go +++ b/datamodel/translate.go @@ -15,6 +15,7 @@ type ( TranslateFunc[IN any, OUT any] func(IN) (OUT, error) TranslateSliceFunc[IN any, OUT any] func(int, IN) (OUT, error) TranslateMapFunc[IN any, OUT any] func(IN) (OUT, error) + ResultFunc[V any] func(V) error ) type continueError struct { @@ -122,12 +123,12 @@ JOBLOOP: return reterr } -// TranslateMapParallel iterates a map in parallel and calls translate() +// TranslateMapParallel iterates a `*orderedmap.Map` in parallel and calls translate() // asynchronously. // translate() or result() may return `io.EOF` to break iteration. -// Results are provided sequentially to result(). Result order is -// nondeterministic. -func TranslateMapParallel[K comparable, V any, OUT any](m orderedmap.Map[K, V], translate TranslateMapFunc[orderedmap.Pair[K, V], OUT], result ActionFunc[OUT]) error { +// Safely handles nil pointer. +// Results are provided sequentially to result() in stable order from `*orderedmap.Map`. +func TranslateMapParallel[K comparable, V any, RV any](m *orderedmap.Map[K, V], translate TranslateFunc[orderedmap.Pair[K, V], RV], result ResultFunc[RV]) error { if m == nil { return nil } @@ -136,54 +137,66 @@ func TranslateMapParallel[K comparable, V any, OUT any](m orderedmap.Map[K, V], defer cancel() concurrency := runtime.NumCPU() c := orderedmap.Iterate(ctx, m) - resultChan := make(chan OUT, concurrency) + jobChan := make(chan *jobStatus[RV], concurrency) var reterr error - var mu sync.Mutex var wg sync.WaitGroup + var mu sync.Mutex - // Fan out input translation. + // Fan out translate jobs. wg.Add(1) go func() { - defer wg.Done() + defer func() { + close(jobChan) + wg.Done() + }() for pair := range c { + j := &jobStatus[RV]{ + done: make(chan struct{}), + } + select { + case jobChan <- j: + case <-ctx.Done(): + return + } + wg.Add(1) go func(pair orderedmap.Pair[K, V]) { - defer wg.Done() value, err := translate(pair) - if err == Continue { - return - } if err != nil { mu.Lock() + defer func() { + mu.Unlock() + wg.Done() + cancel() + }() if reterr == nil { reterr = err } - mu.Unlock() - cancel() return } - select { - case resultChan <- value: - case <-ctx.Done(): - } + j.result = value + close(j.done) + wg.Done() }(pair) } }() - go func() { - // Indicate EOF after all translate goroutines finish. - wg.Wait() - close(resultChan) - }() - - // Iterate results. - for value := range resultChan { - err := result(value) - if err != nil { - cancel() - wg.Wait() - reterr = err - break + // Iterate jobChan as jobs complete. + defer wg.Wait() +JOBLOOP: + for j := range jobChan { + select { + case <-j.done: + err := result(j.result) + if err != nil { + cancel() + if err == io.EOF { + return nil + } + return err + } + case <-ctx.Done(): + break JOBLOOP } } diff --git a/datamodel/translate_test.go b/datamodel/translate_test.go index 957eeec..2b21ae2 100644 --- a/datamodel/translate_test.go +++ b/datamodel/translate_test.go @@ -205,7 +205,7 @@ func TestTranslateMapParallel(t *testing.T) { }) t.Run("nil", func(t *testing.T) { - var m orderedmap.Map[string, int] + var m *orderedmap.Map[string, int] var translateCounter int64 translateFunc := func(pair orderedmap.Pair[string, int]) (string, error) { atomic.AddInt64(&translateCounter, 1) @@ -297,26 +297,6 @@ func TestTranslateMapParallel(t *testing.T) { require.NoError(t, err) assert.Less(t, resultCounter, mapSize) }) - - t.Run("Continue in translate", func(t *testing.T) { - m := orderedmap.New[string, int]() - for i := 0; i < mapSize; i++ { - m.Set(fmt.Sprintf("key%d", i), i+1000) - } - - var translateCounter int64 - translateFunc := func(_ orderedmap.Pair[string, int]) (string, error) { - atomic.AddInt64(&translateCounter, 1) - return "", datamodel.Continue - } - resultFunc := func(_ string) error { - t.Fatal("Expected no call to resultFunc()") - return nil - } - err := datamodel.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) - require.NoError(t, err) - assert.Equal(t, int64(mapSize), translateCounter) - }) } func TestTranslatePipeline(t *testing.T) { diff --git a/document_examples_test.go b/document_examples_test.go index a92de38..36c3271 100644 --- a/document_examples_test.go +++ b/document_examples_test.go @@ -6,15 +6,16 @@ package libopenapi import ( "bytes" "fmt" - "github.com/pb33f/libopenapi/datamodel" - "github.com/pb33f/libopenapi/index" - "github.com/pb33f/libopenapi/orderedmap" "log/slog" "net/url" "os" "strings" "testing" + "github.com/pb33f/libopenapi/datamodel" + "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/datamodel/high" v3high "github.com/pb33f/libopenapi/datamodel/high/v3" low "github.com/pb33f/libopenapi/datamodel/low/base" @@ -24,7 +25,6 @@ import ( ) func ExampleNewDocument_fromOpenAPI3Document() { - // How to read in an OpenAPI 3 Specification, into a Document. // load an OpenAPI 3 specification from bytes @@ -32,7 +32,6 @@ func ExampleNewDocument_fromOpenAPI3Document() { // create a new document from specification bytes document, err := NewDocument(petstore) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -59,7 +58,6 @@ func ExampleNewDocument_fromOpenAPI3Document() { } func ExampleNewDocument_fromWithDocumentConfigurationFailure() { - // This example shows how to create a document that prevents the loading of external references/ // from files or the network @@ -85,7 +83,6 @@ func ExampleNewDocument_fromWithDocumentConfigurationFailure() { // create a new document from specification bytes doc, err := NewDocumentWithConfiguration(digitalOcean, config) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -103,11 +100,10 @@ func ExampleNewDocument_fromWithDocumentConfigurationFailure() { fmt.Println("Error building Digital Ocean spec errors reported") } // Output: There are 475 errors logged - //Error building Digital Ocean spec errors reported + // Error building Digital Ocean spec errors reported } func ExampleNewDocument_fromWithDocumentConfigurationSuccess() { - // This example shows how to create a document that prevents the loading of external references/ // from files or the network @@ -128,7 +124,6 @@ func ExampleNewDocument_fromWithDocumentConfigurationSuccess() { // create a new document from specification bytes doc, err := NewDocumentWithConfiguration(digitalOcean, &config) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -147,7 +142,6 @@ func ExampleNewDocument_fromWithDocumentConfigurationSuccess() { } func ExampleNewDocument_fromSwaggerDocument() { - // How to read in a Swagger / OpenAPI 2 Specification, into a Document. // load a Swagger specification from bytes @@ -155,7 +149,6 @@ func ExampleNewDocument_fromSwaggerDocument() { // create a new document from specification bytes document, err := NewDocument(petstore) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -182,13 +175,11 @@ func ExampleNewDocument_fromSwaggerDocument() { } func ExampleNewDocument_fromUnknownVersion() { - // load an unknown version of an OpenAPI spec petstore, _ := os.ReadFile("test_specs/burgershop.openapi.yaml") // create a new document from specification bytes document, err := NewDocument(petstore) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -233,7 +224,6 @@ func ExampleNewDocument_fromUnknownVersion() { } func ExampleNewDocument_mutateValuesAndSerialize() { - // How to mutate values in an OpenAPI Specification, without re-ordering original content. // create very small, and useless spec that does nothing useful, except showcase this feature. @@ -249,7 +239,6 @@ info: ` // create a new document from specification bytes document, err := NewDocument([]byte(spec)) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -289,7 +278,7 @@ info: // print our modified spec! fmt.Println(string(mutatedSpec)) // Output: openapi: 3.1.0 - //info: + // info: // title: A new title for a useless spec // contact: // name: Buckaroo @@ -298,8 +287,7 @@ info: // url: https://pb33f.io/license } -func ExampleCompareDocuments_openAPI() { - +func TestExampleCompareDocuments_openAPI(t *testing.T) { // How to compare two different OpenAPI specifications. // load an original OpenAPI 3 specification from bytes @@ -310,7 +298,6 @@ func ExampleCompareDocuments_openAPI() { // create a new document from original specification bytes originalDoc, err := NewDocument(burgerShopOriginal) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -318,7 +305,6 @@ func ExampleCompareDocuments_openAPI() { // create a new document from updated specification bytes updatedDoc, err := NewDocument(burgerShopUpdated) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -339,14 +325,11 @@ func ExampleCompareDocuments_openAPI() { schemaChanges := documentChanges.ComponentsChanges.SchemaChanges // Print out some interesting stats about the OpenAPI document changes. - fmt.Printf("There are %d changes, of which %d are breaking. %v schemas have changes.", - documentChanges.TotalChanges(), documentChanges.TotalBreakingChanges(), len(schemaChanges)) - //Output: There are 75 changes, of which 19 are breaking. 6 schemas have changes. - + assert.Equal(t, `There are 75 changes, of which 19 are breaking. 6 schemas have changes.`, fmt.Sprintf("There are %d changes, of which %d are breaking. %v schemas have changes.", + documentChanges.TotalChanges(), documentChanges.TotalBreakingChanges(), len(schemaChanges))) } -func ExampleCompareDocuments_swagger() { - +func TestExampleCompareDocuments_swagger(t *testing.T) { // How to compare two different Swagger specifications. // load an original OpenAPI 3 specification from bytes @@ -357,7 +340,6 @@ func ExampleCompareDocuments_swagger() { // create a new document from original specification bytes originalDoc, err := NewDocument(petstoreOriginal) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -365,7 +347,6 @@ func ExampleCompareDocuments_swagger() { // create a new document from updated specification bytes updatedDoc, err := NewDocument(petstoreUpdated) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -386,14 +367,11 @@ func ExampleCompareDocuments_swagger() { schemaChanges := documentChanges.ComponentsChanges.SchemaChanges // Print out some interesting stats about the Swagger document changes. - fmt.Printf("There are %d changes, of which %d are breaking. %v schemas have changes.", - documentChanges.TotalChanges(), documentChanges.TotalBreakingChanges(), len(schemaChanges)) - //Output: There are 52 changes, of which 27 are breaking. 5 schemas have changes. - + assert.Equal(t, `There are 52 changes, of which 27 are breaking. 5 schemas have changes.`, fmt.Sprintf("There are %d changes, of which %d are breaking. %v schemas have changes.", + documentChanges.TotalChanges(), documentChanges.TotalBreakingChanges(), len(schemaChanges))) } func TestDocument_Paths_As_Array(t *testing.T) { - // paths can now be wrapped in an array. spec := `{ "openapi": "3.1.0", @@ -406,7 +384,6 @@ func TestDocument_Paths_As_Array(t *testing.T) { ` // create a new document from specification bytes doc, err := NewDocument([]byte(spec)) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -419,7 +396,6 @@ func TestDocument_Paths_As_Array(t *testing.T) { // during the parsing/indexing/building of a document, you can capture the // []errors thrown which are pointers to *resolver.ResolvingError func ExampleNewDocument_infinite_circular_references() { - // create a specification with an obvious and deliberate circular reference spec := `openapi: "3.1" components: @@ -441,7 +417,6 @@ components: ` // create a new document from specification bytes doc, err := NewDocument([]byte(spec)) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -475,7 +450,6 @@ components: // This tests checks that circular references which are _not_ marked as required pass correctly func TestNewDocument_terminable_circular_references(t *testing.T) { - // create a specification with an obvious and deliberate circular reference spec := `openapi: "3.1" components: @@ -493,7 +467,6 @@ components: ` // create a new document from specification bytes doc, err := NewDocument([]byte(spec)) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -509,7 +482,6 @@ components: // // This example demonstrates how to use the `UnpackExtensions` with custom OpenAPI extensions. func ExampleNewDocument_unpacking_extensions() { - // define an example struct representing a cake type cake struct { Candles int `yaml:"candles"` @@ -565,7 +537,6 @@ components: patty: lamb` // create a new document from specification bytes doc, err := NewDocument([]byte(spec)) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -596,10 +567,10 @@ components: } // extract extension by name for schemaOne - customCakes := schemaOneExtensions["x-custom-cakes"] + customCakes := schemaOneExtensions.GetOrZero("x-custom-cakes") // extract extension by name for schemaOne - customBurgers := parameterOneExtensions["x-custom-burgers"] + customBurgers := parameterOneExtensions.GetOrZero("x-custom-burgers") // print out schemaOne complex extension details. fmt.Printf("schemaOne 'x-custom-cakes' (%s) has %d cakes, 'someCake' has %d candles and %s frosting\n", @@ -618,11 +589,10 @@ components: ) // Output: schemaOne 'x-custom-cakes' (some cakes) has 2 cakes, 'someCake' has 10 candles and blue frosting - //parameterOne 'x-custom-burgers' (some burgers) has 2 burgers, 'anotherBurger' has mayo sauce and a lamb patty + // parameterOne 'x-custom-burgers' (some burgers) has 2 burgers, 'anotherBurger' has mayo sauce and a lamb patty } func ExampleNewDocument_modifyAndReRender() { - // How to read in an OpenAPI 3 Specification, into a Document, // modify the document and then re-render it back to YAML bytes. @@ -631,7 +601,6 @@ func ExampleNewDocument_modifyAndReRender() { // create a new document from specification bytes doc, err := NewDocument(petstore) - // if anything went wrong, an error is thrown if err != nil { panic(fmt.Sprintf("cannot create new document: %e", err)) @@ -681,5 +650,5 @@ func ExampleNewDocument_modifyAndReRender() { fmt.Printf("There were %d original paths. There are now %d paths in the document\n", originalPaths, newPaths) fmt.Printf("The original spec had %d bytes, the new one has %d\n", len(petstore), len(rawBytes)) // Output: There were 13 original paths. There are now 14 paths in the document - //The original spec had 31143 bytes, the new one has 31027 + // The original spec had 31143 bytes, the new one has 31213 } diff --git a/document_iteration_test.go b/document_iteration_test.go index 2f2189c..500225b 100644 --- a/document_iteration_test.go +++ b/document_iteration_test.go @@ -39,7 +39,8 @@ func Test_Speakeasy_Document_Iteration(t *testing.T) { require.Empty(t, errs) for pair := orderedmap.First(m.Model.Paths.PathItems); pair != nil; pair = pair.Next() { - t.Log(pair.Key()) + path := pair.Key() + t.Log(path) iterateOperations(t, pair.Value().GetOperations()) } @@ -57,19 +58,24 @@ func Test_Speakeasy_Document_Iteration(t *testing.T) { } } -func iterateOperations(t *testing.T, ops map[string]*v3.Operation) { - t.Helper() +func iterateOperations(t *testing.T, ops *orderedmap.Map[string, *v3.Operation]) { + for pair := orderedmap.First(ops); pair != nil; pair = pair.Next() { + method := pair.Key() + op := pair.Value() - for method, op := range ops { t.Log(method) - for _, param := range op.Parameters { + for i, param := range op.Parameters { + t.Log("param", i, param.Name) + if param.Schema != nil { handleSchema(t, param.Schema, context{}) } } if op.RequestBody != nil { + t.Log("request body") + for pair := orderedmap.First(op.RequestBody.Content); pair != nil; pair = pair.Next() { t.Log(pair.Key()) @@ -81,6 +87,10 @@ func iterateOperations(t *testing.T, ops map[string]*v3.Operation) { } } + if orderedmap.Len(op.Responses.Codes) > 0 { + t.Log("responses") + } + for codePair := orderedmap.First(op.Responses.Codes); codePair != nil; codePair = codePair.Next() { t.Log(codePair.Key()) @@ -95,6 +105,10 @@ func iterateOperations(t *testing.T, ops map[string]*v3.Operation) { } } + if orderedmap.Len(op.Responses.Codes) > 0 { + t.Log("callbacks") + } + for callacksPair := orderedmap.First(op.Callbacks); callacksPair != nil; callacksPair = callacksPair.Next() { t.Log(callacksPair.Key()) @@ -108,8 +122,6 @@ func iterateOperations(t *testing.T, ops map[string]*v3.Operation) { } func handleSchema(t *testing.T, schProxy *base.SchemaProxy, ctx context) { - t.Helper() - if checkCircularReference(t, &ctx, schProxy) { return } @@ -119,6 +131,8 @@ func handleSchema(t *testing.T, schProxy *base.SchemaProxy, ctx context) { typ, subTypes := getResolvedType(sch) + t.Log("schema", typ, subTypes) + if len(sch.Enum) > 0 { switch typ { case "string": @@ -178,7 +192,7 @@ func getResolvedType(sch *base.Schema) (string, []string) { return "string", nil } - if sch.Properties.Len() > 0 { + if orderedmap.Len(sch.Properties) > 0 { return "object", nil } @@ -201,8 +215,6 @@ func getResolvedType(sch *base.Schema) (string, []string) { } func handleAllOfAnyOfOneOf(t *testing.T, sch *base.Schema, ctx context) { - t.Helper() - var schemas []*base.SchemaProxy switch { @@ -222,8 +234,6 @@ func handleAllOfAnyOfOneOf(t *testing.T, sch *base.Schema, ctx context) { } func handleArray(t *testing.T, sch *base.Schema, ctx context) { - t.Helper() - ctx.stack = append(ctx.stack, loopFrame{Type: "array", Restricted: sch.MinItems != nil && *sch.MinItems > 0}) if sch.Items != nil && sch.Items.IsA() { @@ -242,8 +252,6 @@ func handleArray(t *testing.T, sch *base.Schema, ctx context) { } func handleObject(t *testing.T, sch *base.Schema, ctx context) { - t.Helper() - for pair := orderedmap.First(sch.Properties); pair != nil; pair = pair.Next() { ctx.stack = append(ctx.stack, loopFrame{Type: "object", Restricted: slices.Contains(sch.Required, pair.Key())}) handleSchema(t, pair.Value(), ctx) diff --git a/document_test.go b/document_test.go index b6644b9..2282f3c 100644 --- a/document_test.go +++ b/document_test.go @@ -14,6 +14,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/high/base" v3high "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "github.com/pb33f/libopenapi/what-changed/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -146,6 +147,109 @@ info: assert.Equal(t, ymlModified, string(serial)) } +func TestDocument_RoundTrip(t *testing.T) { + orig := `openapi: 3.1.0 +info: + title: "The magic API" + description: | + A multi-line description + of the API. That should be retained. +tags: + - name: "Burgers" +security: + - oauth2: [] +paths: + "/test": + parameters: + - $ref: "#/components/parameters/completed_since" + post: + tags: + - "Burgers" + operationId: "test" + requestBody: + description: Callback payload + content: + 'application/json': + schema: + type: string + responses: + "200": + description: "OK" + content: + application/json: + schema: + type: object + properties: + data: + $ref: "#/components/schemas/test" + arr: + type: array + items: + $ref: "#/components/schemas/test" + callbacks: + BurgerCallback: + x-break-everything: please + "{$request.query.queryUrl}": + post: + requestBody: + description: Callback payload + content: + application/json: + schema: + type: string + responses: + '200': + description: callback successfully processes +components: + schemas: + test: + type: string + parameters: + completed_since: + in: query + name: completed_since + required: false + explode: false + schema: + example: 2012-02-22T02:06:58.158Z + format: date-time + type: string + links: + LocateBurger: + operationId: locateBurger + parameters: + burgerId: '$response.body#/id' + description: Go and get a tasty burger + securitySchemes: + oauth2: + description: |- + We require that applications designed to access the Asana API on behalf of multiple users implement OAuth 2.0. + Asana supports the Authorization Code Grant flow. + flows: + authorizationCode: + authorizationUrl: https://app.asana.com/-/oauth_authorize + refreshUrl: https://app.asana.com/-/oauth_token + scopes: + default: Provides access to all endpoints documented in our API reference. If no scopes are requested, this scope is assumed by default. + email: Provides access to the user’s email through the OpenID Connect user info endpoint. + openid: Provides access to OpenID Connect ID tokens and the OpenID Connect user info endpoint. + profile: Provides access to the user’s name and profile photo through the OpenID Connect user info endpoint. + tokenUrl: https://app.asana.com/-/oauth_token + type: oauth2 +` + + doc, err := NewDocument([]byte(orig)) + require.NoError(t, err) + + _, errs := doc.BuildV3Model() + require.Empty(t, errs) + + out, err := doc.Render() + require.NoError(t, err) + + assert.Equal(t, orig, string(out)) +} + func TestDocument_RenderAndReload_ChangeCheck_Burgershop(t *testing.T) { bs, _ := os.ReadFile("test_specs/burgershop.openapi.yaml") doc, _ := NewDocument(bs) @@ -201,6 +305,8 @@ func TestDocument_RenderAndReload_ChangeCheck_Asana(t *testing.T) { dat, newDoc, _, _ := doc.RenderAndReload() assert.NotNil(t, dat) + assert.Equal(t, string(bs), string(dat)) + // compare documents compReport, errs := CompareDocuments(doc, newDoc) @@ -209,10 +315,10 @@ func TestDocument_RenderAndReload_ChangeCheck_Asana(t *testing.T) { assert.Nil(t, errs) tc := compReport.TotalChanges() - assert.Equal(t, 21, tc) + assert.Equal(t, 0, tc) // there are some properties re-rendered that trigger changes. - assert.Equal(t, 21, len(flatChanges)) + assert.Equal(t, 0, len(flatChanges)) } func TestDocument_RenderAndReload(t *testing.T) { @@ -241,7 +347,7 @@ func TestDocument_RenderAndReload(t *testing.T) { })}, ) - h.Components.Schemas.GetOrZero("Order").Schema().Properties.GetOrZero("status").Schema().Example = "I am a teapot, filled with love." + h.Components.Schemas.GetOrZero("Order").Schema().Properties.GetOrZero("status").Schema().Example = utils.CreateStringNode("I am a teapot, filled with love.") h.Components.SecuritySchemes.GetOrZero("petstore_auth").Flows.Implicit.AuthorizationUrl = "https://pb33f.io" bytes, _, newDocModel, e := doc.RenderAndReload() @@ -257,8 +363,10 @@ func TestDocument_RenderAndReload(t *testing.T) { assert.Len(t, h.Paths.PathItems.GetOrZero("/pet/findByTags").Get.Tags, 3) yu := h.Paths.PathItems.GetOrZero("/pet/{petId}").Delete.Security assert.Equal(t, "read:abook", yu[len(yu)-1].Requirements.GetOrZero("pizza-and-cake")[0]) - assert.Equal(t, "I am a teapot, filled with love.", - h.Components.Schemas.GetOrZero("Order").Schema().Properties.GetOrZero("status").Schema().Example) + + var example string + _ = h.Components.Schemas.GetOrZero("Order").Schema().Properties.GetOrZero("status").Schema().Example.Decode(&example) + assert.Equal(t, "I am a teapot, filled with love.", example) assert.Equal(t, "https://pb33f.io", h.Components.SecuritySchemes.GetOrZero("petstore_auth").Flows.Implicit.AuthorizationUrl) @@ -291,7 +399,7 @@ func TestDocument_Render(t *testing.T) { })}, ) - h.Components.Schemas.GetOrZero("Order").Schema().Properties.GetOrZero("status").Schema().Example = "I am a teapot, filled with love." + h.Components.Schemas.GetOrZero("Order").Schema().Properties.GetOrZero("status").Schema().Example = utils.CreateStringNode("I am a teapot, filled with love.") h.Components.SecuritySchemes.GetOrZero("petstore_auth").Flows.Implicit.AuthorizationUrl = "https://pb33f.io" bytes, e := doc.Render() @@ -314,8 +422,10 @@ func TestDocument_Render(t *testing.T) { assert.Len(t, h.Paths.PathItems.GetOrZero("/pet/findByTags").Get.Tags, 3) yu := h.Paths.PathItems.GetOrZero("/pet/{petId}").Delete.Security assert.Equal(t, "read:abook", yu[len(yu)-1].Requirements.GetOrZero("pizza-and-cake")[0]) - assert.Equal(t, "I am a teapot, filled with love.", - h.Components.Schemas.GetOrZero("Order").Schema().Properties.GetOrZero("status").Schema().Example) + + var example string + _ = h.Components.Schemas.GetOrZero("Order").Schema().Properties.GetOrZero("status").Schema().Example.Decode(&example) + assert.Equal(t, "I am a teapot, filled with love.", example) assert.Equal(t, "https://pb33f.io", h.Components.SecuritySchemes.GetOrZero("petstore_auth").Flows.Implicit.AuthorizationUrl) @@ -425,7 +535,7 @@ func TestDocument_Serialize_JSON_Modified(t *testing.T) { newTitle := v3Doc.Model.Info.GoLow().Title.Mutate("The magic API - but now, altered!") v3Doc.Model.Info.GoLow().Title = newTitle - assert.Equal(t, "The magic API - but now, altered!", v3Doc.Model.Info.GoLow().Title.Value) + assert.Equal(t, "The magic API - but now, altered!", v3Doc.Model.Info.GoLow().Title.GetValue()) serial, err := doc.Serialize() assert.NoError(t, err) @@ -461,7 +571,7 @@ paths: // print it out. fmt.Printf("param1: %s, is reference? %t, original reference %s", operation.Parameters[0].Description, operation.GoLow().Parameters.Value[0].IsReference(), - operation.GoLow().Parameters.Value[0].Reference) + operation.GoLow().Parameters.Value[0].GetReference()) } func TestDocument_BuildModel_CompareDocsV3_LeftError(t *testing.T) { @@ -1056,14 +1166,14 @@ func TestDocument_Render_PreserveOrder(t *testing.T) { example := &base.Example{ Summary: fmt.Sprintf("Summary example %d", i), Description: "Description example", - Value: testExampleDetails{ + Value: utils.CreateYamlNode(testExampleDetails{ Message: "Foobar message", Domain: testExampleDomain{ ID: "12345", Name: "example.com", Type: "Foobar type", }, - }, + }), } exampleName := fmt.Sprintf("FoobarExample%d", i) mediaTypeResp.Examples.Set(exampleName, example) diff --git a/orderedmap/builder.go b/orderedmap/builder.go new file mode 100644 index 0000000..92a4348 --- /dev/null +++ b/orderedmap/builder.go @@ -0,0 +1,130 @@ +package orderedmap + +import ( + "fmt" + "strings" + + "github.com/pb33f/libopenapi/datamodel/high/nodes" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" +) + +type Marshaler interface { + MarshalYAML() (interface{}, error) +} + +type NodeBuilder interface { + AddYAMLNode(parent *yaml.Node, entry *nodes.NodeEntry) *yaml.Node +} + +type MapToYamlNoder interface { + ToYamlNode(n NodeBuilder, l any) *yaml.Node +} + +type HasKeyNode interface { + GetKeyNode() *yaml.Node +} + +type HasValueNode interface { + GetValueNode() *yaml.Node +} + +type HasValueUntyped interface { + GetValueUntyped() any +} + +type FindValueUntyped interface { + FindValueUntyped(k string) any +} + +func (o *Map[K, V]) ToYamlNode(n NodeBuilder, l any) *yaml.Node { + p := utils.CreateEmptyMapNode() + + var vn *yaml.Node + + i := 99999 + if l != nil { + if hvn, ok := l.(HasValueNode); ok { + vn = hvn.GetValueNode() + if vn != nil && len(vn.Content) > 0 { + i = vn.Content[0].Line + } + } + } + + for pair := o.First(); pair != nil; pair = pair.Next() { + var k any = pair.Key() + if m, ok := k.(Marshaler); ok { // TODO marshal inline? + k, _ = m.MarshalYAML() + } + + var y any + y, ok := k.(yaml.Node) + if !ok { + y, ok = k.(*yaml.Node) + } + if ok { + b, _ := yaml.Marshal(y) + k = strings.TrimSpace(string(b)) + } + + ks := k.(string) + + var keyStyle yaml.Style + keyNode := findKeyNode(ks, vn) + if keyNode != nil { + keyStyle = keyNode.Style + } + + var lv any + if l != nil { + if hvut, ok := l.(HasValueUntyped); ok { + vut := hvut.GetValueUntyped() + if m, ok := vut.(FindValueUntyped); ok { + lv = m.FindValueUntyped(ks) + } + } + } + + n.AddYAMLNode(p, &nodes.NodeEntry{ + Tag: ks, + Key: ks, + Line: i, + Value: pair.Value(), + KeyStyle: keyStyle, + LowValue: lv, + }) + i++ + } + + return p +} + +func findKeyNode(key string, m *yaml.Node) *yaml.Node { + if m == nil { + return nil + } + + for i := 0; i < len(m.Content); i += 2 { + if m.Content[i].Value == key { + return m.Content[i] + } + } + return nil +} + +func (o *Map[K, V]) FindValueUntyped(key string) any { + for pair := o.First(); pair != nil; pair = pair.Next() { + var k any = pair.Key() + if hvut, ok := k.(HasValueUntyped); ok { + if fmt.Sprintf("%v", hvut.GetValueUntyped()) == key { + return pair.Value() + } + } + if fmt.Sprintf("%v", k) == key { + return pair.Value() + } + } + + return nil +} diff --git a/orderedmap/orderedmap.go b/orderedmap/orderedmap.go index d50c613..dd17724 100644 --- a/orderedmap/orderedmap.go +++ b/orderedmap/orderedmap.go @@ -6,26 +6,14 @@ package orderedmap import ( "context" - "io" - "runtime" - "sync" + "fmt" + "reflect" + "slices" + "strings" wk8orderedmap "github.com/wk8/go-ordered-map/v2" ) -type Map[K comparable, V any] interface { - Lengthiness - Get(K) (V, bool) - GetOrZero(K) V - Set(K, V) (V, bool) - Delete(K) (V, bool) - First() Pair[K, V] -} - -type Lengthiness interface { - Len() int -} - type Pair[K comparable, V any] interface { Key() K KeyPtr() *K @@ -34,7 +22,7 @@ type Pair[K comparable, V any] interface { Next() Pair[K, V] } -type wrapOrderedMap[K comparable, V any] struct { +type Map[K comparable, V any] struct { *wk8orderedmap.OrderedMap[K, V] } @@ -42,20 +30,22 @@ type wrapPair[K comparable, V any] struct { *wk8orderedmap.Pair[K, V] } -type ( - ActionFunc[K comparable, V any] func(Pair[K, V]) error - TranslateFunc[IN any, OUT any] func(IN) (OUT, error) - ResultFunc[V any] func(V) error -) - // New creates an ordered map generic object. -func New[K comparable, V any]() Map[K, V] { - return &wrapOrderedMap[K, V]{ +func New[K comparable, V any]() *Map[K, V] { + return &Map[K, V]{ OrderedMap: wk8orderedmap.New[K, V](), } } -func (o *wrapOrderedMap[K, V]) GetOrZero(k K) V { +func (o *Map[K, V]) GetKeyType() reflect.Type { + return reflect.TypeOf(new(K)) +} + +func (o *Map[K, V]) GetValueType() reflect.Type { + return reflect.TypeOf(new(V)) +} + +func (o *Map[K, V]) GetOrZero(k K) V { v, ok := o.OrderedMap.Get(k) if !ok { var zero V @@ -64,7 +54,7 @@ func (o *wrapOrderedMap[K, V]) GetOrZero(k K) V { return v } -func (o *wrapOrderedMap[K, V]) First() Pair[K, V] { +func (o *Map[K, V]) First() Pair[K, V] { if o == nil { return nil } @@ -90,7 +80,7 @@ func NewPair[K comparable, V any](key K, value V) Pair[K, V] { // FromPairs creates an `OrderedMap` from an array of pairs. // Use `NewPair()` to generate input parameters. -func FromPairs[K comparable, V any](pairs ...Pair[K, V]) Map[K, V] { +func FromPairs[K comparable, V any](pairs ...Pair[K, V]) *Map[K, V] { om := New[K, V]() for _, pair := range pairs { om.Set(pair.Key(), pair.Value()) @@ -99,8 +89,8 @@ func FromPairs[K comparable, V any](pairs ...Pair[K, V]) Map[K, V] { } // IsZero is required to support `omitempty` tag for YAML/JSON marshaling. -func (o *wrapOrderedMap[K, V]) IsZero() bool { - return o.Len() == 0 +func (o *Map[K, V]) IsZero() bool { + return Len(o) == 0 } func (p *wrapPair[K, V]) Next() Pair[K, V] { @@ -131,19 +121,18 @@ func (p *wrapPair[K, V]) ValuePtr() *V { // Len returns the length of a container implementing a `Len()` method. // Safely returns zero on nil pointer. -func Len(l Lengthiness) int { - if l == nil { +func Len[K comparable, V any](m *Map[K, V]) int { + if m == nil { return 0 } - return l.Len() + return m.Len() } -// ToOrderedMap converts map built-in to OrderedMap. // Iterate the map in order. // Safely handles nil pointer. // Be sure to iterate to end or cancel the context when done to release // resources. -func Iterate[K comparable, V any](ctx context.Context, m Map[K, V]) <-chan Pair[K, V] { +func Iterate[K comparable, V any](ctx context.Context, m *Map[K, V]) <-chan Pair[K, V] { c := make(chan Pair[K, V]) if Len(m) == 0 { close(c) @@ -163,7 +152,7 @@ func Iterate[K comparable, V any](ctx context.Context, m Map[K, V]) <-chan Pair[ } // ToOrderedMap converts a `map` to `OrderedMap`. -func ToOrderedMap[K comparable, V any](m map[K]V) Map[K, V] { +func ToOrderedMap[K comparable, V any](m map[K]V) *Map[K, V] { om := New[K, V]() for k, v := range m { om.Set(k, v) @@ -173,7 +162,7 @@ func ToOrderedMap[K comparable, V any](m map[K]V) Map[K, V] { // First returns map's first pair for iteration. // Safely handles nil pointer. -func First[K comparable, V any](m Map[K, V]) Pair[K, V] { +func First[K comparable, V any](m *Map[K, V]) Pair[K, V] { if m == nil { return nil } @@ -181,12 +170,12 @@ func First[K comparable, V any](m Map[K, V]) Pair[K, V] { } // Cast converts `any` to `Map`. -func Cast[K comparable, V any](v any) Map[K, V] { +func Cast[K comparable, V any](v any) *Map[K, V] { if v == nil { return nil } - m, ok := v.(*wrapOrderedMap[K, V]) + m, ok := v.(*Map[K, V]) if !ok { return nil } @@ -194,90 +183,33 @@ func Cast[K comparable, V any](v any) Map[K, V] { return m } -type jobStatus[T any] struct { - done chan struct{} - result T -} - -// TranslateMapParallel iterates a `Map` in parallel and calls translate() -// asynchronously. -// translate() or result() may return `io.EOF` to break iteration. -// Safely handles nil pointer. -// Results are provided sequentially to result() in stable order from `Map`. -func TranslateMapParallel[K comparable, V any, RV any](m Map[K, V], translate TranslateFunc[Pair[K, V], RV], result ResultFunc[RV]) error { +func SortAlpha[K comparable, V any](m *Map[K, V]) *Map[K, V] { if m == nil { return nil } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - concurrency := runtime.NumCPU() - c := Iterate(ctx, m) - jobChan := make(chan *jobStatus[RV], concurrency) - var reterr error - var wg sync.WaitGroup - var mu sync.Mutex + om := New[K, V]() - // Fan out translate jobs. - wg.Add(1) - go func() { - defer func() { - close(jobChan) - wg.Done() - }() - for pair := range c { - j := &jobStatus[RV]{ - done: make(chan struct{}), - } - select { - case jobChan <- j: - case <-ctx.Done(): - return - } - - wg.Add(1) - go func(pair Pair[K, V]) { - value, err := translate(pair) - if err != nil { - mu.Lock() - defer func() { - mu.Unlock() - wg.Done() - cancel() - }() - if reterr == nil { - reterr = err - } - return - } - j.result = value - close(j.done) - wg.Done() - }(pair) - } - }() - - // Iterate jobChan as jobs complete. - defer wg.Wait() -JOBLOOP: - for j := range jobChan { - select { - case <-j.done: - err := result(j.result) - if err != nil { - cancel() - if err == io.EOF { - return nil - } - return err - } - case <-ctx.Done(): - break JOBLOOP - } + type key struct { + key string + k K } - if reterr == io.EOF { - return nil + keys := []key{} + for pair := m.First(); pair != nil; pair = pair.Next() { + keys = append(keys, key{ + key: fmt.Sprintf("%v", pair.Key()), + k: pair.Key(), + }) } - return reterr + + slices.SortFunc(keys, func(a, b key) int { + return strings.Compare(a.key, b.key) + }) + + for _, k := range keys { + om.Set(k.k, m.GetOrZero(k.k)) + } + + return om } diff --git a/orderedmap/orderedmap_test.go b/orderedmap/orderedmap_test.go index d79deab..139a23e 100644 --- a/orderedmap/orderedmap_test.go +++ b/orderedmap/orderedmap_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -90,7 +91,7 @@ func TestMap(t *testing.T) { assert.Equal(t, mapSize, orderedmap.Len(m)) t.Run("Nil pointer", func(t *testing.T) { - var m orderedmap.Map[string, int] + var m *orderedmap.Map[string, int] assert.Zero(t, orderedmap.Len(m)) }) }) @@ -180,7 +181,7 @@ func TestMap(t *testing.T) { resultCounter++ return nil } - err := orderedmap.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) + err := datamodel.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) require.NoError(t, err) assert.Equal(t, int64(mapSize), translateCounter) assert.Equal(t, mapSize, resultCounter) @@ -200,7 +201,7 @@ func TestMap(t *testing.T) { resultCounter++ return nil } - err := orderedmap.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) + err := datamodel.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) require.ErrorContains(t, err, "Foobar") assert.Zero(t, resultCounter) }) @@ -219,7 +220,7 @@ func TestMap(t *testing.T) { resultCounter++ return errors.New("Foobar") } - err := orderedmap.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) + err := datamodel.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) require.ErrorContains(t, err, "Foobar") assert.Equal(t, 1, resultCounter) }) @@ -238,7 +239,7 @@ func TestMap(t *testing.T) { resultCounter++ return nil } - err := orderedmap.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) + err := datamodel.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) require.NoError(t, err) assert.Zero(t, resultCounter) }) @@ -257,7 +258,7 @@ func TestMap(t *testing.T) { resultCounter++ return io.EOF } - err := orderedmap.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) + err := datamodel.TranslateMapParallel[string, int, string](m, translateFunc, resultFunc) require.NoError(t, err) assert.Equal(t, 1, resultCounter) }) @@ -298,7 +299,8 @@ func TestFirst(t *testing.T) { func TestLen(t *testing.T) { t.Run("Nil", func(t *testing.T) { - require.Zero(t, orderedmap.Len(nil)) + m := (*orderedmap.Map[string, int])(nil) + require.Zero(t, orderedmap.Len(m)) }) t.Run("Single item", func(t *testing.T) { diff --git a/renderer/mock_generator.go b/renderer/mock_generator.go index db3e705..3155df2 100644 --- a/renderer/mock_generator.go +++ b/renderer/mock_generator.go @@ -13,9 +13,11 @@ import ( "gopkg.in/yaml.v3" ) -const Example = "Example" -const Examples = "Examples" -const Schema = "Schema" +const ( + Example = "Example" + Examples = "Examples" + Schema = "Schema" +) type MockType int @@ -27,7 +29,7 @@ const ( // MockGenerator is used to generate mocks for high-level mockable structs or *base.Schema pointers. // The mock generator will attempt to generate a mock from a struct using the following fields: // - Example: any type, this is the default example to use if no examples are present. -// - Examples: orderedmap.Map[string, *base.Example], this is a map of examples keyed by name. +// - Examples: *orderedmap.Map[string, *base.Example], this is a map of examples keyed by name. // - Schema: *base.SchemaProxy, this is the schema to use if no examples are present. // // The mock generator will attempt to generate a mock from a *base.Schema pointer. @@ -61,7 +63,7 @@ func (mg *MockGenerator) SetPretty() { // GenerateMock generates a mock for a given high-level mockable struct. The mockable struct must contain the following fields: // Example: any type, this is the default example to use if no examples are present. -// Examples: orderedmap.Map[string, *base.Example], this is a map of examples keyed by name. +// Examples: *orderedmap.Map[string, *base.Example], this is a map of examples keyed by name. // Schema: *base.SchemaProxy, this is the schema to use if no examples are present. // The name parameter is optional, if provided, the mock generator will attempt to find an example with the given name. // If no name is provided, the first example will be used. @@ -89,10 +91,21 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) { } // if the value has an example, try and render it out as is. - exampleValue := v.FieldByName(Example).Interface() - if exampleValue != nil { - // try and serialize the example value - return mg.renderMock(exampleValue), nil + f := v.FieldByName(Example) + if !f.IsNil() { + // Pointer/Interface Shenanigans + ex := f.Interface() + if y, ok := ex.(*yaml.Node); ok { + if y != nil { + ex = y + } else { + ex = nil + } + } + if ex != nil { + // try and serialize the example value + return mg.renderMock(ex), nil + } } // if there is no example, but there are multi-examples. @@ -100,8 +113,8 @@ func (mg *MockGenerator) GenerateMock(mock any, name string) ([]byte, error) { examplesValue := examples.Interface() if examplesValue != nil && !examples.IsNil() { - // cast examples to orderedmap.Map[string, any] - examplesMap := examplesValue.(orderedmap.Map[string, *highbase.Example]) + // cast examples to *orderedmap.Map[string, *highbase.Example] + examplesMap := examplesValue.(*orderedmap.Map[string, *highbase.Example]) // if the name is not empty, try and find the example by name for pair := orderedmap.First(examplesMap); pair != nil; pair = pair.Next() { @@ -157,6 +170,11 @@ func (mg *MockGenerator) renderMock(v any) []byte { func (mg *MockGenerator) renderMockJSON(v any) []byte { var data []byte + + if y, ok := v.(*yaml.Node); ok { + _ = y.Decode(&v) + } + // determine the type, render properly. switch reflect.ValueOf(v).Kind() { case reflect.Map, reflect.Slice, reflect.Array, reflect.Struct, reflect.Ptr: diff --git a/renderer/mock_generator_test.go b/renderer/mock_generator_test.go index 2d79757..0e99fe9 100644 --- a/renderer/mock_generator_test.go +++ b/renderer/mock_generator_test.go @@ -13,6 +13,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" lowbase "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -20,13 +21,13 @@ import ( type fakeMockable struct { Schema *highbase.SchemaProxy Example any - Examples orderedmap.Map[string, *highbase.Example] + Examples *orderedmap.Map[string, *highbase.Example] } type fakeMockableButWithASchemaNotAProxy struct { Schema *highbase.Schema Example any - Examples orderedmap.Map[string, *highbase.Example] + Examples *orderedmap.Map[string, *highbase.Example] } var simpleFakeMockSchema = `type: string @@ -55,7 +56,7 @@ func createFakeMock(mock string, values map[string]any, example any) *fakeMockab for k, v := range values { examples.Set(k, &highbase.Example{ - Value: v, + Value: utils.CreateYamlNode(v), }) } return &fakeMockable{ @@ -78,7 +79,7 @@ func createFakeMockWithoutProxy(mock string, values map[string]any, example any) for k, v := range values { examples.Set(k, &highbase.Example{ - Value: v, + Value: utils.CreateYamlNode(v), }) } return &fakeMockableButWithASchemaNotAProxy{ @@ -110,7 +111,6 @@ func TestMockGenerator_GenerateJSONMock_BadObject(t *testing.T) { } func TestMockGenerator_GenerateJSONMock_EmptyObject(t *testing.T) { - mg := NewMockGenerator(JSON) mock, err := mg.GenerateMock(&fakeMockable{}, "") assert.NoError(t, err) @@ -118,7 +118,6 @@ func TestMockGenerator_GenerateJSONMock_EmptyObject(t *testing.T) { } func TestMockGenerator_GenerateJSONMock_SuppliedExample_JSON(t *testing.T) { - fakeExample := map[string]any{ "fish-and-chips": "cod-and-chips-twice", } @@ -130,7 +129,6 @@ func TestMockGenerator_GenerateJSONMock_SuppliedExample_JSON(t *testing.T) { } func TestMockGenerator_GenerateJSONMock_SuppliedExample_YAML(t *testing.T) { - fakeExample := map[string]any{ "fish-and-chips": "cod-and-chips-twice", } @@ -208,7 +206,6 @@ func TestMockGenerator_GenerateJSONMock_MultiExamples_YAML(t *testing.T) { } func TestMockGenerator_GenerateJSONMock_NoExamples_JSON(t *testing.T) { - fake := createFakeMock(simpleFakeMockSchema, nil, nil) mg := NewMockGenerator(JSON) mock, err := mg.GenerateMock(fake, "") @@ -217,16 +214,14 @@ func TestMockGenerator_GenerateJSONMock_NoExamples_JSON(t *testing.T) { } func TestMockGenerator_GenerateJSONMock_NoExamples_YAML(t *testing.T) { - fake := createFakeMock(simpleFakeMockSchema, nil, nil) mg := NewMockGenerator(YAML) mock, err := mg.GenerateMock(fake, "") assert.NoError(t, err) - assert.Equal(t, "magic-herbs", string(mock)) + assert.Equal(t, "magic-herbs", strings.TrimSpace(string(mock))) } func TestMockGenerator_GenerateJSONMock_Object_NoExamples_JSON(t *testing.T) { - fake := createFakeMock(objectFakeMockSchema, nil, nil) mg := NewMockGenerator(JSON) mock, err := mg.GenerateMock(fake, "") @@ -244,7 +239,6 @@ func TestMockGenerator_GenerateJSONMock_Object_NoExamples_JSON(t *testing.T) { } func TestMockGenerator_GenerateJSONMock_Object_NoExamples_YAML(t *testing.T) { - fake := createFakeMock(objectFakeMockSchema, nil, nil) mg := NewMockGenerator(YAML) mock, err := mg.GenerateMock(fake, "") @@ -263,7 +257,6 @@ func TestMockGenerator_GenerateJSONMock_Object_NoExamples_YAML(t *testing.T) { // should result in the exact same output as the above test func TestMockGenerator_GenerateJSONMock_Object_RawSchema(t *testing.T) { - fake := createFakeMockWithoutProxy(objectFakeMockSchema, nil, nil) mg := NewMockGenerator(YAML) diff --git a/renderer/schema_renderer.go b/renderer/schema_renderer.go index 1747dfe..5f801e2 100644 --- a/renderer/schema_renderer.go +++ b/renderer/schema_renderer.go @@ -19,33 +19,35 @@ import ( "golang.org/x/exp/slices" ) -const rootType = "rootType" -const stringType = "string" -const numberType = "number" -const integerType = "integer" -const booleanType = "boolean" -const objectType = "object" -const arrayType = "array" -const int32Type = "int32" -const floatType = "float" -const doubleType = "double" -const byteType = "byte" -const binaryType = "binary" -const passwordType = "password" -const dateType = "date" -const dateTimeType = "date-time" -const timeType = "time" -const emailType = "email" -const hostnameType = "hostname" -const ipv4Type = "ipv4" -const ipv6Type = "ipv6" -const uriType = "uri" -const uriReferenceType = "uri-reference" -const uuidType = "uuid" -const allOfType = "allOf" -const anyOfType = "anyOf" -const oneOfType = "oneOf" -const itemsType = "items" +const ( + rootType = "rootType" + stringType = "string" + numberType = "number" + integerType = "integer" + booleanType = "boolean" + objectType = "object" + arrayType = "array" + int32Type = "int32" + floatType = "float" + doubleType = "double" + byteType = "byte" + binaryType = "binary" + passwordType = "password" + dateType = "date" + dateTimeType = "date-time" + timeType = "time" + emailType = "email" + hostnameType = "hostname" + ipv4Type = "ipv4" + ipv6Type = "ipv6" + uriType = "uri" + uriReferenceType = "uri-reference" + uuidType = "uuid" + allOfType = "allOf" + anyOfType = "anyOf" + oneOfType = "oneOf" + itemsType = "items" +) // used to generate random words if there is no dictionary applied. const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -97,10 +99,12 @@ func (wr *SchemaRenderer) DisableRequiredCheck() { // DiveIntoSchema will dive into a schema and inject values from examples into a map. If there are no examples in // the schema, then the renderer will attempt to generate a value based on the schema type, format and pattern. func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, structure map[string]any, depth int) { - // got an example? use it, we're done here. if schema.Example != nil { - structure[key] = schema.Example + var example any + _ = schema.Example.Decode(&example) + + structure[key] = example return } @@ -114,7 +118,12 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct if slices.Contains(schema.Type, stringType) { // check for an enum, if there is one, then pick a random value from it. if schema.Enum != nil && len(schema.Enum) > 0 { - structure[key] = schema.Enum[rand.Int()%len(schema.Enum)] + enum := schema.Enum[rand.Int()%len(schema.Enum)] + + var example any + _ = enum.Decode(&example) + + structure[key] = example } else { // generate a random value based on the schema format, pattern and length values. @@ -187,7 +196,12 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct if slices.Contains(schema.Type, numberType) || slices.Contains(schema.Type, integerType) { if schema.Enum != nil && len(schema.Enum) > 0 { - structure[key] = schema.Enum[rand.Int()%len(schema.Enum)] + enum := schema.Enum[rand.Int()%len(schema.Enum)] + + var example any + _ = enum.Decode(&example) + + structure[key] = example } else { var minimum int64 = 1 @@ -303,7 +317,6 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct // an array needs an items schema itemsSchema := schema.Items if itemsSchema != nil { - // otherwise the items value is a schema, so we need to dive into it if itemsSchema.IsA() { @@ -326,7 +339,6 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct } } } - } func readFile(file io.Reader) []string { @@ -350,7 +362,6 @@ func ReadDictionary(dictionaryLocation string) []string { // to prevent a stack overflow, the maximum depth is 100 (anything more than this is probably a bug). // set the values to 0 to return the first word returned, essentially ignore the min and max values. func (wr *SchemaRenderer) RandomWord(min, max int64, depth int) string { - // break out if we've gone too deep if depth > 100 { return fmt.Sprintf("no-word-found-%d-%d", min, max) diff --git a/renderer/schema_renderer_test.go b/renderer/schema_renderer_test.go index da37cbc..11a6a8d 100644 --- a/renderer/schema_renderer_test.go +++ b/renderer/schema_renderer_test.go @@ -38,7 +38,6 @@ properties: } func createSchemaRenderer() *SchemaRenderer { - osDict := "/usr/share/dict/words" if _, err := os.Stat(osDict); err != nil { osDict = "" @@ -68,7 +67,6 @@ func getSchema(schema []byte) *highbase.Schema { } func TestRenderExample_StringWithExample(t *testing.T) { - testObject := `type: string example: dog` @@ -79,11 +77,9 @@ example: dog` wr.DiveIntoSchema(compiled, "pb33f", journeyMap, 0) assert.Equal(t, journeyMap["pb33f"], "dog") - } func TestRenderExample_StringWithNoExample(t *testing.T) { - testObject := `type: string` compiled := getSchema([]byte(testObject)) @@ -95,11 +91,9 @@ func TestRenderExample_StringWithNoExample(t *testing.T) { assert.NotNil(t, journeyMap["pb33f"]) assert.GreaterOrEqual(t, len(journeyMap["pb33f"].(string)), 3) assert.LessOrEqual(t, len(journeyMap["pb33f"].(string)), 10) - } func TestRenderExample_StringWithNoExample_Format_Datetime(t *testing.T) { - testObject := `type: string format: date-time` @@ -115,7 +109,6 @@ format: date-time` } func TestRenderExample_StringWithNoExample_Pattern_Email(t *testing.T) { - testObject := `type: string pattern: "^[a-z]{5,10}@[a-z]{5,10}\\.(com|net|org)$"` // an email address @@ -134,7 +127,6 @@ pattern: "^[a-z]{5,10}@[a-z]{5,10}\\.(com|net|org)$"` // an email address } func TestRenderExample_StringWithNoExample_Pattern_PhoneNumber(t *testing.T) { - testObject := `type: string pattern: "^\\([0-9]{3}\\)-[0-9]{3}-[0-9]{4}$"` // a phone number @@ -152,7 +144,6 @@ pattern: "^\\([0-9]{3}\\)-[0-9]{3}-[0-9]{4}$"` // a phone number } func TestRenderExample_StringWithNoExample_Format_Date(t *testing.T) { - testObject := `type: string format: date` @@ -390,7 +381,6 @@ minLength: 3` assert.NotNil(t, journeyMap["pb33f"]) assert.LessOrEqual(t, len(journeyMap["pb33f"].(string)), 8) assert.GreaterOrEqual(t, len(journeyMap["pb33f"].(string)), 3) - } func TestRenderExample_NumberNoExample_Default(t *testing.T) { @@ -771,7 +761,6 @@ properties: assert.Equal(t, journeyMap["pb33f"].(map[string]interface{})["price"].(float64), 19.99) assert.Equal(t, journeyMap["pb33f"].(map[string]interface{})["category"].(string), "shirts") assert.Equal(t, journeyMap["pb33f"].(map[string]interface{})["image"].(string), "https://pb33f.io/images/t-shirt.png") - } func TestRenderExample_TestGiftshopProduct_UsingTopLevelExample(t *testing.T) { @@ -839,7 +828,6 @@ example: assert.Equal(t, journeyMap["pb33f"].(map[string]interface{})["category"].(string), "not-a-category") assert.Equal(t, journeyMap["pb33f"].(map[string]interface{})["image"].(string), "not-an-image") - } func TestRenderExample_TestGiftshopProduct_UsingNoExamples(t *testing.T) { @@ -889,7 +877,6 @@ properties: assert.NotEmpty(t, journeyMap["pb33f"].(map[string]interface{})["price"].(float32)) assert.NotEmpty(t, journeyMap["pb33f"].(map[string]interface{})["category"].(string)) assert.NotEmpty(t, journeyMap["pb33f"].(map[string]interface{})["image"].(string)) - } func TestRenderExample_Test_MultiPolymorphic(t *testing.T) { @@ -931,7 +918,7 @@ properties: burger := journeyMap["pb33f"].(map[string]interface{})["burger"].(map[string]interface{}) assert.NotNil(t, burger) assert.NotEmpty(t, burger["name"].(string)) - assert.NotZero(t, burger["weight"].(int64)) + assert.NotZero(t, burger["weight"].(int)) assert.NotEmpty(t, burger["patty"].(string)) assert.True(t, burger["frozen"].(bool)) } @@ -1083,7 +1070,6 @@ func (errReader) Read(p []byte) (n int, err error) { } func TestReadDictionary_BadReader(t *testing.T) { - words := readFile(errReader(0)) assert.LessOrEqual(t, len(words), 0) } @@ -1113,14 +1099,13 @@ func TestWordRenderer_RandomWordMinMaxZero(t *testing.T) { } func TestRenderSchema_NestedDeep(t *testing.T) { - deepNest := createNestedStructure() journeyMap := make(map[string]any) wr := createSchemaRenderer() wr.DiveIntoSchema(deepNest.Schema(), "pb33f", journeyMap, 0) assert.NotNil(t, journeyMap["pb33f"]) - var journeyLevel = 0 + journeyLevel := 0 var dive func(mapNode map[string]any, level int) // count the levels to validate the recursion hard limit. dive = func(mapNode map[string]any, level int) { @@ -1144,7 +1129,6 @@ func TestCreateRendererUsingDictionary(t *testing.T) { } func createNestedStructure() *highbase.SchemaProxy { - schema := `type: [object] properties: name: diff --git a/test_specs/burgershop.openapi.yaml b/test_specs/burgershop.openapi.yaml index f12d7d6..1a56d95 100644 --- a/test_specs/burgershop.openapi.yaml +++ b/test_specs/burgershop.openapi.yaml @@ -532,7 +532,7 @@ components: not: type: string items: - - $ref: '#/components/schemas/Drink' + $ref: '#/components/schemas/Drink' x-screaming-baby: loud x-something-something: darkside externalDocs: diff --git a/utils/nodes.go b/utils/nodes.go index d8bc1b6..07a2d81 100644 --- a/utils/nodes.go +++ b/utils/nodes.go @@ -3,7 +3,9 @@ package utils -import "gopkg.in/yaml.v3" +import ( + "gopkg.in/yaml.v3" +) func CreateRefNode(ref string) *yaml.Node { m := CreateEmptyMapNode() @@ -23,6 +25,13 @@ func CreateEmptyMapNode() *yaml.Node { return n } +func CreateYamlNode(a any) *yaml.Node { + var n yaml.Node + _ = n.Encode(a) + + return &n +} + func CreateEmptySequenceNode() *yaml.Node { n := &yaml.Node{ Kind: yaml.SequenceNode, diff --git a/what-changed/model/callback.go b/what-changed/model/callback.go index f018138..bcc630c 100644 --- a/what-changed/model/callback.go +++ b/what-changed/model/callback.go @@ -56,7 +56,6 @@ func (c *CallbackChanges) TotalBreakingChanges() int { // CompareCallback will compare two Callback objects and return a pointer to CallbackChanges with all the things // that have changed between them. func CompareCallback(l, r *v3.Callback) *CallbackChanges { - cc := new(CallbackChanges) var changes []*Change @@ -66,12 +65,12 @@ func CompareCallback(l, r *v3.Callback) *CallbackChanges { lValues := make(map[string]low.ValueReference[*v3.PathItem]) rValues := make(map[string]low.ValueReference[*v3.PathItem]) - for pair := orderedmap.First(l.Expression.Value); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(l.Expression); pair != nil; pair = pair.Next() { lHashes[pair.Key().Value] = low.GenerateHashString(pair.Value().Value) lValues[pair.Key().Value] = pair.Value() } - for pair := orderedmap.First(r.Expression.Value); pair != nil; pair = pair.Next() { + for pair := orderedmap.First(r.Expression); pair != nil; pair = pair.Next() { rHashes[pair.Key().Value] = low.GenerateHashString(pair.Value().Value) rValues[pair.Key().Value] = pair.Value() } @@ -94,7 +93,7 @@ func CompareCallback(l, r *v3.Callback) *CallbackChanges { expChanges[k] = ComparePathItems(lValues[k].Value, rValues[k].Value) } - //check right path item hashes + // check right path item hashes for k := range rHashes { lhash := lHashes[k] if lhash == "" { diff --git a/what-changed/model/comparison_functions.go b/what-changed/model/comparison_functions.go index a151c4f..62f2f79 100644 --- a/what-changed/model/comparison_functions.go +++ b/what-changed/model/comparison_functions.go @@ -82,7 +82,7 @@ func FlattenLowLevelMap[T any]( } func FlattenLowLevelOrderedMap[T any]( - lowMap orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], + lowMap *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], ) map[string]*low.ValueReference[T] { flat := make(map[string]*low.ValueReference[T]) @@ -252,13 +252,13 @@ func CheckForModification[T any](l, r *yaml.Node, label string, changes *[]*Chan // CheckMapForChanges checks a left and right low level map for any additions, subtractions or modifications to // values. The compareFunc argument should reference the correct comparison function for the generic type. -func CheckMapForChanges[T any, R any](expLeft, expRight orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], +func CheckMapForChanges[T any, R any](expLeft, expRight *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], changes *[]*Change, label string, compareFunc func(l, r T) R, ) map[string]R { return CheckMapForChangesWithComp(expLeft, expRight, changes, label, compareFunc, true) } -func CheckMapForAdditionRemoval[T any](expLeft, expRight orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], +func CheckMapForAdditionRemoval[T any](expLeft, expRight *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], changes *[]*Change, label string, ) any { // do nothing @@ -282,7 +282,7 @@ func CheckMapForAdditionRemoval[T any](expLeft, expRight orderedmap.Map[low.KeyR // CheckMapForChangesWithComp checks a left and right low level map for any additions, subtractions or modifications to // values. The compareFunc argument should reference the correct comparison function for the generic type. The compare // bit determines if the comparison should be run or not. -func CheckMapForChangesWithComp[T any, R any](expLeft, expRight orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], +func CheckMapForChangesWithComp[T any, R any](expLeft, expRight *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], changes *[]*Change, label string, compareFunc func(l, r T) R, compare bool, ) map[string]R { // stop concurrent threads screwing up changes. @@ -420,20 +420,28 @@ func ExtractStringValueSliceChanges(lParam, rParam []low.ValueReference[string], } } +func toString(v any) string { + if y, ok := v.(*yaml.Node); ok { + _ = y.Encode(&v) + } + + return fmt.Sprint(v) +} + // ExtractRawValueSliceChanges will compare two low level interface{} slices for changes. -func ExtractRawValueSliceChanges(lParam, rParam []low.ValueReference[any], +func ExtractRawValueSliceChanges[T any](lParam, rParam []low.ValueReference[T], changes *[]*Change, label string, breaking bool, ) { lKeys := make([]string, len(lParam)) rKeys := make([]string, len(rParam)) - lValues := make(map[string]low.ValueReference[any]) - rValues := make(map[string]low.ValueReference[any]) + lValues := make(map[string]low.ValueReference[T]) + rValues := make(map[string]low.ValueReference[T]) for i := range lParam { - lKeys[i] = strings.ToLower(fmt.Sprint(lParam[i].Value)) + lKeys[i] = strings.ToLower(toString(lParam[i].Value)) lValues[lKeys[i]] = lParam[i] } for i := range rParam { - rKeys[i] = strings.ToLower(fmt.Sprint(rParam[i].Value)) + rKeys[i] = strings.ToLower(toString(rParam[i].Value)) rValues[rKeys[i]] = rParam[i] } for i := range lValues { diff --git a/what-changed/model/components.go b/what-changed/model/components.go index 025b4a4..125138f 100644 --- a/what-changed/model/components.go +++ b/what-changed/model/components.go @@ -46,7 +46,6 @@ type ComponentsChanges struct { // CompareComponents will compare OpenAPI components for any changes. Accepts Swagger Definition objects // like ParameterDefinitions or Definitions etc. func CompareComponents(l, r any) *ComponentsChanges { - var changes []*Change cc := new(ComponentsChanges) @@ -56,7 +55,7 @@ func CompareComponents(l, r any) *ComponentsChanges { reflect.TypeOf(&v2.ParameterDefinitions{}) == reflect.TypeOf(r) { lDef := l.(*v2.ParameterDefinitions) rDef := r.(*v2.ParameterDefinitions) - var a, b orderedmap.Map[low.KeyReference[string], low.ValueReference[*v2.Parameter]] + var a, b *orderedmap.Map[low.KeyReference[string], low.ValueReference[*v2.Parameter]] if lDef != nil { a = lDef.Definitions } @@ -71,7 +70,7 @@ func CompareComponents(l, r any) *ComponentsChanges { reflect.TypeOf(&v2.ResponsesDefinitions{}) == reflect.TypeOf(r) { lDef := l.(*v2.ResponsesDefinitions) rDef := r.(*v2.ResponsesDefinitions) - var a, b orderedmap.Map[low.KeyReference[string], low.ValueReference[*v2.Response]] + var a, b *orderedmap.Map[low.KeyReference[string], low.ValueReference[*v2.Response]] if lDef != nil { a = lDef.Definitions } @@ -86,7 +85,7 @@ func CompareComponents(l, r any) *ComponentsChanges { reflect.TypeOf(&v2.Definitions{}) == reflect.TypeOf(r) { lDef := l.(*v2.Definitions) rDef := r.(*v2.Definitions) - var a, b orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]] + var a, b *orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]] if lDef != nil { a = lDef.Schemas } @@ -101,7 +100,7 @@ func CompareComponents(l, r any) *ComponentsChanges { reflect.TypeOf(&v2.SecurityDefinitions{}) == reflect.TypeOf(r) { lDef := l.(*v2.SecurityDefinitions) rDef := r.(*v2.SecurityDefinitions) - var a, b orderedmap.Map[low.KeyReference[string], low.ValueReference[*v2.SecurityScheme]] + var a, b *orderedmap.Map[low.KeyReference[string], low.ValueReference[*v2.SecurityScheme]] if lDef != nil { a = lDef.Definitions } @@ -218,9 +217,9 @@ type componentComparison struct { } // run a generic comparison in a thread which in turn splits checks into further threads. -func runComparison[T any, R any](l, r orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], - changes *[]*Change, label string, compareFunc func(l, r T) R, doneChan chan componentComparison) { - +func runComparison[T any, R any](l, r *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]], + changes *[]*Change, label string, compareFunc func(l, r T) R, doneChan chan componentComparison, +) { // for schemas if label == v3.SchemasLabel || label == v2.DefinitionsLabel || label == v3.SecuritySchemesLabel { doneChan <- componentComparison{ diff --git a/what-changed/model/document.go b/what-changed/model/document.go index 3135b42..0d99f61 100644 --- a/what-changed/model/document.go +++ b/what-changed/model/document.go @@ -35,6 +35,10 @@ type DocumentChanges struct { // TotalChanges returns a total count of all changes made in the Document func (d *DocumentChanges) TotalChanges() int { + if d == nil { + return 0 + } + c := d.PropertyChanges.TotalChanges() if d.InfoChanges != nil { c += d.InfoChanges.TotalChanges() @@ -68,6 +72,10 @@ func (d *DocumentChanges) TotalChanges() int { // GetAllChanges returns a slice of all changes made between Document objects func (d *DocumentChanges) GetAllChanges() []*Change { + if d == nil { + return nil + } + var changes []*Change changes = append(changes, d.Changes...) if d.InfoChanges != nil { @@ -133,7 +141,6 @@ func (d *DocumentChanges) TotalBreakingChanges() int { // CompareDocuments will compare any two OpenAPI documents (either Swagger or OpenAPI) and return a pointer to // DocumentChanges that outlines everything that was found to have changed. func CompareDocuments(l, r any) *DocumentChanges { - var changes []*Change var props []*PropertyCheck diff --git a/what-changed/model/examples.go b/what-changed/model/examples.go index 0775308..cf28433 100644 --- a/what-changed/model/examples.go +++ b/what-changed/model/examples.go @@ -7,6 +7,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" v2 "github.com/pb33f/libopenapi/datamodel/low/v2" "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // ExamplesChanges represents changes made between Swagger Examples objects (Not OpenAPI 3). @@ -32,11 +33,10 @@ func (a *ExamplesChanges) TotalBreakingChanges() int { // CompareExamplesV2 compares two Swagger Examples objects, returning a pointer to // ExamplesChanges if anything was found. func CompareExamplesV2(l, r *v2.Examples) *ExamplesChanges { - lHashes := make(map[string]string) rHashes := make(map[string]string) - lValues := make(map[string]low.ValueReference[any]) - rValues := make(map[string]low.ValueReference[any]) + lValues := make(map[string]low.ValueReference[*yaml.Node]) + rValues := make(map[string]low.ValueReference[*yaml.Node]) for pair := orderedmap.First(l.Values); pair != nil; pair = pair.Next() { lHashes[pair.Key().Value] = low.GenerateHashString(pair.Value().Value) @@ -67,7 +67,7 @@ func CompareExamplesV2(l, r *v2.Examples) *ExamplesChanges { } - //check right example hashes + // check right example hashes for k := range rHashes { lhash := lHashes[k] if lhash == "" { diff --git a/what-changed/model/extensions.go b/what-changed/model/extensions.go index 3bb3bc9..2da201e 100644 --- a/what-changed/model/extensions.go +++ b/what-changed/model/extensions.go @@ -4,8 +4,11 @@ package model import ( - "github.com/pb33f/libopenapi/datamodel/low" "strings" + + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // ExtensionChanges represents any changes to custom extensions defined for an OpenAPI object. @@ -34,24 +37,24 @@ func (e *ExtensionChanges) TotalBreakingChanges() int { // // A current limitation relates to extensions being objects and a property of the object changes, // there is currently no support for knowing anything changed - so it is ignored. -func CompareExtensions(l, r map[low.KeyReference[string]]low.ValueReference[any]) *ExtensionChanges { - +func CompareExtensions(l, r *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]]) *ExtensionChanges { // look at the original and then look through the new. - seenLeft := make(map[string]*low.ValueReference[any]) - seenRight := make(map[string]*low.ValueReference[any]) - for i := range l { - h := l[i] - seenLeft[strings.ToLower(i.Value)] = &h + seenLeft := make(map[string]*low.ValueReference[*yaml.Node]) + seenRight := make(map[string]*low.ValueReference[*yaml.Node]) + + for pair := orderedmap.First(l); pair != nil; pair = pair.Next() { + h := pair.Value() + seenLeft[strings.ToLower(pair.Key().Value)] = &h } - for i := range r { - h := r[i] - seenRight[strings.ToLower(i.Value)] = &h + for pair := orderedmap.First(r); pair != nil; pair = pair.Next() { + h := pair.Value() + seenRight[strings.ToLower(pair.Key().Value)] = &h } var changes []*Change for i := range seenLeft { - CheckForObjectAdditionOrRemoval[any](seenLeft, seenRight, i, &changes, false, true) + CheckForObjectAdditionOrRemoval[*yaml.Node](seenLeft, seenRight, i, &changes, false, true) if seenRight[i] != nil { var props []*PropertyCheck @@ -72,7 +75,7 @@ func CompareExtensions(l, r map[low.KeyReference[string]]low.ValueReference[any] } for i := range seenRight { if seenLeft[i] == nil { - CheckForObjectAdditionOrRemoval[any](seenLeft, seenRight, i, &changes, false, true) + CheckForObjectAdditionOrRemoval[*yaml.Node](seenLeft, seenRight, i, &changes, false, true) } } ex := new(ExtensionChanges) @@ -86,11 +89,11 @@ func CompareExtensions(l, r map[low.KeyReference[string]]low.ValueReference[any] // CheckExtensions is a helper method to un-pack a left and right model that contains extensions. Once unpacked // the extensions are compared and returns a pointer to ExtensionChanges. If nothing changed, nil is returned. func CheckExtensions[T low.HasExtensions[T]](l, r T) *ExtensionChanges { - var lExt, rExt map[low.KeyReference[string]]low.ValueReference[any] - if len(l.GetExtensions()) > 0 { + var lExt, rExt *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] + if orderedmap.Len(l.GetExtensions()) > 0 { lExt = l.GetExtensions() } - if len(r.GetExtensions()) > 0 { + if orderedmap.Len(r.GetExtensions()) > 0 { rExt = r.GetExtensions() } return CompareExtensions(lExt, rExt) diff --git a/what-changed/model/parameter.go b/what-changed/model/parameter.go index c6ec78b..3382676 100644 --- a/what-changed/model/parameter.go +++ b/what-changed/model/parameter.go @@ -4,12 +4,14 @@ package model import ( + "reflect" + "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" v2 "github.com/pb33f/libopenapi/datamodel/low/v2" v3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/orderedmap" "gopkg.in/yaml.v3" - "reflect" ) // ParameterChanges represents changes found between Swagger or OpenAPI Parameter objects. @@ -222,14 +224,13 @@ func CompareParametersV3(l, r *v3.Parameter) *ParameterChanges { // CompareParameters compares a left and right Swagger or OpenAPI Parameter object for any changes. If found returns // a pointer to ParameterChanges. If nothing is found, returns nil. func CompareParameters(l, r any) *ParameterChanges { - var changes []*Change var props []*PropertyCheck pc := new(ParameterChanges) var lSchema *base.SchemaProxy var rSchema *base.SchemaProxy - var lext, rext map[low.KeyReference[string]]low.ValueReference[any] + var lext, rext *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] if reflect.TypeOf(&v2.Parameter{}) == reflect.TypeOf(l) && reflect.TypeOf(&v2.Parameter{}) == reflect.TypeOf(r) { lParam := l.(*v2.Parameter) @@ -331,7 +332,7 @@ func CompareParameters(l, r any) *ParameterChanges { return pc } -func checkParameterExample(expLeft, expRight low.NodeReference[any], changes []*Change) { +func checkParameterExample(expLeft, expRight low.NodeReference[*yaml.Node], changes []*Change) { if !expLeft.IsEmpty() && !expRight.IsEmpty() { if low.GenerateHashString(expLeft.GetValue()) != low.GenerateHashString(expRight.GetValue()) { CreateChange(&changes, Modified, v3.ExampleLabel, diff --git a/what-changed/model/paths.go b/what-changed/model/paths.go index 3324181..517fe29 100644 --- a/what-changed/model/paths.go +++ b/what-changed/model/paths.go @@ -11,6 +11,7 @@ import ( v2 "github.com/pb33f/libopenapi/datamodel/low/v2" v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) // PathsChanges represents changes found between two Swagger or OpenAPI Paths Objects. @@ -57,7 +58,6 @@ func (p *PathsChanges) TotalBreakingChanges() int { // ComparePaths compares a left and right Swagger or OpenAPI Paths Object for changes. If found, returns a pointer // to a PathsChanges instance. Returns nil if nothing is found. func ComparePaths(l, r any) *PathsChanges { - var changes []*Change pc := new(PathsChanges) @@ -206,7 +206,7 @@ func ComparePaths(l, r any) *PathsChanges { pc.PathItemsChanges = pathChanges } - var lExt, rExt map[low.KeyReference[string]]low.ValueReference[any] + var lExt, rExt *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] if lPath != nil { lExt = lPath.Extensions } diff --git a/what-changed/model/schema.go b/what-changed/model/schema.go index b78a234..f2661fd 100644 --- a/what-changed/model/schema.go +++ b/what-changed/model/schema.go @@ -142,6 +142,10 @@ func (s *SchemaChanges) GetAllChanges() []*Change { // TotalChanges returns a count of the total number of changes made to this schema and all sub-schemas func (s *SchemaChanges) TotalChanges() int { + if s == nil { + return 0 + } + t := s.PropertyChanges.TotalChanges() if s.DiscriminatorChanges != nil { t += s.DiscriminatorChanges.TotalChanges() @@ -224,6 +228,10 @@ func (s *SchemaChanges) TotalChanges() int { // TotalBreakingChanges returns the total number of breaking changes made to this schema and all sub-schemas. func (s *SchemaChanges) TotalBreakingChanges() int { + if s == nil { + return 0 + } + t := s.PropertyChanges.TotalBreakingChanges() if s.DiscriminatorChanges != nil { t += s.DiscriminatorChanges.TotalBreakingChanges() @@ -322,33 +330,33 @@ func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { if l != nil && r != nil { // if left proxy is a reference and right is a reference (we won't recurse into them) - if l.IsSchemaReference() && r.IsSchemaReference() { + if l.IsReference() && r.IsReference() { // points to the same schema - if l.GetSchemaReference() == r.GetSchemaReference() { + if l.GetReference() == r.GetReference() { // there is nothing to be done at this point. return nil } else { // references are different, that's all we care to know. CreateChange(&changes, Modified, v3.RefLabel, - l.GetValueNode().Content[1], r.GetValueNode().Content[1], true, l.GetSchemaReference(), - r.GetSchemaReference()) + l.GetValueNode().Content[1], r.GetValueNode().Content[1], true, l.GetReference(), + r.GetReference()) sc.PropertyChanges = NewPropertyChanges(changes) return sc } } // changed from inline to ref - if !l.IsSchemaReference() && r.IsSchemaReference() { + if !l.IsReference() && r.IsReference() { CreateChange(&changes, Modified, v3.RefLabel, - l.GetValueNode(), r.GetValueNode().Content[1], true, l, r.GetSchemaReference()) + l.GetValueNode(), r.GetValueNode().Content[1], true, l, r.GetReference()) sc.PropertyChanges = NewPropertyChanges(changes) return sc // we're done here } // changed from ref to inline - if l.IsSchemaReference() && !r.IsSchemaReference() { + if l.IsReference() && !r.IsReference() { CreateChange(&changes, Modified, v3.RefLabel, - l.GetValueNode().Content[1], r.GetValueNode(), true, l.GetSchemaReference(), r) + l.GetValueNode().Content[1], r.GetValueNode(), true, l.GetReference(), r) sc.PropertyChanges = NewPropertyChanges(changes) return sc // done, nothing else to do. } @@ -435,7 +443,7 @@ func checkSchemaXML(lSchema *base.Schema, rSchema *base.Schema, changes *[]*Chan func checkMappedSchemaOfASchema( lSchema, - rSchema orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]], + rSchema *orderedmap.Map[low.KeyReference[string], low.ValueReference[*base.SchemaProxy]], changes *[]*Change, doneChan chan bool, ) (map[string]*SchemaChanges, int) { @@ -909,10 +917,10 @@ func checkSchemaPropertyChanges( j = make(map[string]int) k = make(map[string]int) for i := range lSchema.Enum.Value { - j[fmt.Sprint(lSchema.Enum.Value[i].Value)] = i + j[toString(lSchema.Enum.Value[i].Value)] = i } for i := range rSchema.Enum.Value { - k[fmt.Sprint(rSchema.Enum.Value[i].Value)] = i + k[toString(rSchema.Enum.Value[i].Value)] = i } for g := range k { if _, ok := j[g]; !ok { diff --git a/what-changed/model/security_requirement.go b/what-changed/model/security_requirement.go index b6b98a6..774fdc5 100644 --- a/what-changed/model/security_requirement.go +++ b/what-changed/model/security_requirement.go @@ -34,7 +34,6 @@ func (s *SecurityRequirementChanges) TotalBreakingChanges() int { // CompareSecurityRequirement compares left and right SecurityRequirement objects for changes. If anything // is found, then a pointer to SecurityRequirementChanges is returned, otherwise nil. func CompareSecurityRequirement(l, r *base.SecurityRequirement) *SecurityRequirementChanges { - var changes []*Change sc := new(SecurityRequirementChanges) @@ -57,9 +56,9 @@ func addedSecurityRequirement(vn *yaml.Node, name string, changes *[]*Change) { } // tricky to do this correctly, this is my solution. -func checkSecurityRequirement(lSec, rSec orderedmap.Map[low.KeyReference[string], low.ValueReference[[]low.ValueReference[string]]], - changes *[]*Change) { - +func checkSecurityRequirement(lSec, rSec *orderedmap.Map[low.KeyReference[string], low.ValueReference[[]low.ValueReference[string]]], + changes *[]*Change, +) { lKeys := make([]string, orderedmap.Len(lSec)) rKeys := make([]string, orderedmap.Len(rSec)) lValues := make(map[string]low.ValueReference[[]low.ValueReference[string]])