diff --git a/datamodel/low/arazzo/helpers.go b/datamodel/low/arazzo/helpers.go index eab88049..4bf98431 100644 --- a/datamodel/low/arazzo/helpers.go +++ b/datamodel/low/arazzo/helpers.go @@ -32,9 +32,8 @@ type arazzoBase struct { // It returns the resolved root node (after alias/merge processing) for further extraction. func initBuild(b *arazzoBase, ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) *yaml.Node { *b.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) *b.RootNode = root - utils.CheckForMergeNodes(root) *b.Reference = new(low.Reference) b.NodeMap.Nodes = low.ExtractNodes(ctx, root) ext := low.ExtractExtensions(root) diff --git a/datamodel/low/base/contact.go b/datamodel/low/base/contact.go index 3e157a4c..0d442a58 100644 --- a/datamodel/low/base/contact.go +++ b/datamodel/low/base/contact.go @@ -11,6 +11,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" ) // Contact represents a low-level representation of the Contact definitions found at @@ -34,6 +35,7 @@ type Contact struct { func (c *Contact) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { c.KeyNode = keyNode + root = utils.ResolveMergeNodes(root) c.RootNode = root c.reference = low.Reference{} c.Reference = &c.reference diff --git a/datamodel/low/base/discriminator.go b/datamodel/low/base/discriminator.go index 5bffb871..9b5b4306 100644 --- a/datamodel/low/base/discriminator.go +++ b/datamodel/low/base/discriminator.go @@ -39,7 +39,7 @@ func ValidateDiscriminatorMappingValueNodes(discriminatorNode *yaml.Node) error if discriminatorNode == nil || discriminatorNode.Kind != yaml.MappingNode { return nil } - utils.CheckForMergeNodes(discriminatorNode) + discriminatorNode = utils.ResolveMergeNodes(discriminatorNode) for i := 0; i < len(discriminatorNode.Content); i += 2 { keyNode := utils.NodeAlias(discriminatorNode.Content[i]) @@ -54,7 +54,7 @@ func ValidateDiscriminatorMappingValueNodes(discriminatorNode *yaml.Node) error if mappingNode == nil || mappingNode.Kind != yaml.MappingNode { return fmt.Errorf("discriminator.mapping must be an object") } - utils.CheckForMergeNodes(mappingNode) + mappingNode = utils.ResolveMergeNodes(mappingNode) for j := 0; j < len(mappingNode.Content); j += 2 { keyNode := utils.NodeAlias(mappingNode.Content[j]) diff --git a/datamodel/low/base/example.go b/datamodel/low/base/example.go index 67964599..25a69a1b 100644 --- a/datamodel/low/base/example.go +++ b/datamodel/low/base/example.go @@ -93,9 +93,8 @@ func (ex *Example) Build(ctx context.Context, keyNode, root *yaml.Node, idx *ind if ok, _, ref := utils.IsNodeRefValue(root); ok { ex.SetReference(ref, root) } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) ex.RootNode = root - utils.CheckForMergeNodes(root) ex.nodeStore = low.NodeLines{} ex.Nodes = &ex.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/base/external_doc.go b/datamodel/low/base/external_doc.go index e8b81a54..6e5c4fbb 100644 --- a/datamodel/low/base/external_doc.go +++ b/datamodel/low/base/external_doc.go @@ -63,9 +63,8 @@ func (ex *ExternalDoc) Build(ctx context.Context, keyNode, root *yaml.Node, idx ex.Extensions = nil return nil } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) ex.RootNode = root - utils.CheckForMergeNodes(root) if len(root.Content) > 0 { ex.NodeMap.ExtractNodes(root, false) } else { diff --git a/datamodel/low/base/info.go b/datamodel/low/base/info.go index 073a9130..259f5cd4 100644 --- a/datamodel/low/base/info.go +++ b/datamodel/low/base/info.go @@ -75,9 +75,8 @@ func (i *Info) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.S i.Extensions = nil return nil } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) i.RootNode = root - utils.CheckForMergeNodes(root) if len(root.Content) > 0 { i.NodeMap.ExtractNodes(root, false) } else { diff --git a/datamodel/low/base/license.go b/datamodel/low/base/license.go index 7ef7e8b4..86994066 100644 --- a/datamodel/low/base/license.go +++ b/datamodel/low/base/license.go @@ -47,9 +47,8 @@ func (l *License) Build(ctx context.Context, keyNode, root *yaml.Node, idx *inde l.Extensions = nil return nil } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) l.RootNode = root - utils.CheckForMergeNodes(root) if len(root.Content) > 0 { l.NodeMap.ExtractNodes(root, false) } else { diff --git a/datamodel/low/base/schema_build.go b/datamodel/low/base/schema_build.go index ef3f4708..a414f25f 100644 --- a/datamodel/low/base/schema_build.go +++ b/datamodel/low/base/schema_build.go @@ -43,8 +43,7 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde if root == nil { return fmt.Errorf("cannot build schema from a nil node") } - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) s.reference = low.Reference{} s.Reference = &s.reference @@ -71,7 +70,7 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde if h, _, _ := utils.IsNodeRefValue(root); h { ref, foundIdx, err, fctx := low.LocateRefNodeWithContext(ctx, root, idx) if ref != nil { - root = ref + root = utils.ResolveMergeNodes(ref) if fctx != nil { ctx = fctx s.context = ctx @@ -278,13 +277,14 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde if err := ValidateDiscriminatorMappingValueNodes(discNode); err != nil { return err } + effective := utils.ResolveMergeNodes(discNode) var discriminator Discriminator - _ = low.BuildModel(discNode, &discriminator) + _ = low.BuildModel(effective, &discriminator) discriminator.KeyNode = discLabel - discriminator.RootNode = discNode - discriminator.Nodes = low.ExtractNodes(ctx, discNode) + discriminator.RootNode = effective + discriminator.Nodes = low.ExtractNodes(ctx, effective) s.Discriminator = low.NodeReference[*Discriminator]{Value: &discriminator, KeyNode: discLabel, ValueNode: discNode} - low.AppendRecursiveNodes(&discriminator, discNode) + low.AppendRecursiveNodes(&discriminator, effective) } _, extDocLabel, extDocNode := utils.FindKeyNodeFullTop(ExternalDocsLabel, root.Content) @@ -292,7 +292,7 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde var exDoc ExternalDoc _ = low.BuildModel(extDocNode, &exDoc) _ = exDoc.Build(ctx, extDocLabel, extDocNode, idx) - exDoc.Nodes = low.ExtractNodes(ctx, extDocNode) + exDoc.Nodes = low.ExtractNodes(ctx, exDoc.RootNode) s.ExternalDocs = low.NodeReference[*ExternalDoc]{Value: &exDoc, KeyNode: extDocLabel, ValueNode: extDocNode} } @@ -301,7 +301,7 @@ func (s *Schema) Build(ctx context.Context, root *yaml.Node, idx *index.SpecInde var xml XML _ = low.BuildModel(xmlNode, &xml) _ = xml.Build(xmlNode, idx) - xml.Nodes = low.ExtractNodes(ctx, xmlNode) + xml.Nodes = low.ExtractNodes(ctx, xml.RootNode) s.XML = low.NodeReference[*XML]{Value: &xml, KeyNode: xmlLabel, ValueNode: xmlNode} } diff --git a/datamodel/low/base/schema_proxy.go b/datamodel/low/base/schema_proxy.go index be46091a..c2eae19f 100644 --- a/datamodel/low/base/schema_proxy.go +++ b/datamodel/low/base/schema_proxy.go @@ -240,7 +240,6 @@ func (sp *SchemaProxy) Schema() *Schema { } schema := new(Schema) - utils.CheckForMergeNodes(buildNode) err := schema.Build(sp.ctx, buildNode, sp.idx) if err != nil { sp.buildError = err diff --git a/datamodel/low/base/security_requirement.go b/datamodel/low/base/security_requirement.go index 19f5fda4..f8918702 100644 --- a/datamodel/low/base/security_requirement.go +++ b/datamodel/low/base/security_requirement.go @@ -64,9 +64,8 @@ func (s *SecurityRequirement) Build(ctx context.Context, keyNode, root *yaml.Nod } return nil } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) s.RootNode = root - utils.CheckForMergeNodes(root) if len(root.Content) > 0 { s.NodeMap.ExtractNodes(root, false) } else { diff --git a/datamodel/low/base/tag.go b/datamodel/low/base/tag.go index c900df85..bfd68905 100644 --- a/datamodel/low/base/tag.go +++ b/datamodel/low/base/tag.go @@ -65,9 +65,8 @@ func (t *Tag) GetKeyNode() *yaml.Node { // Build will extract extensions and external docs for the Tag. func (t *Tag) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { t.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) t.RootNode = root - utils.CheckForMergeNodes(root) t.Reference = new(low.Reference) t.Nodes = low.ExtractNodes(ctx, root) t.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/base/xml.go b/datamodel/low/base/xml.go index cccde742..abab17ba 100644 --- a/datamodel/low/base/xml.go +++ b/datamodel/low/base/xml.go @@ -52,8 +52,7 @@ func (x *XML) Build(root *yaml.Node, idx *index.SpecIndex) error { x.Extensions = nil return nil } - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) x.RootNode = root if len(root.Content) > 0 { x.NodeMap.ExtractNodes(root, false) diff --git a/datamodel/low/extraction_functions.go b/datamodel/low/extraction_functions.go index b0ceb04c..7ef955f7 100644 --- a/datamodel/low/extraction_functions.go +++ b/datamodel/low/extraction_functions.go @@ -746,8 +746,7 @@ func ExtractMapNoLookupExtensions[PT Buildable[N], N any]( ) (*orderedmap.Map[KeyReference[string], ValueReference[PT]], error) { valueMap := orderedmap.New[KeyReference[string], ValueReference[PT]]() var circError error - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) if utils.IsNodeMap(root) { var currentKey *yaml.Node skip := false diff --git a/datamodel/low/model_builder.go b/datamodel/low/model_builder.go index 725bc6e0..0cb09622 100644 --- a/datamodel/low/model_builder.go +++ b/datamodel/low/model_builder.go @@ -71,8 +71,7 @@ func BuildModel(node *yaml.Node, model interface{}) error { if node == nil { return nil } - node = utils.NodeAlias(node) - utils.CheckForMergeNodes(node) + node = utils.ResolveMergeNodes(node) if reflect.ValueOf(model).Type().Kind() != reflect.Pointer { return fmt.Errorf("cannot build model on non-pointer: %v", reflect.ValueOf(model).Type().Kind()) @@ -147,6 +146,13 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { return } + // Typed map fields read effective contents without rewriting the source + // mapping retained by NodeReference metadata or another model builder. + content := valueNode.Content + if utils.IsNodeMap(valueNode) { + content = utils.ResolveMergeNodes(valueNode).Content + } + switch field.Type() { case setFieldMapStringNodeReferenceYAMLNodeType: @@ -155,7 +161,7 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if field.CanSet() { items := orderedmap.New[string, NodeReference[*yaml.Node]]() var currentLabel string - for i, sliceItem := range valueNode.Content { + for i, sliceItem := range content { if i%2 == 0 { currentLabel = sliceItem.Value continue @@ -176,7 +182,7 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if field.CanSet() { items := orderedmap.New[string, NodeReference[string]]() var currentLabel string - for i, sliceItem := range valueNode.Content { + for i, sliceItem := range content { if i%2 == 0 { currentLabel = sliceItem.Value continue @@ -202,8 +208,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]NodeReference[*yaml.Node], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]NodeReference[*yaml.Node], 0, len(content)) + for _, sliceItem := range content { items = append(items, NodeReference[*yaml.Node]{ Value: sliceItem, ValueNode: sliceItem, @@ -309,8 +315,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]NodeReference[string], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]NodeReference[string], 0, len(content)) + for _, sliceItem := range content { items = append(items, NodeReference[string]{ Value: sliceItem.Value, ValueNode: sliceItem, @@ -325,8 +331,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]NodeReference[float32], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]NodeReference[float32], 0, len(content)) + for _, sliceItem := range content { fv, _ := strconv.ParseFloat(sliceItem.Value, 32) items = append(items, NodeReference[float32]{ Value: float32(fv), @@ -342,8 +348,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]NodeReference[float64], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]NodeReference[float64], 0, len(content)) + for _, sliceItem := range content { fv, _ := strconv.ParseFloat(sliceItem.Value, 64) items = append(items, NodeReference[float64]{Value: fv, ValueNode: sliceItem}) } @@ -355,8 +361,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]NodeReference[int], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]NodeReference[int], 0, len(content)) + for _, sliceItem := range content { iv, _ := strconv.Atoi(sliceItem.Value) items = append(items, NodeReference[int]{ Value: iv, @@ -372,8 +378,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]NodeReference[int64], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]NodeReference[int64], 0, len(content)) + for _, sliceItem := range content { iv, _ := strconv.ParseInt(sliceItem.Value, 10, 64) items = append(items, NodeReference[int64]{ Value: iv, @@ -389,8 +395,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]NodeReference[bool], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]NodeReference[bool], 0, len(content)) + for _, sliceItem := range content { bv, _ := strconv.ParseBool(sliceItem.Value) items = append(items, NodeReference[bool]{ Value: bv, @@ -408,9 +414,9 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeMap(valueNode) { if field.CanSet() { items := orderedmap.New[KeyReference[string], ValueReference[string]]() - for i := 0; i < len(valueNode.Content)-1; i += 2 { - cf := valueNode.Content[i] - sliceItem := valueNode.Content[i+1] + for i := 0; i < len(content)-1; i += 2 { + cf := content[i] + sliceItem := content[i+1] items.Set(KeyReference[string]{ Value: cf.Value, KeyNode: cf, @@ -428,9 +434,9 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeMap(valueNode) { if field.CanSet() { items := orderedmap.New[KeyReference[string], ValueReference[string]]() - for i := 0; i < len(valueNode.Content)-1; i += 2 { - cf := valueNode.Content[i] - sliceItem := valueNode.Content[i+1] + for i := 0; i < len(content)-1; i += 2 { + cf := content[i] + sliceItem := content[i+1] items.Set(KeyReference[string]{ Value: cf.Value, KeyNode: cf, @@ -450,9 +456,9 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeMap(valueNode) { if field.CanSet() { items := orderedmap.New[KeyReference[string], ValueReference[string]]() - for i := 0; i < len(valueNode.Content)-1; i += 2 { - cf := valueNode.Content[i] - sliceItem := valueNode.Content[i+1] + for i := 0; i < len(content)-1; i += 2 { + cf := content[i] + sliceItem := content[i+1] items.Set(KeyReference[string]{ Value: cf.Value, KeyNode: cf, @@ -473,8 +479,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]ValueReference[string], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]ValueReference[string], 0, len(content)) + for _, sliceItem := range content { items = append(items, ValueReference[string]{ Value: sliceItem.Value, ValueNode: sliceItem, @@ -493,8 +499,8 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) { if utils.IsNodeArray(valueNode) { if field.CanSet() { - items := make([]ValueReference[*yaml.Node], 0, len(valueNode.Content)) - for _, sliceItem := range valueNode.Content { + items := make([]ValueReference[*yaml.Node], 0, len(content)) + for _, sliceItem := range content { items = append(items, ValueReference[*yaml.Node]{ Value: sliceItem, ValueNode: sliceItem, diff --git a/datamodel/low/model_builder_merge_test.go b/datamodel/low/model_builder_merge_test.go new file mode 100644 index 00000000..64938923 --- /dev/null +++ b/datamodel/low/model_builder_merge_test.go @@ -0,0 +1,102 @@ +package low + +import ( + "testing" + + "github.com/pb33f/go-yaml" + "github.com/pb33f/libopenapi/orderedmap" + "github.com/pb33f/libopenapi/utils" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" +) + +func TestBuildModelPreservesMergedSource(t *testing.T) { + const source = `first: &first + name: inherited # original value + fat: 2 +second: &second + name: later + ketchup: 3 +sources: &sources [*first, *second] +item: + <<: *sources + fat: 5 +` + var document yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(source), &document)) + _, item := utils.FindKeyNodeTop("item", document.Content[0].Content) + require.NotNil(t, item) + before, err := yaml.Marshal(&document) + require.NoError(t, err) + _, first := utils.FindKeyNodeTop("first", document.Content[0].Content) + nameKey, nameValue := utils.FindKeyNodeTop("name", first.Content) + var result hotdog + require.NoError(t, BuildModel(item, &result)) + assert.Equal(t, "inherited", result.Name.Value) + assert.Equal(t, 5, result.Fat.Value) + assert.Equal(t, float32(3), result.Ketchup.Value) + assert.Same(t, nameKey, result.Name.KeyNode) + assert.Same(t, nameValue, result.Name.ValueNode) + assert.Equal(t, "# original value", result.Name.ValueNode.LineComment) + after, err := yaml.Marshal(&document) + require.NoError(t, err) + assert.Equal(t, string(before), string(after), "model construction must not replace shared source content") +} + +func TestBuildModelMergedMapFields(t *testing.T) { + const source = `defaults: &defaults + inherited: original + local: overridden +mapped: &mapped + <<: *defaults + local: selected +a: *mapped +b: *mapped +c: *mapped +d: *mapped +e: *mapped +` + var document yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(source), &document)) + before, err := yaml.Marshal(&document) + require.NoError(t, err) + var model struct { + A *orderedmap.Map[string, NodeReference[*yaml.Node]] + B *orderedmap.Map[string, NodeReference[string]] + C *orderedmap.Map[KeyReference[string], ValueReference[string]] + D KeyReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]] + E NodeReference[*orderedmap.Map[KeyReference[string], ValueReference[string]]] + } + require.NoError(t, BuildModel(document.Content[0], &model)) + t.Run("node values", func(t *testing.T) { + inherited, ok := model.A.Get("inherited") + require.True(t, ok) + assert.Equal(t, "original", inherited.Value.Value) + local, ok := model.A.Get("local") + require.True(t, ok) + assert.Equal(t, "selected", local.Value.Value) + }) + t.Run("string values", func(t *testing.T) { + inherited, ok := model.B.Get("inherited") + require.True(t, ok) + assert.Equal(t, "original", inherited.Value) + local, ok := model.B.Get("local") + require.True(t, ok) + assert.Equal(t, "selected", local.Value) + }) + for name, values := range map[string]*orderedmap.Map[KeyReference[string], ValueReference[string]]{ + "plain map": model.C, "key reference": model.D.Value, "node reference": model.E.Value, + } { + t.Run(name, func(t *testing.T) { + require.NotNil(t, values) + got := make(map[string]string) + for key, value := range values.FromOldest() { + got[key.Value] = value.Value + } + assert.Equal(t, map[string]string{"inherited": "original", "local": "selected"}, got) + }) + } + after, err := yaml.Marshal(&document) + require.NoError(t, err) + assert.Equal(t, string(before), string(after)) +} diff --git a/datamodel/low/overlay/action.go b/datamodel/low/overlay/action.go index 234a8b7d..c425725e 100644 --- a/datamodel/low/overlay/action.go +++ b/datamodel/low/overlay/action.go @@ -64,9 +64,8 @@ func (a *Action) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index return err } a.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) a.RootNode = root - utils.CheckForMergeNodes(root) a.Reference = new(low.Reference) a.Nodes = low.ExtractNodes(ctx, root) a.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/overlay/components.go b/datamodel/low/overlay/components.go index b5a35dcb..e466bb6c 100644 --- a/datamodel/low/overlay/components.go +++ b/datamodel/low/overlay/components.go @@ -59,9 +59,8 @@ func (i *Components) Build(ctx context.Context, keyNode, root *yaml.Node, idx *i return err } i.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) i.RootNode = root - utils.CheckForMergeNodes(root) i.Reference = new(low.Reference) i.Nodes = low.ExtractNodes(ctx, root) i.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/overlay/info.go b/datamodel/low/overlay/info.go index fe65f7bf..36323b98 100644 --- a/datamodel/low/overlay/info.go +++ b/datamodel/low/overlay/info.go @@ -61,9 +61,8 @@ func (i *Info) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.S return err } i.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) i.RootNode = root - utils.CheckForMergeNodes(root) if _, ref := findField(RefLabel, root); ref != nil { return fmt.Errorf("info does not support $ref") } diff --git a/datamodel/low/overlay/overlay.go b/datamodel/low/overlay/overlay.go index 01abdfdc..df4a2267 100644 --- a/datamodel/low/overlay/overlay.go +++ b/datamodel/low/overlay/overlay.go @@ -64,9 +64,8 @@ func (o *Overlay) Build(ctx context.Context, keyNode, root *yaml.Node, idx *inde return err } o.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) o.RootNode = root - utils.CheckForMergeNodes(root) o.Reference = new(low.Reference) o.Nodes = low.ExtractNodes(ctx, root) o.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/overlay/reusable_action.go b/datamodel/low/overlay/reusable_action.go index 2fc589c0..bd81c5eb 100644 --- a/datamodel/low/overlay/reusable_action.go +++ b/datamodel/low/overlay/reusable_action.go @@ -59,9 +59,8 @@ func (i *ReusableAction) Build(ctx context.Context, keyNode, root *yaml.Node, id return err } i.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) i.RootNode = root - utils.CheckForMergeNodes(root) i.Reference = new(low.Reference) i.Nodes = low.ExtractNodes(ctx, root) i.Extensions = low.ExtractExtensions(root) diff --git a/datamodel/low/v2/definitions.go b/datamodel/low/v2/definitions.go index 532d51a9..fb824ad1 100644 --- a/datamodel/low/v2/definitions.go +++ b/datamodel/low/v2/definitions.go @@ -75,8 +75,7 @@ func (s *SecurityDefinitions) FindSecurityDefinition(securityDef string) *low.Va // Build will extract all definitions into SchemaProxy instances. func (d *Definitions) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) type buildInput struct { label *yaml.Node value *yaml.Node diff --git a/datamodel/low/v2/examples.go b/datamodel/low/v2/examples.go index 03204694..5e90cd95 100644 --- a/datamodel/low/v2/examples.go +++ b/datamodel/low/v2/examples.go @@ -28,8 +28,7 @@ func (e *Examples) FindExample(name string) *low.ValueReference[*yaml.Node] { // Build will extract all examples and will attempt to unmarshal content into a map or slice based on type. func (e *Examples) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) var keyNode, currNode *yaml.Node e.Values = orderedmap.New[low.KeyReference[string], low.ValueReference[*yaml.Node]]() for i := range root.Content { diff --git a/datamodel/low/v2/header.go b/datamodel/low/v2/header.go index 23984bb0..d64050e8 100644 --- a/datamodel/low/v2/header.go +++ b/datamodel/low/v2/header.go @@ -53,8 +53,7 @@ func (h *Header) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.V // Build will build out items, extensions and default value from the supplied node. func (h *Header) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) h.Extensions = low.ExtractExtensions(root) items, err := low.ExtractObject[*Items](ctx, ItemsLabel, root, idx) if err != nil { diff --git a/datamodel/low/v2/items.go b/datamodel/low/v2/items.go index 394beb5c..8d6beeb6 100644 --- a/datamodel/low/v2/items.go +++ b/datamodel/low/v2/items.go @@ -112,8 +112,7 @@ func (itm *Items) Hash() uint64 { // Build will build out items and default value. func (i *Items) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) i.Extensions = low.ExtractExtensions(root) items, iErr := low.ExtractObject[*Items](ctx, ItemsLabel, root, idx) if iErr != nil { diff --git a/datamodel/low/v2/operation.go b/datamodel/low/v2/operation.go index 61a4f747..2e9b8443 100644 --- a/datamodel/low/v2/operation.go +++ b/datamodel/low/v2/operation.go @@ -38,8 +38,7 @@ type Operation struct { // Build will extract external docs, extensions, parameters, responses and security requirements. func (o *Operation) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) o.Extensions = low.ExtractExtensions(root) // extract externalDocs diff --git a/datamodel/low/v2/parameter.go b/datamodel/low/v2/parameter.go index 55825f21..b793830a 100644 --- a/datamodel/low/v2/parameter.go +++ b/datamodel/low/v2/parameter.go @@ -96,8 +96,7 @@ func (p *Parameter) GetExtensions() *orderedmap.Map[low.KeyReference[string], lo // Build will extract out extensions, schema, items and default value func (p *Parameter) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) p.Extensions = low.ExtractExtensions(root) sch, sErr := base.ExtractSchema(ctx, root, idx) if sErr != nil { diff --git a/datamodel/low/v2/path_item.go b/datamodel/low/v2/path_item.go index c963ec9e..ca2a1969 100644 --- a/datamodel/low/v2/path_item.go +++ b/datamodel/low/v2/path_item.go @@ -51,8 +51,7 @@ func (p *PathItem) GetExtensions() *orderedmap.Map[low.KeyReference[string], low // 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(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) p.Extensions = low.ExtractExtensions(root) skip := false var currentNode *yaml.Node diff --git a/datamodel/low/v2/paths.go b/datamodel/low/v2/paths.go index 1625aff2..7d68b443 100644 --- a/datamodel/low/v2/paths.go +++ b/datamodel/low/v2/paths.go @@ -57,8 +57,7 @@ func (p *Paths) FindExtension(ext string) *low.ValueReference[*yaml.Node] { // Build will extract extensions and paths from node. func (p *Paths) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) p.Extensions = low.ExtractExtensions(root) // Translate YAML nodes to pathsMap using `TranslatePipeline`. diff --git a/datamodel/low/v2/response.go b/datamodel/low/v2/response.go index c52fd023..4dbb2a4f 100644 --- a/datamodel/low/v2/response.go +++ b/datamodel/low/v2/response.go @@ -44,8 +44,7 @@ func (r *Response) FindHeader(hType string) *low.ValueReference[*Header] { // Build will extract schema, extensions, examples and headers from node func (r *Response) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) r.Extensions = low.ExtractExtensions(root) s, err := base.ExtractSchema(ctx, root, idx) if err != nil { diff --git a/datamodel/low/v2/responses.go b/datamodel/low/v2/responses.go index 2626401b..92905b84 100644 --- a/datamodel/low/v2/responses.go +++ b/datamodel/low/v2/responses.go @@ -30,8 +30,7 @@ func (r *Responses) GetExtensions() *orderedmap.Map[low.KeyReference[string], lo // Build will extract default value and extensions from node. func (r *Responses) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(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 c3600657..39200365 100644 --- a/datamodel/low/v2/scopes.go +++ b/datamodel/low/v2/scopes.go @@ -37,8 +37,7 @@ func (s *Scopes) FindScope(scope string) *low.ValueReference[string] { // Build will extract scope values and extensions from node. func (s *Scopes) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) s.Extensions = low.ExtractExtensions(root) valueMap := orderedmap.New[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 0ad21a6e..b70fefa8 100644 --- a/datamodel/low/v2/security_scheme.go +++ b/datamodel/low/v2/security_scheme.go @@ -39,8 +39,7 @@ func (ss *SecurityScheme) GetExtensions() *orderedmap.Map[low.KeyReference[strin // Build will extract extensions and scopes from the node. func (ss *SecurityScheme) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) ss.Extensions = low.ExtractExtensions(root) scopes, sErr := low.ExtractObject[*Scopes](ctx, ScopesLabel, root, idx) diff --git a/datamodel/low/v3/callback.go b/datamodel/low/v3/callback.go index 96bae1f2..79342374 100644 --- a/datamodel/low/v3/callback.go +++ b/datamodel/low/v3/callback.go @@ -73,9 +73,8 @@ func (cb *Callback) Build(ctx context.Context, keyNode, root *yaml.Node, idx *in if ok, _, ref := utils.IsNodeRefValue(root); ok { cb.SetReference(ref, root) } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) cb.RootNode = root - utils.CheckForMergeNodes(root) cb.nodeStore = low.NodeLines{} cb.Nodes = &cb.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/components.go b/datamodel/low/v3/components.go index b6fe82ae..12ac3c97 100644 --- a/datamodel/low/v3/components.go +++ b/datamodel/low/v3/components.go @@ -171,8 +171,7 @@ func (co *Components) FindMediaType(mediaType string) *low.ValueReference[*Media // Build converts root YAML node containing components to low level model. // Process each component in parallel. func (co *Components) Build(ctx context.Context, root *yaml.Node, idx *index.SpecIndex) error { - root = utils.NodeAlias(root) - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) co.reference = low.Reference{} co.Reference = &co.reference co.nodeStore = low.NodeLines{} diff --git a/datamodel/low/v3/create_document.go b/datamodel/low/v3/create_document.go index 90808291..153cbb01 100644 --- a/datamodel/low/v3/create_document.go +++ b/datamodel/low/v3/create_document.go @@ -49,7 +49,7 @@ func selectDocumentNode(root *yaml.Node, preferred documentTopLevelNode, label s if root == nil { return documentTopLevelNode{} } - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) if topOnly { _, key, value := utils.FindKeyNodeFullTop(label, root.Content) return documentTopLevelNode{key: key, value: value} @@ -64,7 +64,7 @@ func collectDocumentTopLevelNodes(root *yaml.Node) documentTopLevelNodes { if root == nil { return nodes } - utils.CheckForMergeNodes(root) + root = utils.ResolveMergeNodes(root) content := root.Content for i := 0; i+1 < len(content); i += 2 { @@ -135,7 +135,7 @@ func CreateDocumentFromConfig(info *datamodel.SpecInfo, config *datamodel.Docume } func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfiguration) (*Document, error) { - rootNode := utils.NodeAlias(info.RootNode.Content[0]) + rootNode := utils.ResolveMergeNodes(info.RootNode.Content[0]) topNodes := collectDocumentTopLevelNodes(rootNode) versionNodeRef := selectDocumentNode(rootNode, topNodes.version, OpenAPILabel, false) labelNode, versionNode := versionNodeRef.key, versionNodeRef.value diff --git a/datamodel/low/v3/encoding.go b/datamodel/low/v3/encoding.go index 4d3ee0d5..e20a1f80 100644 --- a/datamodel/low/v3/encoding.go +++ b/datamodel/low/v3/encoding.go @@ -82,9 +82,8 @@ func (en *Encoding) Hash() uint64 { // Build will extract all Header objects from supplied node. func (en *Encoding) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { en.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) en.RootNode = root - utils.CheckForMergeNodes(root) en.nodeStore = low.NodeLines{} en.Nodes = &en.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/header.go b/datamodel/low/v3/header.go index 15740e8c..f83563df 100644 --- a/datamodel/low/v3/header.go +++ b/datamodel/low/v3/header.go @@ -134,9 +134,8 @@ func (h *Header) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index if ok, _, ref := utils.IsNodeRefValue(root); ok { h.SetReference(ref, root) } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) h.RootNode = root - utils.CheckForMergeNodes(root) h.nodeStore = low.NodeLines{} h.Nodes = &h.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/link.go b/datamodel/low/v3/link.go index d7d94eba..5bdd98fd 100644 --- a/datamodel/low/v3/link.go +++ b/datamodel/low/v3/link.go @@ -87,9 +87,8 @@ func (l *Link) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.S if ok, _, ref := utils.IsNodeRefValue(root); ok { l.SetReference(ref, root) } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) l.RootNode = root - utils.CheckForMergeNodes(root) l.nodeStore = low.NodeLines{} l.Nodes = &l.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/media_type.go b/datamodel/low/v3/media_type.go index 39e3ec3a..f93397b0 100644 --- a/datamodel/low/v3/media_type.go +++ b/datamodel/low/v3/media_type.go @@ -86,9 +86,8 @@ func (mt *MediaType) GetKeyNode() *yaml.Node { // Build will extract examples, extensions, schema and encoding from node. func (mt *MediaType) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { mt.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) mt.RootNode = root - utils.CheckForMergeNodes(root) mt.reference = low.Reference{} mt.Reference = &mt.reference mt.nodeStore = low.NodeLines{} diff --git a/datamodel/low/v3/oauth_flows.go b/datamodel/low/v3/oauth_flows.go index 58abeabb..3bd34d46 100644 --- a/datamodel/low/v3/oauth_flows.go +++ b/datamodel/low/v3/oauth_flows.go @@ -66,9 +66,8 @@ func (o *OAuthFlows) GetKeyNode() *yaml.Node { // Build will extract extensions and all OAuthFlow types from the supplied node. func (o *OAuthFlows) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { o.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) o.RootNode = root - utils.CheckForMergeNodes(root) o.reference = low.Reference{} o.Reference = &o.reference o.nodeStore = low.NodeLines{} @@ -190,6 +189,7 @@ func (o *OAuthFlow) GetRootNode() *yaml.Node { // Build will extract extensions from the node. func (o *OAuthFlow) Build(ctx context.Context, _, root *yaml.Node, idx *index.SpecIndex) error { + root = utils.ResolveMergeNodes(root) o.reference = low.Reference{} o.Reference = &o.reference o.nodeStore = low.NodeLines{} diff --git a/datamodel/low/v3/operation.go b/datamodel/low/v3/operation.go index a6c1042b..017a77fb 100644 --- a/datamodel/low/v3/operation.go +++ b/datamodel/low/v3/operation.go @@ -86,9 +86,8 @@ func (o *Operation) GetKeyNode() *yaml.Node { // Build will extract external docs, parameters, request body, responses, callbacks, security and servers. func (o *Operation) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { o.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) o.RootNode = root - utils.CheckForMergeNodes(root) o.reference = low.Reference{} o.Reference = &o.reference o.nodeStore = low.NodeLines{} diff --git a/datamodel/low/v3/parameter.go b/datamodel/low/v3/parameter.go index bcda2e35..0213e078 100644 --- a/datamodel/low/v3/parameter.go +++ b/datamodel/low/v3/parameter.go @@ -95,8 +95,8 @@ func (p *Parameter) Build(ctx context.Context, keyNode, root *yaml.Node, idx *in } root = utils.NodeAlias(root) p.KeyNode = keyNode + root = utils.ResolveMergeNodes(root) p.RootNode = root - utils.CheckForMergeNodes(root) p.nodeStore = low.NodeLines{} p.Nodes = &p.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/path_item.go b/datamodel/low/v3/path_item.go index b015ef4b..7f8d40fa 100644 --- a/datamodel/low/v3/path_item.go +++ b/datamodel/low/v3/path_item.go @@ -205,8 +205,8 @@ func (p *PathItem) Build(ctx context.Context, keyNode, root *yaml.Node, idx *ind } root = utils.NodeAlias(root) p.KeyNode = keyNode + root = utils.ResolveMergeNodes(root) p.RootNode = root - utils.CheckForMergeNodes(root) p.nodeStore = low.NodeLines{} p.Nodes = &p.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/paths.go b/datamodel/low/v3/paths.go index f3127def..7df0b6ad 100644 --- a/datamodel/low/v3/paths.go +++ b/datamodel/low/v3/paths.go @@ -92,8 +92,8 @@ func (p *Paths) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.Va func (p *Paths) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { root = utils.NodeAlias(root) p.KeyNode = keyNode + root = utils.ResolveMergeNodes(root) p.RootNode = root - utils.CheckForMergeNodes(root) p.reference = low.Reference{} p.Reference = &p.reference p.nodeStore = low.NodeLines{} diff --git a/datamodel/low/v3/request_body.go b/datamodel/low/v3/request_body.go index 0f83dc37..dfa80ba7 100644 --- a/datamodel/low/v3/request_body.go +++ b/datamodel/low/v3/request_body.go @@ -74,9 +74,8 @@ func (rb *RequestBody) Build(ctx context.Context, keyNode, root *yaml.Node, idx if ok, _, ref := utils.IsNodeRefValue(root); ok { rb.SetReference(ref, root) } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) rb.RootNode = root - utils.CheckForMergeNodes(root) rb.nodeStore = low.NodeLines{} rb.Nodes = &rb.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/response.go b/datamodel/low/v3/response.go index d0c33a20..ad79360f 100644 --- a/datamodel/low/v3/response.go +++ b/datamodel/low/v3/response.go @@ -89,9 +89,8 @@ func (r *Response) Build(ctx context.Context, keyNode, root *yaml.Node, idx *ind if ok, _, ref := utils.IsNodeRefValue(root); ok { r.SetReference(ref, root) } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) r.RootNode = root - utils.CheckForMergeNodes(root) r.nodeStore = low.NodeLines{} r.Nodes = &r.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/responses.go b/datamodel/low/v3/responses.go index e59de89c..2b4cb7b9 100644 --- a/datamodel/low/v3/responses.go +++ b/datamodel/low/v3/responses.go @@ -76,7 +76,7 @@ func (r *Responses) GetExtensions() *orderedmap.Map[low.KeyReference[string], lo // Build will extract default response and all Response objects for each code func (r *Responses) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { r.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) r.RootNode = root r.reference = low.Reference{} r.Reference = &r.reference @@ -92,7 +92,6 @@ func (r *Responses) Build(ctx context.Context, keyNode, root *yaml.Node, idx *in r.context = ctx low.ExtractExtensionNodes(ctx, r.Extensions, r.Nodes) - utils.CheckForMergeNodes(root) if utils.IsNodeMap(root) { codes, err := low.ExtractMapNoLookup[*Response](ctx, root, idx) if err != nil { diff --git a/datamodel/low/v3/security_scheme.go b/datamodel/low/v3/security_scheme.go index 7101589a..4ddde894 100644 --- a/datamodel/low/v3/security_scheme.go +++ b/datamodel/low/v3/security_scheme.go @@ -84,9 +84,8 @@ func (ss *SecurityScheme) Build(ctx context.Context, keyNode, root *yaml.Node, i if ok, _, ref := utils.IsNodeRefValue(root); ok { ss.SetReference(ref, root) } - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) ss.RootNode = root - utils.CheckForMergeNodes(root) ss.nodeStore = low.NodeLines{} ss.Nodes = &ss.nodeStore if len(root.Content) > 0 { diff --git a/datamodel/low/v3/server.go b/datamodel/low/v3/server.go index 91bb4259..5248278b 100644 --- a/datamodel/low/v3/server.go +++ b/datamodel/low/v3/server.go @@ -58,9 +58,8 @@ func (s *Server) FindVariable(serverVar string) *low.ValueReference[*ServerVaria // Build will extract server variables from the supplied node. func (s *Server) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { s.KeyNode = keyNode - root = utils.NodeAlias(root) + root = utils.ResolveMergeNodes(root) s.RootNode = root - utils.CheckForMergeNodes(root) s.Reference = new(low.Reference) s.Nodes = low.ExtractNodes(ctx, root) s.Extensions = low.ExtractExtensions(root) @@ -83,15 +82,16 @@ func (s *Server) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index localKeyNode = varNode continue } + effective := utils.ResolveMergeNodes(varNode) variable := ServerVariable{} variable.Reference = new(low.Reference) - _ = low.BuildModel(varNode, &variable) - variable.Nodes = low.ExtractNodesRecursive(ctx, varNode) - variable.Extensions = low.ExtractExtensions(varNode) + _ = low.BuildModel(effective, &variable) + variable.Nodes = low.ExtractNodesRecursive(ctx, effective) + variable.Extensions = low.ExtractExtensions(effective) if localKeyNode != nil { variable.Nodes.Store(localKeyNode.Line, localKeyNode) } - variable.RootNode = varNode + variable.RootNode = effective variable.KeyNode = localKeyNode variablesMap.Set( low.KeyReference[string]{ diff --git a/document_merge_ownership_test.go b/document_merge_ownership_test.go new file mode 100644 index 00000000..763f2281 --- /dev/null +++ b/document_merge_ownership_test.go @@ -0,0 +1,209 @@ +package libopenapi + +import ( + stdcontext "context" + "testing" + + "github.com/pb33f/go-yaml" + lowbase "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/utils" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" +) + +func TestBuildV3ModelMergedComponentsPreservesSource(t *testing.T) { + const source = `openapi: 3.1.1 +info: {title: Merge example, version: '1'} +paths: {} +x-bounds: &bounds + type: integer + minimum: 5 # lower bound + maximum: 10 +x-base: &base + schemas: + Bound: + <<: *bounds + maximum: 9 +x-sources: &sources [*base] +components: + <<: *sources +` + document, err := NewDocument([]byte(source)) + require.NoError(t, err) + root := document.GetSpecInfo().RootNode + before, err := yaml.Marshal(root) + require.NoError(t, err) + _, bounds := utils.FindKeyNodeTop("x-bounds", root.Content[0].Content) + minimumKey, minimumValue := utils.FindKeyNodeTop("minimum", bounds.Content) + model, err := document.BuildV3Model() + require.NoError(t, err) + require.NotNil(t, model.Model.Components) + proxy, ok := model.Model.Components.Schemas.Get("Bound") + require.True(t, ok) + schema := proxy.Schema() + require.NotNil(t, schema) + require.NotNil(t, schema.Minimum) + require.NotNil(t, schema.Maximum) + assert.Equal(t, float64(5), *schema.Minimum) + assert.Equal(t, float64(9), *schema.Maximum) + assert.Same(t, minimumKey, schema.GoLow().Minimum.KeyNode) + assert.Same(t, minimumValue, schema.GoLow().Minimum.ValueNode) + for _, effective := range []*yaml.Node{model.Model.Components.GoLow().GetRootNode(), schema.GoLow().RootNode} { + require.NotNil(t, effective) + for i := 0; i < len(effective.Content); i += 2 { + assert.NotEqual(t, "!!merge", effective.Content[i].Tag, "low-level model roots describe effective fields") + } + } + after, err := yaml.Marshal(root) + require.NoError(t, err) + assert.Equal(t, string(before), string(after), "building low models must leave published source untouched") + + rendered, err := document.Render() + require.NoError(t, err) + reloaded, err := NewDocument(rendered) + require.NoError(t, err) + rebuilt, err := reloaded.BuildV3Model() + require.NoError(t, err) + rebuiltProxy, ok := rebuilt.Model.Components.Schemas.Get("Bound") + require.True(t, ok) + rebuiltSchema := rebuiltProxy.Schema() + require.NotNil(t, rebuiltSchema) + require.NotNil(t, rebuiltSchema.Minimum) + require.NotNil(t, rebuiltSchema.Maximum) + assert.Equal(t, *schema.Minimum, *rebuiltSchema.Minimum) + assert.Equal(t, *schema.Maximum, *rebuiltSchema.Maximum) +} + +func TestBuildV3ModelMergeViewsReachNestedConsumers(t *testing.T) { + const source = `openapi: 3.1.1 +x-doc: &doc {x-note: root} +<<: *doc +x-contact: &contact {name: support, x-note: contact} +x-var: &var {default: prod, x-note: variable} +x-flow: &flow {authorizationUrl: 'https://example.test/auth', scopes: {}, x-note: flow} +x-schema: &schema {type: object, x-note: schema} +x-disc: &disc {propertyName: kind} +x-xml: &xml {name: pet} +x-docs: &docs {url: https://example.test/docs} +info: + title: merge + version: '1' + contact: {<<: *contact} +paths: {} +servers: + - url: 'https://{env}.example.test' + variables: + env: {<<: *var} +components: + securitySchemes: + Auth: + type: oauth2 + flows: + implicit: {<<: *flow} + schemas: + Target: {<<: *schema} + Ref: {$ref: '#/components/schemas/Target'} + Pet: + type: object + discriminator: {<<: *disc} + xml: {<<: *xml} + externalDocs: {<<: *docs} +` + document, err := NewDocument([]byte(source)) + require.NoError(t, err) + root := document.GetSpecInfo().RootNode + before, err := yaml.Marshal(root) + require.NoError(t, err) + model, err := document.BuildV3Model() + require.NoError(t, err) + note, found := model.Model.Extensions.Get("x-note") + assert.True(t, found, "document extensions use the effective root") + if found { + assert.Equal(t, "root", note.Value) + } + contact := model.Model.Info.Contact + variable, found := model.Model.Servers[0].Variables.Get("env") + require.True(t, found) + auth, found := model.Model.Components.SecuritySchemes.Get("Auth") + require.True(t, found) + flow := auth.Flows.Implicit + pet, found := model.Model.Components.Schemas.Get("Pet") + require.True(t, found) + schema := pet.Schema() + require.NotNil(t, schema) + discriminator := schema.Discriminator + require.NotNil(t, discriminator) + for _, tc := range []struct { + name string + root *yaml.Node + field string + }{ + {"contact", contact.GoLow().GetRootNode(), "name"}, + {"variable", variable.GoLow().GetRootNode(), "default"}, + {"flow", flow.GoLow().GetRootNode(), "authorizationUrl"}, + {"discriminator", discriminator.GoLow().GetRootNode(), "propertyName"}, + } { + t.Run(tc.name, func(t *testing.T) { + require.NotNil(t, tc.root) + _, field := utils.FindKeyNodeTop(tc.field, tc.root.Content) + assert.NotNil(t, field, "model root exposes effective fields") + for i := 0; i < len(tc.root.Content); i += 2 { + assert.NotEqual(t, "!!merge", tc.root.Content[i].Tag) + } + }) + } + + for _, tc := range []struct { + name string + anchor string + field string + nodes map[int][]*yaml.Node + }{ + {"xml", "x-xml", "name", schema.XML.GoLow().GetNodes()}, + {"externalDocs", "x-docs", "url", schema.ExternalDocs.GoLow().GetNodes()}, + } { + t.Run(tc.name+" nodes", func(t *testing.T) { + _, anchor := utils.FindKeyNodeTop(tc.anchor, root.Content[0].Content) + key, _ := utils.FindKeyNodeTop(tc.field, anchor.Content) + assert.Contains(t, tc.nodes[key.Line], key, "inherited field belongs to model node map") + }) + } + assert.Equal(t, 1, contact.Extensions.Len()) + assert.Equal(t, 1, variable.Extensions.Len()) + assert.Equal(t, 1, flow.Extensions.Len()) + for _, tc := range []struct { + name string + render func() ([]byte, error) + value string + }{ + {"contact", contact.Render, "contact"}, + {"variable", variable.Render, "variable"}, + {"flow", flow.Render, "flow"}, + } { + t.Run(tc.name+" render", func(t *testing.T) { + rendered, err := tc.render() + require.NoError(t, err) + assert.Contains(t, string(rendered), "x-note: "+tc.value) + }) + } + _, components := utils.FindKeyNodeTop("components", root.Content[0].Content) + _, schemas := utils.FindKeyNodeTop("schemas", components.Content) + _, reference := utils.FindKeyNodeTop("Ref", schemas.Content) + var resolved lowbase.Schema + require.NoError(t, resolved.Build(stdcontext.Background(), reference, model.Model.GoLow().Index)) + assert.Equal(t, 1, resolved.Extensions.Len()) + assert.Same(t, reference, resolved.RootNode, "authored reference root is retained") + _, petNode := utils.FindKeyNodeTop("Pet", schemas.Content) + _, discriminatorNode := utils.FindKeyNodeTop("discriminator", petNode.Content) + assert.Same(t, discriminatorNode, schema.GoLow().Discriminator.ValueNode) + _, info := utils.FindKeyNodeTop("info", root.Content[0].Content) + _, contactNode := utils.FindKeyNodeTop("contact", info.Content) + assert.Same(t, contactNode, model.Model.Info.GoLow().Contact.ValueNode) + _, contactAnchor := utils.FindKeyNodeTop("x-contact", root.Content[0].Content) + nameKey, nameValue := utils.FindKeyNodeTop("name", contactAnchor.Content) + assert.Same(t, nameKey, contact.GoLow().Name.KeyNode) + assert.Same(t, nameValue, contact.GoLow().Name.ValueNode) + after, err := yaml.Marshal(root) + require.NoError(t, err) + assert.Equal(t, string(before), string(after), "all consumers leave source nodes unchanged") +} diff --git a/utils/merge_view_test.go b/utils/merge_view_test.go new file mode 100644 index 00000000..80fe343d --- /dev/null +++ b/utils/merge_view_test.go @@ -0,0 +1,66 @@ +package utils + +import ( + "testing" + + "github.com/pb33f/go-yaml" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" +) + +func TestResolvedMergeNodeRetainsMetadata(t *testing.T) { + const source = `base: &base + inherited: first # inherited comment + local: overridden +second: &second + inherited: second + extra: retained +sources: &sources [*base, *second] +merged: &merged # mapping comment + <<: *sources + local: chosen # local comment +alias: *merged +` + var document yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(source), &document)) + before, err := yaml.Marshal(&document) + require.NoError(t, err) + root := document.Content[0] + _, original := FindKeyNodeTop("merged", root.Content) + _, base := FindKeyNodeTop("base", root.Content) + key, value := FindKeyNodeTop("inherited", base.Content) + alias := root.Content[len(root.Content)-1] + view := ResolveMergeNodes(alias) + require.NotSame(t, original, view) + assert.Equal(t, original.Line, view.Line) + assert.Equal(t, original.Column, view.Column) + assert.Equal(t, original.Anchor, view.Anchor) + assert.Equal(t, original.HeadComment, view.HeadComment) + assert.Equal(t, original.LineComment, view.LineComment) + viewKey, viewValue := FindKeyNodeTop("inherited", view.Content) + assert.Same(t, key, viewKey) + assert.Same(t, value, viewValue) + _, local := FindKeyNodeTop("local", view.Content) + assert.Equal(t, "chosen", local.Value) + _, extra := FindKeyNodeTop("extra", view.Content) + assert.Equal(t, "retained", extra.Value) + assert.False(t, hasMergeKeys(view.Content)) + assert.Same(t, view, ResolveMergeNodes(view)) + after, err := yaml.Marshal(&document) + require.NoError(t, err) + assert.Equal(t, string(before), string(after)) +} + +func TestResolvedMergeNodeMappingCycleTerminates(t *testing.T) { + node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + key := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "local"} + value := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "retained"} + node.Content = []*yaml.Node{ + {Kind: yaml.ScalarNode, Tag: "!!merge", Value: "<<"}, + {Kind: yaml.AliasNode, Alias: node}, + key, value, + } + view := ResolveMergeNodes(node) + assert.Equal(t, []*yaml.Node{key, value}, view.Content) + assert.Len(t, node.Content, 4) +} diff --git a/utils/utils.go b/utils/utils.go index 10624ac8..4deb6b09 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -690,17 +690,24 @@ func NodeMerge(nodes []*yaml.Node) *yaml.Node { for i, v := range nodes { if v.Tag == "!!merge" { if i+1 < len(nodes) { - return resolvedMergeNode(nodes[i+1]) + return ResolveMergeNodes(nodes[i+1]) } } } if len(nodes) > 0 { - return resolvedMergeNode(nodes[0]) + return ResolveMergeNodes(nodes[0]) } return nil } -func resolvedMergeNode(node *yaml.Node) *yaml.Node { +// ResolveMergeNodes returns a read-only view of a mapping with YAML merges expanded. +// Local keys take precedence over merged keys, and earlier merge sources take +// precedence over later sources. Aliases are resolved before expansion. +// A merged mapping is shallow-copied: source nodes are not modified, and original +// key/value nodes and their location/comment metadata are retained. Mappings +// without merges retain their identity. Callers must not mutate shared children +// while readers or model builders use them. +func ResolveMergeNodes(node *yaml.Node) *yaml.Node { node = NodeAlias(node) if node == nil { return nil diff --git a/utils/utils_test.go b/utils/utils_test.go index abbb51c3..fbaf8378 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -1921,7 +1921,7 @@ func TestNodeMerge_FallbackToFirstNode(t *testing.T) { } func TestResolvedMergeNode_DefensivePaths(t *testing.T) { - assert.Nil(t, resolvedMergeNode(nil)) + assert.Nil(t, ResolveMergeNodes(nil)) plain := &yaml.Node{ Kind: yaml.MappingNode, @@ -1931,7 +1931,7 @@ func TestResolvedMergeNode_DefensivePaths(t *testing.T) { {Kind: yaml.ScalarNode, Tag: "!!str", Value: "value"}, }, } - assert.Same(t, plain, resolvedMergeNode(plain)) + assert.Same(t, plain, ResolveMergeNodes(plain)) bareMerge := &yaml.Node{ Kind: yaml.MappingNode, @@ -1940,7 +1940,7 @@ func TestResolvedMergeNode_DefensivePaths(t *testing.T) { {Kind: yaml.ScalarNode, Tag: "!!merge", Value: "<<"}, }, } - assert.Same(t, bareMerge, resolvedMergeNode(bareMerge)) + assert.Same(t, bareMerge, ResolveMergeNodes(bareMerge)) } func TestCheckForMergeNodes_BareMergeNodeDoesNotPanic(t *testing.T) {