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 { 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)) sequences, diagnostics := mergeOverlayMappingsWithCollectionPolicy(a, overlay, incomingOrigins, incomingPolicies, layer, sequencePolicy, declarations) if len(diagnostics) != 0 { return 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 applications { application.layer = layer a.current.applications = append(a.current.applications, application) } } return nil } func mergeOverlayMappingsWithCollectionPolicy( assembly *assemblyAccumulator, overlay *document.Node, incomingOrigins map[string]Origin, incomingPolicies collectionPolicyScope, layer Layer, sequencePolicy sequencePolicy, declarations []sequenceDeclarationSpec, ) ([]sequenceHistoryContribution, []Diagnostic) { result := assembly.current claims := compositionClaims{byPath: make(map[string][]compositionClaim)} claims.append(overlay, Path{}, 0) identityFieldsByPath, resolvedPolicies, diagnostics := resolveStrictCollectionPolicies( claims.ordered, sequencePolicy, result.collectionPolicies, 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 { if claim.kind == document.Sequence { if _, ok := sequencePolicy.identityFieldsAt(claim.path); ok { registered[claim.path.String()] = true registeredPaths[claim.path.String()] = claim.path.clone() } } } for key := range declarationsByPath { registered[key] = true } for key := range result.collectionPolicies.collections { if node, found := documentAtPath(overlay, result.collectionPolicies.collections[key].destination); found && node != nil { registered[key] = true registeredPaths[key] = result.collectionPolicies.collections[key].destination.clone() } } 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]), } } sequenceApplications := make(map[string]sequenceHistoryContribution) policyValues := make(map[string]bool) var combinationDiagnostics []Diagnostic mergeOverlayMappingsWithHooks(result.root, overlay, Path{}, &assembly.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) { assembly.current.collectionPolicies.retireCollectionPoliciesAtOrBelow(path) recordOverlayReplacement(result, path, existing, incoming, layer, true, incomingOrigins) }, merge: func(path Path, existing, incoming *document.Node) (*document.Node, bool) { resolution, active := resolutions[path.String()] if !active { return nil, false } combined, issues := combineSequence(existing, incoming, resolution, sequenceCombinationContext{destination: path}) if len(issues) != 0 { combinationDiagnostics = append(combinationDiagnostics, issues...) return existing, false } applyOverlaySequenceEvidence(result, path, existing, incoming, combined, resolution, incomingOrigins, incomingPolicies, layer) policyValues[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...) } sequenceApplications[path.String()] = history return combined.value, true }, }, ) if len(combinationDiagnostics) != 0 { return nil, canonicalDiagnostics(combinationDiagnostics) } for key, grouped := range declarationsByPath { if policyValues[key] { continue } path := grouped[0].destination applicationIdentity, _ := sequencePolicy.identityFieldsAt(path) current, diagnostics := resolveCollectionPolicy(path, grouped, applicationIdentity, nil) if len(diagnostics) != 0 { combinationDiagnostics = append(combinationDiagnostics, diagnostics...) continue } lower, found := assembly.current.collectionPolicies.at(path) var lowerPolicy *collectionPolicy if found { lowerPolicy = &lower } retained, _ := documentAtPath(assembly.current.root, path) 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 { combinationDiagnostics = append(combinationDiagnostics, issues...) continue } } policy, issues := collectionPolicyWithoutValue(lowerPolicy, current.policy, retained, path) if len(issues) != 0 { combinationDiagnostics = append(combinationDiagnostics, issues...) continue } assembly.current.collectionPolicies.set(path, policy) } if len(combinationDiagnostics) != 0 { return nil, canonicalDiagnostics(combinationDiagnostics) } keys := make([]string, 0, len(sequenceApplications)) for key := range sequenceApplications { keys = append(keys, key) } slices.Sort(keys) sequences := make([]sequenceHistoryContribution, 0, len(keys)) for _, key := range keys { sequences = append(sequences, 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( result *composedDocument, path Path, lower, higher *document.Node, combined sequenceCombinationResult, resolution collectionPolicyResolution, incomingOrigins map[string]Origin, incomingPolicies collectionPolicyScope, layer Layer, ) { lowerPolicy, hadLowerPolicy := result.collectionPolicies.at(path) if resolution.operation == sequenceModeReplace { var lowerValue *collectionPolicy if hadLowerPolicy { lowerValue = &lowerPolicy } result.collectionPolicies.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) result.collectionPolicies.set(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] collectOverlayOriginsFrom(result.origins, incomingOrigins, source, 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) collectOverlayOriginsFrom(result.origins, incomingOrigins, higherItem, 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] collectOverlayOriginsFrom(result.origins, incomingOrigins, higherItem, 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()) } } result.collectionPolicies.set(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) { origins[path.String()] = incomingOrigin(incomingOrigins, path, node, layer) switch node.Kind { case document.Mapping: for _, entry := range node.Mapping { collectOverlayOriginsFrom(origins, incomingOrigins, entry.Value, path.Key(entry.Key), layer) } case document.Sequence: for index, child := range node.Sequence { collectOverlayOriginsFrom(origins, incomingOrigins, child, path.Index(index), 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 }