Define private import defaults and declaration parsing
This commit is contained in:
425
internal/engine/import_spec.go
Normal file
425
internal/engine/import_spec.go
Normal 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
|
||||
}
|
||||
243
internal/engine/import_spec_test.go
Normal file
243
internal/engine/import_spec_test.go
Normal 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
|
||||
}
|
||||
@@ -30,6 +30,7 @@ type Options[T any] struct {
|
||||
Filesystem *FilesystemContext
|
||||
Overrides []Override
|
||||
SourceVersionMode sourceVersionMode
|
||||
importDefaults importDefaults
|
||||
destination Path
|
||||
placementMode placementMode
|
||||
sequencePolicy sequencePolicy
|
||||
|
||||
@@ -8,7 +8,8 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D
|
||||
if len(supplied) == 1 {
|
||||
options = supplied[0]
|
||||
}
|
||||
var diagnostics []Diagnostic
|
||||
options.importDefaults = snapshotImportDefaults(options.importDefaults)
|
||||
diagnostics := validateImportDefaults(options.importDefaults)
|
||||
if options.placementMode != placementModeMapping && options.placementMode != placementModeSequenceItem {
|
||||
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user