Files
ConfigKit/internal/engine/recursive_merge.go

84 lines
3.1 KiB
Go

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
}