Files
ConfigKit/internal/engine/directives.go

267 lines
9.2 KiB
Go

package engine
import (
"path"
"strings"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
const directiveKey = "configkit"
type rootDirectiveContext uint8
const (
rootDirectiveContextImportsOnly rootDirectiveContext = iota + 1
rootDirectiveContextSource
rootDirectiveContextOverlay
)
type rootDirectiveExtraction struct {
body *document.Node
imports []importDeclarationSpec
sequences []sequenceDeclarationSpec
importsPresent bool
sequencesPresent bool
}
func splitImportDirectives(root *document.Node) (*document.Node, []importDeclarationSpec, []Diagnostic) {
extracted, diagnostics := extractRootDirectives(root, rootDirectiveContextImportsOnly)
if len(diagnostics) != 0 {
return nil, nil, diagnostics
}
return extracted.body, extracted.imports, nil
}
func extractRootDirectives(root *document.Node, context rootDirectiveContext) (rootDirectiveExtraction, []Diagnostic) {
if root == nil {
return rootDirectiveExtraction{}, nil
}
body := document.Clone(root)
directiveIndex := -1
for index, entry := range body.Mapping {
if entry.Key != directiveKey {
continue
}
directiveIndex = index
extracted, diagnostics := parseRootDirectiveBlock(entry.Value, Keys(directiveKey), context)
if len(diagnostics) != 0 {
return rootDirectiveExtraction{}, diagnostics
}
extracted.body = body
if directiveIndex >= 0 {
extracted.body.Mapping = append(extracted.body.Mapping[:directiveIndex], extracted.body.Mapping[directiveIndex+1:]...)
}
if diagnostics := nestedDirectiveDiagnostics(extracted.body, Path{}); len(diagnostics) != 0 {
return rootDirectiveExtraction{}, diagnostics
}
return extracted, nil
}
if diagnostics := nestedDirectiveDiagnostics(body, Path{}); len(diagnostics) != 0 {
return rootDirectiveExtraction{}, diagnostics
}
return rootDirectiveExtraction{body: body}, nil
}
func parseRootDirectiveBlock(node *document.Node, directivePath Path, context rootDirectiveContext) (rootDirectiveExtraction, []Diagnostic) {
if node == nil || node.Kind != document.Mapping {
return rootDirectiveExtraction{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, directivePath, "configuration directive must be a mapping", node)}
}
var extracted rootDirectiveExtraction
var diagnostics []Diagnostic
for _, entry := range node.Mapping {
fieldPath := directivePath.Key(entry.Key)
switch entry.Key {
case "imports":
extracted.importsPresent = true
if context == rootDirectiveContextOverlay {
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeOverlayDirective, fieldPath, "overlays cannot contain imports", entry.Value))
continue
}
if context != rootDirectiveContextImportsOnly && context != rootDirectiveContextSource {
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeImportInvalid, fieldPath, "directive extraction context is invalid", entry.Value))
continue
}
parsed, issues := parseImportDeclarations(entry.Value, fieldPath)
extracted.imports = append(extracted.imports, parsed...)
diagnostics = append(diagnostics, issues...)
case "sequences":
extracted.sequencesPresent = true
if context == rootDirectiveContextImportsOnly {
diagnostics = append(diagnostics, sequenceDeclarationDiagnostic(fieldPath, "sequence declarations are not enabled in this extraction context", entry.Value))
continue
}
if context != rootDirectiveContextSource && context != rootDirectiveContextOverlay {
diagnostics = append(diagnostics, sequenceDeclarationDiagnostic(fieldPath, "directive extraction context is invalid", entry.Value))
continue
}
parsed, issues := parseSequenceDeclarationDirective(entry.Value, fieldPath)
extracted.sequences = append(extracted.sequences, parsed...)
diagnostics = append(diagnostics, issues...)
default:
diagnostics = append(diagnostics, nodeImportDiagnostic(CodeImportInvalid, fieldPath, "configuration directive field is not recognized", entry.Value))
}
}
if len(diagnostics) != 0 {
return rootDirectiveExtraction{}, canonicalDiagnostics(diagnostics)
}
return extracted, nil
}
func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
if node.Kind == document.Mapping {
for _, entry := range node.Mapping {
childPath := current.Key(entry.Key)
if entry.Key == directiveKey {
location := publicLocation(entry.KeyLocation)
return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}}
}
if diagnostics := nestedDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
return diagnostics
}
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
if diagnostics := nestedDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
return diagnostics
}
}
}
return nil
}
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
return fragmentVersionClaimProjected(root, rule, -1, nil)
}
func fragmentVersionClaimProjected(root *document.Node, rule *VersionRule, sourceIndex int, ranges []sequenceRange) *document.Node {
return fragmentVersionClaimWithIndex(root, rule, sourceIndex, indexSequenceRanges(ranges))
}
func fragmentVersionClaimWithIndex(root *document.Node, rule *VersionRule, sourceIndex int, ranges sequenceRangeIndex) *document.Node {
if rule == nil {
return nil
}
current := root
currentPath := Path{}
if len(rule.Path.segments) == 0 {
return current
}
for _, segment := range rule.Path.segments {
switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
return current
}
index := documentEntryIndex(current.Mapping, segment.key)
if index < 0 {
return nil
}
current = current.Mapping[index].Value
currentPath = currentPath.Key(segment.key)
case document.Sequence:
index, valid := schemaIndex(segment)
if !valid {
return current
}
if assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: currentPath.String()}]; found {
projected, ok := assigned.inputForOutput(index)
if !ok {
return nil
}
index = projected
}
if index < 0 || index >= len(current.Sequence) {
return nil
}
current = current.Sequence[index]
currentPath = currentPath.Index(index)
default:
// A scalar or null at an ancestor replaces the protected subtree.
return current
}
}
return current
}
func validImportPath(value string) bool {
if value == "" || strings.TrimSpace(value) != value || path.IsAbs(value) || strings.HasPrefix(value, "//") {
return false
}
return !hasImportScheme(value)
}
func hasImportScheme(value string) bool {
colon := strings.IndexByte(value, ':')
slash := strings.IndexByte(value, '/')
if colon <= 0 || (slash >= 0 && slash < colon) {
return false
}
for index := 0; index < colon; index++ {
character := value[index]
if index == 0 {
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') {
return false
}
continue
}
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') &&
(character < '0' || character > '9') && character != '+' && character != '-' && character != '.' {
return false
}
}
return true
}
func nodeImportDiagnostic(code Code, path Path, message string, node *document.Node) Diagnostic {
location := publicLocation(node.Location)
return Diagnostic{Code: code, Severity: SeverityError, Path: path, Message: message, Primary: &location}
}
func overlayDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
if node.Kind == document.Mapping {
for _, entry := range node.Mapping {
childPath := current.Key(entry.Key)
if entry.Key == directiveKey {
location := publicLocation(entry.KeyLocation)
return []Diagnostic{{
Code: CodeOverlayDirective, Severity: SeverityError, Path: childPath,
Message: "overlays cannot contain configuration directives", Primary: &location,
}}
}
if diagnostics := overlayDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
return diagnostics
}
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
if diagnostics := overlayDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
return diagnostics
}
}
}
return nil
}
func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
var diagnostics []Diagnostic
if node.Kind == document.Mapping {
for _, entry := range node.Mapping {
childPath := path.Key(entry.Key)
if entry.Key == directiveKey {
location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column}
diagnostics = append(diagnostics, Diagnostic{
Code: CodeImportDisabled, Severity: SeverityError, Path: childPath,
Message: "configuration directives require imports to be enabled", Primary: &location,
})
}
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(entry.Value, childPath)...)
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(child, path.Index(index))...)
}
}
return diagnostics
}