Files
ConfigKit/internal/engine/overlay.go

647 lines
24 KiB
Go

package engine
import (
"slices"
"strings"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
type overlayRecord struct {
change Change
displaced []displacedDefinition
}
type displacedDefinition struct {
path Path
origin Origin
}
func overlayDocuments(base *composedDocument, overlay *document.Node) (*composedDocument, []Diagnostic) {
if base == nil {
return nil, invalidOverlayDocument()
}
assembly := newAssemblyAccumulator(cloneComposedDocument(base))
if diagnostics := assembly.applyOverlay(overlay, nil, nil, LayerOverlay); len(diagnostics) != 0 {
return nil, diagnostics
}
return assembly.current, nil
}
func invalidOverlayDocument() []Diagnostic {
return []Diagnostic{{
Code: CodeOverlayInvalid, Severity: SeverityError,
Message: "overlay requires mapping document roots",
}}
}
func (a *assemblyAccumulator) applyOverlay(overlay *document.Node, incomingOrigins map[string]Origin, applications []historyApplication, layer Layer) []Diagnostic {
if a == nil || a.current == nil || a.current.root == nil || a.current.root.Kind != document.Mapping || overlay == nil || overlay.Kind != document.Mapping {
return invalidOverlayDocument()
}
mergeOriginContribution(a.current.origins, Path{}, incomingOrigin(incomingOrigins, Path{}, overlay, layer))
mergeOverlayMappings(a.current, a.current.root, overlay, Path{}, layer, true, incomingOrigins, &a.index)
if len(applications) == 0 {
a.current.applications = append(a.current.applications, historyApplication{layer: layer, value: overlay})
} else {
for _, application := range applications {
application.layer = layer
a.current.applications = append(a.current.applications, application)
}
}
return nil
}
// applyOverlayWithCollectionPolicy is the shared recursive-precedence entry for
// overlays. The caller supplies only the policy and declarations owned by this
// contribution; layered loading can use the same operation when its lifecycle
// reaches this boundary.
func (a *assemblyAccumulator) applyOverlayWithCollectionPolicy(
overlay *document.Node,
incomingOrigins map[string]Origin,
incomingPolicies collectionPolicyScope,
applications []historyApplication,
layer Layer,
sequencePolicy sequencePolicy,
declarations []sequenceDeclarationSpec,
) []Diagnostic {
_, diagnostics := a.applyOverlayWithCollectionProjection(overlay, incomingOrigins, incomingPolicies, applications, layer, sequencePolicy, declarations)
return diagnostics
}
// applyOverlayWithCollectionProjection also returns the incoming tier's item
// assignments so acquisition evidence can follow the same operation as values.
func (a *assemblyAccumulator) applyOverlayWithCollectionProjection(
overlay *document.Node,
incomingOrigins map[string]Origin,
incomingPolicies collectionPolicyScope,
applications []historyApplication,
layer Layer,
sequencePolicy sequencePolicy,
declarations []sequenceDeclarationSpec,
) ([]sequenceHistoryContribution, []Diagnostic) {
if a == nil || a.current == nil || a.current.root == nil || a.current.root.Kind != document.Mapping || overlay == nil || overlay.Kind != document.Mapping {
return nil, invalidOverlayDocument()
}
sequences, diagnostics := mergeOverlayMappingsWithCollectionPolicy(a, overlay, incomingOrigins, incomingPolicies, layer, sequencePolicy, declarations)
if len(diagnostics) != 0 {
return nil, diagnostics
}
if len(applications) == 0 {
application := historyApplication{layer: layer, value: overlay, sequences: sequences}
application.sequencesByPath = indexSequenceHistoryContributions(application.sequences)
a.current.applications = append(a.current.applications, application)
} else {
for _, application := range projectTierHistoryApplications(applications, sequences) {
application.layer = layer
a.current.applications = append(a.current.applications, application)
}
}
return sequences, nil
}
func mergeOverlayMappingsWithCollectionPolicy(
assembly *assemblyAccumulator,
overlay *document.Node,
incomingOrigins map[string]Origin,
incomingPolicies collectionPolicyScope,
layer Layer,
sequencePolicy sequencePolicy,
declarations []sequenceDeclarationSpec,
) ([]sequenceHistoryContribution, []Diagnostic) {
prepared, diagnostics := prepareOverlayCollection(
assembly.current.collectionPolicies, overlay, sequencePolicy, declarations,
)
if len(diagnostics) != 0 {
return nil, diagnostics
}
operation := newOverlayMergeOperation(assembly, overlay, incomingOrigins, incomingPolicies, layer, prepared)
operation.apply()
if len(operation.diagnostics) != 0 {
return nil, canonicalDiagnostics(operation.diagnostics)
}
return finalizeOverlayCollection(assembly, prepared, operation)
}
// preparedOverlayCollection contains decisions made against the read-only
// lower policy scope. Incoming declarations remain grouped by destination for
// declaration-only finalization after value application.
type preparedOverlayCollection struct {
declarationsByPath map[string][]sequenceDeclarationSpec
resolutions map[string]collectionPolicyResolution
sequencePolicy sequencePolicy
}
func prepareOverlayCollection(
lowerPolicies collectionPolicyScope,
overlay *document.Node,
sequencePolicy sequencePolicy,
declarations []sequenceDeclarationSpec,
) (*preparedOverlayCollection, []Diagnostic) {
claims := compositionClaims{byPath: make(map[string][]compositionClaim)}
claims.append(overlay, Path{}, 0)
identityFieldsByPath, resolvedPolicies, diagnostics := resolveStrictCollectionPolicies(
claims.ordered, sequencePolicy, lowerPolicies, declarations,
)
if len(diagnostics) != 0 {
return nil, diagnostics
}
declarationsByPath := make(map[string][]sequenceDeclarationSpec, len(declarations))
registeredPaths := make(map[string]Path)
for _, declaration := range declarations {
key := declaration.destination.String()
declarationsByPath[key] = append(declarationsByPath[key], declaration)
registeredPaths[key] = declaration.destination.clone()
}
registered := make(map[string]bool)
for _, claim := range claims.ordered {
key := claim.path.String()
if _, hasLowerPolicy := lowerPolicies.collections[key]; hasLowerPolicy {
registered[key] = true
registeredPaths[key] = claim.path.clone()
}
if claim.kind == document.Sequence {
if _, ok := sequencePolicy.identityFieldsAt(claim.path); ok {
registered[key] = true
registeredPaths[key] = claim.path.clone()
}
}
}
for key := range declarationsByPath {
registered[key] = true
}
resolutions := make(map[string]collectionPolicyResolution, len(registered))
for key := range registered {
path := registeredPaths[key]
policy, hasPolicy := resolvedPolicies.at(path)
if !hasPolicy {
continue
}
operation := sequenceModeReplace
if policy.explicitModePresent {
operation = policy.explicitMode
} else if len(identityFieldsByPath[key]) != 0 {
operation = sequenceModeUpsert
}
resolutions[key] = collectionPolicyResolution{
policy: policy, operation: operation,
effectiveIdentityFields: cloneStrings(identityFieldsByPath[key]),
}
}
return &preparedOverlayCollection{
declarationsByPath: declarationsByPath,
resolutions: resolutions,
sequencePolicy: sequencePolicy,
}, nil
}
// overlayMergeOperation owns recursive value application and its associated
// origin, policy, diagnostic, and sequence-history evidence.
type overlayMergeOperation struct {
assembly *assemblyAccumulator
result *composedDocument
index *mappingIndex
overlay *document.Node
incomingOrigins map[string]Origin
incomingPolicies collectionPolicyScope
layer Layer
prepared *preparedOverlayCollection
diagnostics []Diagnostic
sequenceApplications map[string]sequenceHistoryContribution
valueDestinations map[string]bool
}
func newOverlayMergeOperation(
assembly *assemblyAccumulator,
overlay *document.Node,
incomingOrigins map[string]Origin,
incomingPolicies collectionPolicyScope,
layer Layer,
prepared *preparedOverlayCollection,
) *overlayMergeOperation {
return &overlayMergeOperation{
assembly: assembly, result: assembly.current, index: &assembly.index, overlay: overlay,
incomingOrigins: incomingOrigins, incomingPolicies: incomingPolicies,
layer: layer, prepared: prepared,
sequenceApplications: make(map[string]sequenceHistoryContribution),
valueDestinations: make(map[string]bool),
}
}
func (operation *overlayMergeOperation) apply() {
mergeOriginContribution(operation.result.origins, Path{}, incomingOrigin(operation.incomingOrigins, Path{}, operation.overlay, operation.layer))
mergeOverlayMappingsWithHooks(
operation.result.root, operation.overlay, Path{}, operation.index,
operation.callbacks(),
)
}
func (operation *overlayMergeOperation) callbacks() recursiveMergeHooks {
return recursiveMergeHooks{
added: operation.added,
combined: operation.combined,
replaced: operation.replaced,
merge: operation.merge,
}
}
func (operation *overlayMergeOperation) added(path Path, incoming *document.Node) {
collectOverlayOriginsFrom(operation.result.origins, operation.incomingOrigins, incoming, path, operation.layer)
}
func (operation *overlayMergeOperation) combined(path Path, incoming *document.Node) {
mergeOriginContribution(operation.result.origins, path, incomingOrigin(operation.incomingOrigins, path, incoming, operation.layer))
}
func (operation *overlayMergeOperation) replaced(path Path, existing, incoming *document.Node) {
operation.assembly.retireCollectionPoliciesAtOrBelow(path)
recordOverlayReplacement(operation.result, path, existing, incoming, operation.layer, true, operation.incomingOrigins)
}
func (operation *overlayMergeOperation) merge(path Path, existing, incoming *document.Node) (*document.Node, bool) {
resolution, active := operation.prepared.resolutions[path.String()]
if !active {
return nil, false
}
combined, issues := combineSequence(existing, incoming, resolution, sequenceCombinationContext{destination: path})
if len(issues) != 0 {
operation.diagnostics = append(operation.diagnostics, issues...)
return existing, false
}
applyOverlaySequenceEvidence(
operation.assembly, path, existing, incoming, combined, resolution,
operation.incomingOrigins, operation.incomingPolicies, operation.layer,
)
operation.valueDestinations[path.String()] = true
history := sequenceHistoryContribution{path: path.clone(), mode: sequenceHistoryMapped}
if resolution.operation == sequenceModeReplace {
history.mode = sequenceHistoryReplace
if incoming != nil && incoming.Kind == document.Sequence {
history.length = len(incoming.Sequence)
}
} else {
history.items = append([]sequenceItemProjection(nil), combined.higherItems...)
history.replacements = append([]sequenceItemReplacement(nil), combined.replacements...)
}
operation.sequenceApplications[path.String()] = history
return combined.value, true
}
func finalizeOverlayCollection(
assembly *assemblyAccumulator,
prepared *preparedOverlayCollection,
operation *overlayMergeOperation,
) ([]sequenceHistoryContribution, []Diagnostic) {
var diagnostics []Diagnostic
// Values are complete. Build the retained lookup lazily only if a
// declaration-only destination needs to be checked.
var retainedLookup *documentLookup
for key, grouped := range prepared.declarationsByPath {
if operation.valueDestinations[key] {
continue
}
path := grouped[0].destination
applicationIdentity, _ := prepared.sequencePolicy.identityFieldsAt(path)
current, issues := resolveCollectionPolicy(path, grouped, applicationIdentity, nil)
if len(issues) != 0 {
diagnostics = append(diagnostics, issues...)
continue
}
lower, found := assembly.current.collectionPolicies.at(path)
var lowerPolicy *collectionPolicy
if found {
lowerPolicy = &lower
}
if retainedLookup == nil {
retainedLookup = buildDocumentLookup(assembly.current.root)
}
retained, _ := documentAtPathWithLookup(assembly.current.root, path, retainedLookup)
if retained != nil && retained.Kind == document.Sequence && len(current.effectiveIdentityFields) != 0 {
if _, issues := validateSequenceIdentity(retained, path, current.effectiveIdentityFields, sequenceIdentityDiagnosticCodes{invalid: CodeSequenceInvalid, conflict: CodeSequenceConflict}); len(issues) != 0 {
diagnostics = append(diagnostics, issues...)
continue
}
}
policy, issues := collectionPolicyWithoutValue(lowerPolicy, current.policy, retained, path)
if len(issues) != 0 {
diagnostics = append(diagnostics, issues...)
continue
}
assembly.setCollectionPolicy(path, policy)
}
if len(diagnostics) != 0 {
return nil, canonicalDiagnostics(diagnostics)
}
keys := make([]string, 0, len(operation.sequenceApplications))
for key := range operation.sequenceApplications {
keys = append(keys, key)
}
slices.Sort(keys)
sequences := make([]sequenceHistoryContribution, 0, len(keys))
for _, key := range keys {
sequences = append(sequences, operation.sequenceApplications[key])
}
return sequences, nil
}
func mergeOverlayMappings(result *composedDocument, target, overlay *document.Node, current Path, layer Layer, recordReplacements bool, incomingOrigins map[string]Origin, index *mappingIndex) {
mergeOverlayMappingsWithHooks(target, overlay, current, index, recursiveMergeHooks{
added: func(path Path, incoming *document.Node) {
collectOverlayOriginsFrom(result.origins, incomingOrigins, incoming, path, layer)
},
combined: func(path Path, incoming *document.Node) {
mergeOriginContribution(result.origins, path, incomingOrigin(incomingOrigins, path, incoming, layer))
},
replaced: func(path Path, existing, incoming *document.Node) {
displaced := displacedOrigins(existing, result.origins, path)
previous := cloneOrigin(result.origins[path.String()])
deleteOriginSubtree(result.origins, path, existing)
collectOverlayOriginsFrom(result.origins, incomingOrigins, incoming, path, layer)
if recordReplacements {
result.overlays = append(result.overlays, overlayRecord{
change: Change{
Path: path.clone(), Origin: incomingOrigin(incomingOrigins, path, incoming, layer),
Previous: previousOrigins(previous),
},
displaced: displaced,
})
}
},
})
}
func mergeOverlayMappingsWithHooks(
target, overlay *document.Node,
current Path,
index *mappingIndex,
hooks recursiveMergeHooks,
) {
mergeRecursiveMappings(target, overlay, current, index, hooks)
}
func recordOverlayReplacement(
result *composedDocument,
path Path,
existing, incoming *document.Node,
layer Layer,
recordReplacement bool,
incomingOrigins map[string]Origin,
) {
var displaced []displacedDefinition
var previous Origin
if existing != nil {
displaced = displacedOrigins(existing, result.origins, path)
previous = cloneOrigin(result.origins[path.String()])
deleteOriginSubtree(result.origins, path, existing)
}
collectOverlayOriginsFrom(result.origins, incomingOrigins, incoming, path, layer)
if recordReplacement && existing != nil {
result.overlays = append(result.overlays, overlayRecord{
change: Change{
Path: path.clone(), Origin: incomingOrigin(incomingOrigins, path, incoming, layer),
Previous: previousOrigins(previous),
},
displaced: displaced,
})
}
}
func applyOverlaySequenceEvidence(
assembly *assemblyAccumulator,
path Path,
lower, higher *document.Node,
combined sequenceCombinationResult,
resolution collectionPolicyResolution,
incomingOrigins map[string]Origin,
incomingPolicies collectionPolicyScope,
layer Layer,
) {
result := assembly.current
lowerPolicy, hadLowerPolicy := result.collectionPolicies.at(path)
if resolution.operation == sequenceModeReplace {
var lowerValue *collectionPolicy
if hadLowerPolicy {
lowerValue = &lowerPolicy
}
assembly.retireCollectionPoliciesAtOrBelow(path)
resolution.policy = collectionPolicyForReplacement(lowerValue, resolution.policy)
}
if combined.value == nil || combined.value.Kind != document.Sequence {
recordOverlayReplacement(result, path, lower, higher, layer, true, incomingOrigins)
assembly.setCollectionPolicy(path, resolution.policy)
return
}
if resolution.operation == sequenceModeReplace {
var displaced []displacedDefinition
var previous Origin
if lower != nil {
displaced = displacedOrigins(lower, result.origins, path)
previous = cloneOrigin(result.origins[path.String()])
deleteOriginSubtree(result.origins, path, lower)
}
mergeOriginContribution(result.origins, path, incomingOrigin(incomingOrigins, path, higher, layer))
for _, projection := range combined.higherItems {
if higher != nil && higher.Kind == document.Sequence && projection.inputIndex >= 0 && projection.inputIndex < len(higher.Sequence) {
source := higher.Sequence[projection.inputIndex]
collectOverlayOriginsAt(result.origins, incomingOrigins, source, path.Index(projection.inputIndex), path.Index(projection.outputIndex), layer)
}
}
if lower != nil {
result.overlays = append(result.overlays, overlayRecord{
change: Change{
Path: path.clone(), Origin: incomingOrigin(incomingOrigins, path, higher, layer),
Previous: previousOrigins(previous),
}, displaced: displaced,
})
}
} else {
mergeOriginContribution(result.origins, path, incomingOrigin(incomingOrigins, path, higher, layer))
for _, replacement := range combined.replacements {
lowerPath := path.Index(replacement.lowerIndex)
higherPath := path.Index(replacement.higherIndex)
outputPath := path.Index(replacement.outputIndex)
if lower == nil || lower.Kind != document.Sequence || replacement.lowerIndex < 0 || replacement.lowerIndex >= len(lower.Sequence) ||
higher == nil || higher.Kind != document.Sequence || replacement.higherIndex < 0 || replacement.higherIndex >= len(higher.Sequence) {
continue
}
lowerItem := lower.Sequence[replacement.lowerIndex]
higherItem := higher.Sequence[replacement.higherIndex]
displaced := displacedOrigins(lowerItem, result.origins, lowerPath)
previous := cloneOrigin(result.origins[lowerPath.String()])
deleteOriginSubtree(result.origins, lowerPath, lowerItem)
collectOverlayOriginsAt(result.origins, incomingOrigins, higherItem, higherPath, outputPath, layer)
result.overlays = append(result.overlays, overlayRecord{
change: Change{
Path: outputPath.clone(), Origin: incomingOrigin(incomingOrigins, higherPath, higherItem, layer),
Previous: previousOrigins(previous),
},
displaced: displaced,
})
}
for _, projection := range combined.higherItems {
if higher != nil && higher.Kind == document.Sequence && projection.inputIndex >= 0 && projection.inputIndex < len(higher.Sequence) {
higherItem := higher.Sequence[projection.inputIndex]
collectOverlayOriginsAt(result.origins, incomingOrigins, higherItem, path.Index(projection.inputIndex), path.Index(projection.outputIndex), layer)
}
}
}
policy := cloneCollectionPolicy(resolution.policy)
policy.recordOrder = nil
policy.recordPolicies = nil
lowerProjection := newSequenceItemProjectionIndex(combined.lowerItems)
higherProjection := newSequenceItemProjectionIndex(combined.higherItems)
incomingCollection, hasIncomingCollection := incomingPolicies.at(path)
for outputIndex := range combined.value.Sequence {
if higherIndex, replaced := higherProjection.inputForOutput(outputIndex); replaced {
if record, found := incomingCollection.recordPolicyAt(higherIndex); hasIncomingCollection && found {
policy.setRecordPolicyAt(outputIndex, retainedCollectionRecordPolicy(record))
} else {
policy.setRecordPolicyAt(outputIndex, replacedCollectionRecordPolicy())
}
continue
}
if hadLowerPolicy && resolution.operation != sequenceModeReplace {
if lowerIndex, retained := lowerProjection.inputForOutput(outputIndex); retained {
if record, found := lowerPolicy.recordPolicyAt(lowerIndex); found {
policy.setRecordPolicyAt(outputIndex, retainedCollectionRecordPolicy(record))
}
}
} else {
policy.setRecordPolicyAt(outputIndex, replacedCollectionRecordPolicy())
}
}
assembly.setCollectionPolicy(path, policy)
}
func collectOverlayOrigins(origins map[string]Origin, node *document.Node, path Path, layer Layer) {
collectOverlayOriginsFrom(origins, nil, node, path, layer)
}
func collectOverlayOriginsFrom(origins, incomingOrigins map[string]Origin, node *document.Node, path Path, layer Layer) {
collectOverlayOriginsAt(origins, incomingOrigins, node, path, path, layer)
}
func collectOverlayOriginsAt(origins, incomingOrigins map[string]Origin, node *document.Node, from, to Path, layer Layer) {
walkDocument(node, Path{}, func(relative Path, value *document.Node) {
source := appendDestination(from, relative)
destination := appendDestination(to, relative)
origins[destination.String()] = incomingOrigin(incomingOrigins, source, value, layer)
})
}
func mergeOverlayOrigin(origins map[string]Origin, path Path, location document.Location, layer Layer) {
mergeOriginContribution(origins, path, overlayOrigin(layer, location))
}
func mergeOriginContribution(origins map[string]Origin, path Path, incoming Origin) {
pointer := path.String()
origin := origins[pointer]
origin.Layer = incoming.Layer
origin.Locations = append(origin.Locations, incoming.Locations...)
origins[pointer] = origin
}
func incomingOrigin(origins map[string]Origin, path Path, node *document.Node, layer Layer) Origin {
if origin, ok := origins[path.String()]; ok {
origin = cloneOrigin(origin)
origin.Layer = layer
return origin
}
return overlayOrigin(layer, node.Location)
}
func overlayOrigin(layer Layer, location document.Location) Origin {
return Origin{Layer: layer, Locations: []Location{publicLocation(location)}}
}
func previousOrigins(origin Origin) []Origin {
if origin.Layer == "" {
return nil
}
return []Origin{origin}
}
func displacedOrigins(node *document.Node, origins map[string]Origin, path Path) []displacedDefinition {
var displaced []displacedDefinition
collectDisplacedOrigins(&displaced, node, origins, path, Origin{})
return sortDisplacedDefinitions(displaced)
}
func sortDisplacedDefinitions(displaced []displacedDefinition) []displacedDefinition {
type orderedDefinition struct {
definition displacedDefinition
pointer string
}
ordered := make([]orderedDefinition, len(displaced))
for i, definition := range displaced {
ordered[i] = orderedDefinition{definition: definition, pointer: definition.path.String()}
}
slices.SortFunc(ordered, func(left, right orderedDefinition) int {
return strings.Compare(left.pointer, right.pointer)
})
for i, item := range ordered {
displaced[i] = item.definition
}
return displaced
}
func collectDisplacedOrigins(displaced *[]displacedDefinition, node *document.Node, origins map[string]Origin, path Path, inherited Origin) {
origin, ok := origins[path.String()]
if !ok {
origin = cloneOrigin(inherited)
origin.Locations = []Location{publicLocation(node.Location)}
}
*displaced = append(*displaced, displacedDefinition{path: path.clone(), origin: cloneOrigin(origin)})
switch node.Kind {
case document.Mapping:
for _, entry := range node.Mapping {
collectDisplacedOrigins(displaced, entry.Value, origins, path.Key(entry.Key), origin)
}
case document.Sequence:
for index, child := range node.Sequence {
collectDisplacedOrigins(displaced, child, origins, path.Index(index), origin)
}
}
}
func deleteOriginSubtree(origins map[string]Origin, path Path, node *document.Node) {
delete(origins, path.String())
switch node.Kind {
case document.Mapping:
for _, entry := range node.Mapping {
deleteOriginSubtree(origins, path.Key(entry.Key), entry.Value)
}
case document.Sequence:
for index, child := range node.Sequence {
deleteOriginSubtree(origins, path.Index(index), child)
}
}
}
func cloneOriginIndex(origins map[string]Origin) map[string]Origin {
cloned := make(map[string]Origin, len(origins))
for pointer, origin := range origins {
cloned[pointer] = cloneOrigin(origin)
}
return cloned
}
func cloneOverlayRecords(records []overlayRecord) []overlayRecord {
cloned := make([]overlayRecord, len(records))
for index, record := range records {
cloned[index].change = cloneChange(record.change)
cloned[index].displaced = make([]displacedDefinition, len(record.displaced))
for displacedIndex, displaced := range record.displaced {
cloned[index].displaced[displacedIndex] = displacedDefinition{
path: displaced.path.clone(), origin: cloneOrigin(displaced.origin),
}
}
}
return cloned
}