Files
ConfigKit/internal/engine/import_spec.go

457 lines
15 KiB
Go

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
selectorSites []importFieldSite
glob string
globSegments []string
required *bool
recursive *bool
follow *bool
hidden *bool
pattern []string
patternSet bool
exclude []string
depth int
depthSet bool
destination Path
placement placementMode
fieldSites map[string]importFieldSite
}
// ImportDefaults transports root-owned load defaults across the facade
// boundary. Load options snapshot this value before source acquisition.
type ImportDefaults struct {
// Required controls absence handling when non-nil.
Required *bool
// Follow controls symlink following when non-nil.
Follow *bool
// Hidden controls hidden-entry discovery when non-nil.
Hidden *bool
// Recursive controls descendant directory traversal when non-nil.
Recursive *bool
// Pattern filters directory-search filenames; nil and empty differ.
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) {
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
}
func parseImportDeclarations(node *document.Node, declarationPath Path) ([]importDeclarationSpec, []Diagnostic) {
if node == nil || node.Kind != document.Sequence {
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "imports must be a sequence", node)}
}
var specs []importDeclarationSpec
var diagnostics []Diagnostic
for index, declarationNode := range node.Sequence {
spec, issues := parseImportDeclarationSpec(declarationNode, declarationPath.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 diagnostic := parseImportBooleanAssignment(&spec, entry.Key, entry.Value, fieldPath); diagnostic != nil {
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
}
case "pattern":
if diagnostic := parseImportWildcardAssignment(&spec, entry.Key, entry.Value, fieldPath); diagnostic != nil {
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
}
case "exclude":
if diagnostic := parseImportWildcardAssignment(&spec, entry.Key, entry.Value, fieldPath); diagnostic != nil {
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
}
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":
placement, diagnostic := parseImportPlacement(entry.Value, fieldPath)
if diagnostic != nil {
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
}
spec.placement = placement
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)}
if diagnostic := parseImportSelectorValue(&spec, selectorEntry.Key, selectorEntry.Value, selectorPath); diagnostic != nil {
return importDeclarationSpec{}, []Diagnostic{*diagnostic}
}
return spec, nil
}
func parseImportBooleanAssignment(spec *importDeclarationSpec, field string, node *document.Node, fieldPath Path) *Diagnostic {
if node.Kind != document.Boolean {
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, field+" must be a boolean", node)
return &diagnostic
}
value := node.Text == "true"
switch field {
case "required":
spec.required = &value
case "recursive":
spec.recursive = &value
case "follow":
spec.follow = &value
case "hidden":
spec.hidden = &value
}
return nil
}
func parseImportWildcardAssignment(spec *importDeclarationSpec, field string, node *document.Node, fieldPath Path) *Diagnostic {
values, origins, ok, badIndex := parseStringSet(node)
if !ok {
diagnostic := stringSetDiagnostic(node, fieldPath, field, badIndex)
return &diagnostic
}
if badIndex, ok := invalidWildcard(values); ok {
badPath, badNode := listMemberSite(node, fieldPath, origins[badIndex])
diagnostic := nodeImportDiagnostic(CodeImportInvalid, badPath, field+" contains an invalid wildcard expression", badNode)
return &diagnostic
}
switch field {
case "pattern":
spec.pattern, spec.patternSet = values, true
case "exclude":
spec.exclude = values
}
return nil
}
func parseImportPlacement(node *document.Node, fieldPath Path) (placementMode, *Diagnostic) {
if node.Kind != document.String {
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", node)
return placementModeMapping, &diagnostic
}
switch node.Text {
case "mapping":
return placementModeMapping, nil
case "sequence-item":
return placementModeSequenceItem, nil
default:
diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", node)
return placementModeMapping, &diagnostic
}
}
func parseImportSelectorValue(spec *importDeclarationSpec, field string, node *document.Node, selectorPath Path) *Diagnostic {
switch spec.selector {
case importSelectorPath, importSelectorDirectory:
values, origins, ok, badIndex := parseStringSet(node)
if !ok {
diagnostic := stringSetDiagnostic(node, selectorPath, field, badIndex)
return &diagnostic
}
for index, value := range values {
if !validImportPath(value) {
badPath, badNode := listMemberSite(node, selectorPath, origins[index])
diagnostic := nodeImportDiagnostic(CodeImportInvalid, badPath, field+" contains an invalid source path", badNode)
return &diagnostic
}
badPath, badNode := listMemberSite(node, selectorPath, origins[index])
spec.selectorSites = append(spec.selectorSites, importFieldSite{path: badPath.clone(), location: publicLocation(badNode.Location)})
}
spec.paths = values
case importSelectorGlob:
if node.Kind != document.String || !validImportPath(node.Text) {
diagnostic := nodeImportDiagnostic(CodeImportInvalid, selectorPath, "glob must be a valid nonempty path pattern string", node)
return &diagnostic
}
segments, ok := validGlobPattern(node.Text)
if !ok {
diagnostic := nodeImportDiagnostic(CodeImportInvalid, selectorPath, "glob pattern is invalid", node)
return &diagnostic
}
spec.glob, spec.globSegments = node.Text, segments
}
return 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 {
value = maxInt
continue // Saturate the bound, but validate every remaining character.
}
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
}