diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index ca35214..836f3ef 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -204,7 +204,8 @@ Focused checks: Finding: **F07**, placement handoff. Owners: engine `destination.go`, `load.go`, `layered.go`, normalized input and placement consumers; root `lifecycle.go`, -`layered.go`, and placement fixtures. Status: **not started**. +`layered.go`, and placement fixtures. Status: **complete**. Validation passed: +`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Destination|Placement|Item|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test . -run 'Placement|Destination|SourceInfo|SourceVersion|PublicFacadeBoundary|DependencyImportBoundary'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, and `git diff --check`. 1. Rename the existing engine enum and its constants to `PlacementMode`, `PlacementModeMapping`, and `PlacementModeSequenceItem`, preserving underlying diff --git a/internal/engine/destination.go b/internal/engine/destination.go index efdd4c6..1fe975c 100644 --- a/internal/engine/destination.go +++ b/internal/engine/destination.go @@ -11,26 +11,13 @@ var ( errDestinationLimit = errors.New("configkit: destination exceeds document limits") ) -type placementMode uint8 +type PlacementMode uint8 const ( - placementModeMapping placementMode = iota - placementModeSequenceItem + PlacementModeMapping PlacementMode = iota + PlacementModeSequenceItem ) -// InputWithPlacement copies the root-owned mode into a private engine input. -func InputWithPlacement(input Input, value uint8) Input { - input.placementMode = placementMode(value) - return input -} - -// OptionsWithPlacement copies root-owned controls into private engine options. -func OptionsWithPlacement[T any](options Options[T], destination Path, value uint8) Options[T] { - options.destination = destination.clone() - options.placementMode = placementMode(value) - return options -} - func normalizeDestination(path Path) (Path, error) { if !path.valid() { return Path{}, errInvalidDestination @@ -67,26 +54,26 @@ func invalidDestinationDiagnostic(destination Path) Diagnostic { // placeDocument checks all per-document bounds before allocating destination // wrappers. Aggregate accounting remains the caller's responsibility. func placeDocument(body *document.Node, destination Path, sourceRoot document.Location, maxDepth, maxNodes int) (Path, *document.Node, error) { - return placeDocumentWithMode(body, destination, placementModeMapping, sourceRoot, maxDepth, maxNodes) + return placeDocumentWithMode(body, destination, PlacementModeMapping, sourceRoot, maxDepth, maxNodes) } // placeDocumentWithMode places a source mapping beneath its literal destination. // Sequence-item placement adds one sequence containing the complete mapping. -func placeDocumentWithMode(body *document.Node, destination Path, mode placementMode, sourceRoot document.Location, maxDepth, maxNodes int) (Path, *document.Node, error) { +func placeDocumentWithMode(body *document.Node, destination Path, mode PlacementMode, sourceRoot document.Location, maxDepth, maxNodes int) (Path, *document.Node, error) { normalized, err := normalizeDestination(destination) if err != nil { return Path{}, nil, err } - if mode != placementModeMapping && mode != placementModeSequenceItem { + if mode != PlacementModeMapping && mode != PlacementModeSequenceItem { return Path{}, nil, errInvalidDestination } - if mode == placementModeSequenceItem && len(normalized.segments) == 0 { + if mode == PlacementModeSequenceItem && len(normalized.segments) == 0 { return Path{}, nil, errInvalidDestination } if body == nil { return normalized, nil, nil } - if mode == placementModeSequenceItem && body.Kind != document.Mapping { + if mode == PlacementModeSequenceItem && body.Kind != document.Mapping { return normalized, nil, errInvalidDestination } if !destinationWithinLimitsWithMode(body, normalized, mode, maxDepth, maxNodes) { @@ -94,7 +81,7 @@ func placeDocumentWithMode(body *document.Node, destination Path, mode placement } placed := body - if mode == placementModeSequenceItem { + if mode == PlacementModeSequenceItem { placed = &document.Node{ Kind: document.Sequence, Location: sourceRoot, Sequence: []*document.Node{placed}, @@ -118,20 +105,20 @@ func placeDocumentWithMode(body *document.Node, destination Path, mode placement // destinationWithinLimits expects a normalized mapping-key destination. func destinationWithinLimits(body *document.Node, destination Path, maxDepth, maxNodes int) bool { - return destinationWithinLimitsWithMode(body, destination, placementModeMapping, maxDepth, maxNodes) + return destinationWithinLimitsWithMode(body, destination, PlacementModeMapping, maxDepth, maxNodes) } -func destinationWithinLimitsWithMode(body *document.Node, destination Path, mode placementMode, maxDepth, maxNodes int) bool { +func destinationWithinLimitsWithMode(body *document.Node, destination Path, mode PlacementMode, maxDepth, maxNodes int) bool { if body == nil { return true } - if maxDepth < 0 || maxNodes < 0 || !destination.valid() || (mode != placementModeMapping && mode != placementModeSequenceItem) { + if maxDepth < 0 || maxNodes < 0 || !destination.valid() || (mode != PlacementModeMapping && mode != PlacementModeSequenceItem) { return false } segments := len(destination.segments) sequenceNodes := 0 sequenceDepth := 0 - if mode == placementModeSequenceItem { + if mode == PlacementModeSequenceItem { if segments == 0 { return false } @@ -153,9 +140,9 @@ func destinationWithinLimitsWithMode(body *document.Node, destination Path, mode return countInputNodes(body) <= maxNodes-wrapperNodes } -func destinationWrapperNodeCountForMode(destination Path, mode placementMode) int { +func destinationWrapperNodeCountForMode(destination Path, mode PlacementMode) int { count := destinationWrapperNodeCount(destination) - if mode == placementModeSequenceItem { + if mode == PlacementModeSequenceItem { count++ } return count diff --git a/internal/engine/destination_accounting_test.go b/internal/engine/destination_accounting_test.go index d41df5a..7437024 100644 --- a/internal/engine/destination_accounting_test.go +++ b/internal/engine/destination_accounting_test.go @@ -92,9 +92,9 @@ id: parent childDestination := appendDestination(destination, Keys("children")) otherDestination := Keys("other") wantNodes := parseNodes(mainPath) + parseNodes(fragmentPath) + countInputNodes(&document.Node{Kind: document.Mapping}) + - destinationWrapperNodeCountForMode(destination, placementModeSequenceItem) + - destinationWrapperNodeCountForMode(childDestination, placementModeSequenceItem) + - destinationWrapperNodeCountForMode(otherDestination, placementModeSequenceItem) + destinationWrapperNodeCountForMode(destination, PlacementModeSequenceItem) + + destinationWrapperNodeCountForMode(childDestination, PlacementModeSequenceItem) + + destinationWrapperNodeCountForMode(otherDestination, PlacementModeSequenceItem) limits := defaultImportLimits() for _, test := range []struct { @@ -108,9 +108,9 @@ id: parent t.Run(test.name, func(t *testing.T) { accounting := &importAccounting{nodes: limits.nodes - wantNodes + test.delta} acquired, 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, }}}, 1, nil, accounting) if test.wantError { if len(diagnostics) == 0 || diagnostics[0].Code != CodeSourceLimitExceeded { diff --git a/internal/engine/destination_integration_test.go b/internal/engine/destination_integration_test.go index cc3ad35..9f82781 100644 --- a/internal/engine/destination_integration_test.go +++ b/internal/engine/destination_integration_test.go @@ -201,7 +201,7 @@ func TestLayeredItemVersionProtectionTracksAssignedPeerIndexes(t *testing.T) { sequenceIdentityRegistrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("catalog"), literalSequenceSelector("entries")}, identityFields: []string{"id"}}}, } item := func(label string, value map[string]any) Input { - return Input{Source: Mapping(label, value), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem} + return Input{Source: Mapping(label, value), Destination: Keys("catalog", "entries"), PlacementMode: PlacementModeSequenceItem} } options := func(inputs ...Input) LayeredOptions[map[string]any] { return LayeredOptions[map[string]any]{Definition: definition, SourceVersionMode: sourceVersionModeIfPresent, Underlays: []Underlay{{Inputs: inputs}}} diff --git a/internal/engine/destination_load_test.go b/internal/engine/destination_load_test.go index cebed17..a6d1ff5 100644 --- a/internal/engine/destination_load_test.go +++ b/internal/engine/destination_load_test.go @@ -22,11 +22,11 @@ func TestLayeredLoadPlacesCompletedMappingAsOneItem(t *testing.T) { t.Parallel() loaded, err := LoadLayered[destinationItemConfig]( - Input{Source: NoSource(), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, + Input{Source: NoSource(), Destination: Keys("catalog", "entries"), PlacementMode: PlacementModeSequenceItem}, testSecretBinding(), LayeredOptions[destinationItemConfig]{Underlays: []Underlay{{Inputs: []Input{{ Source: Mapping("empty-item", map[string]any{}), Destination: Keys("catalog", "entries"), - placementMode: placementModeSequenceItem, + PlacementMode: PlacementModeSequenceItem, }}}}}, ) if err != nil { @@ -40,7 +40,7 @@ func TestLayeredLoadPlacesCompletedMappingAsOneItem(t *testing.T) { t.Fatal("sequence index was treated as a literal mapping key during metadata lookup") } sources := result.Sources() - if len(sources) != 1 || sources[0].placementMode != placementModeSequenceItem || sources[0].destinationUnresolved { + if len(sources) != 1 || sources[0].placementMode != PlacementModeSequenceItem || sources[0].destinationUnresolved { t.Fatalf("source placement evidence = %#v", sources) } if got := sources[0].Destination.String(); got != "/catalog/entries" { @@ -48,7 +48,7 @@ func TestLayeredLoadPlacesCompletedMappingAsOneItem(t *testing.T) { } absent, err := LoadLayered[destinationItemConfig]( - Input{Source: NoSource(), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, + Input{Source: NoSource(), Destination: Keys("catalog", "entries"), PlacementMode: PlacementModeSequenceItem}, testSecretBinding(), ) if err != nil { @@ -64,17 +64,17 @@ func TestLayeredItemPlacementPreflightsModeAndDestination(t *testing.T) { for _, test := range []struct { name string - mode placementMode + mode PlacementMode path Path }{ - {name: "root item", mode: placementModeSequenceItem}, - {name: "unknown mode", mode: placementMode(255), path: Keys("catalog")}, + {name: "root item", mode: PlacementModeSequenceItem}, + {name: "unknown mode", mode: PlacementMode(255), path: Keys("catalog")}, } { t.Run(test.name, func(t *testing.T) { t.Parallel() reader := bytes.NewBufferString("value: unused\n") _, err := LoadLayered[destinationItemConfig]( - Input{Source: Reader("reader", reader), Destination: test.path, placementMode: test.mode}, + Input{Source: Reader("reader", reader), Destination: test.path, PlacementMode: test.mode}, testSecretBinding(), ) if err == nil || reader.Len() == 0 { @@ -93,7 +93,7 @@ func TestLoadOptionsPlaceOneMappingItem(t *testing.T) { loaded, err := Load[destinationItemConfig]( Mapping("ordinary", map[string]any{}), testSecretBinding(), - Options[destinationItemConfig]{destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, + Options[destinationItemConfig]{Destination: Keys("catalog", "entries"), PlacementMode: PlacementModeSequenceItem}, ) if err != nil { t.Fatalf("Load: %v", err) diff --git a/internal/engine/destination_test.go b/internal/engine/destination_test.go index b2766b9..6e03424 100644 --- a/internal/engine/destination_test.go +++ b/internal/engine/destination_test.go @@ -54,7 +54,7 @@ func TestMappingPlacementModeMatchesZeroValue(t *testing.T) { if err != nil { t.Fatal(err) } - explicitPath, explicit, err := placeDocumentWithMode(body, destination, placementModeMapping, body.Location, 5, 32) + explicitPath, explicit, err := placeDocumentWithMode(body, destination, PlacementModeMapping, body.Location, 5, 32) if err != nil { t.Fatal(err) } @@ -235,7 +235,7 @@ func TestPlaceDocumentSequenceItemAndMountedBounds(t *testing.T) { root := document.Location{SourceID: "item", Line: 1, Column: 1} body := &document.Node{Kind: document.Mapping, Location: root} destination := Keys("catalog", "entries") - placedPath, placed, err := placeDocumentWithMode(body, destination, placementModeSequenceItem, root, 3, 6) + placedPath, placed, err := placeDocumentWithMode(body, destination, PlacementModeSequenceItem, root, 3, 6) if err != nil { t.Fatal(err) } @@ -253,13 +253,13 @@ func TestPlaceDocumentSequenceItemAndMountedBounds(t *testing.T) { t.Fatal("synthetic sequence or authored mapping lost source coordinates") } - if _, _, err := placeDocumentWithMode(body, Path{}, placementModeSequenceItem, root, 3, 6); !errors.Is(err, errInvalidDestination) { + if _, _, err := placeDocumentWithMode(body, Path{}, PlacementModeSequenceItem, root, 3, 6); !errors.Is(err, errInvalidDestination) { t.Fatalf("root item placement error = %v, want invalid destination", err) } - if _, _, err := placeDocumentWithMode(&document.Node{Kind: document.String}, destination, placementModeSequenceItem, root, 3, 6); !errors.Is(err, errInvalidDestination) { + if _, _, err := placeDocumentWithMode(&document.Node{Kind: document.String}, destination, PlacementModeSequenceItem, root, 3, 6); !errors.Is(err, errInvalidDestination) { t.Fatalf("non-mapping item placement error = %v, want invalid destination", err) } - if _, _, err := placeDocumentWithMode(body, destination, placementMode(255), root, 3, 6); !errors.Is(err, errInvalidDestination) { + if _, _, err := placeDocumentWithMode(body, destination, PlacementMode(255), root, 3, 6); !errors.Is(err, errInvalidDestination) { t.Fatalf("unknown placement mode error = %v, want invalid destination", err) } @@ -275,7 +275,7 @@ func TestPlaceDocumentSequenceItemAndMountedBounds(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { t.Parallel() - _, _, err := placeDocumentWithMode(body, destination, placementModeSequenceItem, root, test.depth, test.nodes) + _, _, err := placeDocumentWithMode(body, destination, PlacementModeSequenceItem, root, test.depth, test.nodes) if test.wantAccept && err != nil { t.Fatalf("exact bounds rejected: %v", err) } diff --git a/internal/engine/discovery_load_test.go b/internal/engine/discovery_load_test.go index ea81e93..56a5f85 100644 --- a/internal/engine/discovery_load_test.go +++ b/internal/engine/discovery_load_test.go @@ -28,7 +28,7 @@ settings: {main: selected} importPath := writeImportFile(t, root, "fragments/extra.yml", "settings: {imported: value}\n") search := SearchConfig("app", SearchDirectory(root)) options := Options[discoveryLoadConfig]{ - Imports: true, Filesystem: &FilesystemContext{Root: root}, destination: Keys("service"), + Imports: true, Filesystem: &FilesystemContext{Root: root}, Destination: Keys("service"), } searched, err := testLoad(search, options) if err != nil { @@ -172,7 +172,7 @@ func TestSearchProbeFailureRetainsFileEvidence(t *testing.T) { load func(Source) (*Loaded[discoveryLoadConfig], error) }{ {"ordinary", "source-1", func(source Source) (*Loaded[discoveryLoadConfig], error) { - return Load(source, testSecretBinding(), Options[discoveryLoadConfig]{destination: Keys("settings")}) + return Load(source, testSecretBinding(), Options[discoveryLoadConfig]{Destination: Keys("settings")}) }}, {"underlay after acquired sources", "source-3", func(source Source) (*Loaded[discoveryLoadConfig], error) { return LoadLayered(Input{Source: File(main)}, testSecretBinding(), LayeredOptions[discoveryLoadConfig]{ diff --git a/internal/engine/import_acquisition.go b/internal/engine/import_acquisition.go index 8cf40c4..2d83cd3 100644 --- a/internal/engine/import_acquisition.go +++ b/internal/engine/import_acquisition.go @@ -94,7 +94,7 @@ func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisi } mainInfo := cloneSourceInfo(*main.source) mainInfo.Destination = destination.clone() - mainIsItem := main.source.placementMode == placementModeSequenceItem + mainIsItem := main.source.placementMode == PlacementModeSequenceItem if mainPath != "" { mainInfo.Path = mainPath } @@ -244,7 +244,7 @@ func (s *importTraversal) walkDeclarations(basePath string, declarations []impor if len(diagnostics) != 0 { return diagnostics } - if policy.placement == placementModeSequenceItem && len(appendDestination(parentDestination, policy.destination).segments) == 0 { + if policy.placement == PlacementModeSequenceItem && len(appendDestination(parentDestination, policy.destination).segments) == 0 { site := spec.fieldSites["placement"] return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: site.path.clone(), Message: "sequence-item placement requires a nonempty destination", Primary: cloneLocation(&site.location)}} } @@ -305,8 +305,8 @@ func (s *importTraversal) admitImportSource(canonical string, identity resourceI label = s.access.label + "/" + canonical } info := SourceInfo{ID: sourceID, Label: label, Path: canonical, Destination: s.sourceDestination(destination), Layer: LayerBase, Kind: kind, ImportedFrom: &location} - if policy.placement == placementModeSequenceItem { - info.placementMode = placementModeSequenceItem + if policy.placement == PlacementModeSequenceItem { + info.placementMode = PlacementModeSequenceItem } // A source's own item placement still has an exact sequence-container // destination. Only an unresolved enclosing item index makes the path a @@ -433,17 +433,17 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo admitted := admission.source placementDestination := destination - mode := placementModeMapping - if policy.placement == placementModeSequenceItem { - mode = placementModeSequenceItem + mode := PlacementModeMapping + if policy.placement == PlacementModeSequenceItem { + mode = PlacementModeSequenceItem } role := importRoleMapping - if mode == placementModeSequenceItem { + if mode == PlacementModeSequenceItem { role = importRoleItem } s.importGraph.addNode(importGraphNode{key: admitted.nodeKey, canonical: admitted.canonical, sourceID: admitted.source.ID, role: role}) s.importGraph.addEdge(importerKey, admitted.nodeKey, location) - if mode == placementModeSequenceItem { + if mode == PlacementModeSequenceItem { route := importRoute{canonical: admitted.canonical, locations: currentRoute.locations, key: admitted.nodeKey} s.active[admitted.identity] = len(s.stack) s.tier.visited[admitted.identity] = route diff --git a/internal/engine/import_projection.go b/internal/engine/import_projection.go index 69da1af..994f4e2 100644 --- a/internal/engine/import_projection.go +++ b/internal/engine/import_projection.go @@ -213,7 +213,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc itemIndex = projected } } - if contribution.source.placementMode == placementModeSequenceItem { + if contribution.source.placementMode == PlacementModeSequenceItem { container := appendDestination(contributionPrefix, contribution.destination) contribution.source.Destination = container.clone() contribution.source.destinationUnresolved = false diff --git a/internal/engine/import_scope.go b/internal/engine/import_scope.go index 009e843..2b71512 100644 --- a/internal/engine/import_scope.go +++ b/internal/engine/import_scope.go @@ -73,12 +73,12 @@ func composeAcquiredInputWithPolicies(input assembledInput, lower collectionPoli } completed.acquisition = []acquisitionScope{cloneAcquisitionScope(localScope)} projectNestedEvidence(completed, localScope) - _, placed, err := placeDocumentWithMode(completed.root, main.destination, placementModeSequenceItem, main.payload.Location, maxDocumentDepth, maxDocumentNodes) + _, placed, err := placeDocumentWithMode(completed.root, main.destination, PlacementModeSequenceItem, main.payload.Location, maxDocumentDepth, maxDocumentNodes) if err != nil { return nil, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, main.source.ID, "configuration exceeds the source depth or node limit after destination placement")} } if !main.placementCharged { - placementNodes := destinationWrapperNodeCountForMode(main.destination, placementModeSequenceItem) + placementNodes := destinationWrapperNodeCountForMode(main.destination, PlacementModeSequenceItem) if accounting.nodes+placementNodes > traversal.policy.importLimits.nodes { return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", main.source.ImportedFrom)} } @@ -154,7 +154,7 @@ func reservePendingItemPlacements(scope *acquisitionScope, limits importLimits, if contribution.pendingItem == nil { continue } - placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem) + placementNodes := destinationWrapperNodeCountForMode(contribution.destination, PlacementModeSequenceItem) if accounting.nodes+placementNodes > limits.nodes { diagnostic := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom) return &diagnostic @@ -228,11 +228,11 @@ func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnos // item has its effective coordinates, not against its local field names. completed.acquisition = []acquisitionScope{cloneAcquisitionScope(*itemScope)} projectNestedEvidence(completed, *itemScope) - _, placed, err := placeDocumentWithMode(completed.root, contribution.destination, placementModeSequenceItem, contribution.payload.Location, maxDocumentDepth, maxDocumentNodes) + _, placed, err := placeDocumentWithMode(completed.root, contribution.destination, PlacementModeSequenceItem, contribution.payload.Location, maxDocumentDepth, maxDocumentNodes) if err != nil { return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", contribution.source.ImportedFrom)} } - placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem) + placementNodes := destinationWrapperNodeCountForMode(contribution.destination, PlacementModeSequenceItem) if !contribution.placementCharged { if s.state.accounting.nodes+placementNodes > s.policy.importLimits.nodes { return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom)} diff --git a/internal/engine/import_spec.go b/internal/engine/import_spec.go index e93c210..7fe2176 100644 --- a/internal/engine/import_spec.go +++ b/internal/engine/import_spec.go @@ -38,7 +38,7 @@ type importDeclarationSpec struct { depth int depthSet bool destination Path - placement placementMode + placement PlacementMode fieldSites map[string]importFieldSite } @@ -66,7 +66,7 @@ type resolvedImportPolicy struct { depthSet bool exclude []string destination Path - placement placementMode + placement PlacementMode } func snapshotImportDefaults(value ImportDefaults) ImportDefaults { @@ -173,7 +173,7 @@ func parseImportDeclarationSpec(node *document.Node, declarationPath Path) (impo spec := importDeclarationSpec{ path: declarationPath.clone(), location: publicLocation(node.Location), - placement: placementModeMapping, + placement: PlacementModeMapping, fieldSites: make(map[string]importFieldSite, len(node.Mapping)), } selectors := 0 @@ -281,19 +281,19 @@ func parseImportWildcardAssignment(spec *importDeclarationSpec, field string, no return nil } -func parseImportPlacement(node *document.Node, fieldPath Path) (placementMode, *Diagnostic) { +func parseImportPlacement(node *document.Node, fieldPath Path) (PlacementMode, *Diagnostic) { if node.Kind != document.String { diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", node) - return placementModeMapping, &diagnostic + return PlacementModeMapping, &diagnostic } switch node.Text { case "mapping": - return placementModeMapping, nil + return PlacementModeMapping, nil case "sequence-item": - return placementModeSequenceItem, nil + return PlacementModeSequenceItem, nil default: diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", node) - return placementModeMapping, &diagnostic + return PlacementModeMapping, &diagnostic } } diff --git a/internal/engine/import_spec_test.go b/internal/engine/import_spec_test.go index 048ca41..d384f75 100644 --- a/internal/engine/import_spec_test.go +++ b/internal/engine/import_spec_test.go @@ -31,7 +31,7 @@ func TestParseImportDeclarationSpecNormalizesPresenceAndPlacement(t *testing.T) if first.selector != importSelectorPath || len(first.paths) != 2 || first.paths[0] != "one.yaml" || first.paths[1] != "two.yaml" { t.Fatalf("path selector = %#v", first) } - if first.required == nil || *first.required || first.destination.String() != "/services" || first.placement != placementModeSequenceItem { + if first.required == nil || *first.required || first.destination.String() != "/services" || first.placement != PlacementModeSequenceItem { t.Fatalf("explicit declaration values = %#v", first) } if first.path.String() != "/configkit/imports/0" || first.fieldSites["required"].path.String() != "/configkit/imports/0/required" { diff --git a/internal/engine/imports_test.go b/internal/engine/imports_test.go index 642c1a9..8719d03 100644 --- a/internal/engine/imports_test.go +++ b/internal/engine/imports_test.go @@ -413,7 +413,7 @@ label: helper loaded, err := LoadLayered( Input{Source: NoSource(), Imports: true}, testSecretBinding(), LayeredOptions[config]{Underlays: []Underlay{{Inputs: []Input{{ - Source: File(itemPath), Imports: true, Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem, + Source: File(itemPath), Imports: true, Destination: Keys("catalog", "entries"), PlacementMode: PlacementModeSequenceItem, }}}}}, ) if err != nil { @@ -503,7 +503,7 @@ func TestLoadItemGlobKeepsDefaultModeIndependentAndOptionalMissesAbsent(t *testi t.Fatalf("config = %#v", result.Config) } sources := result.Sources() - if len(sources) != 3 || sources[1].Path != canonicalTestPath(t, emptyPath) || sources[1].Destination.String() != "/items" || sources[1].placementMode != placementModeSequenceItem || sources[2].placementMode != placementModeMapping || sources[2].Destination.String() != "/other" { + if len(sources) != 3 || sources[1].Path != canonicalTestPath(t, emptyPath) || sources[1].Destination.String() != "/items" || sources[1].placementMode != PlacementModeSequenceItem || sources[2].placementMode != PlacementModeMapping || sources[2].Destination.String() != "/other" { t.Fatalf("source placement evidence = %#v", sources) } } @@ -521,7 +521,7 @@ func TestLoadMainItemPlacementKeepsItsHelpersInTheItem(t *testing.T) { mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: helper.yml\nid: main\n") helperPath := writeImportFile(t, directory, "helper.yml", "value: helper\n") result, err := testLoad[config](File(mainPath), Options[config]{ - Imports: true, destination: Keys("items"), placementMode: placementModeSequenceItem, + Imports: true, Destination: Keys("items"), PlacementMode: PlacementModeSequenceItem, }) if err != nil { t.Fatalf("load main item: %v", err) @@ -530,7 +530,7 @@ func TestLoadMainItemPlacementKeepsItsHelpersInTheItem(t *testing.T) { t.Fatalf("main item = %#v", result.Config.Items) } sources := result.Sources() - if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].placementMode != placementModeSequenceItem || sources[1].Destination.String() != "/items/0" || sources[1].placementMode != placementModeMapping { + if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].placementMode != PlacementModeSequenceItem || sources[1].Destination.String() != "/items/0" || sources[1].placementMode != PlacementModeMapping { t.Fatalf("main item sources = %#v", sources) } if sources[1].Path != canonicalTestPath(t, helperPath) { @@ -549,7 +549,7 @@ func TestProjectAcquiredDestinationsUsesMappedSequenceItemIndex(t *testing.T) { items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}}, }}} scope := acquisitionScope{contributions: []acquiredContribution{{ - source: SourceInfo{ID: "outer-source", placementMode: placementModeSequenceItem}, + source: SourceInfo{ID: "outer-source", placementMode: PlacementModeSequenceItem}, destination: Keys("records"), nested: child, }}} sources := []SourceInfo{{ID: "outer-source"}, {ID: "nested-source"}} @@ -760,7 +760,7 @@ func TestParseImportPlacementDefaultsPerDeclaration(t *testing.T) { - glob: records/*.yml placement: sequence-item `) - if len(declarations) != 2 || declarations[0].placement != placementModeMapping || declarations[1].placement != placementModeSequenceItem { + if len(declarations) != 2 || declarations[0].placement != PlacementModeMapping || declarations[1].placement != PlacementModeSequenceItem { t.Fatalf("declaration placements = %#v", declarations) } } @@ -1107,7 +1107,7 @@ id: item mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: helper.yml}]}\nid: main\n") writeImportFile(t, directory, "helper.yml", "id: helper\n") options := Options[importTestConfig]{ - Imports: true, destination: Keys("items"), placementMode: placementModeSequenceItem, + Imports: true, Destination: Keys("items"), PlacementMode: PlacementModeSequenceItem, } loadError := requireLoadCode(t, File(mainPath), options, CodeComposeConflict) diagnostic := loadError.Diagnostics()[0] diff --git a/internal/engine/layered.go b/internal/engine/layered.go index 6e1a7b9..5cb119c 100644 --- a/internal/engine/layered.go +++ b/internal/engine/layered.go @@ -8,7 +8,7 @@ type Input struct { Imports bool Filesystem *FilesystemContext Destination Path - placementMode placementMode + PlacementMode PlacementMode } // Underlay contains strictly composed contributions at one precedence tier. @@ -38,7 +38,7 @@ func LoadLayered[T any](main Input, secrets SecretBinding, supplied ...LayeredOp request := loadRequest[T]{ main: loadInput{ source: main.Source, imports: main.Imports, filesystem: main.Filesystem, - destination: main.Destination.clone(), placementMode: main.placementMode, + destination: main.Destination.clone(), placementMode: main.PlacementMode, }, definition: options.Definition, sourceVersionMode: options.SourceVersionMode, importDefaults: options.ImportDefaults, @@ -52,7 +52,7 @@ func LoadLayered[T any](main Input, secrets SecretBinding, supplied ...LayeredOp for inputIndex, input := range underlay.Inputs { request.underlays[index].inputs[inputIndex] = loadInput{ source: input.Source, imports: input.Imports, filesystem: input.Filesystem, - destination: input.Destination.clone(), placementMode: input.placementMode, + destination: input.Destination.clone(), placementMode: input.PlacementMode, } } } diff --git a/internal/engine/load.go b/internal/engine/load.go index 80eb594..241b241 100644 --- a/internal/engine/load.go +++ b/internal/engine/load.go @@ -33,8 +33,8 @@ type Options[T any] struct { Overrides []Override SourceVersionMode sourceVersionMode ImportDefaults ImportDefaults - destination Path - placementMode placementMode + Destination Path + PlacementMode PlacementMode } // FilesystemContext supplies filesystem roots for imports and overlays. For an @@ -161,7 +161,7 @@ func loadWithSecretLookupLimit[T any](source Source, secrets SecretBinding, look } input, diagnostics := normalizeInput(loadInput{ source: source, imports: options.Imports, filesystem: options.Filesystem, - destination: options.destination.clone(), placementMode: options.placementMode, + destination: options.Destination.clone(), placementMode: options.PlacementMode, }, options.Overlay) defaults, policy, settingsDiagnostics := loadSettings(options.ImportDefaults, options.Definition, options.SourceVersionMode) diagnostics = append(diagnostics, settingsDiagnostics...) diff --git a/internal/engine/load_request.go b/internal/engine/load_request.go index 91b82eb..b2d1801 100644 --- a/internal/engine/load_request.go +++ b/internal/engine/load_request.go @@ -32,7 +32,7 @@ type loadInput struct { imports bool filesystem *FilesystemContext destination Path - placementMode placementMode + placementMode PlacementMode } type loadGroup struct{ inputs []loadInput } diff --git a/internal/engine/load_request_test.go b/internal/engine/load_request_test.go index f55424a..1b9053d 100644 --- a/internal/engine/load_request_test.go +++ b/internal/engine/load_request_test.go @@ -161,8 +161,8 @@ func TestUnderlayItemConflictUsesScheduledGroupIndex(t *testing.T) { request := loadRequest[map[string]any]{ main: loadInput{source: NoSource()}, underlays: []loadGroup{{inputs: []loadInput{ - {source: File(zItem), destination: Keys("items"), placementMode: placementModeSequenceItem}, - {source: File(aItem), destination: Keys("items"), placementMode: placementModeSequenceItem}, + {source: File(zItem), destination: Keys("items"), placementMode: PlacementModeSequenceItem}, + {source: File(aItem), destination: Keys("items"), placementMode: PlacementModeSequenceItem}, }}}, definition: Definition[map[string]any]{sequenceIdentityRegistrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}}}, } @@ -205,8 +205,8 @@ func TestUnderlayItemHelperConflictUsesScheduledGroupIndex(t *testing.T) { request := loadRequest[map[string]any]{ main: loadInput{source: NoSource()}, underlays: []loadGroup{{inputs: []loadInput{ - {source: File(zBad), imports: true, destination: Keys("items"), placementMode: placementModeSequenceItem}, - {source: File(aGood), destination: Keys("items"), placementMode: placementModeSequenceItem}, + {source: File(zBad), imports: true, destination: Keys("items"), placementMode: PlacementModeSequenceItem}, + {source: File(aGood), destination: Keys("items"), placementMode: PlacementModeSequenceItem}, }}}, definition: Definition[map[string]any]{sequenceIdentityRegistrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}}}, } diff --git a/internal/engine/options.go b/internal/engine/options.go index 4347ec5..6705cd9 100644 --- a/internal/engine/options.go +++ b/internal/engine/options.go @@ -2,7 +2,7 @@ package engine func normalizeInput(input loadInput, ordinaryOverlay string) (loadInput, []Diagnostic) { var diagnostics []Diagnostic - if input.placementMode != placementModeMapping && input.placementMode != placementModeSequenceItem { + if input.placementMode != PlacementModeMapping && input.placementMode != PlacementModeSequenceItem { diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid")) } if normalized, err := normalizeDestination(input.destination); err != nil { @@ -10,7 +10,7 @@ func normalizeInput(input loadInput, ordinaryOverlay string) (loadInput, []Diagn } else { input.destination = normalized } - if input.placementMode == placementModeSequenceItem && len(input.destination.segments) == 0 { + if input.placementMode == PlacementModeSequenceItem && len(input.destination.segments) == 0 { diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "sequence-item placement requires a nonempty destination")) } if input.filesystem != nil { diff --git a/internal/engine/records.go b/internal/engine/records.go index 9d5b0e0..cbf5f4f 100644 --- a/internal/engine/records.go +++ b/internal/engine/records.go @@ -196,7 +196,7 @@ type SourceInfo struct { VersionCheck *SourceVersionCheck importSelections []importSelectionEvidence namespace *namespaceToken - placementMode placementMode + placementMode PlacementMode destinationUnresolved bool } diff --git a/internal/engine/resource_context_test.go b/internal/engine/resource_context_test.go index 7ac7460..5f14ddb 100644 --- a/internal/engine/resource_context_test.go +++ b/internal/engine/resource_context_test.go @@ -158,7 +158,7 @@ func TestLoadingClosesNativeFilesAcrossPipelineOutcomes(t *testing.T) { child: "configkit: {imports: [{path: helper.yml}]}\nid: child\nvalue: child\n", helper: "value: helper\n", options: Options[config]{ - Imports: true, destination: Keys("groups"), placementMode: placementModeSequenceItem, + Imports: true, Destination: Keys("groups"), PlacementMode: PlacementModeSequenceItem, }, wantErr: true, }, diff --git a/internal/engine/selected_overlay.go b/internal/engine/selected_overlay.go index 02f0dea..6077e4c 100644 --- a/internal/engine/selected_overlay.go +++ b/internal/engine/selected_overlay.go @@ -132,7 +132,7 @@ func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAcces var failures []Diagnostic if versionState != nil { processed, failures = versionState.processWithDirectiveContext( - overlayRoot, sourceVersionContribution, Path{}, sourceID, placementModeMapping, rootDirectiveContextOverlay, + overlayRoot, sourceVersionContribution, Path{}, sourceID, PlacementModeMapping, rootDirectiveContextOverlay, ) } else { processed = processSourceVersionWithDirectiveContext( diff --git a/internal/engine/source_version.go b/internal/engine/source_version.go index b3bab6c..799e55c 100644 --- a/internal/engine/source_version.go +++ b/internal/engine/source_version.go @@ -117,19 +117,19 @@ func (evidence *sourceVersionEvidence) sensitivityPaths() []Path { return paths } -func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode) (sourceVersionProcessingResult, []Diagnostic) { +func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement PlacementMode) (sourceVersionProcessingResult, []Diagnostic) { if state == nil || state.mode == sourceVersionModeDefault { return sourceVersionProcessingResult{}, nil } return state.processWithDirectiveContext(root, role, destination, sourceID, placement, rootDirectiveContextImportsOnly) } -func (state *sourceVersionState) processWithDirectiveContext(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode, directiveContext rootDirectiveContext) (sourceVersionProcessingResult, []Diagnostic) { +func (state *sourceVersionState) processWithDirectiveContext(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement PlacementMode, directiveContext rootDirectiveContext) (sourceVersionProcessingResult, []Diagnostic) { if state == nil { return sourceVersionProcessingResult{}, nil } projectedDestination := destination.clone() - if placement == placementModeSequenceItem { + if placement == PlacementModeSequenceItem { projectedDestination = projectedDestination.Index(0) } result := processSourceVersionWithDirectiveContext(root, state.localRule, role, projectedDestination, state.mode, directiveContext) @@ -158,10 +158,10 @@ type sourceVersionProcessingResult struct { } func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule { - return effectiveSourceVersionRuleForPlacement(local, destination, mode, placementModeMapping) + return effectiveSourceVersionRuleForPlacement(local, destination, mode, PlacementModeMapping) } -func effectiveSourceVersionRuleForPlacement(local *VersionRule, destination Path, mode sourceVersionMode, placement placementMode) *VersionRule { +func effectiveSourceVersionRuleForPlacement(local *VersionRule, destination Path, mode sourceVersionMode, placement PlacementMode) *VersionRule { if local == nil { return nil } @@ -170,7 +170,7 @@ func effectiveSourceVersionRuleForPlacement(local *VersionRule, destination Path } effective := *local prefix := destination.clone() - if placement == placementModeSequenceItem { + if placement == PlacementModeSequenceItem { prefix = prefix.Index(0) } effective.Path = Path{ diff --git a/internal/engine/source_version_test.go b/internal/engine/source_version_test.go index db0509c..7fd0493 100644 --- a/internal/engine/source_version_test.go +++ b/internal/engine/source_version_test.go @@ -142,11 +142,11 @@ func TestFragmentVersionClaimUsesAssignedSequenceProjection(t *testing.T) { t.Fatalf("destructive sequence ancestor claim = %#v, want mapping node", node) } - effective := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeRequired, placementModeSequenceItem) + effective := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeRequired, PlacementModeSequenceItem) if got, want := effective.Path.String(), "/catalog/entries/0/version"; got != want { t.Fatalf("item effective version path = %q, want %q", got, want) } - defaultRule := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeDefault, placementModeSequenceItem) + defaultRule := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeDefault, PlacementModeSequenceItem) if got, want := defaultRule.Path.String(), "/version"; got != want { t.Fatalf("default effective version path = %q, want %q", got, want) } @@ -628,7 +628,7 @@ func TestSourceVersionItemPlacementKeepsLocalChecksAndProtectsProjectedPath(t *t load: func(definition Definition[map[string]any]) error { _, err := Load(Mapping("main", map[string]any{"version": "v1", "name": "ordinary"}), testSecretBinding(), Options[map[string]any]{ Definition: definition, SourceVersionMode: sourceVersionModeRequired, - destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem, + Destination: Keys("catalog", "entries"), PlacementMode: PlacementModeSequenceItem, Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}}, }) return err @@ -637,7 +637,7 @@ func TestSourceVersionItemPlacementKeepsLocalChecksAndProtectsProjectedPath(t *t { name: "layered", load: func(definition Definition[map[string]any]) error { - _, err := LoadLayered(Input{Source: Mapping("main", map[string]any{"version": "v1", "name": "layered"}), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, testSecretBinding(), LayeredOptions[map[string]any]{ + _, err := LoadLayered(Input{Source: Mapping("main", map[string]any{"version": "v1", "name": "layered"}), Destination: Keys("catalog", "entries"), PlacementMode: PlacementModeSequenceItem}, testSecretBinding(), LayeredOptions[map[string]any]{ Definition: definition, SourceVersionMode: sourceVersionModeRequired, Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}}, }) @@ -682,7 +682,7 @@ func TestSourceVersionItemEvidenceUsesProjectedSensitivityPath(t *testing.T) { Secret: func(path Path) bool { return path.String() == secretPath.String() }, }, SourceVersionMode: sourceVersionModeRequired, - destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem, + Destination: Keys("catalog", "entries"), PlacementMode: PlacementModeSequenceItem, }) if err != nil { t.Fatalf("item source-version load: %v", err) diff --git a/layered.go b/layered.go index 2165889..49982aa 100644 --- a/layered.go +++ b/layered.go @@ -65,8 +65,9 @@ func toEngineInput(input Input) engine.Input { result := engine.Input{ Source: input.Source.toEngine(), Imports: input.Imports, Filesystem: toEngineFilesystemContext(input.Filesystem), Destination: toEnginePath(input.Destination), + PlacementMode: engine.PlacementMode(input.PlacementMode), } - return engine.InputWithPlacement(result, uint8(input.PlacementMode)) + return result } func toEngineLayeredOptions[T any](options LayeredOptions[T]) engine.LayeredOptions[T] { diff --git a/lifecycle.go b/lifecycle.go index bf52dc1..10c9dc1 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -318,8 +318,9 @@ func toEngineOptions[T any](options Options[T]) engine.Options[T] { Overlay: options.Overlay, SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)), ImportDefaults: toEngineImportDefaults(options.ImportDefaults), + Destination: toEnginePath(options.Destination.clone()), + PlacementMode: engine.PlacementMode(options.PlacementMode), } - result = engine.OptionsWithPlacement(result, toEnginePath(options.Destination.clone()), uint8(options.PlacementMode)) if options.Filesystem != nil { result.Filesystem = toEngineFilesystemContext(options.Filesystem) } diff --git a/records_test.go b/records_test.go index abb5c6b..d3424d3 100644 --- a/records_test.go +++ b/records_test.go @@ -30,7 +30,9 @@ func TestOverlayPublicValues(t *testing.T) { func TestSourceInfoDestinationConversion(t *testing.T) { destination := Keys("collections", "a/b", "0") path := writeRecordConfig(t, "value: main\n") - options := engine.OptionsWithPlacement(engine.Options[map[string]any]{}, engine.Keys("collections", "a/b", "0"), uint8(PlacementModeSequenceItem)) + options := engine.Options[map[string]any]{ + Destination: engine.Keys("collections", "a/b", "0"), PlacementMode: engine.PlacementMode(PlacementModeSequenceItem), + } internal := engineSourceInfoFromLoad(t, path, "source-1", options) converted := fromEngineSourceInfo(internal) if converted.Destination.String() != destination.String() || converted.PlacementMode != PlacementModeSequenceItem || converted.DestinationUnresolved {