Add private sequence-item placement

This commit is contained in:
2026-10-16 15:55:29 +00:00
parent 1cf3b0d78f
commit 1a04bb3835
10 changed files with 298 additions and 46 deletions

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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)
}
})
}
}

View File

@@ -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,
}
}
}

View File

@@ -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

View File

@@ -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")}
}

View File

@@ -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

View File

@@ -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"))

View File

@@ -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