package engine import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" // recursiveMergeHooks lets an assembly owner account for the values changed by // a recursive mapping overlay without making source roles part of the merge. // Hooks run before replacement and receive the still-owned incoming subtree. type recursiveMergeHooks struct { added func(Path, *document.Node) combined func(Path, *document.Node) replaced func(Path, *document.Node, *document.Node) merge func(Path, *document.Node, *document.Node) (*document.Node, bool) } // mergeRecursiveMappings changes an owned mapping and never aliases incoming // values into it. Entry order follows the target, then new incoming entries. func mergeRecursiveMappings(target, incoming *document.Node, current Path, index *mappingIndex, hooks recursiveMergeHooks) { for _, entry := range incoming.Mapping { path := current.Key(entry.Key) position := index.find(target, entry.Key) if position < 0 { if hooks.merge != nil { if merged, handled := hooks.merge(path, nil, entry.Value); handled { cloned := entry cloned.Value = cloneMergeSubtree(merged, path, hooks.merge) index.append(target, cloned) continue } } if hooks.added != nil { hooks.added(path, entry.Value) } cloned := entry cloned.Value = cloneMergeSubtree(entry.Value, path, hooks.merge) index.append(target, cloned) continue } existing := target.Mapping[position].Value if hooks.merge != nil { if merged, handled := hooks.merge(path, existing, entry.Value); handled { target.Mapping[position] = entry target.Mapping[position].Value = cloneMergeSubtree(merged, path, hooks.merge) continue } } if existing.Kind == document.Mapping && entry.Value.Kind == document.Mapping { if hooks.combined != nil { hooks.combined(path, entry.Value) } mergeRecursiveMappings(existing, entry.Value, path, index, hooks) continue } if hooks.replaced != nil { hooks.replaced(path, existing, entry.Value) } target.Mapping[position] = entry target.Mapping[position].Value = cloneMergeSubtree(entry.Value, path, hooks.merge) } } // A copied mapping can contain collection operations even though its ancestor // has no retained mapping. Visit those operations with absent lower values. // Ordinary origin/replacement hooks already accounted for the whole subtree; // do not replay them for every descendant. Records inside sequences remain whole. func cloneMergeSubtree(incoming *document.Node, path Path, merge func(Path, *document.Node, *document.Node) (*document.Node, bool)) *document.Node { if incoming == nil || incoming.Kind != document.Mapping || merge == nil { return document.Clone(incoming) } cloned := *incoming cloned.Mapping = make([]document.Entry, len(incoming.Mapping)) for index, entry := range incoming.Mapping { childPath := path.Key(entry.Key) value := entry.Value if merged, handled := merge(childPath, nil, value); handled { value = merged } entry.Value = cloneMergeSubtree(value, childPath, merge) cloned.Mapping[index] = entry } return &cloned }