Add private sequence-item placement
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Implementation Plan: Sequence Placement and Composition
|
||||
|
||||
Status: implementation in progress; stages 1–6 completed.
|
||||
Status: implementation in progress; stages 1–7 completed.
|
||||
|
||||
## Purpose and authority
|
||||
|
||||
@@ -451,6 +451,22 @@ Scope: placement primitives and engine per-input options; not public exposure.
|
||||
Validation: placement/accounting/option tests and `./scripts/check.sh`.
|
||||
Completion: the private operation places a mapping once with correct ownership.
|
||||
|
||||
Completion evidence: added private placement modes to engine options, load
|
||||
requests, and source records, with preflight rejection of unknown modes and
|
||||
item placement at an empty destination. A completed mapping can now be wrapped
|
||||
in one singleton sequence at its destination; nil payloads remain absent and
|
||||
empty mappings remain real items. Wrapper accounting includes both destination
|
||||
mapping nodes and the added sequence depth/node, with limits checked before
|
||||
allocation. Focused tests cover mapping zero-value equivalence, nested paths,
|
||||
empty and absent payloads, invalid modes/destinations, and exact/exceeded
|
||||
allowances. These checks passed with Go `go1.26.9 linux/amd64`:
|
||||
|
||||
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'Destination|Import|Mounted|PlaceDocument|Options|Layered' -count=1`
|
||||
- `GOTOOLCHAIN=local ./scripts/check.sh`
|
||||
- `git diff --check`
|
||||
|
||||
No environmental limitation affected validation; checks ran on Linux/amd64.
|
||||
|
||||
## Stage 8 — Assemble item scopes and YAML path/glob imports
|
||||
|
||||
Scope: directive parsing and scoped acquisition/composition.
|
||||
|
||||
@@ -11,6 +11,13 @@ var (
|
||||
errDestinationLimit = errors.New("configkit: destination exceeds document limits")
|
||||
)
|
||||
|
||||
type placementMode uint8
|
||||
|
||||
const (
|
||||
placementModeMapping placementMode = iota
|
||||
placementModeSequenceItem
|
||||
)
|
||||
|
||||
func normalizeDestination(path Path) (Path, error) {
|
||||
if !path.valid() {
|
||||
return Path{}, errInvalidDestination
|
||||
@@ -47,18 +54,39 @@ 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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
normalized, err := normalizeDestination(destination)
|
||||
if err != nil {
|
||||
return Path{}, nil, err
|
||||
}
|
||||
if mode != placementModeMapping && mode != placementModeSequenceItem {
|
||||
return Path{}, nil, errInvalidDestination
|
||||
}
|
||||
if mode == placementModeSequenceItem && len(normalized.segments) == 0 {
|
||||
return Path{}, nil, errInvalidDestination
|
||||
}
|
||||
if body == nil {
|
||||
return normalized, nil, nil
|
||||
}
|
||||
if !destinationWithinLimits(body, normalized, maxDepth, maxNodes) {
|
||||
if mode == placementModeSequenceItem && body.Kind != document.Mapping {
|
||||
return normalized, nil, errInvalidDestination
|
||||
}
|
||||
if !destinationWithinLimitsWithMode(body, normalized, mode, maxDepth, maxNodes) {
|
||||
return normalized, nil, errDestinationLimit
|
||||
}
|
||||
|
||||
placed := body
|
||||
if mode == placementModeSequenceItem {
|
||||
placed = &document.Node{
|
||||
Kind: document.Sequence, Location: sourceRoot,
|
||||
Sequence: []*document.Node{placed},
|
||||
}
|
||||
}
|
||||
for index := len(normalized.segments) - 1; index >= 0; index-- {
|
||||
key := normalized.segments[index].key
|
||||
placed = &document.Node{
|
||||
@@ -77,28 +105,49 @@ func placeDocument(body *document.Node, destination Path, sourceRoot document.Lo
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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() {
|
||||
if maxDepth < 0 || maxNodes < 0 || !destination.valid() || (mode != placementModeMapping && mode != placementModeSequenceItem) {
|
||||
return false
|
||||
}
|
||||
segments := len(destination.segments)
|
||||
if segments > maxDepth || segments > maxNodes/2 {
|
||||
sequenceNodes := 0
|
||||
sequenceDepth := 0
|
||||
if mode == placementModeSequenceItem {
|
||||
if segments == 0 {
|
||||
return false
|
||||
}
|
||||
sequenceNodes = 1
|
||||
sequenceDepth = 1
|
||||
}
|
||||
if segments > maxDepth-sequenceDepth || segments > (maxNodes-sequenceNodes)/2 {
|
||||
return false
|
||||
}
|
||||
wrapperNodes := destinationWrapperNodeCount(destination)
|
||||
wrapperNodes := destinationWrapperNodeCount(destination) + sequenceNodes
|
||||
for _, segment := range destination.segments {
|
||||
if segment.kind != pathSegmentKey {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if documentDepth(body) > maxDepth-segments {
|
||||
if documentDepth(body) > maxDepth-segments-sequenceDepth {
|
||||
return false
|
||||
}
|
||||
return countInputNodes(body) <= maxNodes-wrapperNodes
|
||||
}
|
||||
|
||||
func destinationWrapperNodeCountForMode(destination Path, mode placementMode) int {
|
||||
count := destinationWrapperNodeCount(destination)
|
||||
if mode == placementModeSequenceItem {
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func destinationWrapperNodeCount(destination Path) int {
|
||||
return 2 * len(destination.segments)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,98 @@ type destinationLoadConfig struct {
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
type destinationItemConfig struct {
|
||||
Catalog struct {
|
||||
Entries []map[string]string `yaml:"entries"`
|
||||
} `yaml:"catalog"`
|
||||
}
|
||||
|
||||
func TestLayeredLoadPlacesCompletedMappingAsOneItem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
loaded, err := LoadLayered[destinationItemConfig](
|
||||
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,
|
||||
}}}}},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadLayered: %v", err)
|
||||
}
|
||||
result := testLoaded(loaded)
|
||||
if len(result.Config.Catalog.Entries) != 1 || result.Config.Catalog.Entries[0] == nil || len(result.Config.Catalog.Entries[0]) != 0 {
|
||||
t.Fatalf("empty mapping item = %#v", result.Config.Catalog.Entries)
|
||||
}
|
||||
if _, ok := result.Metadata().Lookup(Keys("catalog", "entries", "0")); ok {
|
||||
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 {
|
||||
t.Fatalf("source placement evidence = %#v", sources)
|
||||
}
|
||||
if got := sources[0].Destination.String(); got != "/catalog/entries" {
|
||||
t.Fatalf("source destination = %q", got)
|
||||
}
|
||||
|
||||
absent, err := LoadLayered[destinationItemConfig](
|
||||
Input{Source: NoSource(), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem},
|
||||
testSecretBinding(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NoSource item load: %v", err)
|
||||
}
|
||||
if got := len(testLoaded(absent).Config.Catalog.Entries); got != 0 {
|
||||
t.Fatalf("absent item created %d entries", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLayeredItemPlacementPreflightsModeAndDestination(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
mode placementMode
|
||||
path Path
|
||||
}{
|
||||
{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},
|
||||
testSecretBinding(),
|
||||
)
|
||||
if err == nil || reader.Len() == 0 {
|
||||
t.Fatalf("preflight err %v, unread bytes %d", err, reader.Len())
|
||||
}
|
||||
loadError, ok := err.(*LoadError)
|
||||
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeOptionsInvalid {
|
||||
t.Fatalf("preflight error = %#v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOptionsPlaceOneMappingItem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
loaded, err := Load[destinationItemConfig](
|
||||
Mapping("ordinary", map[string]any{}), testSecretBinding(),
|
||||
Options[destinationItemConfig]{destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
result := testLoaded(loaded)
|
||||
if len(result.Config.Catalog.Entries) != 1 || result.Config.Catalog.Entries[0] == nil {
|
||||
t.Fatalf("ordinary item placement = %#v", result.Config.Catalog.Entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLayeredLoadPlacesNonImportMainAndUnderlayInputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
@@ -39,6 +40,29 @@ func TestNormalizeDestinationUsesLiteralMappingKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingPlacementModeMatchesZeroValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body := &document.Node{
|
||||
Kind: document.Mapping,
|
||||
Mapping: []document.Entry{{
|
||||
Key: "name", Value: &document.Node{Kind: document.String, Text: "entry"},
|
||||
}},
|
||||
}
|
||||
destination := Keys("catalog", "entry")
|
||||
path, zero, err := placeDocument(body, destination, body.Location, 5, 32)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
explicitPath, explicit, err := placeDocumentWithMode(body, destination, placementModeMapping, body.Location, 5, 32)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if path.String() != explicitPath.String() || !reflect.DeepEqual(zero, explicit) {
|
||||
t.Fatalf("zero placement (%q, %#v) differs from mapping mode (%q, %#v)", path.String(), zero, explicitPath.String(), explicit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendDestinationCopiesLiteralSegments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -204,3 +228,60 @@ func TestPlaceDocumentRootAndBounds(t *testing.T) {
|
||||
t.Fatalf("absent body should not consume placement limits: node %p, error %v", absent, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceDocumentSequenceItemAndMountedBounds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if placedPath.String() != "/catalog/entries" || placed.Kind != document.Mapping {
|
||||
t.Fatalf("placed root = %q %#v", placedPath.String(), placed)
|
||||
}
|
||||
container, ok := documentAtPath(placed, destination)
|
||||
if !ok || container.Kind != document.Sequence || len(container.Sequence) != 1 || container.Sequence[0] != body {
|
||||
t.Fatalf("item destination = %#v, present %v", container, ok)
|
||||
}
|
||||
if got, want := countInputNodes(placed), countInputNodes(body)+destinationWrapperNodeCount(destination)+1; got != want {
|
||||
t.Fatalf("item node count = %d, want %d", got, want)
|
||||
}
|
||||
if container.Location != root || container.Sequence[0].Location != root {
|
||||
t.Fatal("synthetic sequence or authored mapping lost source coordinates")
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
t.Fatalf("unknown placement mode error = %v, want invalid destination", err)
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
depth int
|
||||
nodes int
|
||||
wantAccept bool
|
||||
}{
|
||||
{name: "exact allowance", depth: 3, nodes: 6, wantAccept: true},
|
||||
{name: "one too shallow", depth: 2, nodes: 6},
|
||||
{name: "one node short", depth: 3, nodes: 5},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := placeDocumentWithMode(body, destination, placementModeSequenceItem, root, test.depth, test.nodes)
|
||||
if test.wantAccept && err != nil {
|
||||
t.Fatalf("exact bounds rejected: %v", err)
|
||||
}
|
||||
if !test.wantAccept && !errors.Is(err, errDestinationLimit) {
|
||||
t.Fatalf("over-bound placement error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import "os"
|
||||
|
||||
// Input selects one source, its destination, and its independent import policy.
|
||||
type Input struct {
|
||||
Source Source
|
||||
Imports bool
|
||||
Filesystem *FilesystemContext
|
||||
Destination Path
|
||||
Source Source
|
||||
Imports bool
|
||||
Filesystem *FilesystemContext
|
||||
Destination Path
|
||||
placementMode placementMode
|
||||
}
|
||||
|
||||
// Underlay contains strictly composed contributions at one precedence tier.
|
||||
@@ -36,7 +37,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(),
|
||||
destination: main.Destination.clone(), placementMode: main.placementMode,
|
||||
},
|
||||
definition: options.Definition, sourceVersionMode: options.SourceVersionMode,
|
||||
overlay: options.Overlay,
|
||||
@@ -49,7 +50,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(),
|
||||
destination: input.Destination.clone(), placementMode: input.placementMode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ type Options[T any] struct {
|
||||
Filesystem *FilesystemContext
|
||||
Overrides []Override
|
||||
SourceVersionMode sourceVersionMode
|
||||
destination Path
|
||||
placementMode placementMode
|
||||
}
|
||||
|
||||
// FilesystemContext supplies filesystem roots for imports and overlays.
|
||||
@@ -147,7 +149,7 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se
|
||||
definition := options.Definition
|
||||
definition.sequenceRegistrations = nil
|
||||
request := loadRequest[T]{
|
||||
main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem},
|
||||
main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem, destination: options.destination.clone(), placementMode: options.placementMode},
|
||||
definition: definition, sequencePolicy: policy, overlay: options.Overlay,
|
||||
overrides: options.Overrides, ordinary: true,
|
||||
sourceVersionMode: options.SourceVersionMode,
|
||||
@@ -159,7 +161,7 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se
|
||||
func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup) (*Loaded[T], error) {
|
||||
accumulator := diagnosticAccumulator{}
|
||||
accounting := &importAccounting{}
|
||||
mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem}
|
||||
mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(), placementMode: request.main.placementMode}
|
||||
mainOptions.SourceVersionMode = request.sourceVersionMode
|
||||
mainOptions.Definition.Version = request.protectionVersion()
|
||||
mainOptions.Overrides = request.overrides
|
||||
|
||||
@@ -82,6 +82,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
||||
parsed, diagnostics := parseSource(source, "source-"+strconv.Itoa(accounting.sources+1), parseLimits)
|
||||
if parsed.source != nil {
|
||||
parsed.source.Destination = destination.clone()
|
||||
parsed.source.placementMode = options.placementMode
|
||||
}
|
||||
input := assembledInput{parsed: parsed, document: parsed.document, sources: sourceInfos(parsed.source), versionState: versionState}
|
||||
if len(diagnostics) != 0 {
|
||||
@@ -136,7 +137,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
||||
}
|
||||
}
|
||||
if !options.Imports && input.document != nil {
|
||||
_, placed, err := placeDocument(input.document, destination, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes)
|
||||
_, placed, err := placeDocumentWithMode(input.document, destination, options.placementMode, input.document.Location, parseLimits.document.Depth, parseLimits.document.Nodes)
|
||||
if err != nil {
|
||||
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")}
|
||||
}
|
||||
@@ -150,7 +151,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
||||
}},
|
||||
}}
|
||||
rawNodes := countInputNodes(parsed.document)
|
||||
placementNodes := destinationWrapperNodeCount(destination)
|
||||
placementNodes := destinationWrapperNodeCountForMode(destination, options.placementMode)
|
||||
if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes {
|
||||
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the aggregate input limit")}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,11 @@ func (request loadRequest[T]) protectionVersion() *VersionRule {
|
||||
}
|
||||
|
||||
type loadInput struct {
|
||||
source Source
|
||||
imports bool
|
||||
filesystem *FilesystemContext
|
||||
destination Path
|
||||
source Source
|
||||
imports bool
|
||||
filesystem *FilesystemContext
|
||||
destination Path
|
||||
placementMode placementMode
|
||||
}
|
||||
|
||||
type loadGroup struct{ inputs []loadInput }
|
||||
@@ -42,26 +43,18 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi
|
||||
request.sequencePolicy = policy
|
||||
diagnostics = append(diagnostics, policyDiagnostics...)
|
||||
request.definition.sequenceRegistrations = nil
|
||||
if normalized, err := normalizeDestination(request.main.destination); err != nil {
|
||||
diagnostics = append(diagnostics, invalidDestinationDiagnostic(request.main.destination))
|
||||
} else {
|
||||
request.main.destination = normalized
|
||||
}
|
||||
request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode)
|
||||
_, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{
|
||||
Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem,
|
||||
SourceVersionMode: request.sourceVersionMode,
|
||||
mainOptions, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{
|
||||
Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(),
|
||||
SourceVersionMode: request.sourceVersionMode, placementMode: request.main.placementMode,
|
||||
}})
|
||||
request.main.destination = mainOptions.destination
|
||||
diagnostics = append(diagnostics, mainDiagnostics...)
|
||||
request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode)
|
||||
for groupIndex := range request.underlays {
|
||||
for inputIndex := range request.underlays[groupIndex].inputs {
|
||||
input := &request.underlays[groupIndex].inputs[inputIndex]
|
||||
if normalized, err := normalizeDestination(input.destination); err != nil {
|
||||
diagnostics = append(diagnostics, invalidDestinationDiagnostic(input.destination))
|
||||
} else {
|
||||
input.destination = normalized
|
||||
}
|
||||
_, inputDiagnostics := selectOptions(input.source, []Options[T]{{Imports: input.imports, Filesystem: input.filesystem}})
|
||||
inputOptions, inputDiagnostics := selectOptions(input.source, []Options[T]{{Imports: input.imports, Filesystem: input.filesystem, destination: input.destination.clone(), placementMode: input.placementMode}})
|
||||
input.destination = inputOptions.destination
|
||||
diagnostics = append(diagnostics, inputDiagnostics...)
|
||||
}
|
||||
}
|
||||
@@ -165,7 +158,11 @@ func acquireUnderlayGroupWithVersion(group loadGroup, index int, rule *VersionRu
|
||||
var sources []SourceInfo
|
||||
var contexts map[string]ResourceContext
|
||||
for _, contribution := range group.inputs {
|
||||
options := Options[struct{}]{Imports: contribution.imports, Filesystem: contribution.filesystem, Definition: Definition[struct{}]{Version: rule}}
|
||||
options := Options[struct{}]{
|
||||
Imports: contribution.imports, Filesystem: contribution.filesystem,
|
||||
Definition: Definition[struct{}]{Version: rule}, destination: contribution.destination.clone(),
|
||||
placementMode: contribution.placementMode,
|
||||
}
|
||||
acquired, diagnostics := assembleBaseInputTrackedForRole(contribution.source, options, contribution.destination, accounting, visited, hostToken, versionState, sourceVersionContribution)
|
||||
for _, source := range acquired.sources {
|
||||
source.Layer = LayerUnderlay
|
||||
|
||||
@@ -9,6 +9,17 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D
|
||||
options = supplied[0]
|
||||
}
|
||||
var diagnostics []Diagnostic
|
||||
if options.placementMode != placementModeMapping && options.placementMode != placementModeSequenceItem {
|
||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid"))
|
||||
}
|
||||
if normalized, err := normalizeDestination(options.destination); err != nil {
|
||||
diagnostics = append(diagnostics, invalidDestinationDiagnostic(options.destination))
|
||||
} else {
|
||||
options.destination = normalized
|
||||
}
|
||||
if options.placementMode == placementModeSequenceItem && len(options.destination.segments) == 0 {
|
||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "sequence-item placement requires a nonempty destination"))
|
||||
}
|
||||
definition := options.Definition
|
||||
if !validOverlayName(options.Overlay) {
|
||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen"))
|
||||
|
||||
@@ -183,16 +183,18 @@ type Location struct {
|
||||
// namespace-relative filesystem path; synthetic sources have no Path.
|
||||
// ImportedFrom identifies the declaration that included this source.
|
||||
type SourceInfo struct {
|
||||
ID string
|
||||
Label string
|
||||
Path string
|
||||
Destination Path
|
||||
Layer Layer
|
||||
Kind SourceKind
|
||||
UnderlayIndex int
|
||||
ImportedFrom *Location
|
||||
VersionCheck *SourceVersionCheck
|
||||
namespace *namespaceToken
|
||||
ID string
|
||||
Label string
|
||||
Path string
|
||||
Destination Path
|
||||
Layer Layer
|
||||
Kind SourceKind
|
||||
UnderlayIndex int
|
||||
ImportedFrom *Location
|
||||
VersionCheck *SourceVersionCheck
|
||||
namespace *namespaceToken
|
||||
placementMode placementMode
|
||||
destinationUnresolved bool
|
||||
}
|
||||
|
||||
// SourceVersionCheck is a detached, public-safe record of a source-local
|
||||
|
||||
Reference in New Issue
Block a user