Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd730d5c99 |
@@ -7,6 +7,8 @@ import (
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
const directiveKey = "configkit"
|
||||
|
||||
type importDeclaration struct {
|
||||
path string
|
||||
glob string
|
||||
@@ -19,11 +21,11 @@ func splitImportDirectives(root *document.Node) (*document.Node, []importDeclara
|
||||
var declarations []importDeclaration
|
||||
directiveIndex := -1
|
||||
for index, entry := range body.Mapping {
|
||||
if entry.Key != "_configkit" {
|
||||
if entry.Key != directiveKey {
|
||||
continue
|
||||
}
|
||||
directiveIndex = index
|
||||
parsed, diagnostics := parseImportDirective(entry.Value, Keys("_configkit"))
|
||||
parsed, diagnostics := parseImportDirective(entry.Value, Keys(directiveKey))
|
||||
if len(diagnostics) != 0 {
|
||||
return nil, nil, diagnostics
|
||||
}
|
||||
@@ -102,7 +104,7 @@ 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 == "_configkit" {
|
||||
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}}
|
||||
}
|
||||
@@ -183,7 +185,7 @@ func profileDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic
|
||||
if node.Kind == document.Mapping {
|
||||
for _, entry := range node.Mapping {
|
||||
childPath := current.Key(entry.Key)
|
||||
if entry.Key == "_configkit" {
|
||||
if entry.Key == directiveKey {
|
||||
location := publicLocation(entry.KeyLocation)
|
||||
return []Diagnostic{{
|
||||
Code: CodeProfileDirective, Severity: SeverityError, Path: childPath,
|
||||
@@ -209,7 +211,7 @@ func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
|
||||
if node.Kind == document.Mapping {
|
||||
for _, entry := range node.Mapping {
|
||||
childPath := path.Key(entry.Key)
|
||||
if entry.Key == "_configkit" {
|
||||
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,
|
||||
|
||||
@@ -61,7 +61,7 @@ semantic validator, explicit path and glob imports, one selected profile, and
|
||||
filesystem context. It also accepts explicit CLI override records, static secret
|
||||
tags, and a dynamic path predicate. `NoSource` rejects imports, active profiles,
|
||||
and filesystem context as invalid combinations but accepts overrides. Input
|
||||
containing a reserved `_configkit` mapping key is rejected while imports are
|
||||
containing a reserved `configkit` mapping key is rejected while imports are
|
||||
disabled.
|
||||
|
||||
Every source, options, schema, version, defaults, decode, resolution, or validation
|
||||
@@ -419,7 +419,7 @@ origin data; history retains the displaced definitions and identifies the parent
|
||||
replacement. Surviving profile paths have profile origins, while recursively shared
|
||||
mapping containers retain their contributing locations.
|
||||
|
||||
A profile may not contain `_configkit` anywhere, contribute the configured schema
|
||||
A profile may not contain `configkit` anywhere, contribute the configured schema
|
||||
version, or replace an ancestor of that version with a scalar or null. These rules
|
||||
apply even if the supplied version text would be unchanged. Profiles cannot mask a
|
||||
base parse, composition, or version failure because those checks run first.
|
||||
@@ -452,7 +452,7 @@ All records are detached and validated before any is applied. Root and explicit
|
||||
sequence-index paths are invalid; parsed numeric tokens remain mapping keys. A path
|
||||
may create missing mapping ancestors but cannot traverse an existing scalar, null,
|
||||
or sequence. Duplicate and ancestor/descendant-overlapping records are rejected.
|
||||
Neither paths nor values may create `_configkit`, and no record may overlap the
|
||||
Neither paths nor values may create `configkit`, and no record may overlap the
|
||||
configured schema-version path. Unknown fields and effective type mismatches remain
|
||||
ordinary strict-decoder errors.
|
||||
|
||||
@@ -475,12 +475,18 @@ zero.
|
||||
## Explicit imports
|
||||
|
||||
Imports are opt-in. With `Options.Imports` enabled, a document may reserve its root
|
||||
`_configkit` mapping for an `imports` sequence. ConfigKit removes that mapping before
|
||||
`configkit` mapping for an `imports` sequence. ConfigKit removes that mapping before
|
||||
application decoding. Each sequence item is a mapping with exactly one nonempty
|
||||
string `path` or `glob` plus an optional boolean `optional`; all other shapes and
|
||||
fields are `import.invalid`. A `_configkit` key below the root is invalid. With
|
||||
fields are `import.invalid`. A `configkit` key below the root is invalid. With
|
||||
imports disabled, any occurrence remains `import.disabled`.
|
||||
|
||||
The directive key is `configkit`, not the `_configkit` spelling used in v0.1.0.
|
||||
There is no legacy alias: `_configkit` is ordinary application data, subject to
|
||||
normal schema validation, and does not trigger imports. When upgrading, rename
|
||||
directive keys in root files and imported fragments, and rename any application
|
||||
fields previously named `configkit` to avoid the newly reserved key.
|
||||
|
||||
Path declarations use slash-separated relative paths. Absolute paths and network
|
||||
URLs are invalid. Each path resolves from its declaring file's canonical directory,
|
||||
and files are traversed depth-first in declaration order: a file contributes its own
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Base Policy Version:** 0.1.0
|
||||
|
||||
**Last Updated:** 2026-09-06
|
||||
**Last Updated:** 2026-09-07
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -220,5 +220,12 @@ restating or redefining its requirements.
|
||||
|
||||
## Policy Exceptions
|
||||
|
||||
None. Candidate selection, publication, and post-publication verification remain
|
||||
mandatory steps in the [release procedure](../release.md), not policy exceptions.
|
||||
For v0.1.1 only, the maintainer approved renaming the reserved `_configkit` key to
|
||||
`configkit` as an error-correcting point release because there are no current
|
||||
consumers. This intentionally changes the v0.1.0 configuration contract without a
|
||||
minor-version increment. The release note must explain the renamed directive,
|
||||
the lack of a legacy alias, and the newly reserved application key. This exception
|
||||
does not apply to subsequent releases or waive any validation or publication gate.
|
||||
|
||||
Candidate selection, publication, and post-publication verification remain
|
||||
mandatory steps in the [release procedure](../release.md).
|
||||
|
||||
43
docs/releases/v0.1.1.md
Normal file
43
docs/releases/v0.1.1.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# ConfigKit v0.1.1
|
||||
|
||||
This release corrects the spelling of ConfigKit's reserved configuration key.
|
||||
|
||||
## Summary
|
||||
|
||||
The reserved directive key is now `configkit` instead of `_configkit`. Import
|
||||
declarations continue to use the same `imports` sequence and path/glob syntax.
|
||||
See the [configuration contract](../config.md#explicit-imports).
|
||||
|
||||
## Compatibility
|
||||
|
||||
This is an intentional configuration compatibility change, approved as a one-time
|
||||
[patch-release exception](../policy/release.md#policy-exceptions) because there are
|
||||
no current consumers. `_configkit` is no longer a directive or a reserved key; it
|
||||
is ordinary application data and is subject to normal schema validation. There is
|
||||
no legacy alias. `configkit` is now reserved, including in nested mappings, profiles,
|
||||
and override paths and values.
|
||||
|
||||
The Go API, diagnostic codes, Go minimum version, and supported platforms are
|
||||
unchanged. The private YAML dependency remains pinned to
|
||||
`go.yaml.in/yaml/v4 v4.0.0-rc.6`, confirmed as the newest available v4 release
|
||||
during candidate preparation.
|
||||
|
||||
## Upgrade
|
||||
|
||||
Rename `_configkit` directive keys to `configkit` in root configuration files and
|
||||
imported fragments. Rename any application-owned `configkit` fields to avoid the
|
||||
newly reserved key. Update diagnostic-path expectations to the new spelling.
|
||||
Files retaining `_configkit` will not load their declared imports.
|
||||
|
||||
Consumers can select this release with:
|
||||
|
||||
```sh
|
||||
go get gitea.maximumdirect.net/eric/ConfigKit@v0.1.1
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
- Renamed the directive key consistently across imports, profiles, and overrides.
|
||||
- Updated current configuration documentation and the composed consumer fixture.
|
||||
- Updated behavioral tests and added coverage proving the old spelling is not an
|
||||
import alias.
|
||||
@@ -1,4 +1,4 @@
|
||||
_configkit:
|
||||
configkit:
|
||||
imports:
|
||||
- path: artifacts.yml
|
||||
poll_interval: 1m
|
||||
|
||||
@@ -23,7 +23,7 @@ type explanationConcurrency struct {
|
||||
|
||||
func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "config.yml", "_configkit:\n imports: [{path: transcription.yml}]\n")
|
||||
mainPath := writeImportFile(t, directory, "config.yml", "configkit:\n imports: [{path: transcription.yml}]\n")
|
||||
writeImportFile(t, directory, "transcription.yml", "transcription:\n concurrency: 8\n")
|
||||
writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), "transcription:\n concurrency: 1\n")
|
||||
result, err := Load[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{
|
||||
|
||||
10
glob_test.go
10
glob_test.go
@@ -10,13 +10,13 @@ import (
|
||||
|
||||
func TestLoadGlobImportsUseLexicalDeclarationOrderAndNestedDFS(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports:
|
||||
- glob: fragments/*.yml
|
||||
- path: after.yml
|
||||
items: {root: root}
|
||||
`)
|
||||
aPath := writeImportFile(t, directory, "fragments/a.yml", `_configkit:
|
||||
aPath := writeImportFile(t, directory, "fragments/a.yml", `configkit:
|
||||
imports: [{path: nested/child.yml}]
|
||||
items: {alpha: alpha}
|
||||
`)
|
||||
@@ -56,7 +56,7 @@ items: {alpha: alpha}
|
||||
|
||||
func TestLoadGlobImportsSupportEscapesClassesAndExplicitConfD(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports:
|
||||
- glob: 'conf.d/literal\*.yml'
|
||||
- glob: 'conf.d/item-[ab].yml'
|
||||
@@ -126,7 +126,7 @@ func TestLoadGlobImportsPreserveIdentityAndFileRules(t *testing.T) {
|
||||
t.Run("overlapping declarations", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
writeImportFile(t, directory, "conf/value.yml", "items: {value: value}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports:
|
||||
- glob: conf/*.yml
|
||||
- glob: conf/value.*
|
||||
@@ -269,5 +269,5 @@ func writeGlobRoot(t *testing.T, directory, pattern string, optional bool) strin
|
||||
if optional {
|
||||
optionalLine = "\n optional: true"
|
||||
}
|
||||
return writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - glob: '"+pattern+"'"+optionalLine+"\n")
|
||||
return writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - glob: '"+pattern+"'"+optionalLine+"\n")
|
||||
}
|
||||
|
||||
@@ -22,16 +22,37 @@ type importTestConfig struct {
|
||||
Items map[string]string `yaml:"items"`
|
||||
}
|
||||
|
||||
func TestLoadFormerDirectiveKeyIsApplicationData(t *testing.T) {
|
||||
type config struct {
|
||||
Legacy struct {
|
||||
Imports []struct {
|
||||
Path string `yaml:"path"`
|
||||
} `yaml:"imports"`
|
||||
} `yaml:"_configkit"`
|
||||
}
|
||||
for _, imports := range []bool{false, true} {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: nonexistent.yml\n")
|
||||
result, err := Load[config](File(mainPath), Options[config]{Imports: imports})
|
||||
if err != nil {
|
||||
t.Fatalf("Imports=%v: %v", imports, err)
|
||||
}
|
||||
if len(result.Config.Legacy.Imports) != 1 || result.Config.Legacy.Imports[0].Path != "nonexistent.yml" || len(result.Sources()) != 1 {
|
||||
t.Fatalf("Imports=%v: former directive was not retained as application data: %#v", imports, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTraversesPathImportsDepthFirstAndComposesOrigins(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports:
|
||||
- path: first.yml
|
||||
- path: second.yml
|
||||
settings:
|
||||
root: root
|
||||
`)
|
||||
firstPath := writeImportFile(t, directory, "first.yml", `_configkit:
|
||||
firstPath := writeImportFile(t, directory, "first.yml", `configkit:
|
||||
imports:
|
||||
- path: nested/third.yml
|
||||
settings:
|
||||
@@ -71,7 +92,7 @@ settings:
|
||||
func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) {
|
||||
t.Run("all duplicate claimants", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports:
|
||||
- path: one.yml
|
||||
- path: two.yml
|
||||
@@ -97,7 +118,7 @@ func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: fragment.yml\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: fragment.yml\n")
|
||||
writeImportFile(t, directory, "fragment.yml", test.fragment)
|
||||
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true, Definition: Definition[importTestConfig]{Version: versionRule}}, CodeImportVersion)
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
@@ -114,15 +135,15 @@ func TestLoadImportDirectiveValidationAndTemporaryGlobGate(t *testing.T) {
|
||||
body string
|
||||
code Code
|
||||
}{
|
||||
{name: "directive kind", body: "_configkit: []\n", code: CodeImportInvalid},
|
||||
{name: "unknown directive field", body: "_configkit: {unknown: true}\n", code: CodeImportInvalid},
|
||||
{name: "imports kind", body: "_configkit: {imports: {}}\n", code: CodeImportInvalid},
|
||||
{name: "declaration kind", body: "_configkit: {imports: [file.yml]}\n", code: CodeImportInvalid},
|
||||
{name: "missing target", body: "_configkit: {imports: [{optional: true}]}\n", code: CodeImportInvalid},
|
||||
{name: "two targets", body: "_configkit: {imports: [{path: a.yml, glob: '*.yml'}]}\n", code: CodeImportInvalid},
|
||||
{name: "unknown declaration field", body: "_configkit: {imports: [{path: a.yml, extra: true}]}\n", code: CodeImportInvalid},
|
||||
{name: "optional kind", body: "_configkit: {imports: [{path: a.yml, optional: 'yes'}]}\n", code: CodeImportInvalid},
|
||||
{name: "nested directive", body: "nested: {_configkit: {}}\n", code: CodeImportInvalid},
|
||||
{name: "directive kind", body: "configkit: []\n", code: CodeImportInvalid},
|
||||
{name: "unknown directive field", body: "configkit: {unknown: true}\n", code: CodeImportInvalid},
|
||||
{name: "imports kind", body: "configkit: {imports: {}}\n", code: CodeImportInvalid},
|
||||
{name: "declaration kind", body: "configkit: {imports: [file.yml]}\n", code: CodeImportInvalid},
|
||||
{name: "missing target", body: "configkit: {imports: [{optional: true}]}\n", code: CodeImportInvalid},
|
||||
{name: "two targets", body: "configkit: {imports: [{path: a.yml, glob: '*.yml'}]}\n", code: CodeImportInvalid},
|
||||
{name: "unknown declaration field", body: "configkit: {imports: [{path: a.yml, extra: true}]}\n", code: CodeImportInvalid},
|
||||
{name: "optional kind", body: "configkit: {imports: [{path: a.yml, optional: 'yes'}]}\n", code: CodeImportInvalid},
|
||||
{name: "nested directive", body: "nested: {configkit: {}}\n", code: CodeImportInvalid},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -136,7 +157,7 @@ func TestLoadImportDirectiveValidationAndTemporaryGlobGate(t *testing.T) {
|
||||
func TestLoadImportMissingContainmentAndRegularFileRules(t *testing.T) {
|
||||
t.Run("optional missing", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports:
|
||||
- path: missing.yml
|
||||
optional: true
|
||||
@@ -172,7 +193,7 @@ settings: {root: retained}
|
||||
if test.setup != nil {
|
||||
test.setup(t, directory)
|
||||
}
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: \""+test.path+"\"\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: \""+test.path+"\"\n")
|
||||
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, test.code)
|
||||
})
|
||||
}
|
||||
@@ -181,9 +202,9 @@ settings: {root: retained}
|
||||
func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
|
||||
t.Run("cycle", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: a.yml}]\n")
|
||||
writeImportFile(t, directory, "a.yml", "_configkit:\n imports: [{path: b.yml}]\n")
|
||||
writeImportFile(t, directory, "b.yml", "_configkit:\n imports: [{path: main.yml}]\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: a.yml}]\n")
|
||||
writeImportFile(t, directory, "a.yml", "configkit:\n imports: [{path: b.yml}]\n")
|
||||
writeImportFile(t, directory, "b.yml", "configkit:\n imports: [{path: main.yml}]\n")
|
||||
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle)
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
if diagnostic.Primary == nil || len(diagnostic.Related) != 2 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "a.yml") || !strings.Contains(diagnostic.Message, "b.yml") {
|
||||
@@ -193,9 +214,9 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
|
||||
|
||||
t.Run("diamond", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n")
|
||||
writeImportFile(t, directory, "left.yml", "_configkit:\n imports: [{path: shared.yml}]\n")
|
||||
writeImportFile(t, directory, "right.yml", "_configkit:\n imports: [{path: shared.yml}]\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n")
|
||||
writeImportFile(t, directory, "left.yml", "configkit:\n imports: [{path: shared.yml}]\n")
|
||||
writeImportFile(t, directory, "right.yml", "configkit:\n imports: [{path: shared.yml}]\n")
|
||||
writeImportFile(t, directory, "shared.yml", "settings: {root: shared}\n")
|
||||
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated)
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
@@ -216,7 +237,7 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
|
||||
if err := os.Symlink("target.yml", filepath.Join(directory, "second-link.yml")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: first-link.yml}, {path: second-link.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: first-link.yml}, {path: second-link.yml}]}\n")
|
||||
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated)
|
||||
})
|
||||
}
|
||||
@@ -231,7 +252,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
||||
if err := os.Symlink(target, filepath.Join(directory, "link.yml")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: link.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: link.yml}]}\n")
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil || result.Config.Settings.First != "linked" || result.Sources()[1].Path != canonicalTestPath(t, target) {
|
||||
t.Fatalf("symlink result = %#v, %v", result, err)
|
||||
@@ -245,7 +266,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
||||
if err := os.Symlink(target, filepath.Join(root, "escape.yml")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
mainPath := writeImportFile(t, root, "main.yml", "_configkit: {imports: [{path: escape.yml}]}\n")
|
||||
mainPath := writeImportFile(t, root, "main.yml", "configkit: {imports: [{path: escape.yml}]}\n")
|
||||
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot)
|
||||
})
|
||||
|
||||
@@ -255,7 +276,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
||||
if err := os.Link(one, filepath.Join(directory, "two.yml")); err != nil {
|
||||
t.Skipf("hard links unavailable: %v", err)
|
||||
}
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n")
|
||||
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeComposeConflict)
|
||||
})
|
||||
|
||||
@@ -264,7 +285,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
||||
if err := os.Symlink("loop.yml", filepath.Join(directory, "loop.yml")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: loop.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: loop.yml}]}\n")
|
||||
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportInvalid)
|
||||
})
|
||||
}
|
||||
@@ -272,7 +293,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
||||
func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
writeImportFile(t, directory, "fragment.yml", "settings: {first: imported}\n")
|
||||
input := []byte("_configkit: {imports: [{path: fragment.yml}]}\n")
|
||||
input := []byte("configkit: {imports: [{path: fragment.yml}]}\n")
|
||||
context := &FilesystemContext{BaseDir: directory, Root: directory}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
@@ -295,7 +316,7 @@ func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T)
|
||||
|
||||
func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mainPath := writeImportFile(t, root, "configs/main.yml", "_configkit: {imports: [{path: ../shared.yml}]}\n")
|
||||
mainPath := writeImportFile(t, root, "configs/main.yml", "configkit: {imports: [{path: ../shared.yml}]}\n")
|
||||
writeImportFile(t, root, "shared.yml", "settings: {root: shared}\n")
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{
|
||||
Imports: true, Filesystem: &FilesystemContext{Root: root},
|
||||
@@ -309,7 +330,7 @@ func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) {
|
||||
func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) {
|
||||
t.Run("parse", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: broken.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: broken.yml}]}\n")
|
||||
writeImportFile(t, directory, "broken.yml", "settings: [\n")
|
||||
loadError := requireAnyLoadError(t, File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
diagnostics := loadError.Diagnostics()
|
||||
@@ -320,7 +341,7 @@ func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) {
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: unknown.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: unknown.yml}]}\n")
|
||||
writeImportFile(t, directory, "unknown.yml", "unknown: value\n")
|
||||
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeDecodeUnknownField)
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
@@ -332,7 +353,7 @@ func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) {
|
||||
|
||||
func TestOptionalImportDoesNotSuppressUnsafeOrUnreadableTargets(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "outside.yml", "_configkit: {imports: [{path: ../missing.yml, optional: true}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "outside.yml", "configkit: {imports: [{path: ../missing.yml, optional: true}]}\n")
|
||||
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot)
|
||||
|
||||
locked := writeImportFile(t, directory, "locked.yml", "settings: {root: locked}\n")
|
||||
@@ -344,13 +365,13 @@ func TestOptionalImportDoesNotSuppressUnsafeOrUnreadableTargets(t *testing.T) {
|
||||
_ = file.Close()
|
||||
t.Skip("filesystem does not enforce the unreadable-file fixture")
|
||||
}
|
||||
mainPath = writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: locked.yml, optional: true}]}\n")
|
||||
mainPath = writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: locked.yml, optional: true}]}\n")
|
||||
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeSourcePermission)
|
||||
}
|
||||
|
||||
func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n")
|
||||
writeImportFile(t, directory, "one.yml", "settings: {first: one}\n")
|
||||
writeImportFile(t, directory, "two.yml", "settings: {second: two}\n")
|
||||
main := parseImportMain(t, mainPath)
|
||||
@@ -370,7 +391,7 @@ func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
writeImportFile(t, directory, "one.yml", "_configkit: {imports: [{path: nested.yml}]}\n")
|
||||
writeImportFile(t, directory, "one.yml", "configkit: {imports: [{path: nested.yml}]}\n")
|
||||
writeImportFile(t, directory, "nested.yml", "settings: {nested: nested}\n")
|
||||
main = parseImportMain(t, mainPath)
|
||||
_, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, importLimits{depth: 1, sources: 8, bytes: maxTotalBytes, nodes: maxDocumentNodes})
|
||||
@@ -381,7 +402,7 @@ func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) {
|
||||
|
||||
func TestImportTraversalAcceptsExactPrivateBudgets(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: imported.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: imported.yml}]}\n")
|
||||
importedPath := writeImportFile(t, directory, "imported.yml", "settings: {first: imported}\n")
|
||||
main := parseImportMain(t, mainPath)
|
||||
imported := parseImportMain(t, importedPath)
|
||||
|
||||
@@ -240,7 +240,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
result, err := Load[config](Bytes("directive.yml", []byte("nested:\n _configkit: {}\n")))
|
||||
result, err := Load[config](Bytes("directive.yml", []byte("nested:\n configkit: {}\n")))
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, err = %#v, %v", result, err)
|
||||
}
|
||||
@@ -249,7 +249,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
if diagnostic.Code != CodeImportDisabled || diagnostic.Path.String() != "/nested/_configkit" || diagnostic.Primary == nil || diagnostic.Primary.Line != 2 {
|
||||
if diagnostic.Code != CodeImportDisabled || diagnostic.Path.String() != "/nested/configkit" || diagnostic.Primary == nil || diagnostic.Primary.Line != 2 {
|
||||
t.Fatalf("diagnostic = %#v", diagnostic)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func overlappingOverride(previous []Override, valid []bool, path Path) int {
|
||||
|
||||
func pathContainsDirective(path Path) bool {
|
||||
for _, segment := range path.segments {
|
||||
if segment.key == "_configkit" {
|
||||
if segment.key == directiveKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func overrideDirectivePath(node *document.Node, path Path) (Path, bool) {
|
||||
if node.Kind == document.Mapping {
|
||||
for _, entry := range node.Mapping {
|
||||
child := path.Key(entry.Key)
|
||||
if entry.Key == "_configkit" {
|
||||
if entry.Key == directiveKey {
|
||||
return child, true
|
||||
}
|
||||
if found, ok := overrideDirectivePath(entry.Value, child); ok {
|
||||
|
||||
@@ -30,7 +30,7 @@ type overrideLifecycleConfig struct {
|
||||
|
||||
func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: base.yml}]\nversion: v1\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: base.yml}]\nversion: v1\n")
|
||||
writeImportFile(t, directory, "base.yml", `settings:
|
||||
target: base
|
||||
count: 4
|
||||
@@ -187,8 +187,8 @@ func TestOverrideValidationRejectsPathsOverlapValuesAndProtectedTargets(t *testi
|
||||
{name: "nonfinite", overrides: []Override{{Path: Keys("scalar"), Value: math.Inf(1), Flag: "--scalar"}}, code: CodeOverrideInvalidValue},
|
||||
{name: "depth", overrides: []Override{{Path: Keys("nested"), Value: deep, Flag: "--nested"}}, code: CodeOverrideLimitExceeded},
|
||||
{name: "record bytes", overrides: []Override{{Path: Keys("scalar"), Value: strings.Repeat("x", maxSourceBytes+1), Flag: "--scalar"}}, code: CodeOverrideLimitExceeded},
|
||||
{name: "directive path", overrides: []Override{{Path: Keys("_configkit", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective},
|
||||
{name: "directive value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"_configkit": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective},
|
||||
{name: "directive path", overrides: []Override{{Path: Keys("configkit", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective},
|
||||
{name: "directive value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"configkit": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective},
|
||||
{name: "version", overrides: []Override{{Path: Keys("version"), Value: "v2", Flag: "--version"}}, code: CodeOverrideVersion},
|
||||
{name: "version ancestor", overrides: []Override{{Path: Keys("version"), Value: map[string]any{"child": "v2"}, Flag: "--version"}}, code: CodeOverrideVersion},
|
||||
{name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel},
|
||||
|
||||
@@ -23,7 +23,7 @@ type profileTestConfig struct {
|
||||
|
||||
func TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports: [{path: base.yml}]
|
||||
settings:
|
||||
kept: main
|
||||
@@ -133,8 +133,8 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
|
||||
profile string
|
||||
code Code
|
||||
}{
|
||||
{name: "root directive", profile: "_configkit: {}\n", code: CodeProfileDirective},
|
||||
{name: "nested directive", profile: "settings: {_configkit: {}}\n", code: CodeProfileDirective},
|
||||
{name: "root directive", profile: "configkit: {}\n", code: CodeProfileDirective},
|
||||
{name: "nested directive", profile: "settings: {configkit: {}}\n", code: CodeProfileDirective},
|
||||
{name: "equal version", profile: "schema: {version: v1}\n", code: CodeProfileVersion},
|
||||
{name: "ancestor scalar", profile: "schema: null\n", code: CodeProfileVersion},
|
||||
} {
|
||||
@@ -158,7 +158,7 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
|
||||
|
||||
t.Run("profile aliases imported base", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: profiles/testing.yml}]}\n")
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: profiles/testing.yml}]}\n")
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: value}\n")
|
||||
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Imports: true, Profile: "testing"}, CodeProfileRepeated)
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ type layeredConfig struct {
|
||||
|
||||
func TestLayeredProvenanceAgreesWithResolvedSnapshot(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `_configkit:
|
||||
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||
imports: [{path: imported.yml}]
|
||||
shared: {equal: same}
|
||||
values: [1, 2]
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestNonregularSourcesDoNotBlock(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
input := "value: 1\n"
|
||||
if mode == "import" {
|
||||
input = "_configkit:\n imports:\n - path: pipe.yml\n"
|
||||
input = "configkit:\n imports:\n - path: pipe.yml\n"
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "config.yml"), []byte(input), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in New Issue
Block a user