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 operation.sequenceApplications[path.String()] = sequenceHistoryContributionForCombination( path, resolution.operation, incoming, combined, ) 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 }