@@ -20,9 +20,9 @@ func (o *Overlay) ApplyTo(root *yaml.Node) error {
2020 case action .Remove :
2121 err = o .applyRemoveAction (root , action , nil )
2222 case ! action .Update .IsZero ():
23- err = o .applyUpdateAction (root , action , & []string {})
23+ err = o .applyUpdateAction (root , action , & []string {}, false )
2424 case action .Copy != "" :
25- err = o .applyCopyAction (root , action , & []string {})
25+ err = o .applyCopyAction (root , action , & []string {}, false )
2626 }
2727
2828 if err != nil {
@@ -62,10 +62,10 @@ func (o *Overlay) ApplyToStrict(root *yaml.Node) ([]string, error) {
6262 err = o .applyRemoveAction (root , action , & actionWarnings )
6363 case ! action .Update .IsZero ():
6464 actionType = "update"
65- err = o .applyUpdateAction (root , action , & actionWarnings )
65+ err = o .applyUpdateAction (root , action , & actionWarnings , true )
6666 case action .Copy != "" :
6767 actionType = "copy"
68- err = o .applyCopyAction (root , action , & actionWarnings )
68+ err = o .applyCopyAction (root , action , & actionWarnings , true )
6969 default :
7070 err = fmt .Errorf ("unknown action type: %v" , action )
7171 }
@@ -171,7 +171,13 @@ func removeNode(idx parentIndex, node *yaml.Node) {
171171 }
172172}
173173
174- func (o * Overlay ) applyUpdateAction (root * yaml.Node , action Action , warnings * []string ) error {
174+ // mergeOptions carries version and strictness context through the merge call chain.
175+ type mergeOptions struct {
176+ v110 bool
177+ strict bool
178+ }
179+
180+ func (o * Overlay ) applyUpdateAction (root * yaml.Node , action Action , warnings * []string , strict bool ) error {
175181 if action .Target == "" {
176182 return nil
177183 }
@@ -187,9 +193,26 @@ func (o *Overlay) applyUpdateAction(root *yaml.Node, action Action, warnings *[]
187193
188194 nodes := p .Query (root )
189195
196+ opts := mergeOptions {v110 : o .IsV110OrLater (), strict : strict }
197+
198+ // Homogeneity check: 1.1.0 strict mode requires all targets to be the same kind
199+ if strict && opts .v110 && len (nodes ) > 1 {
200+ firstKind := nodes [0 ].Kind
201+ for _ , n := range nodes [1 :] {
202+ if n .Kind != firstKind {
203+ return fmt .Errorf ("target selected mixed node types (%s and %s); all targets must be the same type" ,
204+ nodeKindName (firstKind ), nodeKindName (n .Kind ))
205+ }
206+ }
207+ }
208+
190209 didMakeChange := false
191210 for _ , node := range nodes {
192- didMakeChange = updateNode (node , & action .Update ) || didMakeChange
211+ changed , err := applyMerge (node , & action .Update , opts )
212+ if err != nil {
213+ return err
214+ }
215+ didMakeChange = changed || didMakeChange
193216 }
194217 if ! didMakeChange {
195218 * warnings = append (* warnings , "does nothing" )
@@ -198,37 +221,85 @@ func (o *Overlay) applyUpdateAction(root *yaml.Node, action Action, warnings *[]
198221 return nil
199222}
200223
201- func updateNode (node * yaml.Node , updateNode * yaml.Node ) bool {
202- return mergeNode (node , updateNode )
224+ // applyMerge is the top-level merge dispatch for update/copy actions.
225+ // It implements the Action Object rules (spec §4.4.3) which differ from
226+ // the recursive merge rules applied within object properties.
227+ func applyMerge (node * yaml.Node , update * yaml.Node , opts mergeOptions ) (bool , error ) {
228+ if ! opts .v110 {
229+ // 1.0.0: delegate entirely to recursive merge (preserves existing behavior)
230+ return mergeNode (node , update , opts )
231+ }
232+
233+ // 1.1.0 top-level dispatch per spec §4.4.3:
234+ switch node .Kind {
235+ case yaml .SequenceNode :
236+ // Array target: concatenate if update is array, append if object/primitive
237+ if update .Kind == yaml .SequenceNode {
238+ return mergeSequenceNode (node , update ), nil
239+ }
240+ // Append non-array value as single element
241+ node .Content = append (node .Content , clone (update ))
242+ return true , nil
243+
244+ case yaml .MappingNode :
245+ if update .Kind != yaml .MappingNode {
246+ // Spec: update MUST be an object for object targets
247+ if opts .strict {
248+ return false , fmt .Errorf ("target is object but update is %s" , nodeKindName (update .Kind ))
249+ }
250+ // Lax: replace gracefully
251+ * node = * clone (update )
252+ return true , nil
253+ }
254+ return mergeNode (node , update , opts )
255+
256+ default :
257+ // Primitive target: replace
258+ if update .Kind != yaml .ScalarNode && update .Kind != 0 {
259+ if opts .strict {
260+ return false , fmt .Errorf ("target is primitive but update is %s" , nodeKindName (update .Kind ))
261+ }
262+ // Non-scalar update replaces the whole node
263+ isChanged := node .Value != update .Value || node .Kind != update .Kind
264+ * node = * clone (update )
265+ return isChanged , nil
266+ }
267+ // Scalar-to-scalar: update value only, preserving target's style/tag
268+ isChanged := node .Value != update .Value
269+ node .Value = update .Value
270+ return isChanged , nil
271+ }
203272}
204273
205- func mergeNode (node * yaml.Node , merge * yaml.Node ) bool {
274+ // mergeNode is the recursive merge function used within object property merging.
275+ // Type mismatches at this level follow the recursive merge rules, not the
276+ // top-level Action Object rules.
277+ func mergeNode (node * yaml.Node , merge * yaml.Node , opts mergeOptions ) (bool , error ) {
206278 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
279+ // 1.1.0 strict: "Other property value combinations are incompatible and result in an error"
280+ if opts .v110 && opts .strict {
281+ return false , fmt .Errorf ("type mismatch: target is %s but update is %s" ,
282+ nodeKindName (node .Kind ), nodeKindName (merge .Kind ))
213283 }
284+ // 1.0.0 or 1.1.0 lax: gracefully replace (pre-existing behavior)
214285 * node = * clone (merge )
215- return true
286+ return true , nil
216287 }
217288 switch node .Kind {
289+ case yaml .MappingNode :
290+ return mergeMappingNode (node , merge , opts )
291+ case yaml .SequenceNode :
292+ return mergeSequenceNode (node , merge ), nil
218293 default :
219294 isChanged := node .Value != merge .Value
220295 node .Value = merge .Value
221- return isChanged
222- case yaml .MappingNode :
223- return mergeMappingNode (node , merge )
224- case yaml .SequenceNode :
225- return mergeSequenceNode (node , merge )
296+ return isChanged , nil
226297 }
227298}
228299
229300// mergeMappingNode will perform a shallow merge of the merge node into the main
230301// node.
231- func mergeMappingNode (node * yaml.Node , merge * yaml.Node ) bool {
302+ func mergeMappingNode (node * yaml.Node , merge * yaml.Node , opts mergeOptions ) ( bool , error ) {
232303 anyChange := false
233304
234305 // If the target is an empty flow-style mapping and we're merging content,
@@ -245,15 +316,33 @@ NextKey:
245316 for j := 0 ; j < len (node .Content ); j += 2 {
246317 nodeKey := node .Content [j ].Value
247318 if nodeKey == mergeKey {
248- anyChange = mergeNode (node .Content [j + 1 ], mergeValue ) || anyChange
319+ changed , err := mergeNode (node .Content [j + 1 ], mergeValue , opts )
320+ if err != nil {
321+ return anyChange , fmt .Errorf ("key %q: %w" , mergeKey , err )
322+ }
323+ anyChange = changed || anyChange
249324 continue NextKey
250325 }
251326 }
252327
253328 node .Content = append (node .Content , merge .Content [i ], clone (mergeValue ))
254329 anyChange = true
255330 }
256- return anyChange
331+ return anyChange , nil
332+ }
333+
334+ // nodeKindName returns a human-readable name for a YAML node kind.
335+ func nodeKindName (kind yaml.Kind ) string {
336+ switch kind {
337+ case yaml .MappingNode :
338+ return "object"
339+ case yaml .SequenceNode :
340+ return "array"
341+ case yaml .ScalarNode :
342+ return "scalar"
343+ default :
344+ return "unknown"
345+ }
257346}
258347
259348// mergeSequenceNode will append the merge node's content to the original node.
@@ -285,9 +374,8 @@ func clone(node *yaml.Node) *yaml.Node {
285374 return newNode
286375}
287376
288- // applyCopyAction applies a copy action to the document
289- // This is a stub implementation for the copy feature from Overlay Specification v1.1.0
290- func (o * Overlay ) applyCopyAction (root * yaml.Node , action Action , warnings * []string ) error {
377+ // applyCopyAction applies a copy action to the document.
378+ func (o * Overlay ) applyCopyAction (root * yaml.Node , action Action , warnings * []string , strict bool ) error {
291379 if action .Target == "" {
292380 return nil
293381 }
@@ -328,14 +416,31 @@ func (o *Overlay) applyCopyAction(root *yaml.Node, action Action, warnings *[]st
328416 // Query the target nodes
329417 targetNodes := targetPath .Query (root )
330418
419+ opts := mergeOptions {v110 : o .IsV110OrLater (), strict : strict }
420+
421+ // Homogeneity check: 1.1.0 strict mode requires all targets to be the same kind
422+ if strict && opts .v110 && len (targetNodes ) > 1 {
423+ firstKind := targetNodes [0 ].Kind
424+ for _ , n := range targetNodes [1 :] {
425+ if n .Kind != firstKind {
426+ return fmt .Errorf ("target selected mixed node types (%s and %s); all targets must be the same type" ,
427+ nodeKindName (firstKind ), nodeKindName (n .Kind ))
428+ }
429+ }
430+ }
431+
331432 // Copy the source node to each target
332433 didMakeChange := false
333434 for _ , targetNode := range targetNodes {
334435 // Clone the source node to avoid reference issues
335436 copiedNode := clone (sourceNode )
336437
337438 // Merge the copied node into the target
338- didMakeChange = mergeNode (targetNode , copiedNode ) || didMakeChange
439+ changed , err := applyMerge (targetNode , copiedNode , opts )
440+ if err != nil {
441+ return err
442+ }
443+ didMakeChange = changed || didMakeChange
339444 }
340445
341446 if ! didMakeChange && warnings != nil {
0 commit comments