Resolve inheritance between identified records
This commit is contained in:
@@ -22,6 +22,18 @@ type identifiedInheritanceGraph struct {
|
||||
byIdentity map[string]int
|
||||
}
|
||||
|
||||
func sequenceHasIdentifiedInheritanceReferences(sequence *document.Node) bool {
|
||||
if sequence == nil || sequence.Kind != document.Sequence {
|
||||
return false
|
||||
}
|
||||
for _, item := range sequence.Sequence {
|
||||
if directive, found := inheritanceDirective(item); found && directive.Value.Kind != document.Null {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// analyzeIdentifiedInheritanceGraph validates relationships inside one
|
||||
// identified sequence. It prepares a graph only; mapping-sibling expansion
|
||||
// remains the active inheritance behavior until the reusable scope executor is
|
||||
|
||||
@@ -147,6 +147,93 @@ func TestIdentifiedInheritanceScopeAdapterReusesTemplateExecution(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifiedInheritanceResolvesNestedCollectionsAtEachDestination(t *testing.T) {
|
||||
root := parseItemPolicyDocument(t, "identified-nested", `jobs:
|
||||
- id: child
|
||||
extends: {id: parent}
|
||||
- id: parent
|
||||
extends: {id: base}
|
||||
steps:
|
||||
- id: child-step
|
||||
extends: {id: parent-step}
|
||||
value: child-value
|
||||
- id: parent-step
|
||||
value: parent-value
|
||||
- id: base
|
||||
steps:
|
||||
- id: base-step
|
||||
value: base-value
|
||||
`)
|
||||
identity := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
|
||||
policies := collectionPolicyScope{}
|
||||
policies.set(Keys("jobs"), identity)
|
||||
for index := 0; index < 3; index++ {
|
||||
policies.set(Keys("jobs").Index(index).Key("steps"), identity)
|
||||
}
|
||||
expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("expand nested identified inheritance: %#v", diagnostics)
|
||||
}
|
||||
steps, found := documentAtPath(expanded.root, Keys("jobs").Index(0).Key("steps"))
|
||||
if !found || steps.Kind != document.Sequence || len(steps.Sequence) != 3 {
|
||||
t.Fatalf("child nested steps = %#v, found %t", steps, found)
|
||||
}
|
||||
for index, want := range []string{"base-step", "child-step", "parent-step"} {
|
||||
id, exists := documentAtPath(steps.Sequence[index], Keys("id"))
|
||||
if !exists || id.Text != want {
|
||||
t.Fatalf("child step %d identity = %#v, found %t; want %q", index, id, exists, want)
|
||||
}
|
||||
}
|
||||
value, found := documentAtPath(steps.Sequence[1], Keys("value"))
|
||||
if !found || value.Text != "child-value" {
|
||||
t.Fatalf("nested record inheritance = %#v, found %t", value, found)
|
||||
}
|
||||
if containsInheritanceDirective(expanded.root) {
|
||||
t.Fatal("a nested record relationship remained after destination-local expansion")
|
||||
}
|
||||
childIdentity, found := expanded.origins["/jobs/0/id"]
|
||||
if !found || len(childIdentity.Locations) == 0 || childIdentity.Locations[0].SourceID != "identified-nested" {
|
||||
t.Fatalf("child identity origin = %#v, found %t", childIdentity, found)
|
||||
}
|
||||
if _, found := expanded.origins["/jobs/0/steps/1/value"]; !found {
|
||||
t.Fatal("nested inherited value did not retain its destination origin")
|
||||
}
|
||||
for _, replacement := range expanded.replacements {
|
||||
if replacement.path.String() == "/jobs/0/id" {
|
||||
t.Fatal("parent identity was treated as payload replaced by the child's identity")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifiedInheritanceRespectsOutputNodeBudget(t *testing.T) {
|
||||
root := parseItemPolicyDocument(t, "identified-budget", `jobs:
|
||||
- id: child
|
||||
extends: {id: parent}
|
||||
- id: parent
|
||||
value: inherited
|
||||
`)
|
||||
policies := collectionPolicyScope{}
|
||||
policies.set(Keys("jobs"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||
baseline, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("unbounded identified inheritance: %#v", diagnostics)
|
||||
}
|
||||
outputNodes, within := inheritanceLowerBoundCount(baseline.root, -1)
|
||||
if !within || outputNodes < 2 {
|
||||
t.Fatalf("identified output node count = %d, within %t", outputNodes, within)
|
||||
}
|
||||
limits := defaultProjectionLimits()
|
||||
limits.nodes = outputNodes
|
||||
if result, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, limits); result == nil || len(diagnostics) != 0 {
|
||||
t.Fatalf("at output-node boundary: result = %#v, diagnostics = %#v", result, diagnostics)
|
||||
}
|
||||
limits.nodes = outputNodes - 1
|
||||
if result, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, limits); result != nil ||
|
||||
len(diagnostics) != 1 || diagnostics[0].Code != CodeSchemaProjectionLimit {
|
||||
t.Fatalf("below output-node boundary: result = %#v, diagnostics = %#v", result, diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifiedInheritanceRequiresChildIdentityBeforeParentLookup(t *testing.T) {
|
||||
sequence := parseIdentifiedInheritanceSequence(t, "missing-child-field", `- id: child
|
||||
extends: {id: parent, tenant: acme}
|
||||
|
||||
@@ -220,7 +220,7 @@ func applyInheritanceScopeTemplates(adapter inheritanceScopeAdapter, order []int
|
||||
effectivePolicies := append(cloneInheritancePolicyTemplates(parentPolicies), cloneCollectionPolicyScopeForInheritance(templatePolicies))
|
||||
parentPolicyScope := projectInheritanceCollectionPolicies(resolvedPolicies[node.parent], node.path)
|
||||
childPolicyScope := projectCurrentInheritanceCollectionPolicies(templatePolicies, node.path)
|
||||
merged := document.Clone(parent)
|
||||
merged := inheritanceParentPayload(parent, node.identityFields)
|
||||
mergedPolicyScope, policyDiagnostics := mergeInheritanceCollectionPolicies(
|
||||
parentPolicyScope, childPolicyScope, node.path, parent, adapter.applicationPolicy(),
|
||||
)
|
||||
|
||||
@@ -159,6 +159,25 @@ func preserveInheritanceIdentityFields(merged, child *document.Node, fields []st
|
||||
}
|
||||
}
|
||||
|
||||
func inheritanceParentPayload(parent *document.Node, identityFields []string) *document.Node {
|
||||
cloned := document.Clone(parent)
|
||||
if len(identityFields) == 0 || cloned == nil || cloned.Kind != document.Mapping {
|
||||
return cloned
|
||||
}
|
||||
excluded := make(map[string]struct{}, len(identityFields))
|
||||
for _, field := range identityFields {
|
||||
excluded[field] = struct{}{}
|
||||
}
|
||||
kept := cloned.Mapping[:0]
|
||||
for _, entry := range cloned.Mapping {
|
||||
if _, omit := excluded[entry.Key]; !omit {
|
||||
kept = append(kept, entry)
|
||||
}
|
||||
}
|
||||
cloned.Mapping = kept
|
||||
return cloned
|
||||
}
|
||||
|
||||
func applyIdentifiedInheritanceTemplates(
|
||||
graph *identifiedInheritanceGraph,
|
||||
limits projectionLimits,
|
||||
|
||||
@@ -158,6 +158,11 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
||||
}
|
||||
}
|
||||
case document.Sequence:
|
||||
if sequenceHasIdentifiedInheritanceReferences(*node) {
|
||||
if problem := w.expandIdentifiedSequence(node, path); problem != nil {
|
||||
return problem
|
||||
}
|
||||
}
|
||||
working := **node
|
||||
working.Sequence = slices.Clone(working.Sequence)
|
||||
*node = &working
|
||||
@@ -171,6 +176,60 @@ func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *inheritanceWalker) expandIdentifiedSequence(node **document.Node, path Path) *Diagnostic {
|
||||
policy, hasPolicy := w.collectionPolicies.at(path)
|
||||
applicationFields, _ := w.applicationPolicy.identityFieldsAt(path)
|
||||
var lower *collectionPolicy
|
||||
if hasPolicy {
|
||||
lower = &policy
|
||||
}
|
||||
resolution, policyDiagnostics := resolveCollectionPolicy(path, nil, applicationFields, lower)
|
||||
if len(policyDiagnostics) != 0 {
|
||||
return &policyDiagnostics[0]
|
||||
}
|
||||
// Without an identity rule, retain the existing diagnostic for a reference
|
||||
// that has no enclosing sibling scope. A null cancellation needs no lookup.
|
||||
if len(resolution.effectiveIdentityFields) == 0 {
|
||||
return nil
|
||||
}
|
||||
original := *node
|
||||
graph, diagnostics := analyzeIdentifiedInheritanceGraph(*node, path, resolution.effectiveIdentityFields)
|
||||
if len(diagnostics) != 0 {
|
||||
return &diagnostics[0]
|
||||
}
|
||||
before, _ := inheritanceLowerBoundCount(*node, -1)
|
||||
limits := w.limits
|
||||
if limits.nodes >= 0 {
|
||||
limits.nodes -= w.nodes - before
|
||||
}
|
||||
resolved, diagnostics := applyIdentifiedInheritanceTemplates(
|
||||
graph, limits, w.origins, w.collectionPolicies, w.applicationPolicy, w.retained,
|
||||
)
|
||||
if len(diagnostics) != 0 {
|
||||
return &diagnostics[0]
|
||||
}
|
||||
*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)
|
||||
for pointer, origin := range resolved.origins {
|
||||
w.origins[pointer] = origin
|
||||
}
|
||||
w.transfers = append(w.transfers, resolved.transfers...)
|
||||
w.controls = append(w.controls, resolved.controls...)
|
||||
w.replacements = append(w.replacements, resolved.replacements...)
|
||||
w.operations = append(w.operations, resolved.operations...)
|
||||
after, _ := inheritanceLowerBoundCount(*node, -1)
|
||||
w.nodes += after - before
|
||||
return nil
|
||||
}
|
||||
|
||||
func scopeHasInheritanceDefinitions(scope *document.Node) bool {
|
||||
for _, entry := range scope.Mapping {
|
||||
if entry.Value.Kind != document.Mapping {
|
||||
|
||||
Reference in New Issue
Block a user