diff --git a/datamodel/low/base/contact.go b/datamodel/low/base/contact.go index f3fe32b..1edec06 100644 --- a/datamodel/low/base/contact.go +++ b/datamodel/low/base/contact.go @@ -23,7 +23,7 @@ type Contact struct { } // Build is not implemented for Contact (there is nothing to build). -func (c *Contact) Build(root *yaml.Node, idx *index.SpecIndex) error { +func (c *Contact) Build(_ *yaml.Node, _ *index.SpecIndex) error { c.Reference = new(low.Reference) // not implemented. return nil diff --git a/datamodel/low/base/example.go b/datamodel/low/base/example.go index 341d8e9..ab71083 100644 --- a/datamodel/low/base/example.go +++ b/datamodel/low/base/example.go @@ -61,6 +61,8 @@ func (ex *Example) Hash() [32]byte { // Build extracts extensions and example value func (ex *Example) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) ex.Reference = new(low.Reference) ex.Extensions = low.ExtractExtensions(root) _, ln, vn := utils.FindKeyNodeFull(ValueLabel, root.Content) diff --git a/datamodel/low/base/example_test.go b/datamodel/low/base/example_test.go index 4e8b5c2..89cf521 100644 --- a/datamodel/low/base/example_test.go +++ b/datamodel/low/base/example_test.go @@ -20,7 +20,7 @@ x-cake: hot` var idxNode yaml.Node mErr := yaml.Unmarshal([]byte(yml), &idxNode) assert.NoError(t, mErr) - idx := index.NewSpecIndex(&idxNode) + idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateClosedAPIIndexConfig()) var n Example err := low.BuildModel(idxNode.Content[0], &n) @@ -46,7 +46,7 @@ x-cake: hot` var idxNode yaml.Node mErr := yaml.Unmarshal([]byte(yml), &idxNode) assert.NoError(t, mErr) - idx := index.NewSpecIndex(&idxNode) + idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateClosedAPIIndexConfig()) var n Example err := low.BuildModel(idxNode.Content[0], &n) @@ -73,7 +73,7 @@ value: var idxNode yaml.Node mErr := yaml.Unmarshal([]byte(yml), &idxNode) assert.NoError(t, mErr) - idx := index.NewSpecIndex(&idxNode) + idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateClosedAPIIndexConfig()) var n Example err := low.BuildModel(idxNode.Content[0], &n) @@ -104,7 +104,39 @@ value: var idxNode yaml.Node mErr := yaml.Unmarshal([]byte(yml), &idxNode) assert.NoError(t, mErr) - idx := index.NewSpecIndex(&idxNode) + idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateClosedAPIIndexConfig()) + + var n Example + err := low.BuildModel(idxNode.Content[0], &n) + assert.NoError(t, err) + + err = n.Build(idxNode.Content[0], idx) + assert.NoError(t, err) + 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.") + } +} + +func TestExample_Build_Success_MergeNode(t *testing.T) { + + yml := `x-things: &things + summary: hot + description: cakes + value: + - wow + - such array +<<: *things` + + var idxNode yaml.Node + mErr := yaml.Unmarshal([]byte(yml), &idxNode) + assert.NoError(t, mErr) + idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateClosedAPIIndexConfig()) var n Example err := low.BuildModel(idxNode.Content[0], &n) diff --git a/datamodel/low/base/external_doc.go b/datamodel/low/base/external_doc.go index 0b617af..5215b77 100644 --- a/datamodel/low/base/external_doc.go +++ b/datamodel/low/base/external_doc.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -33,6 +34,8 @@ func (ex *ExternalDoc) FindExtension(ext string) *low.ValueReference[any] { // Build will extract extensions from the ExternalDoc instance. func (ex *ExternalDoc) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) ex.Reference = new(low.Reference) ex.Extensions = low.ExtractExtensions(root) return nil diff --git a/datamodel/low/base/info.go b/datamodel/low/base/info.go index e7c126e..600d46d 100644 --- a/datamodel/low/base/info.go +++ b/datamodel/low/base/info.go @@ -6,6 +6,7 @@ package base import ( "crypto/sha256" "fmt" + "github.com/pb33f/libopenapi/utils" "sort" "strings" @@ -45,6 +46,8 @@ func (i *Info) GetExtensions() map[low.KeyReference[string]]low.ValueReference[a // Build will extract out the Contact and Info objects from the supplied root node. func (i *Info) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) i.Reference = new(low.Reference) i.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/base/license.go b/datamodel/low/base/license.go index 3153ad3..09db332 100644 --- a/datamodel/low/base/license.go +++ b/datamodel/low/base/license.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "strings" ) @@ -25,6 +26,8 @@ type License struct { // Build out a license, complain if both a URL and identifier are present as they are mutually exclusive func (l *License) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) l.Reference = new(low.Reference) if l.URL.Value != "" && l.Identifier.Value != "" { return fmt.Errorf("license cannot have both a URL and an identifier, they are mutually exclusive") diff --git a/datamodel/low/base/schema.go b/datamodel/low/base/schema.go index ea55d9d..ac2b71f 100644 --- a/datamodel/low/base/schema.go +++ b/datamodel/low/base/schema.go @@ -62,11 +62,11 @@ type Schema struct { // Reference to the '$schema' dialect setting (3.1 only) SchemaTypeRef low.NodeReference[string] - // In versions 2 and 3.0, this ExclusiveMaximum can only be a boolean. - ExclusiveMaximum low.NodeReference[*SchemaDynamicValue[bool, float64]] + // In versions 2 and 3.0, this ExclusiveMaximum can only be a boolean. + ExclusiveMaximum low.NodeReference[*SchemaDynamicValue[bool, float64]] - // In versions 2 and 3.0, this ExclusiveMinimum can only be a boolean. - ExclusiveMinimum low.NodeReference[*SchemaDynamicValue[bool, float64]] + // In versions 2 and 3.0, this ExclusiveMinimum can only be a boolean. + ExclusiveMinimum low.NodeReference[*SchemaDynamicValue[bool, float64]] // In versions 2 and 3.0, this Type is a single value, so array will only ever have one value // in version 3.1, Type can be multiple values @@ -103,37 +103,37 @@ type Schema struct { UnevaluatedProperties low.NodeReference[*SchemaDynamicValue[*SchemaProxy, *bool]] Anchor low.NodeReference[string] - // Compatible with all versions - Title low.NodeReference[string] - MultipleOf low.NodeReference[float64] - Maximum low.NodeReference[float64] - Minimum low.NodeReference[float64] - MaxLength low.NodeReference[int64] - MinLength low.NodeReference[int64] - Pattern low.NodeReference[string] - Format low.NodeReference[string] - MaxItems low.NodeReference[int64] - MinItems low.NodeReference[int64] - UniqueItems low.NodeReference[bool] - MaxProperties low.NodeReference[int64] - MinProperties low.NodeReference[int64] - Required low.NodeReference[[]low.ValueReference[string]] - Enum low.NodeReference[[]low.ValueReference[any]] - Not low.NodeReference[*SchemaProxy] - Properties low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*SchemaProxy]] - AdditionalProperties low.NodeReference[any] - Description low.NodeReference[string] - ContentEncoding low.NodeReference[string] - ContentMediaType low.NodeReference[string] - Default low.NodeReference[any] - Nullable low.NodeReference[bool] - ReadOnly low.NodeReference[bool] - WriteOnly low.NodeReference[bool] - XML low.NodeReference[*XML] - ExternalDocs low.NodeReference[*ExternalDoc] - Example low.NodeReference[any] - Deprecated low.NodeReference[bool] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + // Compatible with all versions + Title low.NodeReference[string] + MultipleOf low.NodeReference[float64] + Maximum low.NodeReference[float64] + Minimum low.NodeReference[float64] + MaxLength low.NodeReference[int64] + MinLength low.NodeReference[int64] + Pattern low.NodeReference[string] + Format low.NodeReference[string] + MaxItems low.NodeReference[int64] + MinItems low.NodeReference[int64] + UniqueItems low.NodeReference[bool] + MaxProperties low.NodeReference[int64] + MinProperties low.NodeReference[int64] + Required low.NodeReference[[]low.ValueReference[string]] + Enum low.NodeReference[[]low.ValueReference[any]] + Not low.NodeReference[*SchemaProxy] + Properties low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*SchemaProxy]] + AdditionalProperties low.NodeReference[any] + Description low.NodeReference[string] + ContentEncoding low.NodeReference[string] + ContentMediaType low.NodeReference[string] + Default low.NodeReference[any] + Nullable low.NodeReference[bool] + ReadOnly low.NodeReference[bool] + WriteOnly low.NodeReference[bool] + XML low.NodeReference[*XML] + ExternalDocs low.NodeReference[*ExternalDoc] + Example low.NodeReference[any] + Deprecated low.NodeReference[bool] + Extensions map[low.KeyReference[string]]low.ValueReference[any] // Parent Proxy refers back to the low level SchemaProxy that is proxying this schema. ParentProxy *SchemaProxy @@ -531,6 +531,8 @@ func (s *Schema) GetExtensions() map[low.KeyReference[string]]low.ValueReference // - UnevaluatedProperties // - Anchor func (s *Schema) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) s.Reference = new(low.Reference) if h, _, _ := utils.IsNodeRefValue(root); h { ref, err := low.LocateRefNode(root, idx) @@ -581,47 +583,47 @@ func (s *Schema) Build(root *yaml.Node, idx *index.SpecIndex) error { } } - // determine exclusive minimum type, bool (3.0) or int (3.1) - _, exMinLabel, exMinValue := utils.FindKeyNodeFullTop(ExclusiveMinimumLabel, root.Content) - if exMinValue != nil { - if utils.IsNodeBoolValue(exMinValue) { - val, _ := strconv.ParseBool(exMinValue.Value) - s.ExclusiveMinimum = low.NodeReference[*SchemaDynamicValue[bool, float64]]{ - KeyNode: exMinLabel, - ValueNode: exMinValue, - Value: &SchemaDynamicValue[bool, float64]{N: 0, A: val}, - } - } - if utils.IsNodeIntValue(exMinValue) { - val, _ := strconv.ParseFloat(exMinValue.Value, 64) - s.ExclusiveMinimum = low.NodeReference[*SchemaDynamicValue[bool, float64]]{ - KeyNode: exMinLabel, - ValueNode: exMinValue, - Value: &SchemaDynamicValue[bool, float64]{N: 1, B: val}, - } - } - } + // determine exclusive minimum type, bool (3.0) or int (3.1) + _, exMinLabel, exMinValue := utils.FindKeyNodeFullTop(ExclusiveMinimumLabel, root.Content) + if exMinValue != nil { + if utils.IsNodeBoolValue(exMinValue) { + val, _ := strconv.ParseBool(exMinValue.Value) + s.ExclusiveMinimum = low.NodeReference[*SchemaDynamicValue[bool, float64]]{ + KeyNode: exMinLabel, + ValueNode: exMinValue, + Value: &SchemaDynamicValue[bool, float64]{N: 0, A: val}, + } + } + if utils.IsNodeIntValue(exMinValue) { + val, _ := strconv.ParseFloat(exMinValue.Value, 64) + s.ExclusiveMinimum = low.NodeReference[*SchemaDynamicValue[bool, float64]]{ + KeyNode: exMinLabel, + ValueNode: exMinValue, + Value: &SchemaDynamicValue[bool, float64]{N: 1, B: val}, + } + } + } - // determine exclusive maximum type, bool (3.0) or int (3.1) - _, exMaxLabel, exMaxValue := utils.FindKeyNodeFullTop(ExclusiveMaximumLabel, root.Content) - if exMaxValue != nil { - if utils.IsNodeBoolValue(exMaxValue) { - val, _ := strconv.ParseBool(exMaxValue.Value) - s.ExclusiveMaximum = low.NodeReference[*SchemaDynamicValue[bool, float64]]{ - KeyNode: exMaxLabel, - ValueNode: exMaxValue, - Value: &SchemaDynamicValue[bool, float64]{N: 0, A: val}, - } - } - if utils.IsNodeIntValue(exMaxValue) { - val, _ := strconv.ParseFloat(exMaxValue.Value, 64) - s.ExclusiveMaximum = low.NodeReference[*SchemaDynamicValue[bool, float64]]{ - KeyNode: exMaxLabel, - ValueNode: exMaxValue, - Value: &SchemaDynamicValue[bool, float64]{N: 1, B: val}, - } - } - } + // determine exclusive maximum type, bool (3.0) or int (3.1) + _, exMaxLabel, exMaxValue := utils.FindKeyNodeFullTop(ExclusiveMaximumLabel, root.Content) + if exMaxValue != nil { + if utils.IsNodeBoolValue(exMaxValue) { + val, _ := strconv.ParseBool(exMaxValue.Value) + s.ExclusiveMaximum = low.NodeReference[*SchemaDynamicValue[bool, float64]]{ + KeyNode: exMaxLabel, + ValueNode: exMaxValue, + Value: &SchemaDynamicValue[bool, float64]{N: 0, A: val}, + } + } + if utils.IsNodeIntValue(exMaxValue) { + val, _ := strconv.ParseFloat(exMaxValue.Value, 64) + s.ExclusiveMaximum = low.NodeReference[*SchemaDynamicValue[bool, float64]]{ + KeyNode: exMaxLabel, + ValueNode: exMaxValue, + Value: &SchemaDynamicValue[bool, float64]{N: 1, B: val}, + } + } + } // handle schema reference type if set. (3.1) _, schemaRefLabel, schemaRefNode := utils.FindKeyNodeFullTop(SchemaTypeLabel, root.Content) diff --git a/datamodel/low/base/schema_proxy.go b/datamodel/low/base/schema_proxy.go index a3ac15e..5b1792e 100644 --- a/datamodel/low/base/schema_proxy.go +++ b/datamodel/low/base/schema_proxy.go @@ -80,6 +80,7 @@ func (sp *SchemaProxy) Schema() *Schema { return sp.rendered } schema := new(Schema) + utils.CheckForMergeNodes(sp.vn) err := schema.Build(sp.vn, sp.idx) if err != nil { sp.buildError = err diff --git a/datamodel/low/base/schema_proxy_test.go b/datamodel/low/base/schema_proxy_test.go index 09b04d7..f6ef8a4 100644 --- a/datamodel/low/base/schema_proxy_test.go +++ b/datamodel/low/base/schema_proxy_test.go @@ -73,3 +73,24 @@ func TestSchemaProxy_Build_HashInline(t *testing.T) { 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 + type: string + enum: ["Onboarding", "Monitoring", "Re-Assessment"] + description: The type of life cycle +<<: *life_cycle_types_def` + + var sch SchemaProxy + var idxNode yaml.Node + _ = yaml.Unmarshal([]byte(yml), &idxNode) + + err := sch.Build(idxNode.Content[0], nil) + assert.NoError(t, err) + assert.Len(t, sch.Schema().Enum.Value, 3) + assert.Equal(t, "The type of life cycle", sch.Schema().Description.Value) + +} diff --git a/datamodel/low/base/security_requirement.go b/datamodel/low/base/security_requirement.go index 16a4d22..f978c36 100644 --- a/datamodel/low/base/security_requirement.go +++ b/datamodel/low/base/security_requirement.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -28,6 +29,8 @@ type SecurityRequirement struct { // Build will extract security requirements from the node (the structure is odd, to be honest) func (s *SecurityRequirement) Build(root *yaml.Node, _ *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) s.Reference = new(low.Reference) var labelNode *yaml.Node valueMap := make(map[low.KeyReference[string]]low.ValueReference[[]low.ValueReference[string]]) diff --git a/datamodel/low/base/tag.go b/datamodel/low/base/tag.go index 4dd1032..13d7e5e 100644 --- a/datamodel/low/base/tag.go +++ b/datamodel/low/base/tag.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -34,6 +35,8 @@ func (t *Tag) FindExtension(ext string) *low.ValueReference[any] { // Build will extract extensions and external docs for the Tag. func (t *Tag) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) t.Reference = new(low.Reference) t.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/base/xml.go b/datamodel/low/base/xml.go index 94344c8..d6d9420 100644 --- a/datamodel/low/base/xml.go +++ b/datamodel/low/base/xml.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -31,6 +32,8 @@ type XML struct { // Build will extract extensions from the XML instance. func (x *XML) Build(root *yaml.Node, _ *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) x.Reference = new(low.Reference) x.Extensions = low.ExtractExtensions(root) return nil diff --git a/datamodel/low/extraction_functions.go b/datamodel/low/extraction_functions.go index 366f87a..05b55fa 100644 --- a/datamodel/low/extraction_functions.go +++ b/datamodel/low/extraction_functions.go @@ -6,14 +6,13 @@ package low import ( "crypto/sha256" "fmt" - "reflect" - "strconv" - "strings" - "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/utils" "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" "gopkg.in/yaml.v3" + "reflect" + "strconv" + "strings" ) // FindItemInMap accepts a string key and a collection of KeyReference[string] and ValueReference[T]. Every @@ -86,14 +85,14 @@ func LocateRefNode(root *yaml.Node, idx *index.SpecIndex) (*yaml.Node, error) { found[rv].Node.Column) } } - return found[rv].Node, nil + return utils.NodeAlias(found[rv].Node), nil } } // perform a search for the reference in the index foundRefs := idx.SearchIndexForReference(rv) if len(foundRefs) > 0 { - return foundRefs[0].Node, nil + return utils.NodeAlias(foundRefs[0].Node), nil } // let's try something else to find our references. @@ -106,7 +105,7 @@ func LocateRefNode(root *yaml.Node, idx *index.SpecIndex) (*yaml.Node, error) { nodes, fErr := path.Find(idx.GetRootNode()) if fErr == nil { if len(nodes) > 0 { - return nodes[0], nil + return utils.NodeAlias(nodes[0]), nil } } } @@ -123,6 +122,7 @@ func ExtractObjectRaw[T Buildable[N], N any](root *yaml.Node, idx *index.SpecInd var circError error var isReference bool var referenceValue string + root = utils.NodeAlias(root) if h, _, rv := utils.IsNodeRefValue(root); h { ref, err := LocateRefNode(root, idx) if ref != nil { @@ -167,6 +167,7 @@ func ExtractObject[T Buildable[N], N any](label string, root *yaml.Node, idx *in var circError error var isReference bool var referenceValue string + root = utils.NodeAlias(root) if rf, rl, refVal := utils.IsNodeRefValue(root); rf { ref, err := LocateRefNode(root, idx) if ref != nil { @@ -251,6 +252,7 @@ func ExtractArray[T Buildable[N], N any](label string, root *yaml.Node, idx *ind ) { var ln, vn *yaml.Node var circError error + root = utils.NodeAlias(root) if rf, rl, _ := utils.IsNodeRefValue(root); rf { ref, err := LocateRefNode(root, idx) if ref != nil { @@ -370,7 +372,10 @@ func ExtractMapNoLookupExtensions[PT Buildable[N], N any]( if utils.IsNodeMap(root) { var currentKey *yaml.Node skip := false - for i, node := range root.Content { + rlen := len(root.Content) + + for i := 0; i < rlen; i++ { + node := root.Content[i] if !includeExtensions { if strings.HasPrefix(strings.ToLower(node.Value), "x-") { skip = true @@ -386,6 +391,14 @@ func ExtractMapNoLookupExtensions[PT Buildable[N], N any]( continue } + if currentKey.Tag == "!!merge" && currentKey.Value == "<<" { + root.Content = append(root.Content, utils.NodeAlias(node).Content...) + rlen = len(root.Content) + currentKey = nil + continue + } + node = utils.NodeAlias(node) + var isReference bool var referenceValue string // if value is a reference, we have to look it up in the index! @@ -470,6 +483,7 @@ func ExtractMapExtensions[PT Buildable[N], N any]( var referenceValue string var labelNode, valueNode *yaml.Node var circError error + root = utils.NodeAlias(root) if rf, rl, rv := utils.IsNodeRefValue(root); rf { // locate reference in index. ref, err := LocateRefNode(root, idx) @@ -515,6 +529,7 @@ func ExtractMapExtensions[PT Buildable[N], N any]( buildMap := func(label *yaml.Node, value *yaml.Node, c chan mappingResult[PT], ec chan<- error, ref string) { var n PT = new(N) + value = utils.NodeAlias(value) _ = BuildModel(value, n) err := n.Build(value, idx) if err != nil { @@ -544,6 +559,7 @@ func ExtractMapExtensions[PT Buildable[N], N any]( totalKeys := 0 for i, en := range valueNode.Content { + en = utils.NodeAlias(en) referenceValue = "" if i%2 == 0 { currentLabelNode = en @@ -620,6 +636,7 @@ func ExtractMap[PT Buildable[N], N any]( // // int64, float64, bool, string func ExtractExtensions(root *yaml.Node) map[KeyReference[string]]ValueReference[any] { + root = utils.NodeAlias(root) extensions := utils.FindExtensionNodes(root.Content) extensionMap := make(map[KeyReference[string]]ValueReference[any]) for _, ext := range extensions { diff --git a/datamodel/low/model_builder.go b/datamodel/low/model_builder.go index d8098e3..321145b 100644 --- a/datamodel/low/model_builder.go +++ b/datamodel/low/model_builder.go @@ -23,6 +23,8 @@ func BuildModel(node *yaml.Node, model interface{}) error { if node == nil { return nil } + node = utils.NodeAlias(node) + utils.CheckForMergeNodes(node) if reflect.ValueOf(model).Type().Kind() != reflect.Pointer { return fmt.Errorf("cannot build model on non-pointer: %v", reflect.ValueOf(model).Type().Kind()) @@ -51,6 +53,7 @@ func BuildModel(node *yaml.Node, model interface{}) error { kind := field.Kind() switch kind { case reflect.Struct, reflect.Slice, reflect.Map, reflect.Pointer: + vn = utils.NodeAlias(vn) err := SetField(&field, vn, kn) if err != nil { return err @@ -213,31 +216,31 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) er case reflect.TypeOf(NodeReference[float32]{}): - if utils.IsNodeNumberValue(valueNode) { - if field.CanSet() { - fv, _ := strconv.ParseFloat(valueNode.Value, 32) - nr := NodeReference[float32]{ - Value: float32(fv), - ValueNode: valueNode, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(nr)) - } - } + if utils.IsNodeNumberValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.ParseFloat(valueNode.Value, 32) + nr := NodeReference[float32]{ + Value: float32(fv), + ValueNode: valueNode, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(nr)) + } + } case reflect.TypeOf(NodeReference[float64]{}): - if utils.IsNodeNumberValue(valueNode) { - if field.CanSet() { - fv, _ := strconv.ParseFloat(valueNode.Value, 64) - nr := NodeReference[float64]{ - Value: fv, - ValueNode: valueNode, - KeyNode: keyNode, - } - field.Set(reflect.ValueOf(nr)) - } - } + if utils.IsNodeNumberValue(valueNode) { + if field.CanSet() { + fv, _ := strconv.ParseFloat(valueNode.Value, 64) + nr := NodeReference[float64]{ + Value: fv, + ValueNode: valueNode, + KeyNode: keyNode, + } + field.Set(reflect.ValueOf(nr)) + } + } case reflect.TypeOf([]NodeReference[string]{}): diff --git a/datamodel/low/v2/definitions.go b/datamodel/low/v2/definitions.go index a707b00..20e157d 100644 --- a/datamodel/low/v2/definitions.go +++ b/datamodel/low/v2/definitions.go @@ -8,6 +8,7 @@ import ( "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" "sort" "strings" @@ -71,6 +72,8 @@ func (s *SecurityDefinitions) FindSecurityDefinition(securityDef string) *low.Va // Build will extract all definitions into SchemaProxy instances. func (d *Definitions) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) errorChan := make(chan error) resultChan := make(chan definitionResult[*base.SchemaProxy]) var defLabel *yaml.Node diff --git a/datamodel/low/v2/examples.go b/datamodel/low/v2/examples.go index 57345c8..7e213ba 100644 --- a/datamodel/low/v2/examples.go +++ b/datamodel/low/v2/examples.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -27,6 +28,8 @@ func (e *Examples) FindExample(name string) *low.ValueReference[any] { // Build will extract all examples and will attempt to unmarshal content into a map or slice based on type. func (e *Examples) Build(root *yaml.Node, _ *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) var keyNode, currNode *yaml.Node var err error e.Values = make(map[low.KeyReference[string]]low.ValueReference[any]) diff --git a/datamodel/low/v2/header.go b/datamodel/low/v2/header.go index f0385ff..c75c330 100644 --- a/datamodel/low/v2/header.go +++ b/datamodel/low/v2/header.go @@ -52,6 +52,8 @@ func (h *Header) GetExtensions() map[low.KeyReference[string]]low.ValueReference // Build will build out items, extensions and default value from the supplied node. func (h *Header) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) h.Extensions = low.ExtractExtensions(root) items, err := low.ExtractObject[*Items](ItemsLabel, root, idx) if err != nil { diff --git a/datamodel/low/v2/items.go b/datamodel/low/v2/items.go index 53fc0c6..0c1f081 100644 --- a/datamodel/low/v2/items.go +++ b/datamodel/low/v2/items.go @@ -103,6 +103,8 @@ func (i *Items) Hash() [32]byte { // Build will build out items and default value. func (i *Items) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) i.Extensions = low.ExtractExtensions(root) items, iErr := low.ExtractObject[*Items](ItemsLabel, root, idx) if iErr != nil { diff --git a/datamodel/low/v2/operation.go b/datamodel/low/v2/operation.go index b5a89cf..fca309b 100644 --- a/datamodel/low/v2/operation.go +++ b/datamodel/low/v2/operation.go @@ -9,6 +9,7 @@ import ( "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" "sort" "strings" @@ -36,6 +37,8 @@ type Operation struct { // Build will extract external docs, extensions, parameters, responses and security requirements. func (o *Operation) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) o.Extensions = low.ExtractExtensions(root) // extract externalDocs diff --git a/datamodel/low/v2/parameter.go b/datamodel/low/v2/parameter.go index 2fb63a1..5d94c43 100644 --- a/datamodel/low/v2/parameter.go +++ b/datamodel/low/v2/parameter.go @@ -95,6 +95,8 @@ func (p *Parameter) GetExtensions() map[low.KeyReference[string]]low.ValueRefere // Build will extract out extensions, schema, items and default value func (p *Parameter) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) p.Extensions = low.ExtractExtensions(root) sch, sErr := base.ExtractSchema(root, idx) if sErr != nil { diff --git a/datamodel/low/v2/path_item.go b/datamodel/low/v2/path_item.go index 5c367f3..b98517d 100644 --- a/datamodel/low/v2/path_item.go +++ b/datamodel/low/v2/path_item.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -47,6 +48,8 @@ func (p *PathItem) GetExtensions() map[low.KeyReference[string]]low.ValueReferen // Build will extract extensions, parameters and operations for all methods. Every method is handled // asynchronously, in order to keep things moving quickly for complex operations. func (p *PathItem) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) p.Extensions = low.ExtractExtensions(root) skip := false var currentNode *yaml.Node @@ -120,7 +123,7 @@ func (p *PathItem) Build(root *yaml.Node, idx *index.SpecIndex) error { wg.Add(1) - go low.BuildModelAsync(pathNode, &op, &wg, &errors) + low.BuildModelAsync(pathNode, &op, &wg, &errors) opRef := low.NodeReference[*Operation]{ Value: &op, diff --git a/datamodel/low/v2/paths.go b/datamodel/low/v2/paths.go index 7cf19bf..600814c 100644 --- a/datamodel/low/v2/paths.go +++ b/datamodel/low/v2/paths.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -51,6 +52,8 @@ func (p *Paths) FindExtension(ext string) *low.ValueReference[any] { // Build will extract extensions and paths from node. func (p *Paths) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) p.Extensions = low.ExtractExtensions(root) skip := false var currentNode *yaml.Node diff --git a/datamodel/low/v2/response.go b/datamodel/low/v2/response.go index c202aae..e86b910 100644 --- a/datamodel/low/v2/response.go +++ b/datamodel/low/v2/response.go @@ -9,6 +9,7 @@ import ( "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" "sort" "strings" @@ -43,6 +44,8 @@ func (r *Response) FindHeader(hType string) *low.ValueReference[*Header] { // Build will extract schema, extensions, examples and headers from node func (r *Response) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) r.Extensions = low.ExtractExtensions(root) s, err := base.ExtractSchema(root, idx) if err != nil { diff --git a/datamodel/low/v2/responses.go b/datamodel/low/v2/responses.go index 339837e..eaf6e8a 100644 --- a/datamodel/low/v2/responses.go +++ b/datamodel/low/v2/responses.go @@ -28,6 +28,8 @@ func (r *Responses) GetExtensions() map[low.KeyReference[string]]low.ValueRefere // Build will extract default value and extensions from node. func (r *Responses) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) r.Extensions = low.ExtractExtensions(root) if utils.IsNodeMap(root) { diff --git a/datamodel/low/v2/scopes.go b/datamodel/low/v2/scopes.go index 01b3b69..ad8fe35 100644 --- a/datamodel/low/v2/scopes.go +++ b/datamodel/low/v2/scopes.go @@ -35,6 +35,8 @@ func (s *Scopes) FindScope(scope string) *low.ValueReference[string] { // Build will extract scope values and extensions from node. func (s *Scopes) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) s.Extensions = low.ExtractExtensions(root) valueMap := make(map[low.KeyReference[string]]low.ValueReference[string]) if utils.IsNodeMap(root) { diff --git a/datamodel/low/v2/security_scheme.go b/datamodel/low/v2/security_scheme.go index c9180e9..b9804d4 100644 --- a/datamodel/low/v2/security_scheme.go +++ b/datamodel/low/v2/security_scheme.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -38,6 +39,8 @@ func (ss *SecurityScheme) GetExtensions() map[low.KeyReference[string]]low.Value // Build will extract extensions and scopes from the node. func (ss *SecurityScheme) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) ss.Extensions = low.ExtractExtensions(root) scopes, sErr := low.ExtractObject[*Scopes](ScopesLabel, root, idx) diff --git a/datamodel/low/v3/callback.go b/datamodel/low/v3/callback.go index 88fe5dc..f9274c8 100644 --- a/datamodel/low/v3/callback.go +++ b/datamodel/low/v3/callback.go @@ -6,6 +6,7 @@ package v3 import ( "crypto/sha256" "fmt" + "github.com/pb33f/libopenapi/utils" "sort" "strings" @@ -39,6 +40,8 @@ func (cb *Callback) FindExpression(exp string) *low.ValueReference[*PathItem] { // Build will extract extensions, expressions and PathItem objects for Callback func (cb *Callback) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) cb.Reference = new(low.Reference) cb.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/components.go b/datamodel/low/v3/components.go index c3a8650..500218f 100644 --- a/datamodel/low/v3/components.go +++ b/datamodel/low/v3/components.go @@ -127,6 +127,8 @@ func (co *Components) FindCallback(callback string) *low.ValueReference[*Callbac } func (co *Components) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) co.Reference = new(low.Reference) co.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/encoding.go b/datamodel/low/v3/encoding.go index 82fc444..2e2a4c9 100644 --- a/datamodel/low/v3/encoding.go +++ b/datamodel/low/v3/encoding.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "strings" ) @@ -58,6 +59,8 @@ func (en *Encoding) Hash() [32]byte { // Build will extract all Header objects from supplied node. func (en *Encoding) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) en.Reference = new(low.Reference) headers, hL, hN, err := low.ExtractMap[*Header](HeadersLabel, root, idx) if err != nil { diff --git a/datamodel/low/v3/header.go b/datamodel/low/v3/header.go index 188dfd7..2ebc0c1 100644 --- a/datamodel/low/v3/header.go +++ b/datamodel/low/v3/header.go @@ -96,6 +96,8 @@ func (h *Header) Hash() [32]byte { // Build will extract extensions, examples, schema and content/media types from node. func (h *Header) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) h.Reference = new(low.Reference) h.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/link.go b/datamodel/low/v3/link.go index 8be2508..51288b9 100644 --- a/datamodel/low/v3/link.go +++ b/datamodel/low/v3/link.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -53,6 +54,8 @@ func (l *Link) FindExtension(ext string) *low.ValueReference[any] { // Build will extract extensions and servers from the node. func (l *Link) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) l.Reference = new(low.Reference) l.Extensions = low.ExtractExtensions(root) // extract server. diff --git a/datamodel/low/v3/media_type.go b/datamodel/low/v3/media_type.go index 703bf55..9f17cba 100644 --- a/datamodel/low/v3/media_type.go +++ b/datamodel/low/v3/media_type.go @@ -55,6 +55,8 @@ func (mt *MediaType) GetAllExamples() map[low.KeyReference[string]]low.ValueRefe // Build will extract examples, extensions, schema and encoding from node. func (mt *MediaType) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) mt.Reference = new(low.Reference) mt.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/oauth_flows.go b/datamodel/low/v3/oauth_flows.go index 137ffb2..618218d 100644 --- a/datamodel/low/v3/oauth_flows.go +++ b/datamodel/low/v3/oauth_flows.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -36,6 +37,8 @@ func (o *OAuthFlows) FindExtension(ext string) *low.ValueReference[any] { // Build will extract extensions and all OAuthFlow types from the supplied node. func (o *OAuthFlows) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) o.Reference = new(low.Reference) o.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/operation.go b/datamodel/low/v3/operation.go index 4351d2e..57c92ff 100644 --- a/datamodel/low/v3/operation.go +++ b/datamodel/low/v3/operation.go @@ -9,6 +9,7 @@ import ( "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" "sort" "strings" @@ -55,6 +56,8 @@ func (o *Operation) FindSecurityRequirement(name string) []low.ValueReference[st // Build will extract external docs, parameters, request body, responses, callbacks, security and servers. func (o *Operation) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) o.Reference = new(low.Reference) o.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/parameter.go b/datamodel/low/v3/parameter.go index 5f8272d..747c6d8 100644 --- a/datamodel/low/v3/parameter.go +++ b/datamodel/low/v3/parameter.go @@ -59,6 +59,8 @@ func (p *Parameter) GetExtensions() map[low.KeyReference[string]]low.ValueRefere // Build will extract examples, extensions and content/media types. func (p *Parameter) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) p.Reference = new(low.Reference) p.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/path_item.go b/datamodel/low/v3/path_item.go index c178bd2..ffa83aa 100644 --- a/datamodel/low/v3/path_item.go +++ b/datamodel/low/v3/path_item.go @@ -109,6 +109,8 @@ func (p *PathItem) GetExtensions() map[low.KeyReference[string]]low.ValueReferen // Build extracts extensions, parameters, servers and each http method defined. // everything is extracted asynchronously for speed. func (p *PathItem) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) p.Reference = new(low.Reference) p.Extensions = low.ExtractExtensions(root) skip := false @@ -232,7 +234,7 @@ func (p *PathItem) Build(root *yaml.Node, idx *index.SpecIndex) error { } } wg.Add(1) - go low.BuildModelAsync(pathNode, &op, &wg, &errors) + low.BuildModelAsync(pathNode, &op, &wg, &errors) opRef := low.NodeReference[*Operation]{ Value: &op, diff --git a/datamodel/low/v3/paths.go b/datamodel/low/v3/paths.go index c7c8f04..3472e71 100644 --- a/datamodel/low/v3/paths.go +++ b/datamodel/low/v3/paths.go @@ -59,6 +59,8 @@ func (p *Paths) GetExtensions() map[low.KeyReference[string]]low.ValueReference[ // Build will extract extensions and all PathItems. This happens asynchronously for speed. func (p *Paths) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) p.Reference = new(low.Reference) p.Extensions = low.ExtractExtensions(root) skip := false diff --git a/datamodel/low/v3/request_body.go b/datamodel/low/v3/request_body.go index 782fbe3..665b041 100644 --- a/datamodel/low/v3/request_body.go +++ b/datamodel/low/v3/request_body.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -40,6 +41,8 @@ func (rb *RequestBody) FindContent(cType string) *low.ValueReference[*MediaType] // Build will extract extensions and MediaType objects from the node. func (rb *RequestBody) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) rb.Reference = new(low.Reference) rb.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/response.go b/datamodel/low/v3/response.go index 4163195..226363b 100644 --- a/datamodel/low/v3/response.go +++ b/datamodel/low/v3/response.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -54,6 +55,8 @@ func (r *Response) FindLink(hType string) *low.ValueReference[*Link] { // Build will extract headers, extensions, content and links from node. func (r *Response) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) r.Reference = new(low.Reference) r.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/responses.go b/datamodel/low/v3/responses.go index 7a40c7b..8d7d44e 100644 --- a/datamodel/low/v3/responses.go +++ b/datamodel/low/v3/responses.go @@ -46,8 +46,10 @@ func (r *Responses) GetExtensions() map[low.KeyReference[string]]low.ValueRefere // Build will extract default response and all Response objects for each code func (r *Responses) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) r.Reference = new(low.Reference) r.Extensions = low.ExtractExtensions(root) + utils.CheckForMergeNodes(root) if utils.IsNodeMap(root) { codes, err := low.ExtractMapNoLookup[*Response](root, idx) diff --git a/datamodel/low/v3/security_scheme.go b/datamodel/low/v3/security_scheme.go index 373510a..999ee3f 100644 --- a/datamodel/low/v3/security_scheme.go +++ b/datamodel/low/v3/security_scheme.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "sort" "strings" @@ -48,6 +49,8 @@ func (ss *SecurityScheme) GetExtensions() map[low.KeyReference[string]]low.Value // Build will extract OAuthFlows and extensions from the node. func (ss *SecurityScheme) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) ss.Reference = new(low.Reference) ss.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v3/server.go b/datamodel/low/v3/server.go index dc074d2..2688588 100644 --- a/datamodel/low/v3/server.go +++ b/datamodel/low/v3/server.go @@ -35,6 +35,8 @@ func (s *Server) FindVariable(serverVar string) *low.ValueReference[*ServerVaria // Build will extract server variables from the supplied node. func (s *Server) Build(root *yaml.Node, idx *index.SpecIndex) error { + root = utils.NodeAlias(root) + utils.CheckForMergeNodes(root) s.Reference = new(low.Reference) s.Extensions = low.ExtractExtensions(root) kn, vars := utils.FindKeyNode(VariablesLabel, root.Content) diff --git a/document_test.go b/document_test.go index f2f6620..67b8a0e 100644 --- a/document_test.go +++ b/document_test.go @@ -505,33 +505,34 @@ paths: assert.Equal(t, d, strings.TrimSpace(string(rend))) } -func TestDocument_RemoteWithoutBaseURL(t *testing.T) { - - // This test will push the index to do try and locate remote references that use relative references - spec := `openapi: 3.0.2 -info: - title: Test - version: 1.0.0 -paths: - /test: - get: - parameters: - - $ref: "https://schemas.opengis.net/ogcapi/features/part2/1.0/openapi/ogcapi-features-2.yaml#/components/parameters/crs"` - - config := datamodel.NewOpenDocumentConfiguration() - - doc, err := NewDocumentWithConfiguration([]byte(spec), config) - if err != nil { - panic(err) - } - - result, errs := doc.BuildV3Model() - if len(errs) > 0 { - panic(errs) - } - - assert.Equal(t, "crs", result.Model.Paths.PathItems["/test"].Get.Parameters[0].Name) -} +// disabled for now as the host is timing out +//func TestDocument_RemoteWithoutBaseURL(t *testing.T) { +// +// // This test will push the index to do try and locate remote references that use relative references +// spec := `openapi: 3.0.2 +//info: +// title: Test +// version: 1.0.0 +//paths: +// /test: +// get: +// parameters: +// - $ref: "https://schemas.opengis.net/ogcapi/features/part2/1.0/openapi/ogcapi-features-2.yaml#/components/parameters/crs"` +// +// config := datamodel.NewOpenDocumentConfiguration() +// +// doc, err := NewDocumentWithConfiguration([]byte(spec), config) +// if err != nil { +// panic(err) +// } +// +// result, errs := doc.BuildV3Model() +// if len(errs) > 0 { +// panic(errs) +// } +// +// assert.Equal(t, "crs", result.Model.Paths.PathItems["/test"].Get.Parameters[0].Name) +//} func TestDocument_ExampleMap(t *testing.T) { var d = `openapi: "3.1" diff --git a/index/find_component.go b/index/find_component.go index 0aa6881..8afb59d 100644 --- a/index/find_component.go +++ b/index/find_component.go @@ -4,86 +4,86 @@ package index import ( - "fmt" - "io" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "time" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" - "github.com/pb33f/libopenapi/utils" - "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" - "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi/utils" + "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" + "gopkg.in/yaml.v3" ) // FindComponent will locate a component by its reference, returns nil if nothing is found. // This method will recurse through remote, local and file references. For each new external reference // a new index will be created. These indexes can then be traversed recursively. func (index *SpecIndex) FindComponent(componentId string, parent *yaml.Node) *Reference { - if index.root == nil { - return nil - } + if index.root == nil { + return nil + } - remoteLookup := func(id string) (*yaml.Node, *yaml.Node, error) { - if index.config.AllowRemoteLookup { - return index.lookupRemoteReference(id) - } else { - return nil, nil, fmt.Errorf("remote lookups are not permitted, " + - "please set AllowRemoteLookup to true in the configuration") - } - } + remoteLookup := func(id string) (*yaml.Node, *yaml.Node, error) { + if index.config.AllowRemoteLookup { + return index.lookupRemoteReference(id) + } else { + return nil, nil, fmt.Errorf("remote lookups are not permitted, " + + "please set AllowRemoteLookup to true in the configuration") + } + } - fileLookup := func(id string) (*yaml.Node, *yaml.Node, error) { - if index.config.AllowFileLookup { - return index.lookupFileReference(id) - } else { - return nil, nil, fmt.Errorf("local lookups are not permitted, " + - "please set AllowFileLookup to true in the configuration") - } - } + fileLookup := func(id string) (*yaml.Node, *yaml.Node, error) { + if index.config.AllowFileLookup { + return index.lookupFileReference(id) + } else { + return nil, nil, fmt.Errorf("local lookups are not permitted, " + + "please set AllowFileLookup to true in the configuration") + } + } - switch DetermineReferenceResolveType(componentId) { - case LocalResolve: // ideally, every single ref in every single spec is local. however, this is not the case. - return index.FindComponentInRoot(componentId) + switch DetermineReferenceResolveType(componentId) { + case LocalResolve: // ideally, every single ref in every single spec is local. however, this is not the case. + return index.FindComponentInRoot(componentId) - case HttpResolve: - uri := strings.Split(componentId, "#") - if len(uri) >= 2 { - return index.performExternalLookup(uri, componentId, remoteLookup, parent) - } - if len(uri) == 1 { - // if there is no reference, second segment is empty / has no name - // this means there is no component to look-up and the entire file should be pulled in. - // to stop all the other code from breaking (that is expecting a component), let's just post-pend - // a hash to the end of the componentId and ensure the uri slice is as expected. - // described in https://github.com/pb33f/libopenapi/issues/37 - componentId = fmt.Sprintf("%s#", componentId) - uri = append(uri, "") - return index.performExternalLookup(uri, componentId, remoteLookup, parent) - } + case HttpResolve: + uri := strings.Split(componentId, "#") + if len(uri) >= 2 { + return index.performExternalLookup(uri, componentId, remoteLookup, parent) + } + if len(uri) == 1 { + // if there is no reference, second segment is empty / has no name + // this means there is no component to look-up and the entire file should be pulled in. + // to stop all the other code from breaking (that is expecting a component), let's just post-pend + // a hash to the end of the componentId and ensure the uri slice is as expected. + // described in https://github.com/pb33f/libopenapi/issues/37 + componentId = fmt.Sprintf("%s#", componentId) + uri = append(uri, "") + return index.performExternalLookup(uri, componentId, remoteLookup, parent) + } - case FileResolve: - uri := strings.Split(componentId, "#") - if len(uri) == 2 { - return index.performExternalLookup(uri, componentId, fileLookup, parent) - } - if len(uri) == 1 { - // if there is no reference, second segment is empty / has no name - // this means there is no component to look-up and the entire file should be pulled in. - // to stop all the other code from breaking (that is expecting a component), let's just post-pend - // a hash to the end of the componentId and ensure the uri slice is as expected. - // described in https://github.com/pb33f/libopenapi/issues/37 - // - // ^^ this same issue was re-reported in file based lookups in vacuum. - // more info here: https://github.com/daveshanley/vacuum/issues/225 - componentId = fmt.Sprintf("%s#", componentId) - uri = append(uri, "") - return index.performExternalLookup(uri, componentId, fileLookup, parent) - } - } - return nil + case FileResolve: + uri := strings.Split(componentId, "#") + if len(uri) == 2 { + return index.performExternalLookup(uri, componentId, fileLookup, parent) + } + if len(uri) == 1 { + // if there is no reference, second segment is empty / has no name + // this means there is no component to look-up and the entire file should be pulled in. + // to stop all the other code from breaking (that is expecting a component), let's just post-pend + // a hash to the end of the componentId and ensure the uri slice is as expected. + // described in https://github.com/pb33f/libopenapi/issues/37 + // + // ^^ this same issue was re-reported in file based lookups in vacuum. + // more info here: https://github.com/daveshanley/vacuum/issues/225 + componentId = fmt.Sprintf("%s#", componentId) + uri = append(uri, "") + return index.performExternalLookup(uri, componentId, fileLookup, parent) + } + } + return nil } var httpClient = &http.Client{Timeout: time.Duration(60) * time.Second} @@ -91,339 +91,357 @@ var httpClient = &http.Client{Timeout: time.Duration(60) * time.Second} type RemoteURLHandler = func(url string) (*http.Response, error) func getRemoteDoc(g RemoteURLHandler, u string, d chan []byte, e chan error) { - resp, err := g(u) - if err != nil { - e <- err - close(e) - close(d) - return - } - var body []byte - body, _ = io.ReadAll(resp.Body) - d <- body - close(e) - close(d) + resp, err := g(u) + if err != nil { + e <- err + close(e) + close(d) + return + } + var body []byte + body, _ = io.ReadAll(resp.Body) + d <- body + close(e) + close(d) } func (index *SpecIndex) lookupRemoteReference(ref string) (*yaml.Node, *yaml.Node, error) { - // split string to remove file reference - uri := strings.Split(ref, "#") + // split string to remove file reference + uri := strings.Split(ref, "#") - // have we already seen this remote source? - var parsedRemoteDocument *yaml.Node - alreadySeen, foundDocument := index.CheckForSeenRemoteSource(uri[0]) + // have we already seen this remote source? + var parsedRemoteDocument *yaml.Node + alreadySeen, foundDocument := index.CheckForSeenRemoteSource(uri[0]) - if alreadySeen { - parsedRemoteDocument = foundDocument - } else { + if alreadySeen { + parsedRemoteDocument = foundDocument + } else { - d := make(chan bool) - var body []byte - var err error + d := make(chan bool) + var body []byte + var err error - go func(uri string) { - bc := make(chan []byte) - ec := make(chan error) - var getter RemoteURLHandler = httpClient.Get - if index.config != nil && index.config.RemoteURLHandler != nil { - getter = index.config.RemoteURLHandler - } + go func(uri string) { + bc := make(chan []byte) + ec := make(chan error) + var getter RemoteURLHandler = httpClient.Get + if index.config != nil && index.config.RemoteURLHandler != nil { + getter = index.config.RemoteURLHandler + } - // if we have a remote handler, use it instead of the default. - if index.config != nil && index.config.RemoteHandler != nil { - go func() { - remoteFS := index.config.RemoteHandler - remoteFile, rErr := remoteFS.Open(uri) - if rErr != nil { - e := fmt.Errorf("unable to open remote file: %s", rErr) - ec <- e - return - } - b, ioErr := io.ReadAll(remoteFile) - if ioErr != nil { - e := fmt.Errorf("unable to read remote file bytes: %s", ioErr) - ec <- e - return - } - bc <- b - }() - } else { - go getRemoteDoc(getter, uri, bc, ec) - } - select { - case v := <-bc: - body = v - break - case er := <-ec: - err = er - break - } - if len(body) > 0 { - var remoteDoc yaml.Node - er := yaml.Unmarshal(body, &remoteDoc) - if er != nil { - err = er - d <- true - return - } - parsedRemoteDocument = &remoteDoc - if index.config != nil { - index.config.seenRemoteSources.Store(uri, &remoteDoc) - } - } - d <- true - }(uri[0]) + // if we have a remote handler, use it instead of the default. + if index.config != nil && index.config.FSHandler != nil { + go func() { + remoteFS := index.config.FSHandler + remoteFile, rErr := remoteFS.Open(uri) + if rErr != nil { + e := fmt.Errorf("unable to open remote file: %s", rErr) + ec <- e + return + } + b, ioErr := io.ReadAll(remoteFile) + if ioErr != nil { + e := fmt.Errorf("unable to read remote file bytes: %s", ioErr) + ec <- e + return + } + bc <- b + }() + } else { + go getRemoteDoc(getter, uri, bc, ec) + } + select { + case v := <-bc: + body = v + break + case er := <-ec: + err = er + break + } + if len(body) > 0 { + var remoteDoc yaml.Node + er := yaml.Unmarshal(body, &remoteDoc) + if er != nil { + err = er + d <- true + return + } + parsedRemoteDocument = &remoteDoc + if index.config != nil { + index.config.seenRemoteSources.Store(uri, &remoteDoc) + } + } + d <- true + }(uri[0]) - // wait for double go fun. - <-d - if err != nil { - // no bueno. - return nil, nil, err - } - } + // wait for double go fun. + <-d + if err != nil { + // no bueno. + return nil, nil, err + } + } - // lookup item from reference by using a path query. - var query string - if len(uri) >= 2 { - query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", ".")) - } else { - query = "$" - } + // lookup item from reference by using a path query. + var query string + if len(uri) >= 2 { + query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", ".")) + } else { + query = "$" + } - // remove any URL encoding - query = strings.Replace(query, "~1", "./", 1) - query = strings.ReplaceAll(query, "~1", "/") + // remove any URL encoding + query = strings.Replace(query, "~1", "./", 1) + query = strings.ReplaceAll(query, "~1", "/") - path, err := yamlpath.NewPath(query) - if err != nil { - return nil, nil, err - } - result, _ := path.Find(parsedRemoteDocument) - if len(result) == 1 { - return result[0], parsedRemoteDocument, nil - } - return nil, nil, nil + path, err := yamlpath.NewPath(query) + if err != nil { + return nil, nil, err + } + result, _ := path.Find(parsedRemoteDocument) + if len(result) == 1 { + return result[0], parsedRemoteDocument, nil + } + return nil, nil, nil } func (index *SpecIndex) lookupFileReference(ref string) (*yaml.Node, *yaml.Node, error) { - // split string to remove file reference - uri := strings.Split(ref, "#") - file := strings.ReplaceAll(uri[0], "file:", "") - filePath := filepath.Dir(file) - fileName := filepath.Base(file) + // split string to remove file reference + uri := strings.Split(ref, "#") + file := strings.ReplaceAll(uri[0], "file:", "") + filePath := filepath.Dir(file) + fileName := filepath.Base(file) - var parsedRemoteDocument *yaml.Node + var parsedRemoteDocument *yaml.Node - if index.seenRemoteSources[file] != nil { - parsedRemoteDocument = index.seenRemoteSources[file] - } else { + if index.seenRemoteSources[file] != nil { + parsedRemoteDocument = index.seenRemoteSources[file] + } else { - base := index.config.BasePath - fileToRead := filepath.Join(base, filePath, fileName) + base := index.config.BasePath + fileToRead := filepath.Join(base, filePath, fileName) + var body []byte + var err error - // try and read the file off the local file system, if it fails - // check for a baseURL and then ask our remote lookup function to go try and get it. - body, err := os.ReadFile(fileToRead) + // if we have an FS handler, use it instead of the default behavior + if index.config != nil && index.config.FSHandler != nil { + remoteFS := index.config.FSHandler + remoteFile, rErr := remoteFS.Open(fileToRead) + if rErr != nil { + e := fmt.Errorf("unable to open file: %s", rErr) + return nil, nil, e + } + body, err = io.ReadAll(remoteFile) + if err != nil { + e := fmt.Errorf("unable to read file bytes: %s", err) + return nil, nil, e + } - if err != nil { + } else { - // if we have a baseURL, then we can try and get the file from there. - if index.config != nil && index.config.BaseURL != nil { + // try and read the file off the local file system, if it fails + // check for a baseURL and then ask our remote lookup function to go try and get it. + body, err = os.ReadFile(fileToRead) - u := index.config.BaseURL - remoteRef := GenerateCleanSpecConfigBaseURL(u, ref, true) - a, b, e := index.lookupRemoteReference(remoteRef) - if e != nil { - // give up, we can't find the file, not locally, not remotely. It's toast. - return nil, nil, e - } - return a, b, nil + if err != nil { - } else { - // no baseURL? then we can't do anything, give up. - return nil, nil, err - } - } + // if we have a baseURL, then we can try and get the file from there. + if index.config != nil && index.config.BaseURL != nil { - var remoteDoc yaml.Node - err = yaml.Unmarshal(body, &remoteDoc) - if err != nil { - return nil, nil, err - } - parsedRemoteDocument = &remoteDoc - if index.seenLocalSources != nil { - index.sourceLock.Lock() - index.seenLocalSources[file] = &remoteDoc - index.sourceLock.Unlock() - } - } + u := index.config.BaseURL + remoteRef := GenerateCleanSpecConfigBaseURL(u, ref, true) + a, b, e := index.lookupRemoteReference(remoteRef) + if e != nil { + // give up, we can't find the file, not locally, not remotely. It's toast. + return nil, nil, e + } + return a, b, nil - // lookup item from reference by using a path query. - var query string - if len(uri) >= 2 { - query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", ".")) - } else { - query = "$" - } + } else { + // no baseURL? then we can't do anything, give up. + return nil, nil, err + } + } + } + var remoteDoc yaml.Node + err = yaml.Unmarshal(body, &remoteDoc) + if err != nil { + return nil, nil, err + } + parsedRemoteDocument = &remoteDoc + if index.seenLocalSources != nil { + index.sourceLock.Lock() + index.seenLocalSources[file] = &remoteDoc + index.sourceLock.Unlock() + } + } - // remove any URL encoding - query = strings.Replace(query, "~1", "./", 1) - query = strings.ReplaceAll(query, "~1", "/") + // lookup item from reference by using a path query. + var query string + if len(uri) >= 2 { + query = fmt.Sprintf("$%s", strings.ReplaceAll(uri[1], "/", ".")) + } else { + query = "$" + } - path, err := yamlpath.NewPath(query) - if err != nil { - return nil, nil, err - } - result, _ := path.Find(parsedRemoteDocument) - if len(result) == 1 { - return result[0], parsedRemoteDocument, nil - } + // remove any URL encoding + query = strings.Replace(query, "~1", "./", 1) + query = strings.ReplaceAll(query, "~1", "/") - return nil, parsedRemoteDocument, nil + path, err := yamlpath.NewPath(query) + if err != nil { + return nil, nil, err + } + result, _ := path.Find(parsedRemoteDocument) + if len(result) == 1 { + return result[0], parsedRemoteDocument, nil + } + + return nil, parsedRemoteDocument, nil } func (index *SpecIndex) FindComponentInRoot(componentId string) *Reference { - if index.root != nil { + if index.root != nil { - // check component for url encoding. - if strings.Contains(componentId, "%") { - // decode the url. - componentId, _ = url.QueryUnescape(componentId) - } + // check component for url encoding. + if strings.Contains(componentId, "%") { + // decode the url. + componentId, _ = url.QueryUnescape(componentId) + } - name, friendlySearch := utils.ConvertComponentIdIntoFriendlyPathSearch(componentId) - path, err := yamlpath.NewPath(friendlySearch) - if path == nil || err != nil { - return nil // no component found - } - res, _ := path.Find(index.root) + name, friendlySearch := utils.ConvertComponentIdIntoFriendlyPathSearch(componentId) + path, err := yamlpath.NewPath(friendlySearch) + if path == nil || err != nil { + return nil // no component found + } + res, _ := path.Find(index.root) - if len(res) == 1 { - resNode := res[0] - if res[0].Kind == yaml.DocumentNode { - resNode = res[0].Content[0] - } - ref := &Reference{ - Definition: componentId, - Name: name, - Node: resNode, - Path: friendlySearch, - RequiredRefProperties: index.extractDefinitionRequiredRefProperties(res[0], map[string][]string{}), - } + if len(res) == 1 { + resNode := res[0] + if res[0].Kind == yaml.DocumentNode { + resNode = res[0].Content[0] + } + ref := &Reference{ + Definition: componentId, + Name: name, + Node: resNode, + Path: friendlySearch, + RequiredRefProperties: index.extractDefinitionRequiredRefProperties(res[0], map[string][]string{}), + } - return ref - } - } - return nil + return ref + } + } + return nil } func (index *SpecIndex) performExternalLookup(uri []string, componentId string, - lookupFunction ExternalLookupFunction, parent *yaml.Node) *Reference { - if len(uri) > 0 { - index.externalLock.RLock() - externalSpecIndex := index.externalSpecIndex[uri[0]] - index.externalLock.RUnlock() + lookupFunction ExternalLookupFunction, parent *yaml.Node) *Reference { + if len(uri) > 0 { + index.externalLock.RLock() + externalSpecIndex := index.externalSpecIndex[uri[0]] + index.externalLock.RUnlock() - if externalSpecIndex == nil { - _, newRoot, err := lookupFunction(componentId) - if err != nil { - indexError := &IndexingError{ - Err: err, - Node: parent, - Path: componentId, - } - index.errorLock.Lock() - index.refErrors = append(index.refErrors, indexError) - index.errorLock.Unlock() - return nil - } + if externalSpecIndex == nil { + _, newRoot, err := lookupFunction(componentId) + if err != nil { + indexError := &IndexingError{ + Err: err, + Node: parent, + Path: componentId, + } + index.errorLock.Lock() + index.refErrors = append(index.refErrors, indexError) + index.errorLock.Unlock() + return nil + } - // cool, cool, lets index this spec also. This is a recursive action and will keep going - // until all remote references have been found. - var bp *url.URL - var bd string + // cool, cool, lets index this spec also. This is a recursive action and will keep going + // until all remote references have been found. + var bp *url.URL + var bd string - if index.config.BaseURL != nil { - bp = index.config.BaseURL - } - if index.config.BasePath != "" { - bd = index.config.BasePath - } + if index.config.BaseURL != nil { + bp = index.config.BaseURL + } + if index.config.BasePath != "" { + bd = index.config.BasePath + } - var path, newBasePath string - var newUrl *url.URL + var path, newBasePath string + var newUrl *url.URL - if bp != nil { - path = GenerateCleanSpecConfigBaseURL(bp, uri[0], false) - newUrl, _ = url.Parse(path) - newBasePath = filepath.Dir(filepath.Join(index.config.BasePath, filepath.Dir(newUrl.Path))) - } - if bd != "" { - if len(uri[0]) > 0 { - // if there is no base url defined, but we can know we have been requested remotely, - // set the base url to the remote url base path. - // first check if the first param is actually a URL - io, er := url.ParseRequestURI(uri[0]) - if er != nil { - newBasePath = filepath.Dir(filepath.Join(bd, uri[0])) - } else { - if newUrl == nil || newUrl.String() != io.String() { - newUrl, _ = url.Parse(fmt.Sprintf("%s://%s%s", io.Scheme, io.Host, filepath.Dir(io.Path))) - } - newBasePath = filepath.Dir(filepath.Join(bd, uri[1])) - } - } else { - newBasePath = filepath.Dir(filepath.Join(bd, uri[0])) - } - } + if bp != nil { + path = GenerateCleanSpecConfigBaseURL(bp, uri[0], false) + newUrl, _ = url.Parse(path) + newBasePath = filepath.Dir(filepath.Join(index.config.BasePath, filepath.Dir(newUrl.Path))) + } + if bd != "" { + if len(uri[0]) > 0 { + // if there is no base url defined, but we can know we have been requested remotely, + // set the base url to the remote url base path. + // first check if the first param is actually a URL + io, er := url.ParseRequestURI(uri[0]) + if er != nil { + newBasePath = filepath.Dir(filepath.Join(bd, uri[0])) + } else { + if newUrl == nil || newUrl.String() != io.String() { + newUrl, _ = url.Parse(fmt.Sprintf("%s://%s%s", io.Scheme, io.Host, filepath.Dir(io.Path))) + } + newBasePath = filepath.Dir(filepath.Join(bd, uri[1])) + } + } else { + newBasePath = filepath.Dir(filepath.Join(bd, uri[0])) + } + } - if newUrl != nil || newBasePath != "" { - newConfig := &SpecIndexConfig{ - BaseURL: newUrl, - BasePath: newBasePath, - AllowRemoteLookup: index.config.AllowRemoteLookup, - AllowFileLookup: index.config.AllowFileLookup, - ParentIndex: index, - seenRemoteSources: index.config.seenRemoteSources, - remoteLock: index.config.remoteLock, - uri: uri, - } + if newUrl != nil || newBasePath != "" { + newConfig := &SpecIndexConfig{ + BaseURL: newUrl, + BasePath: newBasePath, + AllowRemoteLookup: index.config.AllowRemoteLookup, + AllowFileLookup: index.config.AllowFileLookup, + ParentIndex: index, + seenRemoteSources: index.config.seenRemoteSources, + remoteLock: index.config.remoteLock, + uri: uri, + } - var newIndex *SpecIndex - seen := index.SearchAncestryForSeenURI(uri[0]) - if seen == nil { + var newIndex *SpecIndex + seen := index.SearchAncestryForSeenURI(uri[0]) + if seen == nil { - newIndex = NewSpecIndexWithConfig(newRoot, newConfig) - index.refLock.Lock() - index.externalLock.Lock() - index.externalSpecIndex[uri[0]] = newIndex - index.externalLock.Unlock() - newIndex.relativePath = path - newIndex.parentIndex = index - index.AddChild(newIndex) - index.refLock.Unlock() - externalSpecIndex = newIndex - } else { - externalSpecIndex = seen - } - } - } + newIndex = NewSpecIndexWithConfig(newRoot, newConfig) + index.refLock.Lock() + index.externalLock.Lock() + index.externalSpecIndex[uri[0]] = newIndex + index.externalLock.Unlock() + newIndex.relativePath = path + newIndex.parentIndex = index + index.AddChild(newIndex) + index.refLock.Unlock() + externalSpecIndex = newIndex + } else { + externalSpecIndex = seen + } + } + } - if externalSpecIndex != nil { - foundRef := externalSpecIndex.FindComponentInRoot(uri[1]) - if foundRef != nil { - nameSegs := strings.Split(uri[1], "/") - ref := &Reference{ - Definition: componentId, - Name: nameSegs[len(nameSegs)-1], - Node: foundRef.Node, - IsRemote: true, - RemoteLocation: componentId, - Path: foundRef.Path, - } - return ref - } - } - } - return nil + if externalSpecIndex != nil { + foundRef := externalSpecIndex.FindComponentInRoot(uri[1]) + if foundRef != nil { + nameSegs := strings.Split(uri[1], "/") + ref := &Reference{ + Definition: componentId, + Name: nameSegs[len(nameSegs)-1], + Node: foundRef.Node, + IsRemote: true, + RemoteLocation: componentId, + Path: foundRef.Path, + } + return ref + } + } + } + return nil } diff --git a/index/find_component_test.go b/index/find_component_test.go index 5f50957..b7cc3b8 100644 --- a/index/find_component_test.go +++ b/index/find_component_test.go @@ -288,7 +288,7 @@ paths: _ = yaml.Unmarshal([]byte(spec), &rootNode) c := CreateOpenAPIIndexConfig() - c.RemoteHandler = FS{} + c.FSHandler = FS{} index := NewSpecIndexWithConfig(&rootNode, c) @@ -301,6 +301,35 @@ paths: assert.Equal(t, "query", crsParam.Node.Content[5].Value) } +func TestSpecIndex_UseFileHandler(t *testing.T) { + + spec := `openapi: 3.1.0 +info: + title: Test Remote Handler + version: 1.0.0 +paths: + /test: + get: + parameters: + - $ref: "some-file-that-does-not-exist.yaml"` + + var rootNode yaml.Node + _ = yaml.Unmarshal([]byte(spec), &rootNode) + + c := CreateOpenAPIIndexConfig() + c.FSHandler = FS{} + + index := NewSpecIndexWithConfig(&rootNode, c) + + // extract crs param from index + crsParam := index.GetMappedReferences()["some-file-that-does-not-exist.yaml"] + assert.NotNil(t, crsParam) + assert.True(t, crsParam.IsRemote) + assert.Equal(t, "string", crsParam.Node.Content[1].Value) + assert.Equal(t, "something", crsParam.Node.Content[3].Value) + assert.Equal(t, "query", crsParam.Node.Content[5].Value) +} + func TestSpecIndex_UseRemoteHandler_Error_Open(t *testing.T) { spec := `openapi: 3.1.0 @@ -317,7 +346,7 @@ paths: _ = yaml.Unmarshal([]byte(spec), &rootNode) c := CreateOpenAPIIndexConfig() - c.RemoteHandler = FSBadOpen{} + c.FSHandler = FSBadOpen{} c.RemoteURLHandler = httpClient.Get index := NewSpecIndexWithConfig(&rootNode, c) @@ -327,6 +356,32 @@ paths: assert.Equal(t, "component 'https://-i-cannot-be-opened.com' does not exist in the specification", index.GetReferenceIndexErrors()[1].Error()) } +func TestSpecIndex_UseFileHandler_Error_Open(t *testing.T) { + + spec := `openapi: 3.1.0 +info: + title: Test File Handler + version: 1.0.0 +paths: + /test: + get: + parameters: + - $ref: "I-can-never-be-opened.yaml"` + + var rootNode yaml.Node + _ = yaml.Unmarshal([]byte(spec), &rootNode) + + c := CreateOpenAPIIndexConfig() + c.FSHandler = FSBadOpen{} + c.RemoteURLHandler = httpClient.Get + + index := NewSpecIndexWithConfig(&rootNode, c) + + assert.Len(t, index.GetReferenceIndexErrors(), 2) + assert.Equal(t, "unable to open file: bad file open", index.GetReferenceIndexErrors()[0].Error()) + assert.Equal(t, "component 'I-can-never-be-opened.yaml' does not exist in the specification", index.GetReferenceIndexErrors()[1].Error()) +} + func TestSpecIndex_UseRemoteHandler_Error_Read(t *testing.T) { spec := `openapi: 3.1.0 @@ -343,7 +398,7 @@ paths: _ = yaml.Unmarshal([]byte(spec), &rootNode) c := CreateOpenAPIIndexConfig() - c.RemoteHandler = FSBadRead{} + c.FSHandler = FSBadRead{} c.RemoteURLHandler = httpClient.Get index := NewSpecIndexWithConfig(&rootNode, c) @@ -352,3 +407,29 @@ paths: assert.Equal(t, "unable to read remote file bytes: bad file read", index.GetReferenceIndexErrors()[0].Error()) assert.Equal(t, "component 'https://-i-cannot-be-opened.com' does not exist in the specification", index.GetReferenceIndexErrors()[1].Error()) } + +func TestSpecIndex_UseFileHandler_Error_Read(t *testing.T) { + + spec := `openapi: 3.1.0 +info: + title: Test File Handler + version: 1.0.0 +paths: + /test: + get: + parameters: + - $ref: "I-am-impossible-to-open-forever.yaml"` + + var rootNode yaml.Node + _ = yaml.Unmarshal([]byte(spec), &rootNode) + + c := CreateOpenAPIIndexConfig() + c.FSHandler = FSBadRead{} + c.RemoteURLHandler = httpClient.Get + + index := NewSpecIndexWithConfig(&rootNode, c) + + assert.Len(t, index.GetReferenceIndexErrors(), 2) + assert.Equal(t, "unable to read file bytes: bad file read", index.GetReferenceIndexErrors()[0].Error()) + assert.Equal(t, "component 'I-am-impossible-to-open-forever.yaml' does not exist in the specification", index.GetReferenceIndexErrors()[1].Error()) +} diff --git a/index/index_model.go b/index/index_model.go index 975c576..e76126b 100644 --- a/index/index_model.go +++ b/index/index_model.go @@ -67,10 +67,19 @@ type SpecIndexConfig struct { // Resolves [#132]: https://github.com/pb33f/libopenapi/issues/132 RemoteURLHandler func(url string) (*http.Response, error) - // RemoteHandler is a function that will be used to fetch remote documents, it trumps the RemoteURLHandler - // and will be used instead if it is set. + // FSHandler is an entity that implements the `fs.FS` interface that will be used to fetch local or remote documents. + // This is useful if you want to use a custom file system handler, or if you want to use a custom http client or + // custom network implementation for a lookup. + // + // libopenapi will pass the path to the FSHandler, and it will be up to the handler to determine how to fetch + // the document. This is really useful if your application has a custom file system or uses a database for storing + // documents. + // + // Is the FSHandler is set, it will be used for all lookups, regardless of whether they are local or remote. + // it also overrides the RemoteURLHandler if set. + // // Resolves[#85] https://github.com/pb33f/libopenapi/issues/85 - RemoteHandler fs.FS + FSHandler fs.FS // If resolving locally, the BasePath will be the root from which relative references will be resolved from BasePath string // set the Base Path for resolving relative references if the spec is exploded. diff --git a/utils/utils.go b/utils/utils.go index 61fe73a..e384906 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -196,9 +196,9 @@ func FindFirstKeyNode(key string, nodes []*yaml.Node, depth int) (keyNode *yaml. for i, v := range nodes { if key != "" && key == v.Value { if i+1 >= len(nodes) { - return v, nodes[i] // this is the node we need. + return v, NodeAlias(nodes[i]) // this is the node we need. } - return v, nodes[i+1] // next node is what we need. + return v, NodeAlias(nodes[i+1]) // next node is what we need. } if len(v.Content) > 0 { depth++ @@ -283,12 +283,12 @@ func FindKeyNodeFull(key string, nodes []*yaml.Node) (keyNode *yaml.Node, labelN if key == v.Content[x].Value { if IsNodeMap(v) { if x+1 == len(v.Content) { - return v, v.Content[x], v.Content[x] + return v, v.Content[x], NodeAlias(v.Content[x]) } - return v, v.Content[x], v.Content[x+1] + return v, v.Content[x], NodeAlias(v.Content[x+1]) } if IsNodeArray(v) { - return v, v.Content[x], v.Content[x] + return v, v.Content[x], NodeAlias(v.Content[x]) } } } @@ -304,7 +304,7 @@ func FindKeyNodeFullTop(key string, nodes []*yaml.Node) (keyNode *yaml.Node, lab continue } if i%2 == 0 && key == nodes[i].Value { - return nodes[i], nodes[i], nodes[i+1] // next node is what we need. + return nodes[i], nodes[i], NodeAlias(nodes[i+1]) // next node is what we need. } } return nil, nil, nil @@ -322,7 +322,7 @@ func FindExtensionNodes(nodes []*yaml.Node) []*ExtensionNode { if i+1 < len(nodes) { extensions = append(extensions, &ExtensionNode{ Key: v, - Value: nodes[i+1], + Value: NodeAlias(nodes[i+1]), }) } } @@ -363,12 +363,38 @@ func IsNodeMap(node *yaml.Node) bool { if node == nil { return false } - return node.Tag == "!!map" + n := NodeAlias(node) + return n.Tag == "!!map" +} + +// IsNodeAlias checks if the node is an alias, and lifts out the anchor +func IsNodeAlias(node *yaml.Node) (*yaml.Node, bool) { + if node == nil { + return nil, false + } + if node.Kind == yaml.AliasNode { + node = node.Alias + return node, true + } + return node, false +} + +// NodeAlias checks if the node is an alias, and lifts out the anchor +func NodeAlias(node *yaml.Node) *yaml.Node { + if node == nil { + return nil + } + if node.Kind == yaml.AliasNode { + node = node.Alias + return node + } + return node } // IsNodePolyMorphic will return true if the node contains polymorphic keys. func IsNodePolyMorphic(node *yaml.Node) bool { - for i, v := range node.Content { + n := NodeAlias(node) + for i, v := range n.Content { if i%2 == 0 { if v.Value == "anyOf" || v.Value == "oneOf" || v.Value == "allOf" { return true @@ -383,7 +409,8 @@ func IsNodeArray(node *yaml.Node) bool { if node == nil { return false } - return node.Tag == "!!seq" + n := NodeAlias(node) + return n.Tag == "!!seq" } // IsNodeStringValue checks if a node is a string value @@ -391,7 +418,8 @@ func IsNodeStringValue(node *yaml.Node) bool { if node == nil { return false } - return node.Tag == "!!str" + n := NodeAlias(node) + return n.Tag == "!!str" } // IsNodeIntValue will check if a node is an int value @@ -399,7 +427,8 @@ func IsNodeIntValue(node *yaml.Node) bool { if node == nil { return false } - return node.Tag == "!!int" + n := NodeAlias(node) + return n.Tag == "!!int" } // IsNodeFloatValue will check is a node is a float value. @@ -407,7 +436,8 @@ func IsNodeFloatValue(node *yaml.Node) bool { if node == nil { return false } - return node.Tag == "!!float" + n := NodeAlias(node) + return n.Tag == "!!float" } // IsNodeNumberValue will check if a node can be parsed as a float value. @@ -423,18 +453,20 @@ func IsNodeBoolValue(node *yaml.Node) bool { if node == nil { return false } - return node.Tag == "!!bool" + n := NodeAlias(node) + return n.Tag == "!!bool" } func IsNodeRefValue(node *yaml.Node) (bool, *yaml.Node, string) { + if node == nil { return false, nil, "" } - - for i, r := range node.Content { + n := NodeAlias(node) + for i, r := range n.Content { if i%2 == 0 { if r.Value == "$ref" { - return true, r, node.Content[i+1].Value + return true, r, n.Content[i+1].Value } } } @@ -691,3 +723,26 @@ func DetermineWhitespaceLength(input string) int { return 0 } } + +// CheckForMergeNodes will check the top level of the schema for merge nodes. If any are found, then the merged nodes +// will be appended to the end of the rest of the nodes in the schema. +// Note: this is a destructive operation, so the in-memory node structure will be modified +func CheckForMergeNodes(node *yaml.Node) { + if node == nil { + return + } + total := len(node.Content) + for i := 0; i < total; i++ { + mn := node.Content[i] + if i%2 == 0 { + if mn.Tag == "!!merge" { + an := node.Content[i+1].Alias + if an != nil { + node.Content = append(node.Content, an.Content...) // append the merged nodes + total = len(node.Content) + i += 2 + } + } + } + } +} diff --git a/utils/utils_test.go b/utils/utils_test.go index c78c433..4165f97 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -571,20 +571,20 @@ func TestIsNodeFloatValue(t *testing.T) { } func TestIsNodeNumberValue(t *testing.T) { - n := &yaml.Node{ - Tag: "!!float", - } - assert.True(t, IsNodeNumberValue(n)) - n.Tag = "!!pizza" - assert.False(t, IsNodeNumberValue(n)) + n := &yaml.Node{ + Tag: "!!float", + } + assert.True(t, IsNodeNumberValue(n)) + n.Tag = "!!pizza" + assert.False(t, IsNodeNumberValue(n)) - n = &yaml.Node{ - Tag: "!!int", - } - assert.True(t, IsNodeNumberValue(n)) - n.Tag = "!!pizza" - assert.False(t, IsNodeNumberValue(n)) - assert.False(t, IsNodeNumberValue(nil)) + n = &yaml.Node{ + Tag: "!!int", + } + assert.True(t, IsNodeNumberValue(n)) + n.Tag = "!!pizza" + assert.False(t, IsNodeNumberValue(n)) + assert.False(t, IsNodeNumberValue(nil)) } func TestIsNodeFloatValue_Nil(t *testing.T) { @@ -767,6 +767,69 @@ func TestIsNodeRefValue(t *testing.T) { } +func TestIsNodeAlias(t *testing.T) { + + yml := `things: + &anchorA + - Stuff + - Junk +thangs: *anchorA` + + var node yaml.Node + _ = yaml.Unmarshal([]byte(yml), &node) + + ref, a := IsNodeAlias(node.Content[0].Content[3]) + + assert.True(t, a) + assert.Len(t, ref.Content, 2) + +} + +func TestNodeAlias(t *testing.T) { + + yml := `things: + &anchorA + - Stuff + - Junk +thangs: *anchorA` + + var node yaml.Node + _ = yaml.Unmarshal([]byte(yml), &node) + + ref := NodeAlias(node.Content[0].Content[3]) + + assert.Len(t, ref.Content, 2) + +} + +func TestCheckForMergeNodes(t *testing.T) { + + yml := `x-common-definitions: + life_cycle_types: &life_cycle_types_def + type: string + enum: ["Onboarding", "Monitoring", "Re-Assessment"] + description: The type of life cycle +<<: *life_cycle_types_def` + + var node yaml.Node + _ = yaml.Unmarshal([]byte(yml), &node) + + mainNode := node.Content[0] + + CheckForMergeNodes(mainNode) + + _, _, enumVal := FindKeyNodeFullTop("enum", mainNode.Content) + _, _, descriptionVal := FindKeyNodeFullTop("description", mainNode.Content) + + assert.Equal(t, "The type of life cycle", descriptionVal.Value) + assert.Len(t, enumVal.Content, 3) + +} + +func TestCheckForMergeNodes_Empty_NoPanic(t *testing.T) { + CheckForMergeNodes(nil) +} + func TestIsNodeRefValue_False(t *testing.T) { f := &yaml.Node{