Track collection policy ownership
This commit is contained in:
@@ -287,6 +287,9 @@ Scope: private scoped-state transformations built on Stage 4's resolution helper
|
||||
Validation: focused collection-policy tests and the everyday check. This stage
|
||||
must not add path-existence, shape, nullability, or global YAML final-check rules.
|
||||
|
||||
**Completion:** Implemented and verified with focused collection-policy tests,
|
||||
`go test -race ./internal/engine`, and `./scripts/check.sh`.
|
||||
|
||||
## Stage 6 — Implement The Shared Sequence Operations
|
||||
|
||||
Scope: a pure document-level combination helper, not separate implementations
|
||||
|
||||
@@ -7,6 +7,8 @@ type collectionPolicy struct {
|
||||
explicitMode sequenceMode
|
||||
explicitModePresent bool
|
||||
modeLocation *Location
|
||||
recordOrder []*collectionRecordOwner
|
||||
recordPolicies map[*collectionRecordOwner]collectionPolicyScope
|
||||
}
|
||||
|
||||
type collectionPolicyResolution struct {
|
||||
|
||||
242
internal/engine/collection_policy_state.go
Normal file
242
internal/engine/collection_policy_state.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
type collectionPolicyScope struct {
|
||||
collections map[string]collectionPolicyEntry
|
||||
}
|
||||
|
||||
type collectionPolicyEntry struct {
|
||||
destination Path
|
||||
policy collectionPolicy
|
||||
}
|
||||
|
||||
type collectionRecordPolicy struct {
|
||||
owner *collectionRecordOwner
|
||||
scope collectionPolicyScope
|
||||
}
|
||||
|
||||
// The nonzero field ensures simultaneously live record owners have distinct
|
||||
// addresses. The token is per-record ownership, not a source or identity key.
|
||||
type collectionRecordOwner struct {
|
||||
marker byte
|
||||
}
|
||||
|
||||
type projectedCollectionPolicy struct {
|
||||
path Path
|
||||
policy collectionPolicy
|
||||
}
|
||||
|
||||
func newCollectionRecordPolicy() collectionRecordPolicy {
|
||||
return collectionRecordPolicy{owner: &collectionRecordOwner{marker: 1}}
|
||||
}
|
||||
|
||||
func retainedCollectionRecordPolicy(record collectionRecordPolicy) collectionRecordPolicy {
|
||||
return collectionRecordPolicy{
|
||||
owner: record.owner,
|
||||
scope: cloneCollectionPolicyScope(record.scope),
|
||||
}
|
||||
}
|
||||
|
||||
func replacedCollectionRecordPolicy() collectionRecordPolicy {
|
||||
return newCollectionRecordPolicy()
|
||||
}
|
||||
|
||||
func (policy *collectionPolicy) setRecordPolicy(record collectionRecordPolicy) {
|
||||
if record.owner == nil {
|
||||
record.owner = newCollectionRecordPolicy().owner
|
||||
}
|
||||
if policy.recordPolicies == nil {
|
||||
policy.recordPolicies = make(map[*collectionRecordOwner]collectionPolicyScope)
|
||||
}
|
||||
if _, exists := policy.recordPolicies[record.owner]; !exists {
|
||||
policy.recordOrder = append(policy.recordOrder, record.owner)
|
||||
}
|
||||
policy.recordPolicies[record.owner] = cloneCollectionPolicyScope(record.scope)
|
||||
}
|
||||
|
||||
func (policy collectionPolicy) recordPolicyAt(index int) (collectionRecordPolicy, bool) {
|
||||
if index < 0 || index >= len(policy.recordOrder) {
|
||||
return collectionRecordPolicy{}, false
|
||||
}
|
||||
owner := policy.recordOrder[index]
|
||||
scope, ok := policy.recordPolicies[owner]
|
||||
if !ok {
|
||||
return collectionRecordPolicy{}, false
|
||||
}
|
||||
return collectionRecordPolicy{owner: owner, scope: cloneCollectionPolicyScope(scope)}, true
|
||||
}
|
||||
|
||||
func collectionPolicyForReplacement(lower *collectionPolicy, current collectionPolicy) collectionPolicy {
|
||||
result := cloneCollectionPolicy(current)
|
||||
if !current.explicitModePresent && lower != nil && lower.explicitModePresent {
|
||||
result.explicitMode = lower.explicitMode
|
||||
result.explicitModePresent = true
|
||||
result.modeLocation = cloneLocation(lower.modeLocation)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// collectionPolicyWithoutValue keeps identity and record ownership because no
|
||||
// collection value was replaced. A key-only declaration can remain pending
|
||||
// when no sequence is present, or validate the retained sequence immediately.
|
||||
func collectionPolicyWithoutValue(
|
||||
lower *collectionPolicy,
|
||||
current collectionPolicy,
|
||||
retainedValue *document.Node,
|
||||
path Path,
|
||||
) (collectionPolicy, []Diagnostic) {
|
||||
result := collectionPolicy{}
|
||||
if lower != nil {
|
||||
result = cloneCollectionPolicy(*lower)
|
||||
}
|
||||
if lower != nil && lower.identityPresent && current.identityPresent && !equalSequenceIdentityFields(lower.identityFields, current.identityFields) {
|
||||
return collectionPolicy{}, []Diagnostic{collectionPolicyConflictDiagnostic(
|
||||
path, current.identityLocation, lower.identityLocation,
|
||||
"collection identity declaration conflicts with retained identity",
|
||||
)}
|
||||
}
|
||||
if current.identityPresent {
|
||||
result.identityFields = cloneStrings(current.identityFields)
|
||||
result.identityPresent = true
|
||||
result.identityLocation = cloneLocation(current.identityLocation)
|
||||
}
|
||||
if current.explicitModePresent {
|
||||
result.explicitMode = current.explicitMode
|
||||
result.explicitModePresent = true
|
||||
result.modeLocation = cloneLocation(current.modeLocation)
|
||||
}
|
||||
if current.identityPresent && retainedValue != nil && retainedValue.Kind == document.Sequence {
|
||||
_, diagnostics := validateSequenceIdentity(retainedValue, path, result.identityFields, sequenceIdentityDiagnosticCodes{
|
||||
invalid: CodeSequenceInvalid, conflict: CodeSequenceConflict,
|
||||
})
|
||||
if len(diagnostics) != 0 {
|
||||
return collectionPolicy{}, diagnostics
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (scope *collectionPolicyScope) set(destination Path, policy collectionPolicy) {
|
||||
if scope.collections == nil {
|
||||
scope.collections = make(map[string]collectionPolicyEntry)
|
||||
}
|
||||
scope.collections[destination.String()] = collectionPolicyEntry{
|
||||
destination: destination.clone(), policy: cloneCollectionPolicy(policy),
|
||||
}
|
||||
}
|
||||
|
||||
func (scope collectionPolicyScope) at(destination Path) (collectionPolicy, bool) {
|
||||
entry, ok := scope.collections[destination.String()]
|
||||
if !ok {
|
||||
return collectionPolicy{}, false
|
||||
}
|
||||
return cloneCollectionPolicy(entry.policy), true
|
||||
}
|
||||
|
||||
// retireCollectionPoliciesAtOrBelow clears YAML identity and record-owned state
|
||||
// below a replaced ancestor while retaining independently scoped explicit modes.
|
||||
func (scope *collectionPolicyScope) retireCollectionPoliciesAtOrBelow(ancestor Path) {
|
||||
if scope == nil {
|
||||
return
|
||||
}
|
||||
for key, entry := range scope.collections {
|
||||
if !pathHasPrefix(entry.destination, ancestor) {
|
||||
continue
|
||||
}
|
||||
entry.policy.identityFields = nil
|
||||
entry.policy.identityPresent = false
|
||||
entry.policy.identityLocation = nil
|
||||
entry.policy.recordOrder = nil
|
||||
entry.policy.recordPolicies = nil
|
||||
scope.collections[key] = entry
|
||||
}
|
||||
}
|
||||
|
||||
func projectCollectionPolicyScope(scope collectionPolicyScope, prefix Path) []projectedCollectionPolicy {
|
||||
keys := make([]string, 0, len(scope.collections))
|
||||
for key := range scope.collections {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var projected []projectedCollectionPolicy
|
||||
for _, key := range keys {
|
||||
entry := scope.collections[key]
|
||||
concrete := appendDestination(prefix, entry.destination)
|
||||
projected = append(projected, projectedCollectionPolicy{path: concrete, policy: cloneCollectionPolicy(entry.policy)})
|
||||
for index, owner := range entry.policy.recordOrder {
|
||||
if recordScope, ok := entry.policy.recordPolicies[owner]; ok {
|
||||
projected = append(projected, projectCollectionPolicyScope(recordScope, concrete.Index(index))...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return projected
|
||||
}
|
||||
|
||||
func cloneCollectionPolicyScope(scope collectionPolicyScope) collectionPolicyScope {
|
||||
if scope.collections == nil {
|
||||
return collectionPolicyScope{}
|
||||
}
|
||||
cloned := collectionPolicyScope{collections: make(map[string]collectionPolicyEntry, len(scope.collections))}
|
||||
for key, entry := range scope.collections {
|
||||
cloned.collections[key] = collectionPolicyEntry{
|
||||
destination: entry.destination.clone(), policy: cloneCollectionPolicy(entry.policy),
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneCollectionPolicyScopeForInheritance(scope collectionPolicyScope) collectionPolicyScope {
|
||||
if scope.collections == nil {
|
||||
return collectionPolicyScope{}
|
||||
}
|
||||
cloned := collectionPolicyScope{collections: make(map[string]collectionPolicyEntry, len(scope.collections))}
|
||||
for key, entry := range scope.collections {
|
||||
cloned.collections[key] = collectionPolicyEntry{
|
||||
destination: entry.destination.clone(), policy: cloneCollectionPolicyForInheritance(entry.policy),
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneCollectionPolicy(policy collectionPolicy) collectionPolicy {
|
||||
cloned := policy
|
||||
cloned.identityFields = cloneStrings(policy.identityFields)
|
||||
cloned.identityLocation = cloneLocation(policy.identityLocation)
|
||||
cloned.modeLocation = cloneLocation(policy.modeLocation)
|
||||
if policy.recordOrder != nil {
|
||||
cloned.recordOrder = append([]*collectionRecordOwner(nil), policy.recordOrder...)
|
||||
}
|
||||
if policy.recordPolicies != nil {
|
||||
cloned.recordPolicies = make(map[*collectionRecordOwner]collectionPolicyScope, len(policy.recordPolicies))
|
||||
for owner, scope := range policy.recordPolicies {
|
||||
cloned.recordPolicies[owner] = cloneCollectionPolicyScope(scope)
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneCollectionPolicyForInheritance(policy collectionPolicy) collectionPolicy {
|
||||
cloned := policy
|
||||
cloned.identityFields = cloneStrings(policy.identityFields)
|
||||
cloned.identityLocation = cloneLocation(policy.identityLocation)
|
||||
cloned.modeLocation = cloneLocation(policy.modeLocation)
|
||||
if policy.recordOrder != nil {
|
||||
cloned.recordOrder = make([]*collectionRecordOwner, 0, len(policy.recordOrder))
|
||||
}
|
||||
if policy.recordPolicies != nil {
|
||||
cloned.recordPolicies = make(map[*collectionRecordOwner]collectionPolicyScope, len(policy.recordPolicies))
|
||||
for _, sourceOwner := range policy.recordOrder {
|
||||
owner := newCollectionRecordPolicy().owner
|
||||
cloned.recordOrder = append(cloned.recordOrder, owner)
|
||||
if sourceScope, ok := policy.recordPolicies[sourceOwner]; ok {
|
||||
cloned.recordPolicies[owner] = cloneCollectionPolicyScopeForInheritance(sourceScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
292
internal/engine/collection_policy_state_test.go
Normal file
292
internal/engine/collection_policy_state_test.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
func TestCollectionPolicyWithoutValueRetainsIdentityAndMode(t *testing.T) {
|
||||
lower := collectionPolicy{
|
||||
identityFields: []string{"id"},
|
||||
identityPresent: true,
|
||||
explicitMode: sequenceModeAppend,
|
||||
explicitModePresent: true,
|
||||
}
|
||||
lower.setRecordPolicy(newCollectionRecordPolicy())
|
||||
current := collectionPolicy{explicitMode: sequenceModeReplace, explicitModePresent: true}
|
||||
retained, diagnostics := collectionPolicyWithoutValue(&lower, current, nil, Keys("prompts"))
|
||||
if len(diagnostics) != 0 || !retained.identityPresent || !reflect.DeepEqual(retained.identityFields, []string{"id"}) {
|
||||
t.Fatalf("no-value replace reset identity: %#v, %#v", retained, diagnostics)
|
||||
}
|
||||
if retained.explicitMode != sequenceModeReplace || !retained.explicitModePresent || len(retained.recordOrder) != 1 {
|
||||
t.Fatalf("no-value policy update lost mode or record ownership: %#v", retained)
|
||||
}
|
||||
retainedRecord, _ := retained.recordPolicyAt(0)
|
||||
lowerRecord, _ := lower.recordPolicyAt(0)
|
||||
if retainedRecord.owner != lowerRecord.owner {
|
||||
t.Fatal("no-value policy update changed record ownership")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyWithoutValueLeavesAbsentAndNonSequenceDestinationsUnconstrained(t *testing.T) {
|
||||
current := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
|
||||
for _, value := range []*document.Node{nil, parsePolicyNode(t, "value: scalar\n").Mapping[0].Value} {
|
||||
retained, diagnostics := collectionPolicyWithoutValue(nil, current, value, Keys("prompts"))
|
||||
if len(diagnostics) != 0 || !retained.identityPresent || !reflect.DeepEqual(retained.identityFields, []string{"id"}) {
|
||||
t.Fatalf("pending identity at absent/non-sequence destination = %#v, %#v", retained, diagnostics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyWithoutValueValidatesRetainedSequenceImmediately(t *testing.T) {
|
||||
current := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
|
||||
valid := parsePolicyNode(t, "items: [{id: one}, {id: two}]\n").Mapping[0].Value
|
||||
retained, diagnostics := collectionPolicyWithoutValue(nil, current, valid, Keys("prompts"))
|
||||
if len(diagnostics) != 0 || !retained.identityPresent {
|
||||
t.Fatalf("valid retained records were rejected: %#v, %#v", retained, diagnostics)
|
||||
}
|
||||
|
||||
invalid := parsePolicyNode(t, "items: [{name: missing-id}]\n").Mapping[0].Value
|
||||
retained, diagnostics = collectionPolicyWithoutValue(nil, current, invalid, Keys("prompts"))
|
||||
if !reflect.DeepEqual(retained, collectionPolicy{}) || len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != "/prompts/0/id" {
|
||||
t.Fatalf("invalid retained records = %#v, %#v", retained, diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyWithoutValueRejectsIdentityChange(t *testing.T) {
|
||||
lower := collectionPolicy{identityFields: []string{"id"}, identityPresent: true}
|
||||
current := collectionPolicy{identityFields: []string{"name"}, identityPresent: true}
|
||||
retained, diagnostics := collectionPolicyWithoutValue(&lower, current, nil, Keys("prompts"))
|
||||
if !reflect.DeepEqual(retained, collectionPolicy{}) || len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict {
|
||||
t.Fatalf("no-value identity change = %#v, %#v", retained, diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyLifetimePersistenceAndReplacementTable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
current collectionPolicy
|
||||
replaceValue bool
|
||||
wantIdentity []string
|
||||
wantIdentitySet bool
|
||||
wantMode sequenceMode
|
||||
wantRecords int
|
||||
}{
|
||||
{
|
||||
name: "mode-only replace declaration does not reset retained identity",
|
||||
current: collectionPolicy{
|
||||
explicitMode: sequenceModeReplace, explicitModePresent: true,
|
||||
},
|
||||
wantIdentity: []string{"id"}, wantIdentitySet: true,
|
||||
wantMode: sequenceModeReplace, wantRecords: 1,
|
||||
},
|
||||
{
|
||||
name: "whole-sequence replacement resets lower identity",
|
||||
replaceValue: true,
|
||||
wantIdentitySet: false,
|
||||
wantMode: sequenceModeAppend,
|
||||
wantRecords: 0,
|
||||
},
|
||||
{
|
||||
name: "replacement can establish a new identity",
|
||||
current: collectionPolicy{identityFields: []string{"name"}, identityPresent: true},
|
||||
replaceValue: true,
|
||||
wantIdentity: []string{"name"},
|
||||
wantIdentitySet: true,
|
||||
wantMode: sequenceModeAppend,
|
||||
wantRecords: 0,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
lower := collectionPolicy{
|
||||
identityFields: []string{"id"}, identityPresent: true,
|
||||
explicitMode: sequenceModeAppend, explicitModePresent: true,
|
||||
}
|
||||
lower.setRecordPolicy(newCollectionRecordPolicy())
|
||||
var got collectionPolicy
|
||||
if test.replaceValue {
|
||||
got = collectionPolicyForReplacement(&lower, test.current)
|
||||
} else {
|
||||
var diagnostics []Diagnostic
|
||||
got, diagnostics = collectionPolicyWithoutValue(&lower, test.current, nil, Keys("prompts"))
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("policy diagnostics = %#v", diagnostics)
|
||||
}
|
||||
}
|
||||
if got.identityPresent != test.wantIdentitySet || !reflect.DeepEqual(got.identityFields, test.wantIdentity) || got.explicitMode != test.wantMode || len(got.recordOrder) != test.wantRecords {
|
||||
t.Fatalf("transition result = %#v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyValueReplacementResetsIdentityButRetainsExplicitMode(t *testing.T) {
|
||||
lower := collectionPolicy{
|
||||
identityFields: []string{"id"},
|
||||
identityPresent: true,
|
||||
explicitMode: sequenceModeAppend,
|
||||
explicitModePresent: true,
|
||||
}
|
||||
lower.setRecordPolicy(newCollectionRecordPolicy())
|
||||
current := collectionPolicy{identityFields: []string{"name"}, identityPresent: true}
|
||||
replaced := collectionPolicyForReplacement(&lower, current)
|
||||
if !replaced.identityPresent || !reflect.DeepEqual(replaced.identityFields, []string{"name"}) {
|
||||
t.Fatalf("replacement identity was not reset: %#v", replaced)
|
||||
}
|
||||
if replaced.explicitMode != sequenceModeAppend || !replaced.explicitModePresent || len(replaced.recordOrder) != 0 {
|
||||
t.Fatalf("replacement failed to retain explicit mode or retire lower record policy: %#v", replaced)
|
||||
}
|
||||
|
||||
cleared := collectionPolicyForReplacement(&lower, collectionPolicy{})
|
||||
if cleared.identityPresent || len(cleared.identityFields) != 0 || cleared.explicitMode != sequenceModeAppend || !cleared.explicitModePresent {
|
||||
t.Fatalf("replacement without a new key did not clear YAML identity while retaining mode: %#v", cleared)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionRecordRetentionAndReplacementUseOpaqueOwners(t *testing.T) {
|
||||
oldRecord := newCollectionRecordPolicy()
|
||||
oldRecord.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||
retained := retainedCollectionRecordPolicy(oldRecord)
|
||||
if retained.owner != oldRecord.owner {
|
||||
t.Fatal("retained record did not keep its owner")
|
||||
}
|
||||
if got, ok := retained.scope.at(Keys("steps")); !ok || !got.identityPresent {
|
||||
t.Fatalf("retained record lost its local policy: %#v, %t", got, ok)
|
||||
}
|
||||
if retained.scope.collections["/steps"].policy.identityFields[0] = "changed"; oldRecord.scope.collections["/steps"].policy.identityFields[0] != "id" {
|
||||
t.Fatal("retained record policy aliases its source")
|
||||
}
|
||||
|
||||
// A whole-record replacement at the same list position, even with equal
|
||||
// authored identity/source metadata, receives new ownership and no old rules.
|
||||
replacement := replacedCollectionRecordPolicy()
|
||||
if replacement.owner == oldRecord.owner || len(replacement.scope.collections) != 0 {
|
||||
t.Fatal("replacement record inherited discarded owner policy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyAncestorRetirementPreservesIndependentMode(t *testing.T) {
|
||||
var scope collectionPolicyScope
|
||||
owner := newCollectionRecordPolicy()
|
||||
owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||
scope.set(Keys("catalog", "prompts"), collectionPolicyWithRecords(collectionPolicy{
|
||||
identityFields: []string{"id"},
|
||||
identityPresent: true,
|
||||
explicitMode: sequenceModeAppend,
|
||||
explicitModePresent: true,
|
||||
}, owner))
|
||||
scope.retireCollectionPoliciesAtOrBelow(Keys("catalog"))
|
||||
policy, ok := scope.at(Keys("catalog", "prompts"))
|
||||
if !ok || policy.identityPresent || len(policy.recordOrder) != 0 || policy.explicitMode != sequenceModeAppend || !policy.explicitModePresent {
|
||||
t.Fatalf("ancestor retirement = %#v, %t", policy, ok)
|
||||
}
|
||||
reintroduced, diagnostics := collectionPolicyWithoutValue(&policy, collectionPolicy{}, nil, Keys("catalog", "prompts"))
|
||||
if len(diagnostics) != 0 || reintroduced.identityPresent || reintroduced.explicitMode != sequenceModeAppend {
|
||||
t.Fatalf("retired identity was resurrected while mode persisted: %#v, %#v", reintroduced, diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualOuterIdentitiesHaveDistinctRecordOwners(t *testing.T) {
|
||||
retained := newCollectionRecordPolicy()
|
||||
replacement := replacedCollectionRecordPolicy()
|
||||
if retained.owner == replacement.owner {
|
||||
t.Fatal("replacement with the same outer identity reused the old owner")
|
||||
}
|
||||
if retained.owner == nil || replacement.owner == nil {
|
||||
t.Fatal("record owner token is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyScopesKeepRootAndItemRulesIndependent(t *testing.T) {
|
||||
var root collectionPolicyScope
|
||||
root.set(Keys("steps"), collectionPolicy{identityFields: []string{"root-id"}, identityPresent: true})
|
||||
owner := newCollectionRecordPolicy()
|
||||
owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"item-id"}, identityPresent: true})
|
||||
root.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, owner))
|
||||
|
||||
root.retireCollectionPoliciesAtOrBelow(Keys("steps"))
|
||||
rootPolicy, ok := root.at(Keys("steps"))
|
||||
if !ok || rootPolicy.identityPresent {
|
||||
t.Fatalf("root-local policy was not retired: %#v", rootPolicy)
|
||||
}
|
||||
itemPolicy, ok := root.at(Keys("items"))
|
||||
if !ok || len(itemPolicy.recordOrder) != 1 {
|
||||
t.Fatalf("unrelated item owner disappeared: %#v", itemPolicy)
|
||||
}
|
||||
itemRecord, _ := itemPolicy.recordPolicyAt(0)
|
||||
nested, ok := itemRecord.scope.at(Keys("steps"))
|
||||
if !ok || !nested.identityPresent || !reflect.DeepEqual(nested.identityFields, []string{"item-id"}) {
|
||||
t.Fatalf("item-local policy was coupled to root destination: %#v", nested)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyProjectionUsesConcretePlacement(t *testing.T) {
|
||||
first := newCollectionRecordPolicy()
|
||||
first.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"step"}, identityPresent: true})
|
||||
second := newCollectionRecordPolicy()
|
||||
second.scope.set(Keys("steps"), collectionPolicy{explicitMode: sequenceModeAppend, explicitModePresent: true})
|
||||
var scope collectionPolicyScope
|
||||
scope.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, first, second))
|
||||
|
||||
projected := projectCollectionPolicyScope(scope, Keys("mounted"))
|
||||
if len(projected) != 3 {
|
||||
t.Fatalf("projected policies = %#v", projected)
|
||||
}
|
||||
want := []string{"/mounted/items", "/mounted/items/0/steps", "/mounted/items/1/steps"}
|
||||
for index, policy := range projected {
|
||||
if policy.path.String() != want[index] {
|
||||
t.Fatalf("projected path[%d] = %q, want %q", index, policy.path.String(), want[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPolicyInheritanceCopiesOwnersAndState(t *testing.T) {
|
||||
owner := newCollectionRecordPolicy()
|
||||
owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||
original := collectionPolicyWithRecords(collectionPolicy{}, owner)
|
||||
copy := cloneCollectionPolicyForInheritance(original)
|
||||
copyOwner := copy.recordOrder[0]
|
||||
if copyOwner == owner.owner {
|
||||
t.Fatal("inheritance copy retained the source record owner")
|
||||
}
|
||||
copy.recordPolicies[copyOwner].collections["/steps"].policy.identityFields[0] = "changed"
|
||||
if owner.scope.collections["/steps"].policy.identityFields[0] != "id" {
|
||||
t.Fatal("inheritance copy aliases source policy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquisitionScopeCloneOwnsCollectionPolicyCopy(t *testing.T) {
|
||||
owner := newCollectionRecordPolicy()
|
||||
owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
||||
var scope acquisitionScope
|
||||
scope.collectionPolicies.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, owner))
|
||||
cloned := cloneAcquisitionScope(scope)
|
||||
clonedOwner := cloned.collectionPolicies.collections["/items"].policy.recordOrder[0]
|
||||
cloned.collectionPolicies.collections["/items"].policy.recordPolicies[clonedOwner].collections["/steps"].policy.identityFields[0] = "changed"
|
||||
sourceOwner := scope.collectionPolicies.collections["/items"].policy.recordOrder[0]
|
||||
if scope.collectionPolicies.collections["/items"].policy.recordPolicies[sourceOwner].collections["/steps"].policy.identityFields[0] != "id" {
|
||||
t.Fatal("acquisition scope clone aliases collection policy")
|
||||
}
|
||||
if clonedOwner != owner.owner {
|
||||
t.Fatal("acquisition scope clone changed record ownership")
|
||||
}
|
||||
}
|
||||
|
||||
func collectionPolicyWithRecords(policy collectionPolicy, records ...collectionRecordPolicy) collectionPolicy {
|
||||
for _, record := range records {
|
||||
policy.setRecordPolicy(record)
|
||||
}
|
||||
return policy
|
||||
}
|
||||
|
||||
func parsePolicyNode(t *testing.T, input string) *document.Node {
|
||||
t.Helper()
|
||||
parsed, diagnostics := parseSource(Bytes("collection-policy-state.yml", []byte(input)), "collection-policy-state", defaultParseLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("parse policy value: %#v", diagnostics)
|
||||
}
|
||||
return parsed.document
|
||||
}
|
||||
@@ -39,13 +39,17 @@ type acquiredContribution struct {
|
||||
// acquisitionScope is the ordered strict mapping scope produced by one input
|
||||
// and its ordinary imports.
|
||||
type acquisitionScope struct {
|
||||
owner importNodeKey
|
||||
destination Path
|
||||
contributions []acquiredContribution
|
||||
owner importNodeKey
|
||||
destination Path
|
||||
contributions []acquiredContribution
|
||||
collectionPolicies collectionPolicyScope
|
||||
}
|
||||
|
||||
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
|
||||
cloned := acquisitionScope{owner: scope.owner, destination: scope.destination.clone()}
|
||||
cloned := acquisitionScope{
|
||||
owner: scope.owner, destination: scope.destination.clone(),
|
||||
collectionPolicies: cloneCollectionPolicyScope(scope.collectionPolicies),
|
||||
}
|
||||
if scope.contributions != nil {
|
||||
cloned.contributions = make([]acquiredContribution, len(scope.contributions))
|
||||
for index, contribution := range scope.contributions {
|
||||
|
||||
Reference in New Issue
Block a user