Integrate collection policies across layered assembly
This commit is contained in:
@@ -130,16 +130,20 @@ nothing. Underlay groups are listed from lowest to highest priority. Repeating
|
|||||||
a file within one group remains an import/repetition error; the same reusable
|
a file within one group remains an import/repetition error; the same reusable
|
||||||
source may be selected again in another tier.
|
source may be selected again in another tier.
|
||||||
|
|
||||||
ConfigKit acquires and composes the main input first, constructs the schema,
|
ConfigKit acquires the main input first and constructs the schema. It then
|
||||||
and checks the optional application version from the main tier only. Underlay
|
acquires and strictly composes underlay groups from lowest to highest priority,
|
||||||
roots cannot claim that version path (`underlay.version`); imported fragments
|
passing each group's accumulated lower collection policy into the next group.
|
||||||
retain `import.version` protection. It then acquires and composes underlay
|
Each group is applied once as a precedence tier. The main tier is then composed
|
||||||
groups in order, recursively combines the groups, and applies the main tier,
|
with the accumulated underlay policy, and the optional application version is
|
||||||
one selected overlay, and explicit CLI overrides. Scalar, sequence, and null
|
checked against that composed main tier only. Underlay roots cannot claim that
|
||||||
definitions replace lower values between tiers; mappings combine recursively.
|
version path (`underlay.version`); imported fragments retain `import.version`
|
||||||
Syntax and strict-composition errors inside a tier cannot be masked by a
|
protection. ConfigKit then applies the selected overlay and explicit CLI
|
||||||
higher-priority tier. Defaults, typed decoding, resolution, secret lookup,
|
overrides. Scalar, sequence, and null definitions replace lower values between
|
||||||
validation, and publication run once on the final assembled input.
|
tiers unless a collection policy defines the sequence operation; mappings
|
||||||
|
combine recursively. Syntax and strict-composition errors inside a tier cannot
|
||||||
|
be masked by a higher-priority tier. Defaults, typed decoding, resolution,
|
||||||
|
secret lookup, validation, and publication run once on the final assembled
|
||||||
|
input.
|
||||||
|
|
||||||
An active layered overlay requires `LayeredOptions.OverlayFilesystem` explicitly,
|
An active layered overlay requires `LayeredOptions.OverlayFilesystem` explicitly,
|
||||||
independent of the main source's namespace. Its name and root-relative lookup
|
independent of the main source's namespace. Its name and root-relative lookup
|
||||||
@@ -149,7 +153,10 @@ Ordinary `Load` continues to infer its selected-overlay context from its source.
|
|||||||
|
|
||||||
Source IDs follow acquisition order—main and its depth-first imports, then each
|
Source IDs follow acquisition order—main and its depth-first imports, then each
|
||||||
underlay tier's inputs and depth-first imports, then overlay and CLI—even though
|
underlay tier's inputs and depth-first imports, then overlay and CLI—even though
|
||||||
history follows precedence order. Source `Layer` describes precedence while
|
history follows precedence order. Default main-version checking happens after
|
||||||
|
underlay composition because the main tier inherits the accumulated collection
|
||||||
|
policy. Enabled per-source version checks retain acquisition order and source
|
||||||
|
local headers. Source `Layer` describes precedence while
|
||||||
`Kind` describes acquisition. Underlay records use `LayerUnderlay` and a
|
`Kind` describes acquisition. Underlay records use `LayerUnderlay` and a
|
||||||
one-based `UnderlayIndex` giving their original tier position; all other records
|
one-based `UnderlayIndex` giving their original tier position; all other records
|
||||||
have index zero. Explicit empty groups and `NoSource` consume no source ID.
|
have index zero. Explicit empty groups and `NoSource` consume no source ID.
|
||||||
@@ -824,8 +831,8 @@ headers are consumed before strict composition and cannot supply a registered
|
|||||||
record identity. Final typed decoding and application validation still run on the
|
record identity. Final typed decoding and application validation still run on the
|
||||||
completed configuration.
|
completed configuration.
|
||||||
|
|
||||||
Ordinary sources and selected overlays may declare collection behavior under
|
Ordinary sources, layered inputs, and selected overlays may declare collection
|
||||||
`configkit.sequences` at the document root. Each entry has a required literal
|
behavior under `configkit.sequences` at the document root. Each entry has a required literal
|
||||||
`destination` and may specify `key` as one string or a nonempty list of strings,
|
`destination` and may specify `key` as one string or a nonempty list of strings,
|
||||||
plus a `mode` of `replace`, `append`, or `upsert`. These controls are removed
|
plus a `mode` of `replace`, `append`, or `upsert`. These controls are removed
|
||||||
before application decoding. A declaration destination is relative to that
|
before application decoding. A declaration destination is relative to that
|
||||||
@@ -841,11 +848,14 @@ replaces a complete matched record at its existing position and appends new
|
|||||||
identities in incoming order; it does not merge record fields.
|
identities in incoming order; it does not merge record fields.
|
||||||
|
|
||||||
Declarations govern ordinary strict source composition, including loads with
|
Declarations govern ordinary strict source composition, including loads with
|
||||||
imports disabled, and selected-overlay precedence. Overlay declarations apply
|
imports disabled, and each strict scope in layered loading. Layered groups and
|
||||||
after the base source graph is composed and must agree with retained collection
|
main apply their resolved sequence operation between precedence tiers; a higher
|
||||||
identity and any application registration. They do not change layered
|
strict scope inherits the lower identity and explicit mode before peer
|
||||||
precedence, inheritance, or CLI behavior. A root `configkit.imports`
|
composition. Overlay declarations apply after the base source graph is composed
|
||||||
declaration still requires `Options.Imports`; `NoSource` has no declaration scope.
|
and must agree with retained collection identity and any application
|
||||||
|
registration. These policies do not change inheritance or CLI behavior. A root
|
||||||
|
`configkit.imports` declaration still requires `Options.Imports`; `NoSource` has
|
||||||
|
no declaration scope.
|
||||||
|
|
||||||
An empty mapping contributes no child definitions. Two empty mappings compose, and
|
An empty mapping contributes no child definitions. Two empty mappings compose, and
|
||||||
an empty mapping neither clears nor conflicts with another mapping's children.
|
an empty mapping neither clears nor conflicts with another mapping's children.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Sequence Identity And Combination Implementation Plan
|
# Sequence Identity And Combination Implementation Plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–14 complete.
|
Status: implementation in progress; Stages 1–15 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
|
||||||
@@ -556,6 +556,12 @@ Do not absorb unfinished work from Stages 5–14 into this stage.
|
|||||||
Validation: layered/grouped, source-version, dependency scheduling, destination,
|
Validation: layered/grouped, source-version, dependency scheduling, destination,
|
||||||
and metadata tests plus the everyday check.
|
and metadata tests plus the everyday check.
|
||||||
|
|
||||||
|
**Completion:** Implemented lower collection-policy propagation across underlay
|
||||||
|
groups, main composition, and layered overlays. Verified policy-only lower
|
||||||
|
tiers, inherited identity and explicit mode, explicit replacement, source
|
||||||
|
version behavior, sequence history, and consumer examples. Validation:
|
||||||
|
`go test ./internal/engine -count=1` and `./scripts/check.sh`.
|
||||||
|
|
||||||
## Stage 16 — Verify Replacement And Nested Policy Ownership End To End
|
## Stage 16 — Verify Replacement And Nested Policy Ownership End To End
|
||||||
|
|
||||||
Scope: the consequential lifetime cases across the now-active source assembly.
|
Scope: the consequential lifetime cases across the now-active source assembly.
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
configkit:
|
configkit:
|
||||||
|
sequences:
|
||||||
|
- destination: [prompts]
|
||||||
|
key: id
|
||||||
|
mode: replace
|
||||||
imports:
|
imports:
|
||||||
- dir: prompts
|
- dir: prompts
|
||||||
exclude: [generated-ignored.yml]
|
exclude: [generated-ignored.yml]
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
configkit:
|
||||||
|
sequences:
|
||||||
|
- destination: [prompts]
|
||||||
|
key: id
|
||||||
|
mode: replace
|
||||||
prompts:
|
prompts:
|
||||||
- id: invalid
|
- id: invalid
|
||||||
name: Invalid prompt
|
name: Invalid prompt
|
||||||
|
|||||||
@@ -107,7 +107,13 @@ func TestFacadeRegisteredReplacementExplainsRemovedDescendants(t *testing.T) {
|
|||||||
{"removed field", []any{map[string]any{"id": "main"}}, configkit.Keys("records").Index(0).Key("text")},
|
{"removed field", []any{map[string]any{"id": "main"}}, configkit.Keys("records").Index(0).Key("text")},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.Mapping("main", map[string]any{"records": test.upper})}, configkit.LayeredOptions[map[string]any]{
|
mainYAML := "configkit:\n sequences:\n - destination: [records]\n key: id\n mode: replace\n"
|
||||||
|
if len(test.upper) == 0 {
|
||||||
|
mainYAML += "records: []\n"
|
||||||
|
} else {
|
||||||
|
mainYAML += "records: [{id: main}]\n"
|
||||||
|
}
|
||||||
|
result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.Bytes("main", []byte(mainYAML))}, configkit.LayeredOptions[map[string]any]{
|
||||||
Definition: configkit.Definition[map[string]any]{SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}},
|
Definition: configkit.Definition[map[string]any]{SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}},
|
||||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"records": []any{map[string]any{"id": "one", "text": "old"}, map[string]any{"id": "two"}}})}}}},
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"records": []any{map[string]any{"id": "one", "text": "old"}, map[string]any{"id": "two"}}})}}}},
|
||||||
})
|
})
|
||||||
@@ -240,8 +246,8 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("registered sequence precedence load: %v", err)
|
t.Fatalf("registered sequence precedence load: %v", err)
|
||||||
}
|
}
|
||||||
if got := precedence.Config["entries"].([]any); len(got) != 1 || got[0].(map[string]any)["id"] != "main" {
|
if got := precedence.Config["entries"].([]any); len(got) != 2 || got[0].(map[string]any)["id"] != "lower" || got[1].(map[string]any)["id"] != "main" {
|
||||||
t.Fatalf("higher-priority list did not replace the lower sequence: %#v", precedence.Config)
|
t.Fatalf("registered upsert did not retain the lower sequence: %#v", precedence.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
@@ -506,6 +512,31 @@ func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLayeredSequenceUpsertHistoryRetainsLowerTierRecords(t *testing.T) {
|
||||||
|
definition := configkit.Definition[map[string]any]{
|
||||||
|
SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("entries"), IdentityField: "id"}},
|
||||||
|
}
|
||||||
|
result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.Bytes("main", []byte("entries: [{id: main}]\n"))}, configkit.LayeredOptions[map[string]any]{
|
||||||
|
Definition: definition,
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
||||||
|
Source: configkit.Bytes("lower", []byte("entries: [{id: lower}]\n")),
|
||||||
|
}}}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for index, id := range []string{"lower", "main"} {
|
||||||
|
path := configkit.Keys("entries").Index(index).Key("id")
|
||||||
|
explanation, ok := result.Explain(path)
|
||||||
|
if !ok || !explanation.Effective.Present || explanation.Effective.YAML != id+"\n" || len(explanation.History) == 0 {
|
||||||
|
t.Fatalf("tier history at %s = %#v, %v", path.String(), explanation, ok)
|
||||||
|
}
|
||||||
|
if explanation.History[len(explanation.History)-1].After.YAML != id+"\n" {
|
||||||
|
t.Fatalf("tier history at %s does not retain final record: %#v", path.String(), explanation.History)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFacadeSequenceReplacementKeepsTypedAndHistoricalEvidenceAligned(t *testing.T) {
|
func TestFacadeSequenceReplacementKeepsTypedAndHistoricalEvidenceAligned(t *testing.T) {
|
||||||
type record struct {
|
type record struct {
|
||||||
ID string `yaml:"id"`
|
ID string `yaml:"id"`
|
||||||
@@ -523,7 +554,12 @@ func TestFacadeSequenceReplacementKeepsTypedAndHistoricalEvidenceAligned(t *test
|
|||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
mainPath := filepath.Join(directory, "main.yml")
|
mainPath := filepath.Join(directory, "main.yml")
|
||||||
underlayPath := filepath.Join(directory, "underlay.yml")
|
underlayPath := filepath.Join(directory, "underlay.yml")
|
||||||
if err := os.WriteFile(mainPath, []byte(`entries:
|
if err := os.WriteFile(mainPath, []byte(`configkit:
|
||||||
|
sequences:
|
||||||
|
- destination: [entries]
|
||||||
|
key: id
|
||||||
|
mode: replace
|
||||||
|
entries:
|
||||||
- id: current
|
- id: current
|
||||||
count: 0
|
count: 0
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@@ -104,11 +104,7 @@ func TestFacadeSourceVersionOwnershipAcceptsContextualIndexes(t *testing.T) {
|
|||||||
if err != nil || result == nil {
|
if err != nil || result == nil {
|
||||||
t.Fatalf("grouped=%t path=%#v: %v", grouped, path, err)
|
t.Fatalf("grouped=%t path=%#v: %v", grouped, path, err)
|
||||||
}
|
}
|
||||||
want := 2
|
if len(result.Config["records"].([]any)) != 2 {
|
||||||
if grouped {
|
|
||||||
want = 1
|
|
||||||
}
|
|
||||||
if len(result.Config["records"].([]any)) != want {
|
|
||||||
t.Fatal("version path changed composition")
|
t.Fatal("version path changed composition")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestDestinationPlacementCountsAgainstSharedNodeBudget(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
accounting := &importAccounting{nodes: limits.nodes - perInput + test.delta}
|
accounting := &importAccounting{nodes: limits.nodes - perInput + test.delta}
|
||||||
composition, _, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{
|
composition, _, _, diagnostics := testAcquireUnderlayGroup(loadGroup{inputs: []loadInput{{
|
||||||
source: empty, destination: destination,
|
source: empty, destination: destination,
|
||||||
}}}, 1, nil, accounting)
|
}}}, 1, nil, accounting)
|
||||||
if test.wantError {
|
if test.wantError {
|
||||||
@@ -62,7 +62,7 @@ func TestMultipleMountedInputsSharePlacementCharges(t *testing.T) {
|
|||||||
perInput := countInputNodes(&document.Node{Kind: document.Mapping}) + destinationWrapperNodeCount(Keys("collection", "one"))
|
perInput := countInputNodes(&document.Node{Kind: document.Mapping}) + destinationWrapperNodeCount(Keys("collection", "one"))
|
||||||
charge := len(inputs) * perInput
|
charge := len(inputs) * perInput
|
||||||
accounting := &importAccounting{nodes: limits.nodes - charge}
|
accounting := &importAccounting{nodes: limits.nodes - charge}
|
||||||
composition, _, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: inputs}, 1, nil, accounting)
|
composition, _, _, diagnostics := testAcquireUnderlayGroup(loadGroup{inputs: inputs}, 1, nil, accounting)
|
||||||
if len(diagnostics) != 0 || composition == nil || accounting.nodes != limits.nodes || composition.inputNodes != charge {
|
if len(diagnostics) != 0 || composition == nil || accounting.nodes != limits.nodes || composition.inputNodes != charge {
|
||||||
t.Fatalf("shared placement accounting = %#v, %#v, accounting %#v; want charge %d", composition, diagnostics, accounting, charge)
|
t.Fatalf("shared placement accounting = %#v, %#v, accounting %#v; want charge %d", composition, diagnostics, accounting, charge)
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ id: parent
|
|||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
accounting := &importAccounting{nodes: limits.nodes - wantNodes + test.delta}
|
accounting := &importAccounting{nodes: limits.nodes - wantNodes + test.delta}
|
||||||
composition, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{
|
composition, sources, _, diagnostics := testAcquireUnderlayGroup(loadGroup{inputs: []loadInput{{
|
||||||
source: File(mainPath), imports: true, destination: destination, placementMode: placementModeSequenceItem,
|
source: File(mainPath), imports: true, destination: destination, placementMode: placementModeSequenceItem,
|
||||||
}, {
|
}, {
|
||||||
source: Mapping("other", map[string]any{"id": "other"}), destination: otherDestination, placementMode: placementModeSequenceItem,
|
source: Mapping("other", map[string]any{"id": "other"}), destination: otherDestination, placementMode: placementModeSequenceItem,
|
||||||
@@ -160,7 +160,7 @@ func TestImportedEmptyBodiesChargeEachDestinationWrapper(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
accounting := &importAccounting{nodes: limits.nodes - rawNodes - placementNodes + test.delta}
|
accounting := &importAccounting{nodes: limits.nodes - rawNodes - placementNodes + test.delta}
|
||||||
composition, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{
|
composition, sources, _, diagnostics := testAcquireUnderlayGroup(loadGroup{inputs: []loadInput{{
|
||||||
source: mainSource, imports: true, destination: destination,
|
source: mainSource, imports: true, destination: destination,
|
||||||
}}}, 1, nil, accounting)
|
}}}, 1, nil, accounting)
|
||||||
if test.wantError {
|
if test.wantError {
|
||||||
@@ -387,7 +387,7 @@ func TestNoSourceAndRootPlacementAddNoWrapperCharges(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
accounting := &importAccounting{}
|
accounting := &importAccounting{}
|
||||||
composition, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{
|
composition, sources, _, diagnostics := testAcquireUnderlayGroup(loadGroup{inputs: []loadInput{
|
||||||
{source: NoSource(), destination: Keys("absent")},
|
{source: NoSource(), destination: Keys("absent")},
|
||||||
{source: Mapping("root", map[string]any{})},
|
{source: Mapping("root", map[string]any{})},
|
||||||
}}, 1, nil, accounting)
|
}}, 1, nil, accounting)
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func TestLayeredItemVersionProtectionTracksAssignedPeerIndexes(t *testing.T) {
|
|||||||
t.Fatalf("unrelated appended peer rejected: %v", err)
|
t.Fatalf("unrelated appended peer rejected: %v", err)
|
||||||
}
|
}
|
||||||
entries := layeredMapValue(t, testLoaded(loaded).Config, "catalog", "entries").([]any)
|
entries := layeredMapValue(t, testLoaded(loaded).Config, "catalog", "entries").([]any)
|
||||||
if len(entries) != 1 || entries[0].(map[string]any)["id"] != "main" {
|
if len(entries) != 3 || entries[0].(map[string]any)["id"] != "first" || entries[1].(map[string]any)["id"] != "peer" || entries[2].(map[string]any)["id"] != "main" {
|
||||||
t.Fatalf("main item projection = %#v", entries)
|
t.Fatalf("main item projection = %#v", entries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const (
|
|||||||
rootDirectiveContextImportsOnly rootDirectiveContext = iota + 1
|
rootDirectiveContextImportsOnly rootDirectiveContext = iota + 1
|
||||||
rootDirectiveContextSource
|
rootDirectiveContextSource
|
||||||
rootDirectiveContextOverlay
|
rootDirectiveContextOverlay
|
||||||
|
rootDirectiveContextLayered
|
||||||
)
|
)
|
||||||
|
|
||||||
type rootDirectiveExtraction struct {
|
type rootDirectiveExtraction struct {
|
||||||
@@ -78,7 +79,7 @@ func parseRootDirectiveBlock(node *document.Node, directivePath Path, context ro
|
|||||||
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeOverlayDirective, fieldPath, "overlays cannot contain imports", entry.Value))
|
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeOverlayDirective, fieldPath, "overlays cannot contain imports", entry.Value))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if context != rootDirectiveContextImportsOnly && context != rootDirectiveContextSource {
|
if context != rootDirectiveContextImportsOnly && context != rootDirectiveContextSource && context != rootDirectiveContextLayered {
|
||||||
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeImportInvalid, fieldPath, "directive extraction context is invalid", entry.Value))
|
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeImportInvalid, fieldPath, "directive extraction context is invalid", entry.Value))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -91,7 +92,7 @@ func parseRootDirectiveBlock(node *document.Node, directivePath Path, context ro
|
|||||||
diagnostics = append(diagnostics, sequenceDeclarationDiagnostic(fieldPath, "sequence declarations are not enabled in this extraction context", entry.Value))
|
diagnostics = append(diagnostics, sequenceDeclarationDiagnostic(fieldPath, "sequence declarations are not enabled in this extraction context", entry.Value))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if context != rootDirectiveContextSource && context != rootDirectiveContextOverlay {
|
if context != rootDirectiveContextSource && context != rootDirectiveContextOverlay && context != rootDirectiveContextLayered {
|
||||||
diagnostics = append(diagnostics, sequenceDeclarationDiagnostic(fieldPath, "directive extraction context is invalid", entry.Value))
|
diagnostics = append(diagnostics, sequenceDeclarationDiagnostic(fieldPath, "directive extraction context is invalid", entry.Value))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem
|
|||||||
parsed: main, composition: acquired, acquisition: cloneAcquisitionScopes(acquired.acquisition),
|
parsed: main, composition: acquired, acquisition: cloneAcquisitionScopes(acquired.acquisition),
|
||||||
sources: state.sources, versionState: versionState, accounting: accounting,
|
sources: state.sources, versionState: versionState, accounting: accounting,
|
||||||
}
|
}
|
||||||
composed, diagnostics := composeAcquiredInputWithPolicy(input, policy)
|
composed, diagnostics := composeAcquiredInputWithPolicies(input, collectionPolicyScope{}, policy)
|
||||||
return composed, cloneSourceInfos(state.sources), diagnostics
|
return composed, cloneSourceInfos(state.sources), diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,10 +294,6 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem
|
|||||||
// acquisition scopes to the established strict-composition behavior. It uses
|
// acquisition scopes to the established strict-composition behavior. It uses
|
||||||
// the already acquired documents and graph; source handles are closed before
|
// the already acquired documents and graph; source handles are closed before
|
||||||
// this handoff.
|
// this handoff.
|
||||||
func composeAcquiredInputWithPolicy(input assembledInput, policy sequencePolicy) (*composedDocument, []Diagnostic) {
|
|
||||||
return composeAcquiredInputWithPolicies(input, collectionPolicyScope{}, policy)
|
|
||||||
}
|
|
||||||
|
|
||||||
func composeAcquiredInputWithPolicies(input assembledInput, lower collectionPolicyScope, policy sequencePolicy) (*composedDocument, []Diagnostic) {
|
func composeAcquiredInputWithPolicies(input assembledInput, lower collectionPolicyScope, policy sequencePolicy) (*composedDocument, []Diagnostic) {
|
||||||
if input.composition == nil || input.composition.root != nil || len(input.composition.acquisition) == 0 {
|
if input.composition == nil || input.composition.root != nil || len(input.composition.acquisition) == 0 {
|
||||||
return input.composition, nil
|
return input.composition, nil
|
||||||
|
|||||||
@@ -169,13 +169,15 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
|
|||||||
mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(), placementMode: request.main.placementMode}
|
mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(), placementMode: request.main.placementMode}
|
||||||
mainOptions.importDefaults = snapshotImportDefaults(request.importDefaults)
|
mainOptions.importDefaults = snapshotImportDefaults(request.importDefaults)
|
||||||
mainOptions.sequencePolicy = request.sequencePolicy
|
mainOptions.sequencePolicy = request.sequencePolicy
|
||||||
mainOptions.deferAssembly = request.ordinary
|
mainOptions.deferAssembly = true
|
||||||
mainOptions.SourceVersionMode = request.sourceVersionMode
|
mainOptions.SourceVersionMode = request.sourceVersionMode
|
||||||
mainOptions.Definition.Version = request.protectionVersion()
|
mainOptions.Definition.Version = request.protectionVersion()
|
||||||
mainOptions.Overrides = request.overrides
|
mainOptions.Overrides = request.overrides
|
||||||
if request.ordinary {
|
if request.ordinary {
|
||||||
mainOptions.Overlay = request.overlay
|
mainOptions.Overlay = request.overlay
|
||||||
mainOptions.directiveContext = rootDirectiveContextSource
|
mainOptions.directiveContext = rootDirectiveContextSource
|
||||||
|
} else {
|
||||||
|
mainOptions.directiveContext = rootDirectiveContextLayered
|
||||||
}
|
}
|
||||||
|
|
||||||
var schema *schemaDescriptor
|
var schema *schemaDescriptor
|
||||||
@@ -216,7 +218,7 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo
|
|||||||
}
|
}
|
||||||
input.document = input.composition.root
|
input.document = input.composition.root
|
||||||
}
|
}
|
||||||
if request.sourceVersionMode == sourceVersionModeDefault {
|
if request.sourceVersionMode == sourceVersionModeDefault && request.ordinary {
|
||||||
versionDiagnostics := checkVersion(request.definition.Version, input.document)
|
versionDiagnostics := checkVersion(request.definition.Version, input.document)
|
||||||
accumulator.add(versionDiagnostics...)
|
accumulator.add(versionDiagnostics...)
|
||||||
if hasErrors(versionDiagnostics) {
|
if hasErrors(versionDiagnostics) {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
|||||||
}
|
}
|
||||||
if !options.Imports && parsed.document != nil {
|
if !options.Imports && parsed.document != nil {
|
||||||
var directiveDiagnostics []Diagnostic
|
var directiveDiagnostics []Diagnostic
|
||||||
if directiveContext == rootDirectiveContextSource {
|
if directiveContext == rootDirectiveContextSource || directiveContext == rootDirectiveContextLayered {
|
||||||
directiveDiagnostics = disabledRootImportDiagnostics(parsed.document)
|
directiveDiagnostics = disabledRootImportDiagnostics(parsed.document)
|
||||||
} else {
|
} else {
|
||||||
directiveDiagnostics = disabledDirectiveDiagnostics(parsed.document, Path{})
|
directiveDiagnostics = disabledDirectiveDiagnostics(parsed.document, Path{})
|
||||||
@@ -159,6 +159,33 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
|||||||
if role == sourceVersionMain && versionState != nil {
|
if role == sourceVersionMain && versionState != nil {
|
||||||
versionState.mainProcessing = &processed
|
versionState.mainProcessing = &processed
|
||||||
}
|
}
|
||||||
|
} else if directiveContext == rootDirectiveContextLayered {
|
||||||
|
extracted, failures := extractRootDirectives(parsed.document, rootDirectiveContextLayered)
|
||||||
|
if len(failures) != 0 {
|
||||||
|
return input, failures
|
||||||
|
}
|
||||||
|
processed := sourceVersionProcessingResult{
|
||||||
|
payload: extracted.body, imports: extracted.imports, sequences: extracted.sequences,
|
||||||
|
directiveContext: rootDirectiveContextLayered,
|
||||||
|
}
|
||||||
|
if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
||||||
|
var sourceID string
|
||||||
|
if parsed.source != nil {
|
||||||
|
sourceID = parsed.source.ID
|
||||||
|
}
|
||||||
|
versionProcessed, versionFailures := versionState.process(extracted.body, role, destination, sourceID, options.placementMode)
|
||||||
|
if len(versionFailures) != 0 {
|
||||||
|
return input, versionFailures
|
||||||
|
}
|
||||||
|
versionProcessed.imports = extracted.imports
|
||||||
|
versionProcessed.sequences = append(cloneSequenceDeclarations(extracted.sequences), versionProcessed.sequences...)
|
||||||
|
versionProcessed.directiveContext = rootDirectiveContextLayered
|
||||||
|
processed = versionProcessed
|
||||||
|
if role == sourceVersionMain {
|
||||||
|
versionState.mainProcessing = &processed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
versionProcessing = &processed
|
||||||
} else if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
} else if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
||||||
var sourceID string
|
var sourceID string
|
||||||
if parsed.source != nil {
|
if parsed.source != nil {
|
||||||
@@ -302,9 +329,13 @@ func compositionForInput(input assembledInput) (*composedDocument, []Diagnostic)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func compositionForInputWithPolicy(input assembledInput, policy sequencePolicy) (*composedDocument, []Diagnostic) {
|
func compositionForInputWithPolicy(input assembledInput, policy sequencePolicy) (*composedDocument, []Diagnostic) {
|
||||||
|
return compositionForInputWithPolicies(input, collectionPolicyScope{}, policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compositionForInputWithPolicies(input assembledInput, lower collectionPolicyScope, policy sequencePolicy) (*composedDocument, []Diagnostic) {
|
||||||
if input.composition != nil {
|
if input.composition != nil {
|
||||||
if input.composition.root == nil && len(input.composition.acquisition) != 0 {
|
if input.composition.root == nil && len(input.composition.acquisition) != 0 {
|
||||||
return composeAcquiredInputWithPolicy(input, policy)
|
return composeAcquiredInputWithPolicies(input, lower, policy)
|
||||||
}
|
}
|
||||||
return input.composition, nil
|
return input.composition, nil
|
||||||
}
|
}
|
||||||
@@ -318,12 +349,12 @@ func compositionForInputWithPolicy(input assembledInput, policy sequencePolicy)
|
|||||||
acquired.inputNodes = countInputNodes(input.parsed.document)
|
acquired.inputNodes = countInputNodes(input.parsed.document)
|
||||||
}
|
}
|
||||||
input.composition = acquired
|
input.composition = acquired
|
||||||
return composeAcquiredInputWithPolicy(input, policy)
|
return composeAcquiredInputWithPolicies(input, lower, policy)
|
||||||
}
|
}
|
||||||
if input.document == nil {
|
if input.document == nil {
|
||||||
return &composedDocument{root: &document.Node{Kind: document.Mapping}, origins: make(map[string]Origin)}, nil
|
return &composedDocument{root: &document.Node{Kind: document.Mapping}, origins: make(map[string]Origin)}, nil
|
||||||
}
|
}
|
||||||
composition, diagnostics := composeDocumentsWithPolicy(policy, input.document)
|
composition, diagnostics := composeDocumentsWithCollectionPolicy(policy, lower, nil, input.document)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, diagnostics
|
return nil, diagnostics
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, accounting *importAccounting) (assembledInput, []Diagnostic) {
|
func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, accounting *importAccounting) (assembledInput, []Diagnostic) {
|
||||||
groups := make([]*composedDocument, 0, len(request.underlays))
|
var assembly *assemblyAccumulator
|
||||||
for index, group := range request.underlays {
|
for index, group := range request.underlays {
|
||||||
acquired, diagnostics := acquireUnderlayGroupScopes(group, index+1, request.protectionVersion(), accounting, input.versionState, request.sequencePolicy)
|
acquired, diagnostics := acquireUnderlayGroupScopes(group, index+1, request.protectionVersion(), accounting, input.versionState, request.sequencePolicy)
|
||||||
mergeResourceContexts(&input.contexts, acquired.contexts)
|
mergeResourceContexts(&input.contexts, acquired.contexts)
|
||||||
@@ -100,35 +100,71 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
input.sources = append(input.sources, acquired.sources...)
|
input.sources = append(input.sources, acquired.sources...)
|
||||||
return input, diagnostics
|
return input, diagnostics
|
||||||
}
|
}
|
||||||
composition, diagnostics := composeUnderlayGroup(acquired, request.protectionVersion(), request.sequencePolicy, collectionPolicyScope{})
|
var lower collectionPolicyScope
|
||||||
|
if assembly != nil && assembly.current != nil {
|
||||||
|
lower = assembly.current.collectionPolicies
|
||||||
|
}
|
||||||
|
composition, diagnostics := composeUnderlayGroup(acquired, request.protectionVersion(), request.sequencePolicy, lower)
|
||||||
input.sources = append(input.sources, acquired.sources...)
|
input.sources = append(input.sources, acquired.sources...)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return input, diagnostics
|
return input, diagnostics
|
||||||
}
|
}
|
||||||
if composition != nil {
|
if composition != nil {
|
||||||
groups = append(groups, composition)
|
if assembly == nil {
|
||||||
|
assembly = newAssemblyAccumulator(composition)
|
||||||
|
} else {
|
||||||
|
var declarations []sequenceDeclarationSpec
|
||||||
|
for _, scope := range composition.acquisition {
|
||||||
|
declarations = append(declarations, acquiredSequenceDeclarations(scope)...)
|
||||||
|
}
|
||||||
|
if diagnostics := assembly.applyOverlayWithCollectionPolicy(
|
||||||
|
composition.root, composition.origins, composition.applications, LayerUnderlay,
|
||||||
|
request.sequencePolicy, declarations,
|
||||||
|
); len(diagnostics) != 0 {
|
||||||
|
return input, diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var assembly *assemblyAccumulator
|
mainPresent := input.document != nil || input.composition != nil || len(input.acquisition) != 0
|
||||||
for _, group := range groups {
|
var mainComposition *composedDocument
|
||||||
if assembly == nil {
|
if mainPresent {
|
||||||
assembly = newAssemblyAccumulator(group)
|
var lower collectionPolicyScope
|
||||||
continue
|
if assembly != nil && assembly.current != nil {
|
||||||
|
lower = assembly.current.collectionPolicies
|
||||||
}
|
}
|
||||||
if diagnostics := assembly.applyOverlay(group.root, group.origins, group.applications, LayerUnderlay); len(diagnostics) != 0 {
|
var diagnostics []Diagnostic
|
||||||
return input, diagnostics
|
mainComposition, diagnostics = compositionForInputWithPolicies(input, lower, request.sequencePolicy)
|
||||||
}
|
|
||||||
}
|
|
||||||
if input.document != nil {
|
|
||||||
mainComposition, diagnostics := compositionForInputWithPolicy(input, request.sequencePolicy)
|
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return input, diagnostics
|
return input, diagnostics
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if request.sourceVersionMode == sourceVersionModeDefault {
|
||||||
|
var mainRoot *document.Node
|
||||||
|
if mainComposition != nil {
|
||||||
|
mainRoot = mainComposition.root
|
||||||
|
} else if input.document != nil {
|
||||||
|
mainRoot = input.document
|
||||||
|
}
|
||||||
|
if diagnostics := checkVersion(request.definition.Version, mainRoot); hasErrors(diagnostics) {
|
||||||
|
return input, diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mainComposition != nil {
|
||||||
if assembly == nil {
|
if assembly == nil {
|
||||||
assembly = newAssemblyAccumulator(mainComposition)
|
assembly = newAssemblyAccumulator(mainComposition)
|
||||||
} else if diagnostics := assembly.applyOverlay(mainComposition.root, mainComposition.origins, mainComposition.applications, LayerBase); len(diagnostics) != 0 {
|
} else {
|
||||||
return input, diagnostics
|
var declarations []sequenceDeclarationSpec
|
||||||
|
for _, scope := range mainComposition.acquisition {
|
||||||
|
declarations = append(declarations, acquiredSequenceDeclarations(scope)...)
|
||||||
|
}
|
||||||
|
if diagnostics := assembly.applyOverlayWithCollectionPolicy(
|
||||||
|
mainComposition.root, mainComposition.origins, mainComposition.applications, LayerBase,
|
||||||
|
request.sequencePolicy, declarations,
|
||||||
|
); len(diagnostics) != 0 {
|
||||||
|
return input, diagnostics
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if assembly == nil && (activeOverlay(request.overlay) || len(request.overrides) != 0) {
|
if assembly == nil && (activeOverlay(request.overlay) || len(request.overrides) != 0) {
|
||||||
@@ -146,10 +182,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
selection := Source{kind: sourceBytes}
|
selection := Source{kind: sourceBytes}
|
||||||
selected := parsedSource{document: &document.Node{Kind: document.Mapping}, source: &SourceInfo{}}
|
selected := parsedSource{document: &document.Node{Kind: document.Mapping}, source: &SourceInfo{}}
|
||||||
var diagnostics []Diagnostic
|
var diagnostics []Diagnostic
|
||||||
var overlayPolicy []sequencePolicy
|
overlayPolicy := []sequencePolicy{request.sequencePolicy}
|
||||||
if request.ordinary {
|
|
||||||
overlayPolicy = []sequencePolicy{request.sequencePolicy}
|
|
||||||
}
|
|
||||||
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits(), input.versionState, overlayPolicy...)
|
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits(), input.versionState, overlayPolicy...)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return input, diagnostics
|
return input, diagnostics
|
||||||
@@ -172,23 +205,6 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
|
|||||||
return input, nil
|
return input, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func acquireUnderlayGroup(group loadGroup, index int, rule *VersionRule, accounting *importAccounting) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) {
|
|
||||||
return acquireUnderlayGroupWithVersion(group, index, rule, accounting, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func acquireUnderlayGroupWithVersion(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) {
|
|
||||||
return acquireUnderlayGroupWithPolicyVersion(group, index, rule, accounting, versionState, sequencePolicy{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy sequencePolicy) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) {
|
|
||||||
acquired, diagnostics := acquireUnderlayGroupScopes(group, index, rule, accounting, versionState, policy)
|
|
||||||
if len(diagnostics) != 0 {
|
|
||||||
return nil, acquired.sources, acquired.contexts, diagnostics
|
|
||||||
}
|
|
||||||
composition, diagnostics := composeUnderlayGroup(acquired, rule, policy, collectionPolicyScope{})
|
|
||||||
return composition, acquired.sources, acquired.contexts, diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy sequencePolicy) (*acquiredUnderlayGroup, []Diagnostic) {
|
func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy sequencePolicy) (*acquiredUnderlayGroup, []Diagnostic) {
|
||||||
initialBytes, initialNodes := accounting.bytes, accounting.nodes
|
initialBytes, initialNodes := accounting.bytes, accounting.nodes
|
||||||
visited := make(map[resourceIdentity]importRoute)
|
visited := make(map[resourceIdentity]importRoute)
|
||||||
@@ -208,6 +224,7 @@ func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, a
|
|||||||
Imports: contribution.imports, Filesystem: contribution.filesystem,
|
Imports: contribution.imports, Filesystem: contribution.filesystem,
|
||||||
Definition: Definition[struct{}]{Version: rule}, destination: contribution.destination.clone(),
|
Definition: Definition[struct{}]{Version: rule}, destination: contribution.destination.clone(),
|
||||||
placementMode: contribution.placementMode, sequencePolicy: policy, deferAssembly: true,
|
placementMode: contribution.placementMode, sequencePolicy: policy, deferAssembly: true,
|
||||||
|
directiveContext: rootDirectiveContextLayered,
|
||||||
importDefaults: func() importDefaults {
|
importDefaults: func() importDefaults {
|
||||||
if versionState == nil {
|
if versionState == nil {
|
||||||
return importDefaults{}
|
return importDefaults{}
|
||||||
|
|||||||
@@ -355,6 +355,69 @@ func TestGroupedLoadAbsentMainAndVersionProtection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLayeredCollectionPolicyFlowsBetweenTiers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
policyOnly := Bytes("lower-policy", []byte("configkit:\n sequences:\n - destination: [items]\n key: id\n mode: append\n"))
|
||||||
|
duplicate := func(name string) loadInput {
|
||||||
|
return loadInput{source: Bytes(name, []byte("items: [{id: repeated}]\n"))}
|
||||||
|
}
|
||||||
|
request := loadRequest[map[string]any]{
|
||||||
|
main: loadInput{source: NoSource()},
|
||||||
|
underlays: []loadGroup{
|
||||||
|
{inputs: []loadInput{{source: policyOnly}}},
|
||||||
|
{inputs: []loadInput{duplicate("first-peer"), duplicate("second-peer")}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := testGroupedLoad(request); err != nil && groupedErrorCode(t, err) != CodeComposeConflict {
|
||||||
|
loadErr, _ := err.(*LoadError)
|
||||||
|
t.Fatalf("higher strict group code = %s, want %s: %#v", groupedErrorCode(t, err), CodeComposeConflict, loadErr.Diagnostics())
|
||||||
|
} else if err == nil {
|
||||||
|
t.Fatal("higher strict group accepted duplicate inherited identities")
|
||||||
|
}
|
||||||
|
|
||||||
|
request = loadRequest[map[string]any]{
|
||||||
|
main: loadInput{source: NoSource()},
|
||||||
|
underlays: []loadGroup{
|
||||||
|
{inputs: []loadInput{{source: Bytes("lower", []byte("configkit:\n sequences:\n - destination: [items]\n key: id\n mode: append\nitems: [{id: repeated}]\n"))}}},
|
||||||
|
{inputs: []loadInput{duplicate("higher")}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := testGroupedLoad(request); err == nil || groupedErrorCode(t, err) != CodeSequenceConflict {
|
||||||
|
t.Fatalf("higher tier did not inherit explicit append mode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request = loadRequest[map[string]any]{
|
||||||
|
main: loadInput{source: Bytes("main", []byte("configkit:\n sequences:\n - destination: [items]\n key: name\n mode: replace\nitems: [{name: higher}]\n"))},
|
||||||
|
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("configkit:\n sequences:\n - destination: [items]\n key: id\n mode: append\nitems: [{id: lower}]\n"))}}}},
|
||||||
|
}
|
||||||
|
result, err := testGroupedLoad(request)
|
||||||
|
if err != nil || !reflect.DeepEqual(result.Config["items"], []any{map[string]any{"name": "higher"}}) {
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
if loadErr, ok := err.(*LoadError); ok {
|
||||||
|
diagnostics = loadErr.Diagnostics()
|
||||||
|
}
|
||||||
|
t.Fatalf("replacement did not retire lower identity: %#v, %v (%#v)", result, err, diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
filesystem := NewFilesystem("overlay", fstest.MapFS{
|
||||||
|
"overlays/testing.yml": &fstest.MapFile{Data: []byte("configkit:\n sequences:\n - destination: [items]\n key: id\n mode: append\nitems: [{id: overlay}]\n")},
|
||||||
|
})
|
||||||
|
definition := Definition[map[string]any]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items"), identityField: "id"}}}
|
||||||
|
request = loadRequest[map[string]any]{
|
||||||
|
main: loadInput{source: Bytes("main", []byte("items: [{id: main}]\n"))},
|
||||||
|
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("items: [{id: lower}]\n"))}}}},
|
||||||
|
definition: definition, sequencePolicy: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
|
||||||
|
overlay: "testing", overlayFilesystem: &FilesystemContext{Root: ".", BaseDir: ".", FS: &filesystem},
|
||||||
|
}
|
||||||
|
result, err = testGroupedLoad(request)
|
||||||
|
if err != nil || !reflect.DeepEqual(result.Config["items"], []any{
|
||||||
|
map[string]any{"id": "lower"}, map[string]any{"id": "main"}, map[string]any{"id": "overlay"},
|
||||||
|
}) {
|
||||||
|
t.Fatalf("layered selected overlay sequence policy = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGroupedLoadStrictConflictsRepetitionAndBudgets(t *testing.T) {
|
func TestGroupedLoadStrictConflictsRepetitionAndBudgets(t *testing.T) {
|
||||||
main := loadInput{source: Bytes("main", []byte("port: 5\n"))}
|
main := loadInput{source: Bytes("main", []byte("port: 5\n"))}
|
||||||
request := loadRequest[groupedConfig]{
|
request := loadRequest[groupedConfig]{
|
||||||
@@ -393,18 +456,27 @@ func TestGroupedLoadStrictConflictsRepetitionAndBudgets(t *testing.T) {
|
|||||||
|
|
||||||
accounting := &importAccounting{sources: maxImportSources - 1}
|
accounting := &importAccounting{sources: maxImportSources - 1}
|
||||||
group := loadGroup{inputs: []loadInput{{source: Mapping("first", map[string]any{})}, {source: Mapping("second", map[string]any{})}}}
|
group := loadGroup{inputs: []loadInput{{source: Mapping("first", map[string]any{})}, {source: Mapping("second", map[string]any{})}}}
|
||||||
_, _, _, diagnostics := acquireUnderlayGroup(group, 1, nil, accounting)
|
_, _, _, diagnostics := testAcquireUnderlayGroup(group, 1, nil, accounting)
|
||||||
if len(diagnostics) == 0 || diagnostics[0].Code != CodeSourceLimitExceeded {
|
if len(diagnostics) == 0 || diagnostics[0].Code != CodeSourceLimitExceeded {
|
||||||
t.Fatalf("shared source budget = %#v", diagnostics)
|
t.Fatalf("shared source budget = %#v", diagnostics)
|
||||||
}
|
}
|
||||||
accounting = &importAccounting{bytes: maxTotalBytes - 2}
|
accounting = &importAccounting{bytes: maxTotalBytes - 2}
|
||||||
group.inputs = []loadInput{{source: Mapping("first", map[string]any{"port": "one"})}}
|
group.inputs = []loadInput{{source: Mapping("first", map[string]any{"port": "one"})}}
|
||||||
_, _, _, diagnostics = acquireUnderlayGroup(group, 1, nil, accounting)
|
_, _, _, diagnostics = testAcquireUnderlayGroup(group, 1, nil, accounting)
|
||||||
if len(diagnostics) == 0 || diagnostics[0].Code != CodeSourceLimitExceeded {
|
if len(diagnostics) == 0 || diagnostics[0].Code != CodeSourceLimitExceeded {
|
||||||
t.Fatalf("shared byte budget = %#v", diagnostics)
|
t.Fatalf("shared byte budget = %#v", diagnostics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAcquireUnderlayGroup(group loadGroup, index int, rule *VersionRule, accounting *importAccounting) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) {
|
||||||
|
acquired, diagnostics := acquireUnderlayGroupScopes(group, index, rule, accounting, nil, sequencePolicy{})
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
return nil, acquired.sources, acquired.contexts, diagnostics
|
||||||
|
}
|
||||||
|
composition, diagnostics := composeUnderlayGroup(acquired, rule, sequencePolicy{}, collectionPolicyScope{})
|
||||||
|
return composition, acquired.sources, acquired.contexts, diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
func TestGroupedLoadInvalidIntermediateTypeCanBeReplaced(t *testing.T) {
|
func TestGroupedLoadInvalidIntermediateTypeCanBeReplaced(t *testing.T) {
|
||||||
request := loadRequest[groupedConfig]{
|
request := loadRequest[groupedConfig]{
|
||||||
main: loadInput{source: Bytes("main", []byte("port: 9\n"))},
|
main: loadInput{source: Bytes("main", []byte("port: 9\n"))},
|
||||||
@@ -446,11 +518,11 @@ func TestGroupedAccountingChargesMixedOccurrencesOnce(t *testing.T) {
|
|||||||
"child-two.yml": &fstest.MapFile{Data: []byte(child)},
|
"child-two.yml": &fstest.MapFile{Data: []byte(child)},
|
||||||
})
|
})
|
||||||
accounting := &importAccounting{}
|
accounting := &importAccounting{}
|
||||||
first, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("one.yml"), imports: true}}}, 1, nil, accounting)
|
first, sources, _, diagnostics := testAcquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("one.yml"), imports: true}}}, 1, nil, accounting)
|
||||||
if len(diagnostics) != 0 || first == nil || len(sources) != 2 || accounting.sources != 2 || accounting.bytes != int64(len(one)+len(child)) {
|
if len(diagnostics) != 0 || first == nil || len(sources) != 2 || accounting.sources != 2 || accounting.bytes != int64(len(one)+len(child)) {
|
||||||
t.Fatalf("first acquisition = %#v, %#v, %#v, accounting %#v", first, sources, diagnostics, accounting)
|
t.Fatalf("first acquisition = %#v, %#v, %#v, accounting %#v", first, sources, diagnostics, accounting)
|
||||||
}
|
}
|
||||||
second, sources, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("two.yml"), imports: true}}}, 2, nil, accounting)
|
second, sources, _, diagnostics := testAcquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("two.yml"), imports: true}}}, 2, nil, accounting)
|
||||||
if len(diagnostics) != 0 || second == nil || len(sources) != 2 || sources[0].ID != "source-3" || sources[1].ID != "source-4" || accounting.sources != 4 || accounting.bytes != int64(len(one)+len(two)+2*len(child)) {
|
if len(diagnostics) != 0 || second == nil || len(sources) != 2 || sources[0].ID != "source-3" || sources[1].ID != "source-4" || accounting.sources != 4 || accounting.bytes != int64(len(one)+len(two)+2*len(child)) {
|
||||||
t.Fatalf("second acquisition = %#v, %#v, %#v, accounting %#v", second, sources, diagnostics, accounting)
|
t.Fatalf("second acquisition = %#v, %#v, %#v, accounting %#v", second, sources, diagnostics, accounting)
|
||||||
}
|
}
|
||||||
@@ -477,7 +549,7 @@ func TestGroupedGlobEntryBudgetIsShared(t *testing.T) {
|
|||||||
"parts/b.yml": &fstest.MapFile{Data: []byte("right: two\n")},
|
"parts/b.yml": &fstest.MapFile{Data: []byte("right: two\n")},
|
||||||
})
|
})
|
||||||
accounting := &importAccounting{entries: maxGlobEntries - 1}
|
accounting := &importAccounting{entries: maxGlobEntries - 1}
|
||||||
_, _, _, diagnostics := acquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("main.yml"), imports: true}}}, 1, nil, accounting)
|
_, _, _, diagnostics := testAcquireUnderlayGroup(loadGroup{inputs: []loadInput{{source: filesystem.File("main.yml"), imports: true}}}, 1, nil, accounting)
|
||||||
if len(diagnostics) == 0 || diagnostics[0].Code != CodeImportLimitExceeded || accounting.entries <= maxGlobEntries {
|
if len(diagnostics) == 0 || diagnostics[0].Code != CodeImportLimitExceeded || accounting.entries <= maxGlobEntries {
|
||||||
t.Fatalf("shared glob budget = %#v, accounting %#v", diagnostics, accounting)
|
t.Fatalf("shared glob budget = %#v, accounting %#v", diagnostics, accounting)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("strict underlay group appends while separate tiers reset identities", func(t *testing.T) {
|
t.Run("registered sequence policy appends across strict groups and tiers", func(t *testing.T) {
|
||||||
request := loadRequest[config]{
|
request := loadRequest[config]{
|
||||||
main: loadInput{source: Bytes("main", []byte("items: [{id: shared}]\n"))},
|
main: loadInput{source: Bytes("main", []byte("items: [{id: shared}]\n"))},
|
||||||
underlays: []loadGroup{{inputs: []loadInput{
|
underlays: []loadGroup{{inputs: []loadInput{
|
||||||
@@ -330,7 +330,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
|
|||||||
definition: definition, sequencePolicy: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
|
definition: definition, sequencePolicy: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
|
||||||
}
|
}
|
||||||
result, err := testGroupedLoad(request)
|
result, err := testGroupedLoad(request)
|
||||||
if err != nil || len(result.Config.Items) != 1 || result.Config.Items[0].ID != "shared" {
|
if err != nil || len(result.Config.Items) != 3 || result.Config.Items[0].ID != "lower" || result.Config.Items[1].ID != "other" || result.Config.Items[2].ID != "shared" {
|
||||||
t.Fatalf("grouped load = %#v, %v", result, err)
|
t.Fatalf("grouped load = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty registered contribution clears lower sequence", func(t *testing.T) {
|
t.Run("empty registered contribution follows the accumulated sequence mode", func(t *testing.T) {
|
||||||
result, err := testLoad[config](Bytes("main", []byte("items: []\n")), registered)
|
result, err := testLoad[config](Bytes("main", []byte("items: []\n")), registered)
|
||||||
if err != nil || len(result.Config.Items) != 0 {
|
if err != nil || len(result.Config.Items) != 0 {
|
||||||
t.Fatalf("empty sequence load = %#v, %v", result, err)
|
t.Fatalf("empty sequence load = %#v, %v", result, err)
|
||||||
@@ -355,7 +355,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
|
|||||||
definition: definition, sequencePolicy: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
|
definition: definition, sequencePolicy: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
|
||||||
}
|
}
|
||||||
result, err = testGroupedLoad(request)
|
result, err = testGroupedLoad(request)
|
||||||
if err != nil || len(result.Config.Items) != 0 {
|
if err != nil || len(result.Config.Items) != 1 || result.Config.Items[0].ID != "lower" {
|
||||||
t.Fatalf("empty higher tier = %#v, %v", result, err)
|
t.Fatalf("empty higher tier = %#v, %v", result, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user