Define private import defaults and declaration parsing

This commit is contained in:
2026-10-19 20:02:05 +00:00
parent ba577429e6
commit b498e5bee5
4 changed files with 671 additions and 1 deletions

View File

@@ -0,0 +1,425 @@
package engine
import (
"path"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
type importSelector uint8
const (
importSelectorPath importSelector = iota + 1
importSelectorDirectory
importSelectorGlob
)
type importFieldSite struct {
path Path
location Location
}
// importDeclarationSpec preserves authored presence until discovery policy is resolved.
type importDeclarationSpec struct {
path Path
location Location
selector importSelector
paths []string
glob string
globSegments []string
required *bool
recursive *bool
follow *bool
hidden *bool
pattern []string
patternSet bool
exclude []string
excludeSet bool
depth int
depthSet bool
destination Path
placement placementMode
fieldSites map[string]importFieldSite
}
// importDefaults are the engine's private copy of load-wide discovery defaults.
type importDefaults struct {
required *bool
follow *bool
hidden *bool
recursive *bool
pattern []string
}
type resolvedImportPolicy struct {
required bool
follow bool
hidden bool
recursive bool
pattern []string
depth int
depthSet bool
exclude []string
destination Path
placement placementMode
}
func snapshotImportDefaults(value importDefaults) importDefaults {
return importDefaults{
required: cloneBool(value.required),
follow: cloneBool(value.follow),
hidden: cloneBool(value.hidden),
recursive: cloneBool(value.recursive),
pattern: cloneStrings(value.pattern),
}
}
func cloneBool(value *bool) *bool {
if value == nil {
return nil
}
cloned := *value
return &cloned
}
func cloneStrings(values []string) []string {
if values == nil {
return nil
}
return append(make([]string, 0, len(values)), values...)
}
func validateImportDefaults(value importDefaults) []Diagnostic {
for _, pattern := range value.pattern {
if _, err := path.Match(pattern, ""); err != nil {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "import default pattern is invalid")}
}
}
return nil
}
func resolveImportPolicy(spec importDeclarationSpec, defaults importDefaults) (resolvedImportPolicy, []Diagnostic) {
defaults = snapshotImportDefaults(defaults)
policy := resolvedImportPolicy{
required: boolOr(spec.required, defaults.required, false),
follow: true,
hidden: true,
recursive: false,
destination: spec.destination.clone(),
placement: spec.placement,
depth: spec.depth,
depthSet: spec.depthSet,
exclude: cloneStrings(spec.exclude),
}
if spec.selector == importSelectorDirectory || spec.selector == importSelectorGlob {
policy.follow = boolOr(spec.follow, defaults.follow, true)
policy.hidden = boolOr(spec.hidden, defaults.hidden, true)
}
if spec.selector == importSelectorDirectory {
policy.recursive = boolOr(spec.recursive, defaults.recursive, false)
if spec.patternSet {
policy.pattern = cloneStrings(spec.pattern)
} else {
policy.pattern = cloneStrings(defaults.pattern)
}
}
if spec.depthSet && !policy.recursive {
site := spec.fieldSites["depth"]
location := site.location
return resolvedImportPolicy{}, []Diagnostic{{
Code: CodeImportInvalid, Severity: SeverityError,
Path: site.path.clone(), Message: "depth requires effective recursive discovery",
Primary: &location,
}}
}
return policy, nil
}
func boolOr(explicit, application *bool, fallback bool) bool {
if explicit != nil {
return *explicit
}
if application != nil {
return *application
}
return fallback
}
// parseImportSpecificationDirective validates every declaration before callers select candidates.
func parseImportSpecificationDirective(node *document.Node, directivePath Path) ([]importDeclarationSpec, []Diagnostic) {
if node.Kind != document.Mapping {
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, directivePath, "configuration directive must be a mapping", node)}
}
var specs []importDeclarationSpec
var diagnostics []Diagnostic
for _, entry := range node.Mapping {
fieldPath := directivePath.Key(entry.Key)
if entry.Key != "imports" {
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeImportInvalid, fieldPath, "configuration directive field is not recognized", entry.Value))
continue
}
if entry.Value.Kind != document.Sequence {
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeImportInvalid, fieldPath, "imports must be a sequence", entry.Value))
continue
}
for index, declarationNode := range entry.Value.Sequence {
spec, issues := parseImportDeclarationSpec(declarationNode, fieldPath.Index(index))
if len(issues) != 0 {
diagnostics = append(diagnostics, issues...)
continue
}
specs = append(specs, spec)
}
}
return specs, canonicalDiagnostics(diagnostics)
}
func parseImportDeclarationSpec(node *document.Node, declarationPath Path) (importDeclarationSpec, []Diagnostic) {
if node.Kind != document.Mapping {
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration must be a mapping", node)}
}
spec := importDeclarationSpec{
path: declarationPath.clone(),
location: publicLocation(node.Location),
placement: placementModeMapping,
fieldSites: make(map[string]importFieldSite, len(node.Mapping)),
}
selectors := 0
selectorIndex := -1
for index, entry := range node.Mapping {
if selectorForField(entry.Key) != 0 {
selectors++
selectorIndex = index
}
}
if selectors != 1 {
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration requires exactly one path, dir, or glob selector", node)}
}
selectorEntry := node.Mapping[selectorIndex]
spec.selector = selectorForField(selectorEntry.Key)
for _, entry := range node.Mapping {
fieldPath := declarationPath.Key(entry.Key)
spec.fieldSites[entry.Key] = importFieldSite{path: fieldPath.clone(), location: publicLocation(entry.Value.Location)}
if selectorForField(entry.Key) != 0 {
continue
}
if !importFieldApplies(entry.Key, spec.selector) {
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not supported by this selector", entry.Value)}
}
switch entry.Key {
case "required", "recursive", "follow", "hidden":
if entry.Value.Kind != document.Boolean {
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, entry.Key+" must be a boolean", entry.Value)}
}
value := entry.Value.Text == "true"
switch entry.Key {
case "required":
spec.required = &value
case "recursive":
spec.recursive = &value
case "follow":
spec.follow = &value
case "hidden":
spec.hidden = &value
}
case "pattern":
values, origins, ok, badIndex := parseStringSet(entry.Value)
if !ok {
return importDeclarationSpec{}, []Diagnostic{stringSetDiagnostic(entry.Value, fieldPath, "pattern", badIndex)}
}
if badIndex, ok := invalidWildcard(values); ok {
badPath, badNode := listMemberSite(entry.Value, fieldPath, origins[badIndex])
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, badPath, "pattern contains an invalid wildcard expression", badNode)}
}
spec.pattern, spec.patternSet = values, true
case "exclude":
values, origins, ok, badIndex := parseStringSet(entry.Value)
if !ok {
return importDeclarationSpec{}, []Diagnostic{stringSetDiagnostic(entry.Value, fieldPath, "exclude", badIndex)}
}
if badIndex, ok := invalidWildcard(values); ok {
badPath, badNode := listMemberSite(entry.Value, fieldPath, origins[badIndex])
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, badPath, "exclude contains an invalid wildcard expression", badNode)}
}
spec.exclude, spec.excludeSet = values, true
case "depth":
depth, ok := parseImportDepth(entry.Value)
if !ok {
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "depth must be a nonnegative integer", entry.Value)}
}
spec.depth, spec.depthSet = depth, true
case "destination":
destination, diagnostic := parseImportDestination(entry.Value, fieldPath)
if diagnostic != nil {
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
}
spec.destination = destination
case "placement":
if entry.Value.Kind != document.String {
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", entry.Value)}
}
switch entry.Value.Text {
case "mapping":
spec.placement = placementModeMapping
case "sequence-item":
spec.placement = placementModeSequenceItem
default:
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", entry.Value)}
}
default:
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)}
}
}
selectorPath := declarationPath.Key(selectorEntry.Key)
spec.fieldSites[selectorEntry.Key] = importFieldSite{path: selectorPath.clone(), location: publicLocation(selectorEntry.Value.Location)}
switch spec.selector {
case importSelectorPath, importSelectorDirectory:
values, origins, ok, badIndex := parseStringSet(selectorEntry.Value)
if !ok {
name := selectorEntry.Key
return importDeclarationSpec{}, []Diagnostic{stringSetDiagnostic(selectorEntry.Value, selectorPath, name, badIndex)}
}
for index, value := range values {
if !validImportPath(value) {
badPath, badNode := listMemberSite(selectorEntry.Value, selectorPath, origins[index])
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, badPath, selectorEntry.Key+" contains an invalid source path", badNode)}
}
}
spec.paths = values
case importSelectorGlob:
if selectorEntry.Value.Kind != document.String || !validImportPath(selectorEntry.Value.Text) {
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, selectorPath, "glob must be a valid nonempty path pattern string", selectorEntry.Value)}
}
segments, ok := validGlobPattern(selectorEntry.Value.Text)
if !ok {
return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, selectorPath, "glob pattern is invalid", selectorEntry.Value)}
}
spec.glob, spec.globSegments = selectorEntry.Value.Text, segments
}
return spec, nil
}
func selectorForField(field string) importSelector {
switch field {
case "path":
return importSelectorPath
case "dir":
return importSelectorDirectory
case "glob":
return importSelectorGlob
default:
return 0
}
}
func importFieldApplies(field string, selector importSelector) bool {
switch field {
case "required", "exclude", "destination", "placement":
return true
case "pattern", "recursive", "depth":
return selector == importSelectorDirectory
case "follow", "hidden":
return selector == importSelectorDirectory || selector == importSelectorGlob
default:
return false
}
}
func parseStringSet(node *document.Node) ([]string, []int, bool, int) {
if node.Kind == document.String {
return []string{node.Text}, []int{-1}, true, -1
}
if node.Kind != document.Sequence {
return nil, nil, false, -1
}
values := make([]string, 0, len(node.Sequence))
origins := make([]int, 0, len(node.Sequence))
seen := make(map[string]struct{}, len(node.Sequence))
for index, item := range node.Sequence {
if item.Kind != document.String {
return nil, nil, false, index
}
if _, exists := seen[item.Text]; exists {
continue
}
seen[item.Text] = struct{}{}
values = append(values, item.Text)
origins = append(origins, index)
}
return values, origins, true, -1
}
func stringSetDiagnostic(node *document.Node, fieldPath Path, field string, badIndex int) Diagnostic {
if badIndex >= 0 {
badPath, item := listMemberSite(node, fieldPath, badIndex)
return nodeImportDiagnostic(CodeImportInvalid, badPath, field+" members must be strings", item)
}
return nodeImportDiagnostic(CodeImportInvalid, fieldPath, field+" must be a string or a flat list of strings", node)
}
func invalidWildcard(values []string) (int, bool) {
for index, value := range values {
if _, err := path.Match(value, ""); err != nil {
return index, true
}
}
return -1, false
}
func listMemberSite(node *document.Node, fieldPath Path, index int) (Path, *document.Node) {
if node.Kind != document.Sequence || index < 0 {
return fieldPath, node
}
return fieldPath.Index(index), node.Sequence[index]
}
func parseImportDepth(node *document.Node) (int, bool) {
if node.Kind != document.Number || node.Text == "" {
return 0, false
}
if node.Text == "-0" {
return 0, true
}
var value uint64
maxInt := uint64(^uint(0) >> 1)
for index := 0; index < len(node.Text); index++ {
character := node.Text[index]
if character < '0' || character > '9' {
return 0, false
}
digit := uint64(character - '0')
if value > (maxInt-digit)/10 {
return int(maxInt), true
}
value = value*10 + digit
}
return int(value), true
}
func parseImportDestination(node *document.Node, fieldPath Path) (Path, *Diagnostic) {
if node.Kind != document.Sequence {
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must be a sequence of strings", node)
return Path{}, &diagnostic
}
relative := Path{segments: make([]pathSegment, 0, len(node.Sequence))}
for index, item := range node.Sequence {
memberPath := fieldPath.Index(index)
if item.Kind != document.String {
diagnostic := nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be strings", item)
return Path{}, &diagnostic
}
member := Path{segments: []pathSegment{{kind: pathSegmentKey, key: item.Text}}}
if _, err := normalizeDestination(member); err != nil {
diagnostic := nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be non-reserved mapping keys", item)
return Path{}, &diagnostic
}
relative.segments = append(relative.segments, pathSegment{kind: pathSegmentKey, key: item.Text})
}
normalized, err := normalizeDestination(relative)
if err != nil {
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must contain only non-reserved mapping keys", node)
return Path{}, &diagnostic
}
return normalized, nil
}

View File

@@ -0,0 +1,243 @@
package engine
import (
"errors"
"io"
"strings"
"testing"
)
func TestParseImportDeclarationSpecNormalizesPresenceAndPlacement(t *testing.T) {
t.Parallel()
specs, diagnostics := parseImportSpecsYAML(t, `
- path: [one.yaml, one.yaml, two.yaml]
required: false
destination: [services]
placement: sequence-item
- dir: []
recursive: true
pattern: []
exclude: []
- glob: config/*.yaml
`)
if len(diagnostics) != 0 {
t.Fatalf("diagnostics = %#v", diagnostics)
}
if len(specs) != 3 {
t.Fatalf("spec count = %d, want 3", len(specs))
}
first := specs[0]
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 {
t.Fatalf("explicit declaration values = %#v", first)
}
if first.path.String() != "/configkit/imports/0" || first.fieldSites["required"].path.String() != "/configkit/imports/0/required" {
t.Fatalf("stored paths = declaration %q, required %q", first.path.String(), first.fieldSites["required"].path.String())
}
second := specs[1]
if second.selector != importSelectorDirectory || second.paths == nil || len(second.paths) != 0 || !second.patternSet || second.pattern == nil || len(second.pattern) != 0 || !second.excludeSet || second.exclude == nil {
t.Fatalf("empty list presence was lost: %#v", second)
}
third := specs[2]
if third.selector != importSelectorGlob || third.glob != "config/*.yaml" || len(third.globSegments) != 2 {
t.Fatalf("glob selector = %#v", third)
}
}
func TestParseImportDeclarationSpecValidatesSelectorMatrixAndTypes(t *testing.T) {
t.Parallel()
tests := []struct {
name, yaml, path string
}{
{name: "missing selector", yaml: " - required: true\n", path: "/configkit/imports/0"},
{name: "multiple selectors", yaml: " - path: one.yaml\n dir: fragments\n", path: "/configkit/imports/0"},
{name: "removed optional", yaml: " - path: one.yaml\n optional: true\n", path: "/configkit/imports/0/optional"},
{name: "inapplicable field", yaml: " - path: one.yaml\n pattern: '*.yaml'\n", path: "/configkit/imports/0/pattern"},
{name: "selector null", yaml: " - path: null\n", path: "/configkit/imports/0/path"},
{name: "empty path", yaml: " - path: ''\n", path: "/configkit/imports/0/path"},
{name: "nested selector list", yaml: " - path: [[one.yaml]]\n", path: "/configkit/imports/0/path/0"},
{name: "null selector member", yaml: " - dir: [fragments, null]\n", path: "/configkit/imports/0/dir/1"},
{name: "empty glob", yaml: " - glob: ''\n", path: "/configkit/imports/0/glob"},
{name: "recursive glob wildcard", yaml: " - glob: '**/*.yaml'\n", path: "/configkit/imports/0/glob"},
{name: "null boolean", yaml: " - path: one.yaml\n required: null\n", path: "/configkit/imports/0/required"},
{name: "bad pattern member with empty roots", yaml: " - dir: []\n pattern: ['*', '[']\n", path: "/configkit/imports/0/pattern/1"},
{name: "bad exclusion member", yaml: " - glob: '*.yaml'\n exclude: ['[']\n", path: "/configkit/imports/0/exclude/0"},
{name: "invalid destination", yaml: " - path: one.yaml\n destination: [configkit]\n", path: "/configkit/imports/0/destination/0"},
{name: "invalid placement", yaml: " - path: one.yaml\n placement: sequence\n", path: "/configkit/imports/0/placement"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
_, diagnostics := parseImportSpecsYAML(t, test.yaml)
if len(diagnostics) == 0 {
t.Fatal("expected a declaration diagnostic")
}
if diagnostics[0].Code != CodeImportInvalid || diagnostics[0].Path.String() != test.path {
t.Fatalf("diagnostic = %#v, want %s at %s", diagnostics[0], CodeImportInvalid, test.path)
}
if diagnostics[0].Primary == nil || diagnostics[0].Primary.Line == 0 {
t.Fatalf("diagnostic has no source location: %#v", diagnostics[0])
}
})
}
}
func TestParseImportDeclarationSpecValidatesDepthSyntaxAndPolicy(t *testing.T) {
t.Parallel()
for _, test := range []struct {
value string
valid bool
want int
}{
{value: "0", valid: true, want: 0},
{value: "-0", valid: true, want: 0},
{value: "999999999999999999999999999999999999", valid: true, want: int(^uint(0) >> 1)},
{value: "-1"},
{value: "1.5"},
{value: "1e3"},
{value: "'2'"},
{value: "true"},
{value: "null"},
} {
t.Run(test.value, func(t *testing.T) {
t.Parallel()
specs, diagnostics := parseImportSpecsYAML(t, " - dir: []\n recursive: true\n depth: "+test.value+"\n")
if !test.valid {
if len(diagnostics) == 0 || diagnostics[0].Path.String() != "/configkit/imports/0/depth" {
t.Fatalf("diagnostics = %#v, want invalid depth", diagnostics)
}
return
}
if len(diagnostics) != 0 || len(specs) != 1 || specs[0].depth != test.want {
t.Fatalf("specs, diagnostics = %#v, %#v; want depth %d", specs, diagnostics, test.want)
}
})
}
specs, diagnostics := parseImportSpecsYAML(t, " - dir: []\n depth: 1\n")
if len(diagnostics) != 0 {
t.Fatalf("syntax-only parse diagnostics = %#v", diagnostics)
}
_, diagnostics = resolveImportPolicy(specs[0], importDefaults{})
if len(diagnostics) != 1 || diagnostics[0].Path.String() != "/configkit/imports/0/depth" {
t.Fatalf("policy diagnostics = %#v, want depth applicability error", diagnostics)
}
recursive := true
policy, diagnostics := resolveImportPolicy(specs[0], importDefaults{recursive: &recursive})
if len(diagnostics) != 0 || !policy.recursive || policy.depth != 1 || !policy.depthSet {
t.Fatalf("application-default policy, diagnostics = %#v, %#v", policy, diagnostics)
}
}
func TestResolveImportPolicyAppliesDefaultsIndependently(t *testing.T) {
t.Parallel()
omittedSpecs, diagnostics := parseImportSpecsYAML(t, " - dir: fragments\n")
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
omitted, diagnostics := resolveImportPolicy(omittedSpecs[0], importDefaults{})
if len(diagnostics) != 0 || omitted.required || omitted.recursive || !omitted.follow || !omitted.hidden || omitted.pattern != nil {
t.Fatalf("library defaults = %#v, diagnostics = %#v", omitted, diagnostics)
}
specs, diagnostics := parseImportSpecsYAML(t, `
- dir: fragments
required: false
recursive: false
follow: false
hidden: false
pattern: []
`)
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
appRequired, appRecursive, appFollow, appHidden := true, true, true, true
defaults := importDefaults{
required: &appRequired,
recursive: &appRecursive,
follow: &appFollow,
hidden: &appHidden,
pattern: []string{"*.yaml"},
}
policy, diagnostics := resolveImportPolicy(specs[0], defaults)
if len(diagnostics) != 0 {
t.Fatalf("policy diagnostics = %#v", diagnostics)
}
if policy.required || policy.recursive || policy.follow || policy.hidden || policy.pattern == nil || len(policy.pattern) != 0 {
t.Fatalf("explicit values did not override application defaults: %#v", policy)
}
pathSpecs, diagnostics := parseImportSpecsYAML(t, " - path: file.yaml\n")
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
pathPolicy, diagnostics := resolveImportPolicy(pathSpecs[0], defaults)
if len(diagnostics) != 0 || pathPolicy.required != true || pathPolicy.recursive || pathPolicy.pattern != nil {
t.Fatalf("path policy applied unsupported defaults: %#v, %#v", pathPolicy, diagnostics)
}
defaulted, diagnostics := resolveImportPolicy(omittedSpecs[0], defaults)
if len(diagnostics) != 0 || len(defaulted.pattern) != 1 || defaulted.pattern[0] != "*.yaml" {
t.Fatalf("directory pattern default = %#v, diagnostics = %#v", defaulted.pattern, diagnostics)
}
snapshot := snapshotImportDefaults(defaults)
*defaults.required = false
defaults.pattern[0] = "*.toml"
if snapshot.required == nil || !*snapshot.required || snapshot.pattern[0] != "*.yaml" {
t.Fatalf("snapshot shares mutable inputs: %#v", snapshot)
}
empty := snapshotImportDefaults(importDefaults{pattern: []string{}})
if empty.pattern == nil || len(empty.pattern) != 0 {
t.Fatalf("snapshot lost explicit empty pattern: %#v", empty)
}
if snapshotImportDefaults(importDefaults{}).pattern != nil {
t.Fatal("snapshot changed omitted pattern into explicit empty")
}
}
func TestInvalidImportDefaultPatternsFailBeforeReadingSource(t *testing.T) {
t.Parallel()
reader := &countingImportSpecReader{Reader: strings.NewReader("not: read\n")}
_, err := Load(Reader("invalid-default-pattern.yml", reader), testSecretBinding(), Options[struct{}]{
importDefaults: importDefaults{pattern: []string{"["}},
})
var loadErr *LoadError
if !errors.As(err, &loadErr) || len(loadErr.Diagnostics()) == 0 || loadErr.Diagnostics()[0].Code != CodeOptionsInvalid {
t.Fatalf("load error = %T %v, want invalid-options LoadError", err, err)
}
if reader.reads != 0 {
t.Fatalf("reader consumed %d bytes before invalid defaults failed", reader.reads)
}
}
func parseImportSpecsYAML(t *testing.T, declarations string) ([]importDeclarationSpec, []Diagnostic) {
t.Helper()
parsed, diagnostics := parseSource(Bytes("import specification fixture", []byte("configkit:\n imports:\n"+declarations)), "source-1", defaultParseLimits())
if len(diagnostics) != 0 {
return nil, diagnostics
}
index := documentEntryIndex(parsed.document.Mapping, directiveKey)
if index < 0 {
t.Fatal("missing root directive in test fixture")
}
return parseImportSpecificationDirective(parsed.document.Mapping[index].Value, Keys(directiveKey))
}
type countingImportSpecReader struct {
io.Reader
reads int
}
func (reader *countingImportSpecReader) Read(buffer []byte) (int, error) {
read, err := reader.Reader.Read(buffer)
reader.reads += read
return read, err
}

View File

@@ -30,6 +30,7 @@ type Options[T any] struct {
Filesystem *FilesystemContext Filesystem *FilesystemContext
Overrides []Override Overrides []Override
SourceVersionMode sourceVersionMode SourceVersionMode sourceVersionMode
importDefaults importDefaults
destination Path destination Path
placementMode placementMode placementMode placementMode
sequencePolicy sequencePolicy sequencePolicy sequencePolicy

View File

@@ -8,7 +8,8 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D
if len(supplied) == 1 { if len(supplied) == 1 {
options = supplied[0] options = supplied[0]
} }
var diagnostics []Diagnostic options.importDefaults = snapshotImportDefaults(options.importDefaults)
diagnostics := validateImportDefaults(options.importDefaults)
if options.placementMode != placementModeMapping && options.placementMode != placementModeSequenceItem { if options.placementMode != placementModeMapping && options.placementMode != placementModeSequenceItem {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid"))
} }