Add relative destinations to import declarations
This commit is contained in:
@@ -91,6 +91,27 @@ func parseImportDeclaration(node *document.Node, declarationPath Path) (importDe
|
||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "optional must be a boolean", entry.Value)}
|
||||
}
|
||||
declaration.optional = entry.Value.Text == "true"
|
||||
case "destination":
|
||||
if entry.Value.Kind != document.Sequence {
|
||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must be a sequence of strings", entry.Value)}
|
||||
}
|
||||
relative := Path{segments: make([]pathSegment, 0, len(entry.Value.Sequence))}
|
||||
for index, item := range entry.Value.Sequence {
|
||||
memberPath := fieldPath.Index(index)
|
||||
if item.Kind != document.String {
|
||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be strings", item)}
|
||||
}
|
||||
member := Path{segments: []pathSegment{{kind: pathSegmentKey, key: item.Text}}}
|
||||
if _, err := normalizeDestination(member); err != nil {
|
||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be non-reserved mapping keys", item)}
|
||||
}
|
||||
relative.segments = append(relative.segments, pathSegment{kind: pathSegmentKey, key: item.Text})
|
||||
}
|
||||
normalized, err := normalizeDestination(relative)
|
||||
if err != nil {
|
||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must contain only non-reserved mapping keys", entry.Value)}
|
||||
}
|
||||
declaration.destination = normalized
|
||||
default:
|
||||
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,105 @@ func TestLoadImportDirectiveValidationAndTemporaryGlobGate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseImportDestinationUsesLiteralStringSegments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
destination string
|
||||
want Path
|
||||
}{
|
||||
{name: "omitted"},
|
||||
{name: "empty sequence", destination: " destination: []\n"},
|
||||
{
|
||||
name: "literal keys",
|
||||
destination: " destination: ['', '.', '..', 'a/b', '~', '0', configkit2, myextends]\n",
|
||||
want: Keys("", ".", "..", "a/b", "~", "0", "configkit2", "myextends"),
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
body := "configkit:\n imports:\n - path: child.yml\n" + test.destination
|
||||
declarations := parseImportDeclarationsYAML(t, body)
|
||||
if len(declarations) != 1 {
|
||||
t.Fatalf("declarations = %#v", declarations)
|
||||
}
|
||||
got := declarations[0].destination
|
||||
if got.String() != test.want.String() {
|
||||
t.Fatalf("destination = %q, want %q", got.String(), test.want.String())
|
||||
}
|
||||
for index, segment := range got.segments {
|
||||
if segment.kind != pathSegmentKey {
|
||||
t.Errorf("segment %d kind = %v, want mapping key", index, segment.kind)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseImportDestinationRejectsInvalidShapesAndMembers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
value string
|
||||
wantPath string
|
||||
badMember string
|
||||
}{
|
||||
{name: "null", value: "null", wantPath: "/configkit/imports/0/destination", badMember: "null"},
|
||||
{name: "scalar string", value: "prompts", wantPath: "/configkit/imports/0/destination", badMember: "prompts"},
|
||||
{name: "number", value: "3", wantPath: "/configkit/imports/0/destination", badMember: "3"},
|
||||
{name: "non-string number", value: "[valid, 7]", wantPath: "/configkit/imports/0/destination/1", badMember: "7"},
|
||||
{name: "non-string null", value: "[valid, null]", wantPath: "/configkit/imports/0/destination/1", badMember: "null"},
|
||||
{name: "non-string boolean", value: "[valid, true]", wantPath: "/configkit/imports/0/destination/1", badMember: "true"},
|
||||
{name: "non-string mapping", value: "[valid, {key: value}]", wantPath: "/configkit/imports/0/destination/1", badMember: "{key"},
|
||||
{name: "reserved configkit", value: "[valid, configkit]", wantPath: "/configkit/imports/0/destination/1", badMember: "configkit"},
|
||||
{name: "reserved extends", value: "[valid, extends]", wantPath: "/configkit/imports/0/destination/1", badMember: "extends"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
body := "configkit:\n imports:\n - path: child.yml\n destination: " + test.value + "\n"
|
||||
parsed, diagnostics := parseImportDeclarationYAML(body)
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportInvalid || diagnostics[0].Path.String() != test.wantPath {
|
||||
t.Fatalf("parse = %#v, want import.invalid at %q", diagnostics, test.wantPath)
|
||||
}
|
||||
line := strings.Split(body, "\n")[3]
|
||||
wantColumn := strings.Index(line, test.badMember) + 1
|
||||
if diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-1" || diagnostics[0].Primary.Line != 4 || diagnostics[0].Primary.Column != wantColumn {
|
||||
t.Fatalf("invalid destination location = %#v, want line 4 column %d (parsed %v)", diagnostics[0].Primary, wantColumn, parsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportDestinationValidationPrecedesOptionalSuppression(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
body string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "optional missing path",
|
||||
body: "configkit:\n imports:\n - path: absent.yml\n optional: true\n destination: null\n",
|
||||
path: "/configkit/imports/0/destination",
|
||||
},
|
||||
{
|
||||
name: "optional unmatched glob",
|
||||
body: "configkit:\n imports:\n - glob: absent/*.yml\n optional: true\n destination: [extends]\n",
|
||||
path: "/configkit/imports/0/destination/0",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", test.body)
|
||||
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportInvalid)
|
||||
if diagnostics := loadError.Diagnostics(); diagnostics[0].Path.String() != test.path {
|
||||
t.Fatalf("diagnostic path = %q, want %q", diagnostics[0].Path.String(), test.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadImportMissingContainmentAndRegularFileRules(t *testing.T) {
|
||||
t.Run("optional missing", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
@@ -486,3 +585,21 @@ func parseImportMain(t *testing.T, filename string) parsedSource {
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseImportDeclarationsYAML(t *testing.T, input string) []importDeclaration {
|
||||
t.Helper()
|
||||
declarations, diagnostics := parseImportDeclarationYAML(input)
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("import parse diagnostics = %#v", diagnostics)
|
||||
}
|
||||
return declarations
|
||||
}
|
||||
|
||||
func parseImportDeclarationYAML(input string) ([]importDeclaration, []Diagnostic) {
|
||||
parsed, diagnostics := parseSource(Bytes("import fixture", []byte(input)), "source-1", defaultParseLimits())
|
||||
if len(diagnostics) != 0 {
|
||||
return nil, diagnostics
|
||||
}
|
||||
_, declarations, diagnostics := splitImportDirectives(parsed.document)
|
||||
return declarations, diagnostics
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user