Skip to content

Commit 4b76a66

Browse files
fix: append non-array update values to arrays per overlay spec (#167)
1 parent a5af699 commit 4b76a66

2 files changed

Lines changed: 253 additions & 0 deletions

File tree

overlay/apply.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ func updateNode(node *yaml.Node, updateNode *yaml.Node) bool {
204204

205205
func mergeNode(node *yaml.Node, merge *yaml.Node) bool {
206206
if node.Kind != merge.Kind {
207+
// Per Overlay 1.1.0 spec: "For arrays, the update value SHALL be
208+
// appended to the targeted array. [...] otherwise the update value
209+
// SHALL be appended as a single element."
210+
if node.Kind == yaml.SequenceNode {
211+
node.Content = append(node.Content, clone(merge))
212+
return true
213+
}
207214
*node = *clone(merge)
208215
return true
209216
}

overlay/apply_test.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,252 @@ func TestApplyTo_CopyVersionToHeader(t *testing.T) {
285285
NodeMatchesFile(t, node, "testdata/openapi-version-header-expected.yaml")
286286
}
287287

288+
func TestApplyTo_UpdateArrayAppend(t *testing.T) {
289+
t.Parallel()
290+
291+
tests := []struct {
292+
name string
293+
inputYAML string
294+
overlayYAML string
295+
expectedTags int
296+
expectedNames []string
297+
}{
298+
{
299+
name: "single object appended to array",
300+
inputYAML: `openapi: 3.1.0
301+
info:
302+
title: Test API
303+
version: 1.0.0
304+
tags:
305+
- name: existing
306+
description: This tag already exists
307+
`,
308+
overlayYAML: `overlay: 1.1.0
309+
info:
310+
title: Append single object
311+
version: 1.0.0
312+
actions:
313+
- target: "$.tags"
314+
update:
315+
name: newTag1
316+
description: This tag should be appended
317+
`,
318+
expectedTags: 2,
319+
expectedNames: []string{"existing", "newTag1"},
320+
},
321+
{
322+
name: "multiple objects appended to array via separate actions",
323+
inputYAML: `openapi: 3.1.0
324+
info:
325+
title: Test API
326+
version: 1.0.0
327+
tags:
328+
- name: existing
329+
description: This tag already exists
330+
`,
331+
overlayYAML: `overlay: 1.1.0
332+
info:
333+
title: Append multiple objects
334+
version: 1.0.0
335+
actions:
336+
- target: "$.tags"
337+
update:
338+
name: newTag1
339+
description: First appended tag
340+
- target: "$.tags"
341+
update:
342+
name: newTag2
343+
description: Second appended tag
344+
`,
345+
expectedTags: 3,
346+
expectedNames: []string{"existing", "newTag1", "newTag2"},
347+
},
348+
{
349+
name: "array appended to array",
350+
inputYAML: `openapi: 3.1.0
351+
info:
352+
title: Test API
353+
version: 1.0.0
354+
tags:
355+
- name: existing
356+
description: This tag already exists
357+
`,
358+
overlayYAML: `overlay: 1.1.0
359+
info:
360+
title: Append array to array
361+
version: 1.0.0
362+
actions:
363+
- target: "$.tags"
364+
update:
365+
- name: newTag1
366+
description: First appended tag
367+
- name: newTag2
368+
description: Second appended tag
369+
`,
370+
expectedTags: 3,
371+
expectedNames: []string{"existing", "newTag1", "newTag2"},
372+
},
373+
}
374+
375+
for _, tt := range tests {
376+
t.Run(tt.name, func(t *testing.T) {
377+
t.Parallel()
378+
379+
// Parse the input spec
380+
var specNode yaml.Node
381+
err := yaml.Unmarshal([]byte(tt.inputYAML), &specNode)
382+
require.NoError(t, err, "unmarshal input spec should succeed")
383+
384+
// Parse the overlay
385+
var o overlay.Overlay
386+
err = yaml.Unmarshal([]byte(tt.overlayYAML), &o)
387+
require.NoError(t, err, "unmarshal overlay should succeed")
388+
389+
// Apply the overlay
390+
err = o.ApplyTo(&specNode)
391+
require.NoError(t, err, "apply overlay should succeed")
392+
393+
// Find the tags node in the result
394+
root := specNode.Content[0] // DocumentNode -> MappingNode
395+
var tagsNode *yaml.Node
396+
for i := 0; i < len(root.Content); i += 2 {
397+
if root.Content[i].Value == "tags" {
398+
tagsNode = root.Content[i+1]
399+
break
400+
}
401+
}
402+
403+
require.NotNil(t, tagsNode, "tags node should exist")
404+
assert.Equal(t, yaml.SequenceNode, tagsNode.Kind, "tags should remain a sequence/array")
405+
assert.Len(t, tagsNode.Content, tt.expectedTags, "tags array should have expected number of elements")
406+
407+
// Verify each tag name
408+
for i, expectedName := range tt.expectedNames {
409+
require.Greater(t, len(tagsNode.Content), i, "should have enough tag elements")
410+
tagNode := tagsNode.Content[i]
411+
assert.Equal(t, yaml.MappingNode, tagNode.Kind, "each tag should be a mapping node")
412+
413+
// Find the "name" key in the tag
414+
var nameValue string
415+
for j := 0; j < len(tagNode.Content); j += 2 {
416+
if tagNode.Content[j].Value == "name" {
417+
nameValue = tagNode.Content[j+1].Value
418+
break
419+
}
420+
}
421+
assert.Equal(t, expectedName, nameValue, "tag name at index "+strconv.Itoa(i)+" should match")
422+
}
423+
})
424+
}
425+
}
426+
427+
func TestApplyTo_UpdateNestedArrayAppend(t *testing.T) {
428+
t.Parallel()
429+
430+
tests := []struct {
431+
name string
432+
inputYAML string
433+
overlayYAML string
434+
expectedYAML string
435+
}{
436+
{
437+
name: "nested array in object merge is concatenated",
438+
inputYAML: `openapi: 3.1.0
439+
info:
440+
title: Test API
441+
version: 1.0.0
442+
paths:
443+
/pets:
444+
get:
445+
parameters:
446+
- name: limit
447+
in: query
448+
`,
449+
overlayYAML: `overlay: 1.1.0
450+
info:
451+
title: Append nested array
452+
version: 1.0.0
453+
actions:
454+
- target: "$.paths['/pets'].get"
455+
update:
456+
parameters:
457+
- name: offset
458+
in: query
459+
`,
460+
expectedYAML: `openapi: 3.1.0
461+
info:
462+
title: Test API
463+
version: 1.0.0
464+
paths:
465+
/pets:
466+
get:
467+
parameters:
468+
- name: limit
469+
in: query
470+
- name: offset
471+
in: query
472+
`,
473+
},
474+
{
475+
name: "scalar update appended to array",
476+
inputYAML: `openapi: 3.1.0
477+
info:
478+
title: Test API
479+
version: 1.0.0
480+
paths:
481+
/pets:
482+
get:
483+
tags:
484+
- pets
485+
`,
486+
overlayYAML: `overlay: 1.1.0
487+
info:
488+
title: Append scalar to array
489+
version: 1.0.0
490+
actions:
491+
- target: "$.paths['/pets'].get.tags"
492+
update: admin
493+
`,
494+
expectedYAML: `openapi: 3.1.0
495+
info:
496+
title: Test API
497+
version: 1.0.0
498+
paths:
499+
/pets:
500+
get:
501+
tags:
502+
- pets
503+
- admin
504+
`,
505+
},
506+
}
507+
508+
for _, tt := range tests {
509+
t.Run(tt.name, func(t *testing.T) {
510+
t.Parallel()
511+
512+
var specNode yaml.Node
513+
err := yaml.Unmarshal([]byte(tt.inputYAML), &specNode)
514+
require.NoError(t, err, "unmarshal input spec should succeed")
515+
516+
var o overlay.Overlay
517+
err = yaml.Unmarshal([]byte(tt.overlayYAML), &o)
518+
require.NoError(t, err, "unmarshal overlay should succeed")
519+
520+
err = o.ApplyTo(&specNode)
521+
require.NoError(t, err, "apply overlay should succeed")
522+
523+
var buf bytes.Buffer
524+
enc := yaml.NewEncoder(&buf)
525+
enc.SetIndent(2)
526+
err = enc.Encode(&specNode)
527+
require.NoError(t, err, "encode result should succeed")
528+
529+
assert.Equal(t, tt.expectedYAML, buf.String(), "output should match expected YAML")
530+
})
531+
}
532+
}
533+
288534
func TestApplyToOld(t *testing.T) {
289535
t.Parallel()
290536

0 commit comments

Comments
 (0)