Apply collection policies during sibling inheritance
This commit is contained in:
@@ -248,8 +248,14 @@ between tiers before resolving relationships. An omitted `extends` retains a
|
|||||||
lower-priority relationship; `extends: null` cancels it while preserving the
|
lower-priority relationship; `extends: null` cancels it while preserving the
|
||||||
child's own settings. Superseded references are historical evidence, not active
|
child's own settings. Superseded references are historical evidence, not active
|
||||||
relationships to validate. Each child recursively overlays its own settings
|
relationships to validate. Each child recursively overlays its own settings
|
||||||
over its resolved parent: explicit zero, false, empty string, sequence, and
|
over its resolved parent: explicit zero, false, empty string, and
|
||||||
null values replace inherited values, while mappings combine recursively.
|
null values replace inherited values, while mappings combine recursively.
|
||||||
|
Sequences at collection destinations use the shared collection operation with
|
||||||
|
the parent as lower input and the child as higher input. The destination's
|
||||||
|
explicit mode applies; otherwise a copied identity selects upsert and an
|
||||||
|
unkeyed destination replaces. A parent's explicit mode does not move to a
|
||||||
|
different destination. Sequences outside collection destinations remain atomic
|
||||||
|
replacements.
|
||||||
The child retains its key. All effective relationships resolve, including
|
The child retains its key. All effective relationships resolve, including
|
||||||
entries the application may never select for execution. Partial parent
|
entries the application may never select for execution. Partial parent
|
||||||
definitions undergo normal typed decoding and application-owned validation,
|
definitions undergo normal typed decoding and application-owned validation,
|
||||||
@@ -853,7 +859,11 @@ main apply their resolved sequence operation between precedence tiers; a higher
|
|||||||
strict scope inherits the lower identity and explicit mode before peer
|
strict scope inherits the lower identity and explicit mode before peer
|
||||||
composition. Overlay declarations apply after the base source graph is composed
|
composition. Overlay declarations apply after the base source graph is composed
|
||||||
and must agree with retained collection identity and any application
|
and must agree with retained collection identity and any application
|
||||||
registration. These policies do not change inheritance or CLI behavior. A root
|
registration. During mapping-sibling inheritance, a parent's YAML identity can
|
||||||
|
follow its collection to the child destination, while the parent's explicit
|
||||||
|
operation stays at the parent destination. A child declaration selects the
|
||||||
|
operation at its destination; absent one, keyed collections use upsert and
|
||||||
|
unkeyed collections use replace. These policies do not change CLI behavior. A root
|
||||||
`configkit.imports` declaration still requires `Options.Imports`; `NoSource` has
|
`configkit.imports` declaration still requires `Options.Imports`; `NoSource` has
|
||||||
no declaration scope.
|
no declaration scope.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Sequence Identity And Combination Implementation Plan
|
# Sequence Identity And Combination Implementation Plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–17 complete.
|
Status: implementation in progress; Stages 1–18 complete.
|
||||||
|
|
||||||
Audience: a Luna coding agent implementing one numbered stage per prompt, in
|
Audience: a Luna coding agent implementing one numbered stage per prompt, in
|
||||||
order. This plan starts at commit
|
order. This plan starts at commit
|
||||||
@@ -641,6 +641,12 @@ handoff; existing mapping-sibling relationship selection remains unchanged.
|
|||||||
Validation: inheritance, history, provenance, sensitivity, and resource-context
|
Validation: inheritance, history, provenance, sensitivity, and resource-context
|
||||||
tests plus the everyday check. No identified-record parent lookup is active yet.
|
tests plus the everyday check. No identified-record parent lookup is active yet.
|
||||||
|
|
||||||
|
**Completion:** Activated collection-aware sequence operations at mapping-sibling
|
||||||
|
inheritance destinations, including identity transfer, destination-local modes,
|
||||||
|
record-owned policy projection, and application registrations. Verified focused
|
||||||
|
inheritance and sequence tests, `go test -race ./internal/engine`, and
|
||||||
|
`./scripts/check.sh`.
|
||||||
|
|
||||||
## Stage 19 — Build Identified-Record Inheritance Graphs
|
## Stage 19 — Build Identified-Record Inheritance Graphs
|
||||||
|
|
||||||
Scope: relationship extraction/validation and shared scheduling for sequence
|
Scope: relationship extraction/validation and shared scheduling for sequence
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type inheritanceOperation struct {
|
|||||||
parentPolicies []collectionPolicyScope
|
parentPolicies []collectionPolicyScope
|
||||||
templatePolicies collectionPolicyScope
|
templatePolicies collectionPolicyScope
|
||||||
effectivePolicies []collectionPolicyScope
|
effectivePolicies []collectionPolicyScope
|
||||||
|
sequences []sequenceHistoryContribution
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneInheritancePolicyTemplates(templates []collectionPolicyScope) []collectionPolicyScope {
|
func cloneInheritancePolicyTemplates(templates []collectionPolicyScope) []collectionPolicyScope {
|
||||||
@@ -105,6 +106,7 @@ func cloneInheritanceOperations(operations []inheritanceOperation) []inheritance
|
|||||||
cloned[index].parentPolicies = cloneInheritancePolicyTemplates(operation.parentPolicies)
|
cloned[index].parentPolicies = cloneInheritancePolicyTemplates(operation.parentPolicies)
|
||||||
cloned[index].templatePolicies = cloneCollectionPolicyScopeForInheritance(operation.templatePolicies)
|
cloned[index].templatePolicies = cloneCollectionPolicyScopeForInheritance(operation.templatePolicies)
|
||||||
cloned[index].effectivePolicies = cloneInheritancePolicyTemplates(operation.effectivePolicies)
|
cloned[index].effectivePolicies = cloneInheritancePolicyTemplates(operation.effectivePolicies)
|
||||||
|
cloned[index].sequences = cloneSequenceHistoryContributions(operation.sequences)
|
||||||
}
|
}
|
||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|||||||
293
internal/engine/inheritance_collection.go
Normal file
293
internal/engine/inheritance_collection.go
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
|
||||||
|
func projectInheritanceCollectionPolicies(scope collectionPolicyScope, prefix Path) collectionPolicyScope {
|
||||||
|
projected := collectionPolicyScope{}
|
||||||
|
for _, entry := range scope.collections {
|
||||||
|
policy := cloneCollectionPolicyForInheritance(entry.policy)
|
||||||
|
stripInheritanceModes(&policy)
|
||||||
|
projected.set(appendDestination(prefix, entry.destination), policy)
|
||||||
|
}
|
||||||
|
return projected
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectCurrentInheritanceCollectionPolicies(scope collectionPolicyScope, prefix Path) collectionPolicyScope {
|
||||||
|
projected := collectionPolicyScope{}
|
||||||
|
for _, entry := range scope.collections {
|
||||||
|
projected.set(appendDestination(prefix, entry.destination), cloneCollectionPolicyForInheritance(entry.policy))
|
||||||
|
}
|
||||||
|
return projected
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectInheritancePolicyRoots(target *collectionPolicyScope, scope collectionPolicyScope, prefix Path) {
|
||||||
|
for _, entry := range scope.collections {
|
||||||
|
target.set(appendDestination(prefix, entry.destination), cloneCollectionPolicyForInheritance(entry.policy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripInheritanceModes(policy *collectionPolicy) {
|
||||||
|
if policy == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
policy.explicitMode = 0
|
||||||
|
policy.explicitModePresent = false
|
||||||
|
policy.modeLocation = nil
|
||||||
|
for owner, nested := range policy.recordPolicies {
|
||||||
|
for key, entry := range nested.collections {
|
||||||
|
stripInheritanceModes(&entry.policy)
|
||||||
|
nested.collections[key] = entry
|
||||||
|
}
|
||||||
|
policy.recordPolicies[owner] = nested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritanceDeclarationsAt(path Path, policy collectionPolicy) []sequenceDeclarationSpec {
|
||||||
|
if !policy.identityPresent && !policy.explicitModePresent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []sequenceDeclarationSpec{{
|
||||||
|
destination: path.clone(),
|
||||||
|
identityFields: cloneStrings(policy.identityFields), identityPresent: policy.identityPresent,
|
||||||
|
identityLocation: cloneLocation(policy.identityLocation),
|
||||||
|
mode: policy.explicitMode, modePresent: policy.explicitModePresent,
|
||||||
|
modeLocation: cloneLocation(policy.modeLocation),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritanceCollectionResolution(
|
||||||
|
path Path,
|
||||||
|
lowerScope, higherScope collectionPolicyScope,
|
||||||
|
applicationPolicy sequencePolicy,
|
||||||
|
) (collectionPolicyResolution, collectionPolicy, collectionPolicy, bool, []Diagnostic) {
|
||||||
|
lower, hasLower := lowerScope.at(path)
|
||||||
|
higher, hasHigher := higherScope.at(path)
|
||||||
|
applicationIdentity, registered := applicationPolicy.identityFieldsAt(path)
|
||||||
|
if !hasLower && !hasHigher && !registered {
|
||||||
|
return collectionPolicyResolution{}, collectionPolicy{}, collectionPolicy{}, false, nil
|
||||||
|
}
|
||||||
|
lowerForResolution := cloneCollectionPolicy(lower)
|
||||||
|
// The parent policy is copied to a different sibling destination. Its YAML
|
||||||
|
// identity follows the inherited collection, but its explicit mode does not.
|
||||||
|
lowerForResolution.explicitMode = 0
|
||||||
|
lowerForResolution.explicitModePresent = false
|
||||||
|
lowerForResolution.modeLocation = nil
|
||||||
|
var lowerPointer *collectionPolicy
|
||||||
|
if hasLower {
|
||||||
|
lowerPointer = &lowerForResolution
|
||||||
|
}
|
||||||
|
resolution, diagnostics := resolveCollectionPolicy(
|
||||||
|
path, inheritanceDeclarationsAt(path, higher), applicationIdentity, lowerPointer,
|
||||||
|
)
|
||||||
|
return resolution, lower, higher, true, diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeInheritanceCollectionPolicies(
|
||||||
|
parent, child collectionPolicyScope,
|
||||||
|
parentRootPath Path,
|
||||||
|
parentValue *document.Node,
|
||||||
|
applicationPolicy sequencePolicy,
|
||||||
|
) (collectionPolicyScope, []Diagnostic) {
|
||||||
|
result := cloneCollectionPolicyScopeForInheritance(parent)
|
||||||
|
paths := make(map[string]Path, len(parent.collections)+len(child.collections))
|
||||||
|
for key, entry := range parent.collections {
|
||||||
|
paths[key] = entry.destination.clone()
|
||||||
|
}
|
||||||
|
for key, entry := range child.collections {
|
||||||
|
paths[key] = entry.destination.clone()
|
||||||
|
}
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
for _, path := range paths {
|
||||||
|
resolution, lower, _, active, issues := inheritanceCollectionResolution(path, parent, child, applicationPolicy)
|
||||||
|
if len(issues) != 0 {
|
||||||
|
diagnostics = append(diagnostics, issues...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !active {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var lowerPointer *collectionPolicy
|
||||||
|
if _, found := parent.at(path); found {
|
||||||
|
lower.explicitMode = 0
|
||||||
|
lower.explicitModePresent = false
|
||||||
|
lower.modeLocation = nil
|
||||||
|
lowerPointer = &lower
|
||||||
|
}
|
||||||
|
relative, inside := relativeHistoryPath(parentRootPath, path)
|
||||||
|
var retained *document.Node
|
||||||
|
if inside {
|
||||||
|
retained, _ = documentAtPath(parentValue, relative)
|
||||||
|
}
|
||||||
|
policy, foundDiagnostics := collectionPolicyWithoutValue(lowerPointer, resolution.policy, retained, path)
|
||||||
|
if len(foundDiagnostics) != 0 {
|
||||||
|
diagnostics = append(diagnostics, foundDiagnostics...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if higher, ok := child.at(path); ok {
|
||||||
|
policy = inheritanceRecordPoliciesFromHigher(policy, higher)
|
||||||
|
}
|
||||||
|
result.set(path, policy)
|
||||||
|
}
|
||||||
|
return result, canonicalDiagnostics(diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritanceRecordPoliciesFromHigher(policy, higher collectionPolicy) collectionPolicy {
|
||||||
|
if len(higher.recordOrder) == 0 {
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
policy.recordOrder = nil
|
||||||
|
policy.recordPolicies = nil
|
||||||
|
for index := range higher.recordOrder {
|
||||||
|
if record, found := higher.recordPolicyAt(index); found {
|
||||||
|
policy.setRecordPolicyAt(index, currentCollectionRecordPolicy(record))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritedCollectionRecordPolicy(record collectionRecordPolicy) collectionRecordPolicy {
|
||||||
|
cloned := cloneCollectionPolicyForInheritance(collectionPolicy{
|
||||||
|
recordOrder: []*collectionRecordOwner{record.owner},
|
||||||
|
recordPolicies: map[*collectionRecordOwner]collectionPolicyScope{record.owner: record.scope},
|
||||||
|
})
|
||||||
|
if len(cloned.recordOrder) == 0 {
|
||||||
|
return newCollectionRecordPolicy()
|
||||||
|
}
|
||||||
|
owner := cloned.recordOrder[0]
|
||||||
|
scope := cloned.recordPolicies[owner]
|
||||||
|
for key, entry := range scope.collections {
|
||||||
|
stripInheritanceModes(&entry.policy)
|
||||||
|
scope.collections[key] = entry
|
||||||
|
}
|
||||||
|
return collectionRecordPolicy{owner: owner, scope: scope}
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentCollectionRecordPolicy(record collectionRecordPolicy) collectionRecordPolicy {
|
||||||
|
cloned := cloneCollectionPolicyForInheritance(collectionPolicy{
|
||||||
|
recordOrder: []*collectionRecordOwner{record.owner},
|
||||||
|
recordPolicies: map[*collectionRecordOwner]collectionPolicyScope{record.owner: record.scope},
|
||||||
|
})
|
||||||
|
if len(cloned.recordOrder) == 0 {
|
||||||
|
return newCollectionRecordPolicy()
|
||||||
|
}
|
||||||
|
owner := cloned.recordOrder[0]
|
||||||
|
return collectionRecordPolicy{owner: owner, scope: cloned.recordPolicies[owner]}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyInheritanceSequencePolicy(
|
||||||
|
path Path,
|
||||||
|
combined sequenceCombinationResult,
|
||||||
|
resolution collectionPolicyResolution,
|
||||||
|
lowerScope, higherScope collectionPolicyScope,
|
||||||
|
result *collectionPolicyScope,
|
||||||
|
) collectionPolicy {
|
||||||
|
policy := cloneCollectionPolicyForInheritance(resolution.policy)
|
||||||
|
policy.recordOrder = nil
|
||||||
|
policy.recordPolicies = nil
|
||||||
|
if combined.value == nil || combined.value.Kind != document.Sequence {
|
||||||
|
result.retireCollectionPoliciesAtOrBelow(path)
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerPolicy, hasLower := lowerScope.at(path)
|
||||||
|
higherPolicy, hasHigher := higherScope.at(path)
|
||||||
|
lowerProjection := newSequenceItemProjectionIndex(combined.lowerItems)
|
||||||
|
higherProjection := newSequenceItemProjectionIndex(combined.higherItems)
|
||||||
|
for outputIndex := range combined.value.Sequence {
|
||||||
|
if higherIndex, exists := higherProjection.inputForOutput(outputIndex); exists {
|
||||||
|
if record, found := higherPolicy.recordPolicyAt(higherIndex); hasHigher && found {
|
||||||
|
policy.setRecordPolicyAt(outputIndex, currentCollectionRecordPolicy(record))
|
||||||
|
} else {
|
||||||
|
policy.setRecordPolicyAt(outputIndex, replacedCollectionRecordPolicy())
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lowerIndex, exists := lowerProjection.inputForOutput(outputIndex); exists && hasLower {
|
||||||
|
if record, found := lowerPolicy.recordPolicyAt(lowerIndex); found {
|
||||||
|
policy.setRecordPolicyAt(outputIndex, inheritedCollectionRecordPolicy(record))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyInheritanceSequenceOrigins(
|
||||||
|
result *inheritanceScope,
|
||||||
|
origins, incomingOrigins map[string]Origin,
|
||||||
|
path Path,
|
||||||
|
lower, higher *document.Node,
|
||||||
|
combined sequenceCombinationResult,
|
||||||
|
operation sequenceMode,
|
||||||
|
) {
|
||||||
|
if operation == sequenceModeReplace {
|
||||||
|
if lower != nil {
|
||||||
|
displaced := displacedOrigins(lower, origins, path)
|
||||||
|
result.replacements = append(result.replacements, inheritanceReplacement{
|
||||||
|
path: path.clone(), before: lower, after: higher,
|
||||||
|
previous: displaced, effective: cloneOrigin(incomingOrigins[path.String()]),
|
||||||
|
})
|
||||||
|
deleteOriginSubtree(origins, path, lower)
|
||||||
|
}
|
||||||
|
mergeInheritanceOrigin(origins, path, incomingOrigins[path.String()])
|
||||||
|
if higher != nil && higher.Kind != document.Sequence {
|
||||||
|
copyInheritanceSubtree(origins, incomingOrigins, higher, path)
|
||||||
|
}
|
||||||
|
for _, projection := range combined.higherItems {
|
||||||
|
if higher != nil && higher.Kind == document.Sequence && projection.inputIndex >= 0 && projection.inputIndex < len(higher.Sequence) {
|
||||||
|
copyInheritanceSubtree(origins, incomingOrigins, higher.Sequence[projection.inputIndex], path.Index(projection.outputIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeInheritanceOrigin(origins, path, incomingOrigins[path.String()])
|
||||||
|
for _, replacement := range combined.replacements {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
lowerPath := path.Index(replacement.lowerIndex)
|
||||||
|
higherPath := path.Index(replacement.higherIndex)
|
||||||
|
outputPath := path.Index(replacement.outputIndex)
|
||||||
|
previous := displacedOrigins(lower.Sequence[replacement.lowerIndex], origins, lowerPath)
|
||||||
|
deleteOriginSubtree(origins, lowerPath, lower.Sequence[replacement.lowerIndex])
|
||||||
|
copyInheritanceSubtree(origins, incomingOrigins, higher.Sequence[replacement.higherIndex], outputPath)
|
||||||
|
result.replacements = append(result.replacements, inheritanceReplacement{
|
||||||
|
path: outputPath, before: lower.Sequence[replacement.lowerIndex], after: higher.Sequence[replacement.higherIndex],
|
||||||
|
previous: previous, effective: cloneOrigin(incomingOrigins[higherPath.String()]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, projection := range combined.higherItems {
|
||||||
|
if higher != nil && higher.Kind == document.Sequence && projection.inputIndex >= 0 && projection.inputIndex < len(higher.Sequence) {
|
||||||
|
copyInheritanceSubtree(origins, incomingOrigins, higher.Sequence[projection.inputIndex], path.Index(projection.outputIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRegisteredInheritanceSequences(root *document.Node, rootPath Path, applicationPolicy sequencePolicy) []Diagnostic {
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
var visit func(*document.Node, Path)
|
||||||
|
visit = func(node *document.Node, path Path) {
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if node.Kind == document.Sequence {
|
||||||
|
if fields, registered := applicationPolicy.identityFieldsAt(path); registered {
|
||||||
|
if _, issues := validateSequenceIdentity(node, path, fields, sequenceCombinationIdentityCodes); len(issues) != 0 {
|
||||||
|
diagnostics = append(diagnostics, issues...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, item := range node.Sequence {
|
||||||
|
visit(item, path.Index(index))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if node.Kind == document.Mapping {
|
||||||
|
for _, entry := range node.Mapping {
|
||||||
|
visit(entry.Value, path.Key(entry.Key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(root, rootPath)
|
||||||
|
return canonicalDiagnostics(diagnostics)
|
||||||
|
}
|
||||||
281
internal/engine/inheritance_collection_test.go
Normal file
281
internal/engine/inheritance_collection_test.go
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInheritanceCombinesIdentifiedSequencesAtDestination(t *testing.T) {
|
||||||
|
root := parseItemPolicyDocument(t, "inheritance-values", `base:
|
||||||
|
items:
|
||||||
|
- id: shared
|
||||||
|
value: parent
|
||||||
|
child:
|
||||||
|
extends: base
|
||||||
|
items:
|
||||||
|
- id: shared
|
||||||
|
value: child
|
||||||
|
`)
|
||||||
|
modeLocation := Location{SourceID: "base-policy", Line: 4, Column: 5}
|
||||||
|
policy := collectionPolicy{
|
||||||
|
identityFields: []string{"id"}, identityPresent: true,
|
||||||
|
explicitMode: sequenceModeAppend, explicitModePresent: true, modeLocation: &modeLocation,
|
||||||
|
}
|
||||||
|
policies := collectionPolicyScope{}
|
||||||
|
policies.set(Keys("base", "items"), policy)
|
||||||
|
|
||||||
|
expanded, diagnostics := expandInheritanceWithCollectionRules(
|
||||||
|
root, nil, policies, sequencePolicy{}, defaultProjectionLimits(),
|
||||||
|
)
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("expand identified inheritance: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
items, found := documentAtPath(expanded.root, Keys("child", "items"))
|
||||||
|
if !found || items.Kind != document.Sequence || len(items.Sequence) != 1 {
|
||||||
|
t.Fatalf("child items = %#v, found %t; parent mode should not force an append", items, found)
|
||||||
|
}
|
||||||
|
if value, exists := documentAtPath(items.Sequence[0], Keys("value")); !exists || value.Text != "child" {
|
||||||
|
t.Fatalf("matched record = %#v, want complete child replacement", items.Sequence[0])
|
||||||
|
}
|
||||||
|
childPolicy, found := expanded.collectionPolicies.at(Keys("child", "items"))
|
||||||
|
if !found || !childPolicy.identityPresent || childPolicy.identityFields[0] != "id" {
|
||||||
|
t.Fatalf("inherited destination identity = %#v, found %t; scope=%#v", childPolicy, found, expanded.collectionPolicies.collections)
|
||||||
|
}
|
||||||
|
if childPolicy.explicitModePresent {
|
||||||
|
t.Fatalf("parent explicit mode leaked to a different destination: %#v", childPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
var transfer *inheritanceOperation
|
||||||
|
for index := range expanded.operations {
|
||||||
|
if expanded.operations[index].transfer != nil && expanded.operations[index].transfer.to.String() == "/child" {
|
||||||
|
transfer = &expanded.operations[index]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if transfer == nil || len(transfer.sequences) != 1 || transfer.sequences[0].path.String() != "/child/items" || len(transfer.sequences[0].replacements) != 1 {
|
||||||
|
t.Fatalf("inheritance sequence projection = %#v, want one mapped replacement", transfer)
|
||||||
|
}
|
||||||
|
if origin, exists := expanded.origins["/child/items/0/value"]; !exists || len(origin.Locations) == 0 || origin.Locations[0].SourceID != "inheritance-values" {
|
||||||
|
t.Fatalf("replacement origin lost the child's source context: %#v", origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadAppliesYamlCollectionIdentityDuringSiblingInheritance(t *testing.T) {
|
||||||
|
input := `configkit:
|
||||||
|
sequences:
|
||||||
|
- destination: [base, items]
|
||||||
|
key: id
|
||||||
|
mode: append
|
||||||
|
base:
|
||||||
|
items:
|
||||||
|
- id: shared
|
||||||
|
value: parent
|
||||||
|
child:
|
||||||
|
extends: base
|
||||||
|
items:
|
||||||
|
- id: shared
|
||||||
|
value: child
|
||||||
|
`
|
||||||
|
loaded, err := Load[map[string]any](Bytes("inheritance-policy", []byte(input)), testSecretBinding())
|
||||||
|
if err != nil {
|
||||||
|
if loadError, ok := err.(*LoadError); ok {
|
||||||
|
t.Fatalf("load inherited collection policy: %v; diagnostics=%#v", err, loadError.Diagnostics())
|
||||||
|
}
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
value, found := loaded.Inspection.Explain(Keys("child", "items").Index(0).Key("value"))
|
||||||
|
if !found || value.Effective.YAML != "child\n" {
|
||||||
|
t.Fatalf("inherited sequence value = %#v, found %t", value, found)
|
||||||
|
}
|
||||||
|
if len(value.History) == 0 {
|
||||||
|
t.Fatal("inherited sequence replacement has no inspection history")
|
||||||
|
}
|
||||||
|
parentValueVisible, childValueVisible := false, false
|
||||||
|
for _, change := range value.History {
|
||||||
|
parentValueVisible = parentValueVisible || change.Before.YAML == "parent\n" || change.After.YAML == "parent\n"
|
||||||
|
childValueVisible = childValueVisible || change.After.YAML == "child\n"
|
||||||
|
}
|
||||||
|
if !parentValueVisible || !childValueVisible {
|
||||||
|
t.Fatalf("sequence replacement history omitted its parent or child value: %#v", value.History)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceUsesDestinationExplicitModeAndApplicationConstraint(t *testing.T) {
|
||||||
|
t.Run("destination append", func(t *testing.T) {
|
||||||
|
root := parseItemPolicyDocument(t, "append", `base:
|
||||||
|
items:
|
||||||
|
- id: base
|
||||||
|
child:
|
||||||
|
extends: base
|
||||||
|
items:
|
||||||
|
- id: child
|
||||||
|
`)
|
||||||
|
policies := collectionPolicyScope{}
|
||||||
|
policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||||
|
childLocation := Location{SourceID: "child-policy", Line: 4, Column: 5}
|
||||||
|
policies.set(Keys("child", "items"), collectionPolicy{
|
||||||
|
identityFields: []string{"id"}, identityPresent: true,
|
||||||
|
explicitMode: sequenceModeAppend, explicitModePresent: true, modeLocation: &childLocation,
|
||||||
|
})
|
||||||
|
expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("expand with destination append: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
items, _ := documentAtPath(expanded.root, Keys("child", "items"))
|
||||||
|
if len(items.Sequence) != 2 || items.Sequence[0].Mapping[0].Value.Text != "base" || items.Sequence[1].Mapping[0].Value.Text != "child" {
|
||||||
|
t.Fatalf("appended items = %#v", items)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("new matching namespace", func(t *testing.T) {
|
||||||
|
root := parseItemPolicyDocument(t, "dynamic", `base:
|
||||||
|
enabled: true
|
||||||
|
child:
|
||||||
|
extends: base
|
||||||
|
extensions:
|
||||||
|
- id: duplicate
|
||||||
|
- id: duplicate
|
||||||
|
`)
|
||||||
|
applicationPolicy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{{
|
||||||
|
selector: sequenceIdentitySelector{
|
||||||
|
{kind: sequenceSelectorLiteral, key: "child"},
|
||||||
|
{kind: sequenceSelectorLiteral, key: "extensions"},
|
||||||
|
},
|
||||||
|
identityFields: []string{"id"},
|
||||||
|
}})
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("compile identity constraint: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
_, diagnostics = expandInheritanceWithCollectionRules(root, nil, collectionPolicyScope{}, applicationPolicy, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict || diagnostics[0].Path.String() != "/child/extensions" {
|
||||||
|
t.Fatalf("dynamic application constraint diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceReplacementModesAndCancellation(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
kind document.Kind
|
||||||
|
}{
|
||||||
|
{name: "empty sequence", value: "[]", kind: document.Sequence},
|
||||||
|
{name: "null", value: "null", kind: document.Null},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
root := parseItemPolicyDocument(t, "replacement", "base:\n items:\n - id: prior\nchild:\n extends: base\n items: "+test.value+"\n")
|
||||||
|
policies := collectionPolicyScope{}
|
||||||
|
policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||||
|
policies.set(Keys("child", "items"), collectionPolicy{
|
||||||
|
identityFields: []string{"id"}, identityPresent: true,
|
||||||
|
explicitMode: sequenceModeReplace, explicitModePresent: true,
|
||||||
|
})
|
||||||
|
expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("expand replacement: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
items, found := documentAtPath(expanded.root, Keys("child", "items"))
|
||||||
|
if !found || items.Kind != test.kind || (items.Kind == document.Sequence && len(items.Sequence) != 0) {
|
||||||
|
t.Fatalf("replacement value = %#v, found %t", items, found)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range []string{"null", "{}"} {
|
||||||
|
t.Run("append rejects "+value, func(t *testing.T) {
|
||||||
|
root := parseItemPolicyDocument(t, "append-kind", "base:\n items: [{id: prior}]\nchild:\n extends: base\n items: "+value+"\n")
|
||||||
|
policies := collectionPolicyScope{}
|
||||||
|
policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||||
|
policies.set(Keys("child", "items"), collectionPolicy{
|
||||||
|
identityFields: []string{"id"}, identityPresent: true,
|
||||||
|
explicitMode: sequenceModeAppend, explicitModePresent: true,
|
||||||
|
})
|
||||||
|
_, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != "/child/items" {
|
||||||
|
t.Fatalf("append kind diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("incompatible inherited identity", func(t *testing.T) {
|
||||||
|
root := parseItemPolicyDocument(t, "identity", `base:
|
||||||
|
items: [{id: one}]
|
||||||
|
child:
|
||||||
|
extends: base
|
||||||
|
items: [{id: two}]
|
||||||
|
`)
|
||||||
|
policies := collectionPolicyScope{}
|
||||||
|
policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||||
|
policies.set(Keys("child", "items"), collectionPolicy{identityFields: []string{"slug"}, identityPresent: true})
|
||||||
|
_, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict || diagnostics[0].Path.String() != "/child/items" {
|
||||||
|
t.Fatalf("identity conflict diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cancelled relationship", func(t *testing.T) {
|
||||||
|
root := parseItemPolicyDocument(t, "cancel", `base:
|
||||||
|
items: [{id: parent}]
|
||||||
|
child:
|
||||||
|
extends: null
|
||||||
|
items: [{id: local}]
|
||||||
|
`)
|
||||||
|
policies := collectionPolicyScope{}
|
||||||
|
policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||||
|
expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("expand cancelled relationship: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
if _, found := expanded.collectionPolicies.at(Keys("child", "items")); found {
|
||||||
|
t.Fatal("cancelled relationship transferred the parent's collection policy")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInheritanceReplacementRetiresRecordLocalCollectionPolicy(t *testing.T) {
|
||||||
|
root := parseItemPolicyDocument(t, "record-scope", `base:
|
||||||
|
items:
|
||||||
|
- id: shared
|
||||||
|
children:
|
||||||
|
- key: old
|
||||||
|
child:
|
||||||
|
extends: base
|
||||||
|
items:
|
||||||
|
- id: shared
|
||||||
|
children:
|
||||||
|
- key: new
|
||||||
|
`)
|
||||||
|
nested := collectionPolicy{identityFields: []string{"key"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true}
|
||||||
|
recordScope := collectionPolicyScope{}
|
||||||
|
recordScope.set(Keys("children"), nested)
|
||||||
|
outer := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
|
||||||
|
outer.setRecordPolicyAt(0, newCollectionRecordPolicy())
|
||||||
|
owner := outer.recordOrder[0]
|
||||||
|
outer.recordPolicies[owner] = recordScope
|
||||||
|
policies := collectionPolicyScope{}
|
||||||
|
policies.set(Keys("base", "items"), outer)
|
||||||
|
|
||||||
|
expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("expand replacement record: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
children, found := documentAtPath(expanded.root, Keys("child", "items").Index(0).Key("children"))
|
||||||
|
if !found || children.Kind != document.Sequence || len(children.Sequence) != 1 {
|
||||||
|
t.Fatalf("replacement children = %#v, found %t", children, found)
|
||||||
|
}
|
||||||
|
if key, exists := documentAtPath(children.Sequence[0], Keys("key")); !exists || key.Text != "new" {
|
||||||
|
t.Fatalf("replacement child = %#v, want only new record", children.Sequence[0])
|
||||||
|
}
|
||||||
|
childPolicy, exists := expanded.collectionPolicies.at(Keys("child", "items"))
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("inherited outer collection policy is missing")
|
||||||
|
}
|
||||||
|
record, found := childPolicy.recordPolicyAt(0)
|
||||||
|
if !found {
|
||||||
|
t.Fatal("replacement item has no independent record owner")
|
||||||
|
}
|
||||||
|
if nestedPolicy, exists := record.scope.at(Keys("children")); exists && nestedPolicy.explicitModePresent {
|
||||||
|
t.Fatalf("replaced record retained its former nested operation: %#v", nestedPolicy)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ type inheritanceTemplate struct {
|
|||||||
type inheritanceGraph struct {
|
type inheritanceGraph struct {
|
||||||
scope *document.Node
|
scope *document.Node
|
||||||
scopePath Path
|
scopePath Path
|
||||||
|
collectionPolicies collectionPolicyScope
|
||||||
|
applicationPolicy sequencePolicy
|
||||||
nodes []inheritanceTemplate
|
nodes []inheritanceTemplate
|
||||||
edges []dependencyEdge
|
edges []dependencyEdge
|
||||||
byName map[string]int
|
byName map[string]int
|
||||||
@@ -34,6 +36,10 @@ func analyzeInheritanceGraph(scope *document.Node, scopePath Path) (*inheritance
|
|||||||
}
|
}
|
||||||
|
|
||||||
func analyzeInheritanceGraphWithPolicies(scope *document.Node, scopePath Path, policies collectionPolicyScope) (*inheritanceGraph, []Diagnostic) {
|
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 {
|
if scope == nil || scope.Kind != document.Mapping {
|
||||||
return nil, []Diagnostic{{
|
return nil, []Diagnostic{{
|
||||||
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
||||||
@@ -43,6 +49,7 @@ func analyzeInheritanceGraphWithPolicies(scope *document.Node, scopePath Path, p
|
|||||||
|
|
||||||
graph := &inheritanceGraph{
|
graph := &inheritanceGraph{
|
||||||
scope: scope, scopePath: scopePath,
|
scope: scope, scopePath: scopePath,
|
||||||
|
collectionPolicies: cloneCollectionPolicyScopeForInheritance(policies), applicationPolicy: applicationPolicy,
|
||||||
byName: make(map[string]int, len(scope.Mapping)),
|
byName: make(map[string]int, len(scope.Mapping)),
|
||||||
}
|
}
|
||||||
entries := make(map[string]document.Entry, len(scope.Mapping))
|
entries := make(map[string]document.Entry, len(scope.Mapping))
|
||||||
@@ -125,7 +132,11 @@ func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, l
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expandSiblingDefinitionsWithPolicies(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, policies collectionPolicyScope, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
func expandSiblingDefinitionsWithPolicies(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, policies collectionPolicyScope, budget ...*retainedNodeCounter) (*inheritanceScope, []Diagnostic) {
|
||||||
graph, diagnostics := analyzeInheritanceGraphWithPolicies(scope, scopePath, policies)
|
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 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, diagnostics
|
return nil, diagnostics
|
||||||
}
|
}
|
||||||
@@ -137,7 +148,6 @@ func expandSiblingDefinitionsWithPolicies(scope *document.Node, scopePath Path,
|
|||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, diagnostics
|
return nil, diagnostics
|
||||||
}
|
}
|
||||||
result.collectionPolicies = cloneCollectionPolicyScopeForInheritance(policies)
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +158,7 @@ func applyInheritanceTemplates(graph *inheritanceGraph, order []int, limits proj
|
|||||||
resolved := make([]*document.Node, len(graph.nodes))
|
resolved := make([]*document.Node, len(graph.nodes))
|
||||||
resolvedOrigins := make([]map[string]Origin, len(graph.nodes))
|
resolvedOrigins := make([]map[string]Origin, len(graph.nodes))
|
||||||
resolvedPolicyTemplates := make([][]collectionPolicyScope, len(graph.nodes))
|
resolvedPolicyTemplates := make([][]collectionPolicyScope, len(graph.nodes))
|
||||||
|
resolvedPolicies := make([]collectionPolicyScope, len(graph.nodes))
|
||||||
result := &inheritanceScope{}
|
result := &inheritanceScope{}
|
||||||
counter := newRetainedNodeCounter(historyNodeLimit)
|
counter := newRetainedNodeCounter(historyNodeLimit)
|
||||||
if len(budget) != 0 {
|
if len(budget) != 0 {
|
||||||
@@ -202,16 +213,28 @@ func applyInheritanceTemplates(graph *inheritanceGraph, order []int, limits proj
|
|||||||
parent := resolved[node.parent]
|
parent := resolved[node.parent]
|
||||||
parentPolicies := cloneInheritancePolicyTemplates(resolvedPolicyTemplates[node.parent])
|
parentPolicies := cloneInheritancePolicyTemplates(resolvedPolicyTemplates[node.parent])
|
||||||
effectivePolicies := append(cloneInheritancePolicyTemplates(parentPolicies), cloneCollectionPolicyScopeForInheritance(templatePolicies))
|
effectivePolicies := append(cloneInheritancePolicyTemplates(parentPolicies), cloneCollectionPolicyScopeForInheritance(templatePolicies))
|
||||||
|
parentPolicyScope := projectInheritanceCollectionPolicies(resolvedPolicies[node.parent], node.path)
|
||||||
|
childPolicyScope := projectCurrentInheritanceCollectionPolicies(templatePolicies, node.path)
|
||||||
merged := document.Clone(parent)
|
merged := document.Clone(parent)
|
||||||
|
mergedPolicyScope, policyDiagnostics := mergeInheritanceCollectionPolicies(
|
||||||
|
parentPolicyScope, childPolicyScope, node.path, parent, graph.applicationPolicy,
|
||||||
|
)
|
||||||
|
if len(policyDiagnostics) != 0 {
|
||||||
|
return nil, policyDiagnostics
|
||||||
|
}
|
||||||
transfer := inheritanceTransfer{
|
transfer := inheritanceTransfer{
|
||||||
from: graph.nodes[node.parent].path, to: node.path,
|
from: graph.nodes[node.parent].path, to: node.path,
|
||||||
reference: node.path.Key(inheritanceKey),
|
reference: node.path.Key(inheritanceKey),
|
||||||
locations: []Location{publicLocation(node.directive.Value.Location)},
|
locations: []Location{publicLocation(node.directive.Value.Location)},
|
||||||
policyTemplates: parentPolicies, templatePolicies: cloneCollectionPolicyScopeForInheritance(templatePolicies),
|
policyTemplates: parentPolicies, templatePolicies: cloneCollectionPolicyScopeForInheritance(templatePolicies),
|
||||||
projectedPolicies: projectInheritancePolicyTemplates(parentPolicies, node.path),
|
projectedPolicies: projectInheritancePolicyTemplates(
|
||||||
|
[]collectionPolicyScope{resolvedPolicies[node.parent]}, node.path,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
mergedOrigins := transferredInheritanceOrigins(resolvedOrigins[node.parent], transfer.from, node.path, transfer)
|
mergedOrigins := transferredInheritanceOrigins(resolvedOrigins[node.parent], transfer.from, node.path, transfer)
|
||||||
mergeInheritanceOrigin(mergedOrigins, node.path, childOrigins[node.path.String()])
|
mergeInheritanceOrigin(mergedOrigins, node.path, childOrigins[node.path.String()])
|
||||||
|
var sequenceContributions []sequenceHistoryContribution
|
||||||
|
var combinationDiagnostics []Diagnostic
|
||||||
mergeRecursiveMappings(merged, payload, node.path, new(mappingIndex), recursiveMergeHooks{
|
mergeRecursiveMappings(merged, payload, node.path, new(mappingIndex), recursiveMergeHooks{
|
||||||
added: func(childPath Path, incoming *document.Node) {
|
added: func(childPath Path, incoming *document.Node) {
|
||||||
copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath)
|
copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath)
|
||||||
@@ -228,9 +251,57 @@ func applyInheritanceTemplates(graph *inheritanceGraph, order []int, limits proj
|
|||||||
deleteOriginSubtree(mergedOrigins, childPath, existing)
|
deleteOriginSubtree(mergedOrigins, childPath, existing)
|
||||||
copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath)
|
copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath)
|
||||||
},
|
},
|
||||||
|
merge: func(childPath Path, existing, incoming *document.Node) (*document.Node, bool) {
|
||||||
|
resolution, _, _, active, issues := inheritanceCollectionResolution(
|
||||||
|
childPath, parentPolicyScope, childPolicyScope, graph.applicationPolicy,
|
||||||
|
)
|
||||||
|
if len(issues) != 0 {
|
||||||
|
combinationDiagnostics = append(combinationDiagnostics, issues...)
|
||||||
|
return existing, false
|
||||||
|
}
|
||||||
|
if !active {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
lowerPath, within := relativeHistoryPath(node.path, childPath)
|
||||||
|
if !within {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
lowerValue, _ := documentAtPath(parent, lowerPath)
|
||||||
|
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, parentPolicyScope, childPolicyScope, &mergedPolicyScope,
|
||||||
|
)
|
||||||
|
mergedPolicyScope.set(childPath, policy)
|
||||||
|
applyInheritanceSequenceOrigins(
|
||||||
|
result, mergedOrigins, childOrigins, 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
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
if len(combinationDiagnostics) != 0 {
|
||||||
|
return nil, canonicalDiagnostics(combinationDiagnostics)
|
||||||
|
}
|
||||||
|
if problems := validateRegisteredInheritanceSequences(merged, node.path, graph.applicationPolicy); len(problems) != 0 {
|
||||||
|
return nil, problems
|
||||||
|
}
|
||||||
merged.Location = node.entry.Value.Location
|
merged.Location = node.entry.Value.Location
|
||||||
resolved[index], resolvedOrigins[index] = merged, mergedOrigins
|
resolved[index], resolvedOrigins[index] = merged, mergedOrigins
|
||||||
|
resolvedPolicies[index] = inheritancePolicyScopeAtPath(mergedPolicyScope, node.path)
|
||||||
result.transfers = append(result.transfers, transfer)
|
result.transfers = append(result.transfers, transfer)
|
||||||
result.operations = append(result.operations, inheritanceOperation{
|
result.operations = append(result.operations, inheritanceOperation{
|
||||||
transfer: &transfer, parent: parent, effective: merged,
|
transfer: &transfer, parent: parent, effective: merged,
|
||||||
@@ -238,12 +309,14 @@ func applyInheritanceTemplates(graph *inheritanceGraph, order []int, limits proj
|
|||||||
parentPolicies: cloneInheritancePolicyTemplates(parentPolicies),
|
parentPolicies: cloneInheritancePolicyTemplates(parentPolicies),
|
||||||
templatePolicies: cloneCollectionPolicyScopeForInheritance(templatePolicies),
|
templatePolicies: cloneCollectionPolicyScopeForInheritance(templatePolicies),
|
||||||
effectivePolicies: cloneInheritancePolicyTemplates(effectivePolicies),
|
effectivePolicies: cloneInheritancePolicyTemplates(effectivePolicies),
|
||||||
|
sequences: cloneSequenceHistoryContributions(sequenceContributions),
|
||||||
})
|
})
|
||||||
resolvedPolicyTemplates[index] = effectivePolicies
|
resolvedPolicyTemplates[index] = effectivePolicies
|
||||||
} else {
|
} else {
|
||||||
resolved[index] = document.Clone(payload)
|
resolved[index] = document.Clone(payload)
|
||||||
resolvedOrigins[index] = childOrigins
|
resolvedOrigins[index] = childOrigins
|
||||||
resolvedPolicyTemplates[index] = []collectionPolicyScope{templatePolicies}
|
resolvedPolicyTemplates[index] = []collectionPolicyScope{templatePolicies}
|
||||||
|
resolvedPolicies[index] = cloneCollectionPolicyScopeForInheritance(templatePolicies)
|
||||||
}
|
}
|
||||||
|
|
||||||
count, _ := inheritanceLowerBoundCount(resolved[index], limits.nodes)
|
count, _ := inheritanceLowerBoundCount(resolved[index], limits.nodes)
|
||||||
@@ -262,17 +335,21 @@ func applyInheritanceTemplates(graph *inheritanceGraph, order []int, limits proj
|
|||||||
|
|
||||||
root := *graph.scope
|
root := *graph.scope
|
||||||
root.Mapping = make([]document.Entry, len(graph.scope.Mapping))
|
root.Mapping = make([]document.Entry, len(graph.scope.Mapping))
|
||||||
|
result.collectionPolicies = collectionPolicyScope{}
|
||||||
result.origins = directInheritanceOrigins(&document.Node{Kind: document.Mapping, Location: graph.scope.Location}, graph.scopePath, inputOrigins)
|
result.origins = directInheritanceOrigins(&document.Node{Kind: document.Mapping, Location: graph.scope.Location}, graph.scopePath, inputOrigins)
|
||||||
for i, entry := range graph.scope.Mapping {
|
for i, entry := range graph.scope.Mapping {
|
||||||
root.Mapping[i] = entry
|
root.Mapping[i] = entry
|
||||||
if index, found := graph.byName[entry.Key]; found {
|
if index, found := graph.byName[entry.Key]; found {
|
||||||
root.Mapping[i].Value = resolved[index]
|
root.Mapping[i].Value = resolved[index]
|
||||||
|
projectInheritancePolicyRoots(&result.collectionPolicies, resolvedPolicies[index], graph.nodes[index].path)
|
||||||
for pointer, origin := range resolvedOrigins[index] {
|
for pointer, origin := range resolvedOrigins[index] {
|
||||||
result.origins[pointer] = origin
|
result.origins[pointer] = origin
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
root.Mapping[i].Value = document.Clone(entry.Value)
|
root.Mapping[i].Value = document.Clone(entry.Value)
|
||||||
|
localPolicies := inheritancePolicyScopeAtPath(graph.collectionPolicies, graph.scopePath.Key(entry.Key))
|
||||||
|
projectInheritancePolicyRoots(&result.collectionPolicies, localPolicies, graph.scopePath.Key(entry.Key))
|
||||||
for pointer, origin := range directInheritanceOrigins(entry.Value, graph.scopePath.Key(entry.Key), inputOrigins) {
|
for pointer, origin := range directInheritanceOrigins(entry.Value, graph.scopePath.Key(entry.Key), inputOrigins) {
|
||||||
result.origins[pointer] = origin
|
result.origins[pointer] = origin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type inheritanceWalker struct {
|
|||||||
nodes int // logical non-directive nodes in the entire working tree
|
nodes int // logical non-directive nodes in the entire working tree
|
||||||
origins map[string]Origin
|
origins map[string]Origin
|
||||||
collectionPolicies collectionPolicyScope
|
collectionPolicies collectionPolicyScope
|
||||||
|
applicationPolicy sequencePolicy
|
||||||
limits projectionLimits
|
limits projectionLimits
|
||||||
retained *retainedNodeCounter
|
retained *retainedNodeCounter
|
||||||
transfers []inheritanceTransfer
|
transfers []inheritanceTransfer
|
||||||
@@ -34,10 +35,18 @@ func expandInheritanceWithBudgets(input *document.Node, inputOrigins map[string]
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expandInheritanceWithPolicies(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
func expandInheritanceWithPolicies(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
||||||
return expandInheritanceWithPolicyBudgets(input, inputOrigins, policies, limits, historyNodeLimit)
|
return expandInheritanceWithCollectionRules(input, inputOrigins, policies, sequencePolicy{}, limits)
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandInheritanceWithPolicyBudgets(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, limits projectionLimits, historyLimit int) (*inheritanceScope, []Diagnostic) {
|
func expandInheritanceWithPolicyBudgets(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, limits projectionLimits, historyLimit int) (*inheritanceScope, []Diagnostic) {
|
||||||
|
return expandInheritanceWithCollectionRuleBudgets(input, inputOrigins, policies, sequencePolicy{}, limits, historyLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandInheritanceWithCollectionRules(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, applicationPolicy sequencePolicy, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
||||||
|
return expandInheritanceWithCollectionRuleBudgets(input, inputOrigins, policies, applicationPolicy, limits, historyNodeLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandInheritanceWithCollectionRuleBudgets(input *document.Node, inputOrigins map[string]Origin, policies collectionPolicyScope, applicationPolicy sequencePolicy, limits projectionLimits, historyLimit int) (*inheritanceScope, []Diagnostic) {
|
||||||
if input == nil || input.Kind != document.Mapping {
|
if input == nil || input.Kind != document.Mapping {
|
||||||
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
|
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
|
||||||
Message: "inheritance requires a mapping document root"}}
|
Message: "inheritance requires a mapping document root"}}
|
||||||
@@ -54,6 +63,7 @@ func expandInheritanceWithPolicyBudgets(input *document.Node, inputOrigins map[s
|
|||||||
walker := inheritanceWalker{
|
walker := inheritanceWalker{
|
||||||
nodes: nodes, limits: limits, origins: origins,
|
nodes: nodes, limits: limits, origins: origins,
|
||||||
collectionPolicies: cloneCollectionPolicyScopeForInheritance(policies),
|
collectionPolicies: cloneCollectionPolicyScopeForInheritance(policies),
|
||||||
|
applicationPolicy: applicationPolicy,
|
||||||
retained: newRetainedNodeCounter(historyLimit),
|
retained: newRetainedNodeCounter(historyLimit),
|
||||||
}
|
}
|
||||||
if problem := walker.visit(&root, Path{}, true); problem != nil {
|
if problem := walker.visit(&root, Path{}, true); problem != nil {
|
||||||
@@ -102,11 +112,21 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
|||||||
if limits.nodes >= 0 {
|
if limits.nodes >= 0 {
|
||||||
limits.nodes -= w.nodes - before
|
limits.nodes -= w.nodes - before
|
||||||
}
|
}
|
||||||
resolved, diagnostics := expandSiblingDefinitionsWithPolicies(*node, path, limits, w.origins, w.collectionPolicies, w.retained)
|
resolved, diagnostics := expandSiblingDefinitionsWithCollectionRules(
|
||||||
|
*node, path, limits, w.origins, w.collectionPolicies, w.applicationPolicy, w.retained,
|
||||||
|
)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return &diagnostics[0]
|
return &diagnostics[0]
|
||||||
}
|
}
|
||||||
*node = resolved.root
|
*node = resolved.root
|
||||||
|
for pointer, entry := range w.collectionPolicies.collections {
|
||||||
|
if pathHasPrefix(entry.destination, path) {
|
||||||
|
delete(w.collectionPolicies.collections, pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, entry := range resolved.collectionPolicies.collections {
|
||||||
|
w.collectionPolicies.set(entry.destination, entry.policy)
|
||||||
|
}
|
||||||
deleteOriginSubtree(w.origins, path, original)
|
deleteOriginSubtree(w.origins, path, original)
|
||||||
for pointer, origin := range resolved.origins {
|
for pointer, origin := range resolved.origins {
|
||||||
w.origins[pointer] = origin
|
w.origins[pointer] = origin
|
||||||
|
|||||||
@@ -258,8 +258,8 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
|
|||||||
accumulator.add(problems...)
|
accumulator.add(problems...)
|
||||||
return nil, accumulator.loadError()
|
return nil, accumulator.loadError()
|
||||||
}
|
}
|
||||||
expanded, problems := expandInheritanceWithPolicies(
|
expanded, problems := expandInheritanceWithCollectionRules(
|
||||||
composition.root, composition.origins, composition.collectionPolicies, defaultProjectionLimits(),
|
composition.root, composition.origins, composition.collectionPolicies, request.sequencePolicy, defaultProjectionLimits(),
|
||||||
)
|
)
|
||||||
if len(problems) != 0 {
|
if len(problems) != 0 {
|
||||||
accumulator.add(problems...)
|
accumulator.add(problems...)
|
||||||
|
|||||||
Reference in New Issue
Block a user