349 lines
15 KiB
Go
349 lines
15 KiB
Go
package engine
|
|
|
|
import (
|
|
"slices"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
type inheritanceTemplate struct {
|
|
name string
|
|
entry document.Entry
|
|
path Path
|
|
collectionPolicies collectionPolicyScope
|
|
parent int
|
|
hasParent bool
|
|
hasDirective bool
|
|
cancelled bool
|
|
directive document.Entry
|
|
}
|
|
|
|
type inheritanceGraph struct {
|
|
scope *document.Node
|
|
scopePath Path
|
|
collectionPolicies collectionPolicyScope
|
|
applicationPolicy sequencePolicy
|
|
nodes []inheritanceTemplate
|
|
edges []dependencyEdge
|
|
byName map[string]int
|
|
}
|
|
|
|
// analyzeInheritanceGraph retains only direct mapping siblings as graph nodes.
|
|
// It validates declarations separately from applying their templates, leaving
|
|
// nested scopes for the existing destination-local walk.
|
|
func analyzeInheritanceGraph(scope *document.Node, scopePath Path) (*inheritanceGraph, []Diagnostic) {
|
|
return analyzeInheritanceGraphWithPolicies(scope, scopePath, collectionPolicyScope{})
|
|
}
|
|
|
|
func analyzeInheritanceGraphWithPolicies(scope *document.Node, scopePath Path, policies collectionPolicyScope) (*inheritanceGraph, []Diagnostic) {
|
|
return analyzeInheritanceGraphWithCollectionRules(scope, scopePath, policies, sequencePolicy{})
|
|
}
|
|
|
|
func analyzeInheritanceGraphWithCollectionRules(scope *document.Node, scopePath Path, policies collectionPolicyScope, applicationPolicy sequencePolicy) (*inheritanceGraph, []Diagnostic) {
|
|
if scope == nil || scope.Kind != document.Mapping {
|
|
return nil, []Diagnostic{{
|
|
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
|
Message: "inheritance scope must be a mapping",
|
|
}}
|
|
}
|
|
|
|
policyIndex := newInheritancePolicyIndex(policies)
|
|
graph := &inheritanceGraph{
|
|
scope: scope, scopePath: scopePath,
|
|
collectionPolicies: policyIndex.absoluteScope(scopePath), applicationPolicy: applicationPolicy,
|
|
byName: make(map[string]int, len(scope.Mapping)),
|
|
}
|
|
entries := make(map[string]document.Entry, len(scope.Mapping))
|
|
for _, entry := range scope.Mapping {
|
|
entries[entry.Key] = entry
|
|
}
|
|
|
|
names := make([]string, 0, len(scope.Mapping))
|
|
for name, entry := range entries {
|
|
if entry.Value.Kind == document.Mapping {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
slices.Sort(names)
|
|
for _, name := range names {
|
|
index := len(graph.nodes)
|
|
graph.byName[name] = index
|
|
graph.nodes = append(graph.nodes, inheritanceTemplate{
|
|
name: name, entry: entries[name], path: scopePath.Key(name),
|
|
collectionPolicies: policyIndex.localize(scopePath.Key(name)),
|
|
})
|
|
}
|
|
|
|
for _, name := range names {
|
|
index := graph.byName[name]
|
|
node := &graph.nodes[index]
|
|
reference, found := inheritanceDirective(node.entry.Value)
|
|
if !found {
|
|
continue
|
|
}
|
|
node.hasDirective = true
|
|
node.directive = reference
|
|
referencePath := node.path.Key(inheritanceKey)
|
|
location := publicLocation(reference.Value.Location)
|
|
switch reference.Value.Kind {
|
|
case document.Null:
|
|
node.cancelled = true
|
|
case document.String:
|
|
parent, exists := entries[reference.Value.Text]
|
|
if !exists {
|
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceMissing, referencePath, location, nil,
|
|
"referenced sibling does not exist")}
|
|
}
|
|
if parent.Value.Kind != document.Mapping {
|
|
related := []Location{publicLocation(parent.Value.Location)}
|
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, related,
|
|
"referenced sibling is not a mapping")}
|
|
}
|
|
node.parent = graph.byName[reference.Value.Text]
|
|
node.hasParent = true
|
|
graph.edges = append(graph.edges, dependencyEdge{prerequisite: node.parent, dependent: index})
|
|
default:
|
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, nil,
|
|
"inheritance reference must be a sibling name or null")}
|
|
}
|
|
}
|
|
return graph, nil
|
|
}
|
|
|
|
func (graph *inheritanceGraph) schedule() ([]int, []Diagnostic) {
|
|
keys := make([]dependencyKey, len(graph.nodes))
|
|
for i, node := range graph.nodes {
|
|
keys[i] = dependencyKey{"definition", graph.scopePath.String(), node.path.String()}
|
|
}
|
|
order, cycle := scheduleDependencies(keys, graph.edges)
|
|
if len(cycle) == 0 {
|
|
return order, nil
|
|
}
|
|
|
|
parent := graph.nodes[cycle[0]]
|
|
child := graph.nodes[cycle[1]]
|
|
referencePath := child.path.Key(inheritanceKey)
|
|
location := publicLocation(child.directive.Value.Location)
|
|
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceCycle, referencePath, location,
|
|
[]Location{publicLocation(parent.entry.Value.Location)}, "sibling inheritance contains a cycle")}
|
|
}
|
|
|
|
func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
|
return expandSiblingDefinitionsWithPolicies(scope, scopePath, limits, inputOrigins, collectionPolicyScope{}, budget...)
|
|
}
|
|
|
|
func expandSiblingDefinitionsWithPolicies(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, policies collectionPolicyScope, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
|
return expandSiblingDefinitionsWithCollectionRules(scope, scopePath, limits, inputOrigins, policies, sequencePolicy{}, budget...)
|
|
}
|
|
|
|
func expandSiblingDefinitionsWithCollectionRules(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, policies collectionPolicyScope, applicationPolicy sequencePolicy, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
|
graph, diagnostics := analyzeInheritanceGraphWithCollectionRules(scope, scopePath, policies, applicationPolicy)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
order, diagnostics := graph.schedule()
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
result, diagnostics := applyInheritanceTemplates(graph, order, limits, inputOrigins, budget...)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// applyInheritanceTemplates builds detached templates in the supplied
|
|
// dependency-ready order while retaining the existing origins, merge history,
|
|
// and node-accounting evidence. The root keeps its authored mapping order.
|
|
func applyInheritanceTemplates(graph *inheritanceGraph, order []int, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
|
return applyInheritanceScopeTemplates(mappingInheritanceScope{graph: graph}, order, limits, inputOrigins, budget...)
|
|
}
|
|
|
|
func applyInheritanceScopeTemplates(adapter inheritanceScopeAdapter, order []int, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
|
templates := adapter.templates()
|
|
resolved := make([]*document.Node, len(templates))
|
|
resolvedOrigins := make([]map[string]Origin, len(templates))
|
|
resolvedPolicyTemplates := make([][]collectionPolicyScope, len(templates))
|
|
resolvedPolicies := make([]collectionPolicyScope, len(templates))
|
|
result := &inheritanceScope{}
|
|
counter := newRetainedNodeCounter(historyNodeLimit)
|
|
if len(budget) != 0 {
|
|
counter = budget[0]
|
|
}
|
|
retained := 1 // scope container node
|
|
for _, slot := range adapter.slots() {
|
|
if slot.hasTemplate || slot.value.Kind == document.Mapping {
|
|
continue
|
|
}
|
|
remaining := limits.nodes - retained - 1
|
|
if limits.nodes >= 0 && remaining < 0 {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(slot.path, slot.value.Location)}
|
|
}
|
|
count, within := inheritanceLowerBoundCount(slot.value, remaining)
|
|
if !within {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(slot.path, slot.value.Location)}
|
|
}
|
|
retained += 1 + count
|
|
}
|
|
|
|
for _, index := range order {
|
|
node := templates[index]
|
|
operationStart, replacementStart := len(result.operations), len(result.replacements)
|
|
child := prepareInheritanceChild(node, inputOrigins)
|
|
if child.control != nil {
|
|
result.controls = append(result.controls, *child.control)
|
|
result.operations = append(result.operations, inheritanceOperation{
|
|
control: child.control, controlOrigin: child.controlOrigin,
|
|
})
|
|
}
|
|
if limits.nodes >= 0 {
|
|
remaining := limits.nodes - retained - adapter.slotOverhead()
|
|
within := inheritanceLowerBoundWithin(child.payload, remaining)
|
|
if node.hasParent {
|
|
within = inheritanceOverlayLowerBoundWithin(resolved[node.parent], child.payload, remaining)
|
|
}
|
|
if !within {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(node.path, node.entry.Value.Location)}
|
|
}
|
|
}
|
|
|
|
if node.hasParent {
|
|
parent := resolved[node.parent]
|
|
preparedParent := prepareInheritanceParent(
|
|
node, templates[node.parent].path, parent, resolvedPolicyTemplates[node.parent],
|
|
resolvedPolicies[node.parent], child, adapter.applicationPolicy(),
|
|
)
|
|
mergedPolicyScope, policyDiagnostics := mergeInheritanceCollectionPolicies(
|
|
preparedParent.parentPolicyScope, preparedParent.childPolicyScope,
|
|
node.path, preparedParent.payload, child.payload,
|
|
adapter.applicationPolicy(), preparedParent.replaced,
|
|
)
|
|
if len(policyDiagnostics) != 0 {
|
|
return nil, policyDiagnostics
|
|
}
|
|
mergedOrigins := transferredInheritanceOrigins(resolvedOrigins[node.parent], preparedParent.transfer.from, node.path, preparedParent.transfer)
|
|
mergeInheritanceOrigin(mergedOrigins, node.path, child.origins[node.path.String()])
|
|
var sequenceContributions []sequenceHistoryContribution
|
|
var combinationDiagnostics []Diagnostic
|
|
mergeRecursiveMappings(preparedParent.payload, child.payload, node.path, new(mappingIndex), recursiveMergeHooks{
|
|
added: func(childPath Path, incoming *document.Node) {
|
|
copyInheritanceSubtree(mergedOrigins, child.origins, incoming, childPath)
|
|
},
|
|
combined: func(childPath Path, _ *document.Node) {
|
|
mergeInheritanceOrigin(mergedOrigins, childPath, child.origins[childPath.String()])
|
|
},
|
|
replaced: func(childPath Path, existing, incoming *document.Node) {
|
|
previous := displacedOrigins(existing, mergedOrigins, childPath)
|
|
result.replacements = append(result.replacements, inheritanceReplacement{
|
|
path: childPath.clone(), before: existing, after: incoming,
|
|
previous: previous, effective: cloneOrigin(child.origins[childPath.String()]),
|
|
})
|
|
deleteOriginSubtree(mergedOrigins, childPath, existing)
|
|
copyInheritanceSubtree(mergedOrigins, child.origins, incoming, childPath)
|
|
},
|
|
merge: func(childPath Path, existing, incoming *document.Node) (*document.Node, bool) {
|
|
resolution, _, _, active, issues := inheritanceCollectionResolution(
|
|
childPath, preparedParent.parentPolicyScope, preparedParent.childPolicyScope, adapter.applicationPolicy(), incoming,
|
|
)
|
|
if len(issues) != 0 {
|
|
combinationDiagnostics = append(combinationDiagnostics, issues...)
|
|
return existing, false
|
|
}
|
|
if !active {
|
|
return nil, false
|
|
}
|
|
lowerValue := existing
|
|
combined, problems := combineSequence(lowerValue, incoming, resolution, sequenceCombinationContext{destination: childPath})
|
|
if len(problems) != 0 {
|
|
combinationDiagnostics = append(combinationDiagnostics, problems...)
|
|
return existing, false
|
|
}
|
|
policy := applyInheritanceSequencePolicy(
|
|
childPath, combined, resolution, preparedParent.parentPolicyScope, preparedParent.childPolicyScope,
|
|
)
|
|
mergedPolicyScope.set(childPath, policy)
|
|
applyInheritanceSequenceOrigins(
|
|
result, mergedOrigins, child.origins, childPath, lowerValue, incoming, combined, resolution.operation,
|
|
)
|
|
history := sequenceHistoryContribution{path: childPath.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...)
|
|
}
|
|
sequenceContributions = append(sequenceContributions, history)
|
|
return combined.value, true
|
|
},
|
|
})
|
|
preserveInheritanceIdentityFields(preparedParent.payload, child.payload, node.identityFields)
|
|
if len(combinationDiagnostics) != 0 {
|
|
return nil, canonicalDiagnostics(combinationDiagnostics)
|
|
}
|
|
if problems := validateRegisteredInheritanceSequences(preparedParent.payload, node.path, adapter.applicationPolicy()); len(problems) != 0 {
|
|
return nil, problems
|
|
}
|
|
preparedParent.payload.Location = node.entry.Value.Location
|
|
resolved[index], resolvedOrigins[index] = preparedParent.payload, mergedOrigins
|
|
resolvedPolicies[index] = inheritancePolicyScopeAtPath(mergedPolicyScope, node.path)
|
|
result.transfers = append(result.transfers, preparedParent.transfer)
|
|
result.operations = append(result.operations, inheritanceOperation{
|
|
transfer: &preparedParent.transfer, parent: preparedParent.historyParent, effective: preparedParent.payload,
|
|
parentOrigins: resolvedOrigins[node.parent], effectiveOrigins: mergedOrigins,
|
|
parentPolicies: cloneInheritancePolicyTemplates(preparedParent.policyTemplates),
|
|
templatePolicies: cloneCollectionPolicyScopeForInheritance(child.policyTemplates),
|
|
effectivePolicies: cloneInheritancePolicyTemplates(preparedParent.effectivePolicyTemplates),
|
|
sequences: cloneSequenceHistoryContributions(sequenceContributions),
|
|
identityFields: cloneStrings(node.identityFields),
|
|
})
|
|
resolvedPolicyTemplates[index] = preparedParent.effectivePolicyTemplates
|
|
} else {
|
|
resolved[index] = document.Clone(child.payload)
|
|
resolvedOrigins[index] = child.origins
|
|
resolvedPolicyTemplates[index] = []collectionPolicyScope{child.policyTemplates}
|
|
resolvedPolicies[index] = cloneCollectionPolicyScopeForInheritance(child.policyTemplates)
|
|
}
|
|
|
|
count, _ := inheritanceLowerBoundCount(resolved[index], limits.nodes)
|
|
retained += adapter.slotOverhead() + count
|
|
for _, operation := range result.operations[operationStart:] {
|
|
if !counter.add(operation.parent) || !counter.add(operation.effective) {
|
|
return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(node.path, node.entry.Value.Location)}
|
|
}
|
|
}
|
|
for _, replacement := range result.replacements[replacementStart:] {
|
|
if !counter.add(replacement.before) || !counter.add(replacement.after) {
|
|
return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(node.path, node.entry.Value.Location)}
|
|
}
|
|
}
|
|
}
|
|
|
|
root := adapter.rebuild(resolved)
|
|
result.collectionPolicies = collectionPolicyScope{}
|
|
result.origins = directInheritanceOrigins(&document.Node{Kind: root.Kind, Location: root.Location}, adapter.scopePath(), inputOrigins)
|
|
policyIndex := newInheritancePolicyIndex(adapter.collectionPolicies())
|
|
for _, slot := range adapter.slots() {
|
|
if slot.hasTemplate {
|
|
index := slot.template
|
|
projectInheritancePolicyRoots(&result.collectionPolicies, resolvedPolicies[index], templates[index].path)
|
|
for pointer, origin := range resolvedOrigins[index] {
|
|
result.origins[pointer] = origin
|
|
}
|
|
continue
|
|
}
|
|
localPolicies := policyIndex.localize(slot.path)
|
|
projectInheritancePolicyRoots(&result.collectionPolicies, localPolicies, slot.path)
|
|
for pointer, origin := range directInheritanceOrigins(slot.value, slot.path, inputOrigins) {
|
|
result.origins[pointer] = origin
|
|
}
|
|
}
|
|
result.root = root
|
|
if !inheritanceLowerBoundWithin(result.root, limits.nodes) {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(adapter.scopePath(), adapter.scope().Location)}
|
|
}
|
|
return result, nil
|
|
}
|