Scope configuration controls to source roots
This commit is contained in:
@@ -66,6 +66,8 @@ development-guide navigation with the final documentation stage.
|
|||||||
|
|
||||||
## Stage 2 — Implement source-root control extraction and guards
|
## Stage 2 — Implement source-root control extraction and guards
|
||||||
|
|
||||||
|
**Status:** Complete. Focused document-root control and extraction tests pass.
|
||||||
|
|
||||||
**Owners:** [directives.go](../../internal/engine/directives.go), its callers in
|
**Owners:** [directives.go](../../internal/engine/directives.go), its callers in
|
||||||
[acquisition.go](../../internal/engine/acquisition.go) and
|
[acquisition.go](../../internal/engine/acquisition.go) and
|
||||||
[selected_overlay.go](../../internal/engine/selected_overlay.go), and focused
|
[selected_overlay.go](../../internal/engine/selected_overlay.go), and focused
|
||||||
|
|||||||
93
internal/engine/directive_scope_test.go
Normal file
93
internal/engine/directive_scope_test.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDocumentRootControlScope(t *testing.T) {
|
||||||
|
root := parseRootDirectiveDocument(t, `config:
|
||||||
|
imports: [{path: fragment.yaml}]
|
||||||
|
sequences: []
|
||||||
|
service:
|
||||||
|
config: {imports: ordinary, sequences: ordinary}
|
||||||
|
records:
|
||||||
|
- config: {imports: ordinary}
|
||||||
|
configkit: {imports: [{path: absent.yaml}]}
|
||||||
|
_configkit: ordinary
|
||||||
|
`)
|
||||||
|
extracted, diagnostics := extractRootDirectives(root, rootDirectiveContextSource)
|
||||||
|
if len(diagnostics) != 0 || len(extracted.imports) != 1 {
|
||||||
|
t.Fatalf("extraction = %#v, diagnostics = %#v", extracted, diagnostics)
|
||||||
|
}
|
||||||
|
if len(root.Mapping) != 5 || len(extracted.body.Mapping) != 4 {
|
||||||
|
t.Fatalf("source mutated or payload lost: root=%#v body=%#v", root.Mapping, extracted.body.Mapping)
|
||||||
|
}
|
||||||
|
for _, key := range []string{"service", "records", "configkit", "_configkit"} {
|
||||||
|
got := documentEntryIndex(extracted.body.Mapping, key)
|
||||||
|
if got < 0 {
|
||||||
|
t.Errorf("payload missing %q", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
original := documentEntryIndex(root.Mapping, key)
|
||||||
|
if !reflect.DeepEqual(extracted.body.Mapping[got].Value, root.Mapping[original].Value) {
|
||||||
|
t.Errorf("payload for %q changed", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, guard := range []func(*document.Node) []Diagnostic{
|
||||||
|
func(node *document.Node) []Diagnostic { return disabledDirectiveDiagnostics(node, Path{}) },
|
||||||
|
disabledRootImportDiagnostics,
|
||||||
|
func(node *document.Node) []Diagnostic { return overlayDirectiveDiagnostics(node, Path{}, true) },
|
||||||
|
} {
|
||||||
|
if got := guard(extracted.body); len(got) != 0 {
|
||||||
|
t.Errorf("nested application data rejected: %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentRootControlValidation(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
code Code
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{"not a mapping", "config: wrong\n", CodeImportInvalid, "/config"},
|
||||||
|
{"unknown field", "config: {unknown: true}\n", CodeImportInvalid, "/config/unknown"},
|
||||||
|
{"bad imports", "config: {imports: wrong}\n", CodeImportInvalid, "/config/imports"},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, test.input), rootDirectiveContextSource)
|
||||||
|
if len(diagnostics) == 0 || diagnostics[0].Code != test.code || diagnostics[0].Path.String() != test.path {
|
||||||
|
t.Fatalf("diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentRootControlGuards(t *testing.T) {
|
||||||
|
imports := parseRootDirectiveDocument(t, "config: {imports: [{path: fragment.yaml}]}\n")
|
||||||
|
for _, got := range [][]Diagnostic{disabledDirectiveDiagnostics(imports, Path{}), disabledRootImportDiagnostics(imports)} {
|
||||||
|
if len(got) != 1 || got[0].Code != CodeImportDisabled || got[0].Path.String() != "/config" {
|
||||||
|
t.Fatalf("disabled diagnostics = %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := overlayDirectiveDiagnostics(imports, Path{}, true); len(got) != 1 || got[0].Code != CodeOverlayDirective {
|
||||||
|
t.Fatalf("overlay imports diagnostics = %#v", got)
|
||||||
|
}
|
||||||
|
sequences := parseRootDirectiveDocument(t, "config: {sequences: []}\n")
|
||||||
|
if got := disabledRootImportDiagnostics(sequences); len(got) != 0 {
|
||||||
|
t.Fatalf("sequences with imports disabled = %#v", got)
|
||||||
|
}
|
||||||
|
if got := disabledDirectiveDiagnostics(sequences, Path{}); len(got) != 1 || got[0].Code != CodeImportDisabled {
|
||||||
|
t.Fatalf("imports-only guard = %#v", got)
|
||||||
|
}
|
||||||
|
if got := overlayDirectiveDiagnostics(sequences, Path{}, true); len(got) != 0 {
|
||||||
|
t.Fatalf("overlay sequences diagnostics = %#v", got)
|
||||||
|
}
|
||||||
|
if _, got := extractRootDirectives(sequences, rootDirectiveContextImportsOnly); len(got) == 0 || got[0].Code != CodeSequenceInvalid {
|
||||||
|
t.Fatalf("imports-only extraction diagnostics = %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"go.maximumdirect.net/configkit/internal/document"
|
"go.maximumdirect.net/configkit/internal/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
const directiveKey = "configkit"
|
const directiveKey = "config"
|
||||||
|
|
||||||
type rootDirectiveContext uint8
|
type rootDirectiveContext uint8
|
||||||
|
|
||||||
@@ -51,14 +51,8 @@ func extractRootDirectives(root *document.Node, context rootDirectiveContext) (r
|
|||||||
if directiveIndex >= 0 {
|
if directiveIndex >= 0 {
|
||||||
extracted.body.Mapping = append(extracted.body.Mapping[:directiveIndex], extracted.body.Mapping[directiveIndex+1:]...)
|
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
|
return extracted, nil
|
||||||
}
|
}
|
||||||
if diagnostics := nestedDirectiveDiagnostics(body, Path{}); len(diagnostics) != 0 {
|
|
||||||
return rootDirectiveExtraction{}, diagnostics
|
|
||||||
}
|
|
||||||
return rootDirectiveExtraction{body: body}, nil
|
return rootDirectiveExtraction{body: body}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,28 +99,6 @@ func parseRootDirectiveBlock(node *document.Node, directivePath Path, context ro
|
|||||||
return extracted, nil
|
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 {
|
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
|
||||||
return fragmentVersionClaimProjected(root, rule, -1, nil)
|
return fragmentVersionClaimProjected(root, rule, -1, nil)
|
||||||
}
|
}
|
||||||
@@ -230,15 +202,6 @@ func overlayDirectiveDiagnostics(node *document.Node, current Path, allowSequenc
|
|||||||
Message: "overlays may contain only root collection sequence declarations", Primary: &location,
|
Message: "overlays may contain only root collection sequence declarations", Primary: &location,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
if diagnostics := overlayDirectiveDiagnostics(entry.Value, childPath, sequencesAllowed); len(diagnostics) != 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if node.Kind == document.Sequence {
|
|
||||||
for index, child := range node.Sequence {
|
|
||||||
if diagnostics := overlayDirectiveDiagnostics(child, current.Index(index), sequencesAllowed); len(diagnostics) != 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -256,11 +219,6 @@ func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
|
|||||||
Message: "configuration directives require imports to be enabled", Primary: &location,
|
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
|
return diagnostics
|
||||||
@@ -291,39 +249,6 @@ func disabledRootImportDiagnostics(node *document.Node) []Diagnostic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
if entry.Key == directiveKey {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if diagnostic := disabledNestedDirectiveDiagnostics(entry.Value, Path{}.Key(entry.Key)); len(diagnostic) != 0 {
|
|
||||||
return diagnostic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func disabledNestedDirectiveDiagnostics(node *document.Node, path Path) []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}
|
|
||||||
return []Diagnostic{{
|
|
||||||
Code: CodeImportDisabled, Severity: SeverityError, Path: childPath,
|
|
||||||
Message: "configuration directives require imports to be enabled", Primary: &location,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
if diagnostics := disabledNestedDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if node.Kind == document.Sequence {
|
|
||||||
for index, child := range node.Sequence {
|
|
||||||
if diagnostics := disabledNestedDirectiveDiagnostics(child, path.Index(index)); len(diagnostics) != 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func TestParseSequenceDeclarationNormalizesEquivalentFieldSets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractRootDirectivesCombinesAndRemovesImportsAndSequences(t *testing.T) {
|
func TestExtractRootDirectivesCombinesAndRemovesImportsAndSequences(t *testing.T) {
|
||||||
extracted := extractSequenceDeclarationsYAML(t, `configkit:
|
extracted := extractSequenceDeclarationsYAML(t, `config:
|
||||||
imports: [{path: fragments/base.yaml}]
|
imports: [{path: fragments/base.yaml}]
|
||||||
sequences: []
|
sequences: []
|
||||||
value: true
|
value: true
|
||||||
@@ -78,15 +78,15 @@ value: true
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractRootDirectivesAllowsSequencesButRejectsOverlayImports(t *testing.T) {
|
func TestExtractRootDirectivesAllowsSequencesButRejectsOverlayImports(t *testing.T) {
|
||||||
_, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, `configkit:
|
_, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, `config:
|
||||||
imports: [{path: fragments/base.yaml}]
|
imports: [{path: fragments/base.yaml}]
|
||||||
sequences: [{destination: [prompts], key: id}]
|
sequences: [{destination: [prompts], key: id}]
|
||||||
`), rootDirectiveContextOverlay)
|
`), rootDirectiveContextOverlay)
|
||||||
if len(diagnostics) != 1 || diagnostics[0].Code != CodeOverlayDirective || diagnostics[0].Path.String() != "/configkit/imports" {
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeOverlayDirective || diagnostics[0].Path.String() != "/config/imports" {
|
||||||
t.Fatalf("overlay diagnostics = %#v, want imports-only rejection", diagnostics)
|
t.Fatalf("overlay diagnostics = %#v, want imports-only rejection", diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
extracted, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, `configkit:
|
extracted, diagnostics := extractRootDirectives(parseRootDirectiveDocument(t, `config:
|
||||||
sequences: [{destination: [prompts], key: id}]
|
sequences: [{destination: [prompts], key: id}]
|
||||||
`), rootDirectiveContextOverlay)
|
`), rootDirectiveContextOverlay)
|
||||||
if len(diagnostics) != 0 || len(extracted.sequences) != 1 {
|
if len(diagnostics) != 0 || len(extracted.sequences) != 1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user