Bind engine secret handling to facade types

This commit is contained in:
2026-09-12 16:04:31 +00:00
parent 9b4f9ac50e
commit da02a19f2c
28 changed files with 348 additions and 232 deletions

View File

@@ -276,6 +276,30 @@ tests pass; binding instances cannot affect each other. Run
`go test ./internal/engine -run 'Secret|Schema|Decode|Clone|Projection|Validation|Callback'`,
`go test -race ./internal/engine`, and the everyday check.
### Completion
Status: complete (2026-09-12).
`internal/engine` now receives a per-operation `SecretBinding` at schema and
load construction. Its exact `reflect.Type`, `Read`, and `Make` functions carry
the facade-owned `SecretState` through decoding, projection, unresolved and
validated cloning, dynamic-value rejection, secret lookup, resolution helpers,
and validator mutation auditing. The binding is retained by each schema only;
there is no package-global registration or engine-owned replacement secret.
Removed the candidate's production `Secret` and `SecretEnv` declarations.
`secret_binding_test.go` provides a local test secret type, fresh binding for
every test load, and exact-type/isolation coverage using a second local secret
type. Existing secret tests continue to cover defaults, pointers and containers,
resolution-selected names, lookup-once behavior, borrowed callback values,
redaction, and validator replacement without credential disclosure. The root
implementation remains unchanged for this migration handoff.
Linux validation with Go 1.26.7 passed the focused secret/schema/decode/clone/
projection/validation/callback suite, `go test -race ./internal/engine`, and
`./scripts/check.sh`. There were no deviations. The next handoff creates the
temporary facade's root-owned value, source, diagnostic, and secret wrappers.
## Stage 4 — Build candidate value, source, and diagnostic wrappers
Status: not started.

View File

@@ -30,11 +30,11 @@ func TestCallbackAssignmentsAreDetachedAcrossLoads(t *testing.T) {
}
calls := 0
lookup := func(string) (string, bool) { calls++; return "credential", true }
first, err := loadWithSecretLookup(NoSource(), lookup, Options[cfg]{Definition: definition})
first, err := testLoadWithSecretLookup(NoSource(), lookup, Options[cfg]{Definition: definition})
if err != nil {
t.Fatal(err)
}
second, err := loadWithSecretLookup(NoSource(), lookup, Options[cfg]{Definition: definition})
second, err := testLoadWithSecretLookup(NoSource(), lookup, Options[cfg]{Definition: definition})
if err != nil {
t.Fatal(err)
}

View File

@@ -128,11 +128,11 @@ func (o *valueOperation) clone(value reflect.Value, descriptor *typeDescriptor,
return result, nil
case descriptorSecret:
result := reflect.New(value.Type()).Elem()
reference := value.Interface().(Secret)
reference := o.schema.secrets.Read(value)
if o.cloneMode == cloneValidatedSecrets {
result.Set(value)
} else if reference.specified {
result.Set(reflect.ValueOf(SecretEnv(reference.EnvName())))
} else if reference.Specified {
result.Set(o.schema.secrets.Make(SecretState{Name: reference.Name, Specified: true}))
}
return result, nil
case descriptorText:

View File

@@ -179,7 +179,7 @@ func decodeValue(node *document.Node, base reflect.Value, descriptor *typeDescri
if node.Kind != document.String {
return typeMismatch(path, node, "secret environment name")
}
return reflect.ValueOf(SecretEnv(node.Text)), nil
return schema.secrets.Make(SecretState{Name: node.Text, Specified: true}), nil
case descriptorText:
if node.Kind != document.String {
return typeMismatch(path, node, "text string")

View File

@@ -26,7 +26,7 @@ func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) {
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]{
result, err := testLoad[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{
Imports: true, Profile: "testing",
Definition: Definition[explanationConcurrency]{Defaults: func() explanationConcurrency {
value := explanationConcurrency{}
@@ -84,7 +84,7 @@ items: [profile]
labels: {profile: retained}
dynamic: selected
`)
result, err := loadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
result, err := testLoadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
if name != "APP_SECRET" {
t.Fatalf("secret lookup = %q", name)
}
@@ -285,7 +285,7 @@ func TestSecretsBootstrapLoadAndExplanationRemainSeparateAndSafe(t *testing.T) {
type config struct {
Credential Secret `yaml:"credential"`
}
result, err := Load[config](Bytes("config.yml", []byte("credential: "+name+"\n")))
result, err := testLoad[config](Bytes("config.yml", []byte("credential: "+name+"\n")))
if err != nil {
t.Fatal(err)
}
@@ -343,7 +343,7 @@ func TestExplanationReconcilesDefaultsAfterInputReplacement(t *testing.T) {
writeImportFile(t, root, "profiles/p.yml", "m: {b: 2}\n")
opts.Profile = "p"
}
result, err := Load(File(main), opts)
result, err := testLoad(File(main), opts)
if err != nil {
t.Fatal(err)
}
@@ -380,11 +380,11 @@ func TestIntegerVersionInspectionRetainsInputAndReconcilesTypedValue(t *testing.
Defaults: func() config { return config{Version: "4"} },
Version: rule,
}}
first, err := Load(Bytes("version.yml", []byte("version: 4\n")), options)
first, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), options)
if err != nil {
t.Fatal(err)
}
second, err := Load(Bytes("version.yml", []byte("version: 4\n")), options)
second, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), options)
if err != nil {
t.Fatal(err)
}
@@ -449,7 +449,7 @@ func TestSensitiveIntegerVersionStaysRedactedWithoutSecretLookup(t *testing.T) {
Version string `yaml:"version"`
}
lookups := 0
result, err := loadWithSecretLookup(Bytes("version.yml", []byte("version: 987654321\n")), func(string) (string, bool) {
result, err := testLoadWithSecretLookup(Bytes("version.yml", []byte("version: 987654321\n")), func(string) (string, bool) {
lookups++
return "CREDENTIAL_CANARY", true
}, Options[config]{Definition: Definition[config]{
@@ -476,7 +476,7 @@ func TestExplanationReplaysResolutionAdditionsAndOverlappingIntent(t *testing.T)
}
for _, overlap := range []bool{false, true} {
t.Run(fmt.Sprint(overlap), func(t *testing.T) {
result, err := Load(Bytes("base", []byte("m: {old: 1}\n")), Options[cfg]{Definition: Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
result, err := testLoad(Bytes("base", []byte("m: {old: 1}\n")), Options[cfg]{Definition: Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
c.M["new"] = map[string]any{"child": int64(7)}
delete(c.M, "old")
r.Record(Keys("m", "new"), ChangeDerivedDefault, "add nested", Keys("m", "old"))

View File

@@ -26,7 +26,7 @@ items: {alpha: alpha}
var previous []SourceInfo
for run := 0; run < 2; run++ {
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if err != nil {
t.Fatal(err)
}
@@ -68,7 +68,7 @@ items: {root: root}
writeImportFile(t, directory, "conf.d/item-a.yml", "items: {a: a}\n")
writeImportFile(t, directory, "conf.d/question1.yml", "items: {question: question}\n")
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if err != nil {
t.Fatal(err)
}
@@ -79,7 +79,7 @@ items: {root: root}
ignoredPath := writeImportFile(t, directory, "conf.d/not-discovered.yml", "unknown: value\n")
_ = ignoredPath
plainPath := writeImportFile(t, directory, "plain.yml", "items: {root: only}\n")
plain, err := Load[importTestConfig](File(plainPath), Options[importTestConfig]{Imports: true})
plain, err := testLoad[importTestConfig](File(plainPath), Options[importTestConfig]{Imports: true})
if err != nil || len(plain.Sources()) != 1 || plain.Config.Items["root"] != "only" {
t.Fatalf("implicit discovery result = %#v, %v", plain, err)
}
@@ -116,7 +116,7 @@ func TestLoadGlobImportsRejectInvalidAndUnmatchedPatterns(t *testing.T) {
t.Fatal(err)
}
mainPath := writeGlobRoot(t, directory, "empty/*.yml", true)
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if err != nil || len(result.Sources()) != 1 {
t.Fatalf("optional unmatched glob = %#v, %v", result, err)
}
@@ -166,7 +166,7 @@ func TestLoadGlobImportsConfineSymlinkDirectories(t *testing.T) {
t.Skipf("symlinks unavailable: %v", err)
}
mainPath := writeGlobRoot(t, root, "linked/*.yml", false)
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if err != nil || result.Config.Items["linked"] != "linked" {
t.Fatalf("contained directory symlink = %#v, %v", result, err)
}
@@ -232,7 +232,7 @@ func TestGlobTraversalBoundsNonmatchesAndAcceptedCandidates(t *testing.T) {
func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "items: {root: root}\n")
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{
Profile: "default", Filesystem: &FilesystemContext{Root: directory},
})
if err != nil || result.Config.Items["root"] != "root" {
@@ -242,12 +242,12 @@ func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T)
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Filesystem: &FilesystemContext{Root: filepath.Join(directory, "missing")}}, CodeImportInvalid)
input := Bytes("memory.yml", []byte("items: {root: memory}\n"))
result, err = Load[importTestConfig](input, Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory, Root: directory}})
result, err = testLoad[importTestConfig](input, Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory, Root: directory}})
if err != nil || result.Config.Items["root"] != "memory" {
t.Fatalf("byte filesystem context = %#v, %v", result, err)
}
requireLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory}}, CodeOptionsInvalid)
defaultsOnly, err := Load[importTestConfig](NoSource(), Options[importTestConfig]{Profile: "default"})
defaultsOnly, err := testLoad[importTestConfig](NoSource(), Options[importTestConfig]{Profile: "default"})
if err != nil || defaultsOnly == nil {
t.Fatalf("default profile with NoSource = %#v, %v", defaultsOnly, err)
}
@@ -255,7 +255,7 @@ func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T)
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
selectedLink := filepath.Join(directory, "selected-link.yml")
if err := os.Symlink(mainPath, selectedLink); err == nil {
linked, err := Load[importTestConfig](File(selectedLink), Options[importTestConfig]{Filesystem: &FilesystemContext{Root: directory}})
linked, err := testLoad[importTestConfig](File(selectedLink), Options[importTestConfig]{Filesystem: &FilesystemContext{Root: directory}})
if err != nil || linked.Sources()[0].Path != canonicalTestPath(t, mainPath) {
t.Fatalf("selected symlink identity = %#v, %v", linked, err)
}

View File

@@ -33,7 +33,7 @@ func TestLoadFormerDirectiveKeyIsApplicationData(t *testing.T) {
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})
result, err := testLoad[config](File(mainPath), Options[config]{Imports: imports})
if err != nil {
t.Fatalf("Imports=%v: %v", imports, err)
}
@@ -61,7 +61,7 @@ settings:
thirdPath := writeImportFile(t, directory, "nested/third.yml", "settings:\n nested: nested\n")
secondPath := writeImportFile(t, directory, "second.yml", "settings:\n second: second\n")
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if err != nil {
t.Fatal(err)
}
@@ -163,7 +163,7 @@ func TestLoadImportMissingContainmentAndRegularFileRules(t *testing.T) {
optional: true
settings: {root: retained}
`)
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if err != nil || result.Config.Settings.Root != "retained" || len(result.Sources()) != 1 {
t.Fatalf("optional result = %#v, %v", result, err)
}
@@ -253,7 +253,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
t.Skipf("symlinks unavailable: %v", err)
}
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: link.yml}]}\n")
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
result, err := testLoad[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)
}
@@ -303,7 +303,7 @@ func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T)
{name: "reader URI label", source: func() Source { return Reader("https://example.invalid/main.yml", strings.NewReader(string(input))) }},
} {
t.Run(test.name, func(t *testing.T) {
result, err := Load[importTestConfig](test.source(), Options[importTestConfig]{Imports: true, Filesystem: context})
result, err := testLoad[importTestConfig](test.source(), Options[importTestConfig]{Imports: true, Filesystem: context})
if err != nil || result.Config.Settings.First != "imported" {
t.Fatalf("explicit filesystem result = %#v, %v", result, err)
}
@@ -318,7 +318,7 @@ func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) {
root := t.TempDir()
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]{
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{
Imports: true, Filesystem: &FilesystemContext{Root: root},
})
if err != nil || result.Config.Settings.Root != "shared" {
@@ -455,7 +455,7 @@ func requireLoadCode(t *testing.T, source Source, options Options[importTestConf
func requireAnyLoadError(t *testing.T, source Source, options Options[importTestConfig]) *LoadError {
t.Helper()
result, err := Load[importTestConfig](source, options)
result, err := testLoad[importTestConfig](source, options)
if result != nil || err == nil {
t.Fatalf("result, error = %#v, %v", result, err)
}

View File

@@ -46,14 +46,14 @@ func TestParallelLoadsShareRulesButNotMutableValues(t *testing.T) {
go func(index int) {
defer group.Done()
input := fmt.Sprintf("custom: value-%d\n", index)
results[index], errorsByIndex[index] = Load(Bytes("parallel.yml", []byte(input)), Options[independentConfig]{Definition: definition})
results[index], errorsByIndex[index] = testLoad(Bytes("parallel.yml", []byte(input)), Options[independentConfig]{Definition: definition})
}(index)
}
group.Wait()
for index, err := range errorsByIndex {
if err != nil {
t.Fatalf("Load(%d) failed: %v", index, err)
t.Fatalf("testLoad(%d) failed: %v", index, err)
}
}
results[0].Config.Labels["default"] = "mutated"
@@ -88,7 +88,7 @@ func TestConcurrentLoadsOfCopiedReaderSourceHaveOneClaim(t *testing.T) {
go func(candidate Source) {
defer group.Done()
<-start
_, err := Load[config](candidate)
_, err := testLoad[config](candidate)
errorsFound <- err
}(candidate)
}

View File

@@ -110,11 +110,11 @@ func (r *Result[T]) Diagnostics() []Diagnostic {
// Load parses and strictly decodes zero or one selected source, explicitly enabled
// imports, and one selected profile. It accepts at most one Options value and
// returns no result when any error is reported.
func Load[T any](source Source, supplied ...Options[T]) (*Result[T], error) {
return loadWithSecretLookup(source, os.LookupEnv, supplied...)
func Load[T any](source Source, secrets SecretBinding, supplied ...Options[T]) (*Result[T], error) {
return loadWithSecretLookup(source, secrets, os.LookupEnv, supplied...)
}
func loadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
accumulator := diagnosticAccumulator{}
options, diagnostics := selectOptions(source, supplied)
accumulator.add(diagnostics...)
@@ -128,7 +128,7 @@ func loadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ..
if len(diagnostics) != 0 {
return nil, accumulator.loadError()
}
schema, schemaIssue := buildSchema(reflect.TypeFor[T]())
schema, schemaIssue := buildSchema(reflect.TypeFor[T](), secrets)
if schemaIssue != nil {
accumulator.add(diagnosticFromSchemaIssue(schemaIssue))
return nil, accumulator.loadError()

View File

@@ -38,7 +38,7 @@ func TestLoadSingleSourceKindsAndDefaultsOnly(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var defaultsCalls atomic.Int32
result, err := Load(test.source(), Options[config]{Definition: Definition[config]{
result, err := testLoad(test.source(), Options[config]{Definition: Definition[config]{
Defaults: func() config {
defaultsCalls.Add(1)
return config{Name: "default"}
@@ -92,7 +92,7 @@ func TestLoadVersionStatesWarningsAndSafeFailures(t *testing.T) {
validation.Warning(versionPath, "review schema compatibility", Keys("missing_related"))
return nil
}}
result, err := Load(test.source, Options[config]{Definition: Definition[config]{Version: rule}})
result, err := testLoad(test.source, Options[config]{Definition: Definition[config]{Version: rule}})
if test.wantErr != "" {
if result != nil || err == nil {
t.Fatalf("result, err = %#v, %v", result, err)
@@ -144,21 +144,21 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
load func(Source, *VersionRule) (string, error)
}{
{name: "integer into string", source: "version: 4\n", load: func(source Source, rule *VersionRule) (string, error) {
result, err := Load(source, Options[stringConfig]{Definition: Definition[stringConfig]{Version: rule}})
result, err := testLoad(source, Options[stringConfig]{Definition: Definition[stringConfig]{Version: rule}})
if err != nil {
return "", err
}
return result.Config.Version, nil
}},
{name: "string into string", source: "version: \"4\"\n", load: func(source Source, rule *VersionRule) (string, error) {
result, err := Load(source, Options[stringConfig]{Definition: Definition[stringConfig]{Version: rule}})
result, err := testLoad(source, Options[stringConfig]{Definition: Definition[stringConfig]{Version: rule}})
if err != nil {
return "", err
}
return result.Config.Version, nil
}},
{name: "integer into pointer", source: "version: 4\n", load: func(source Source, rule *VersionRule) (string, error) {
result, err := Load(source, Options[pointerConfig]{Definition: Definition[pointerConfig]{Version: rule}})
result, err := testLoad(source, Options[pointerConfig]{Definition: Definition[pointerConfig]{Version: rule}})
if err != nil {
return "", err
}
@@ -168,7 +168,7 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
return *result.Config.Version, nil
}},
{name: "string into pointer", source: "version: \"4\"\n", load: func(source Source, rule *VersionRule) (string, error) {
result, err := Load(source, Options[pointerConfig]{Definition: Definition[pointerConfig]{Version: rule}})
result, err := testLoad(source, Options[pointerConfig]{Definition: Definition[pointerConfig]{Version: rule}})
if err != nil {
return "", err
}
@@ -194,7 +194,7 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
})
}
if result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[stringConfig]{}); result != nil {
if result, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[stringConfig]{}); result != nil {
t.Fatalf("load without rule = %#v, %v", result, err)
} else {
var loadError *LoadError
@@ -207,7 +207,7 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
Label string `yaml:"label"`
}
rule := &VersionRule{Path: versionPath, Check: func(Version, *Validation) error { return nil }}
if result, err := Load(Bytes("version.yml", []byte("version: 4\nlabel: 5\n")), Options[unrelatedConfig]{Definition: Definition[unrelatedConfig]{Version: rule}}); result != nil {
if result, err := testLoad(Bytes("version.yml", []byte("version: 4\nlabel: 5\n")), Options[unrelatedConfig]{Definition: Definition[unrelatedConfig]{Version: rule}}); result != nil {
t.Fatalf("unrelated coercion = %#v, %v", result, err)
} else {
var loadError *LoadError
@@ -240,7 +240,7 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
checked = version
return nil
}}
result, err := Load(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
result, err := testLoad(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
if err != nil {
t.Fatal(err)
}
@@ -266,7 +266,7 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
called = true
return nil
}}
result, err := Load(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
result, err := testLoad(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
if result != nil || called {
t.Fatalf("result = %#v, checker called = %v", result, called)
}
@@ -289,7 +289,7 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
}
return nil
}}
result, err := Load(Bytes("nested.yml", []byte("versions:\n \"0\": 4\nsibling: quoted\n")), Options[nested]{Definition: Definition[nested]{Version: rule}})
result, err := testLoad(Bytes("nested.yml", []byte("versions:\n \"0\": 4\nsibling: quoted\n")), Options[nested]{Definition: Definition[nested]{Version: rule}})
if err != nil {
t.Fatal(err)
}
@@ -299,7 +299,7 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
type sequenceConfig struct {
Versions []namedVersion `yaml:"versions"`
}
sequenceResult, err := Load(Bytes("sequence.yml", []byte("versions: [6]\n")), Options[sequenceConfig]{Definition: Definition[sequenceConfig]{Version: &VersionRule{Path: parsed, Check: func(Version, *Validation) error { return nil }}}})
sequenceResult, err := testLoad(Bytes("sequence.yml", []byte("versions: [6]\n")), Options[sequenceConfig]{Definition: Definition[sequenceConfig]{Version: &VersionRule{Path: parsed, Check: func(Version, *Validation) error { return nil }}}})
if err != nil || len(sequenceResult.Config.Versions) != 1 || sequenceResult.Config.Versions[0] != "6" {
t.Fatalf("sequence configuration = %#v, %v", sequenceResult, err)
}
@@ -311,13 +311,13 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
if err != nil {
t.Fatal(err)
}
escapedResult, err := Load(Bytes("escaped.yml", []byte("versions:\n a/b: 9\n")), Options[escaped]{Definition: Definition[escaped]{Version: &VersionRule{Path: escapedPath, Check: func(Version, *Validation) error { return nil }}}})
escapedResult, err := testLoad(Bytes("escaped.yml", []byte("versions:\n a/b: 9\n")), Options[escaped]{Definition: Definition[escaped]{Version: &VersionRule{Path: escapedPath, Check: func(Version, *Validation) error { return nil }}}})
if err != nil || escapedResult.Config.Versions["a/b"] != "9" {
t.Fatalf("escaped configuration = %#v, %v", escapedResult, err)
}
siblingRule := &VersionRule{Path: Keys("versions", "target"), Check: func(Version, *Validation) error { return nil }}
if result, err := Load(Bytes("sibling.yml", []byte("versions:\n target: 4\n other: 5\n")), Options[escaped]{Definition: Definition[escaped]{Version: siblingRule}}); result != nil {
if result, err := testLoad(Bytes("sibling.yml", []byte("versions:\n target: 4\n other: 5\n")), Options[escaped]{Definition: Definition[escaped]{Version: siblingRule}}); result != nil {
t.Fatalf("same-type sibling result = %#v, %v", result, err)
} else {
requireDiagnostic(t, err, CodeDecodeType, "/versions/other")
@@ -332,16 +332,16 @@ func TestLoadVersionDestinationTypesKeepOrdinaryDecoding(t *testing.T) {
type config struct {
Version int8 `yaml:"version"`
}
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
result, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
if err != nil || result.Config.Version != 4 {
t.Fatalf("result = %#v, %v", result, err)
}
result, err = Load(Bytes("version.yml", []byte("version: 999\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
result, err = testLoad(Bytes("version.yml", []byte("version: 999\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
if result != nil {
t.Fatalf("range result = %#v", result)
}
requireDiagnostic(t, err, CodeDecodeRange, "/version")
result, err = Load(Bytes("version.yml", []byte("version: \"4\"\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
result, err = testLoad(Bytes("version.yml", []byte("version: \"4\"\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
if result != nil {
t.Fatalf("quoted integer result = %#v", result)
}
@@ -352,11 +352,11 @@ func TestLoadVersionDestinationTypesKeepOrdinaryDecoding(t *testing.T) {
type config struct {
Version any `yaml:"version"`
}
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
result, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
if err != nil || result.Config.Version != int64(4) {
t.Fatalf("result = %#v, %v", result, err)
}
result, err = Load(Bytes("version.yml", []byte("version: 123456789012345678901234567890\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
result, err = testLoad(Bytes("version.yml", []byte("version: 123456789012345678901234567890\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
if result != nil {
t.Fatalf("range result = %#v", result)
}
@@ -371,21 +371,21 @@ func TestLoadVersionDestinationTypesKeepOrdinaryDecoding(t *testing.T) {
type config struct {
Version validatedText `yaml:"version"`
}
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
_, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
return err
}},
{name: "duration", load: func() error {
type config struct {
Version time.Duration `yaml:"version"`
}
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
_, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
return err
}},
{name: "secret", load: func() error {
type config struct {
Version Secret `yaml:"version"`
}
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
_, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
return err
}},
} {
@@ -409,7 +409,7 @@ func TestLoadVersionPolicyStopsLaterHooks(t *testing.T) {
validation.Error(Keys("version"), "unsupported configuration version")
return nil
}}
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{
Defaults: func() config { defaultsCalled = true; return config{} },
Version: rule,
Resolve: func(*config, *Resolution) error { resolutionCalled = true; return nil },
@@ -440,7 +440,7 @@ func TestLoadVersionReportSuppressesReturnedErrorTextAndDefaults(t *testing.T) {
Name string `yaml:"name"`
}
var defaultsCalls atomic.Int32
result, err := Load(Bytes("version.yml", []byte("name: value\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("version.yml", []byte("name: value\n")), Options[config]{Definition: Definition[config]{
Defaults: func() config {
defaultsCalls.Add(1)
return config{Name: "default"}
@@ -474,7 +474,7 @@ func TestLoadMetadataReflectsDefaultsInputAndDetachedRecords(t *testing.T) {
Nested nested `yaml:"nested"`
Nullable map[string]string `yaml:"nullable"`
}
result, err := Load(Bytes("metadata.yml", []byte("nested: {set: configured}\nnullable: null\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("metadata.yml", []byte("nested: {set: configured}\nnullable: null\n")), Options[config]{Definition: Definition[config]{
Defaults: func() config {
return config{Nested: nested{Kept: "default", Set: "default"}, Nullable: map[string]string{"key": "value"}}
},
@@ -538,7 +538,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := Load(test.source, test.options...)
result, err := testLoad(test.source, test.options...)
if result != nil || err == nil {
t.Fatalf("result, err = %#v, %v", result, err)
}
@@ -549,7 +549,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) {
})
}
result, err := Load[config](Bytes("directive.yml", []byte("nested:\n configkit: {}\n")))
result, err := testLoad[config](Bytes("directive.yml", []byte("nested:\n configkit: {}\n")))
if result != nil || err == nil {
t.Fatalf("result, err = %#v, %v", result, err)
}
@@ -577,9 +577,9 @@ func TestLoadReturnsNilForSourceSchemaAndDecodeFailures(t *testing.T) {
run func() (any, error)
code Code
}{
{name: "source", run: func() (any, error) { return Load[validConfig](File(filepath.Join(t.TempDir(), "missing.yml"))) }, code: CodeSourceMissing},
{name: "schema", run: func() (any, error) { return Load[invalidSchema](Bytes("schema.yml", []byte("{}\n"))) }, code: CodeSchemaUnsupportedType},
{name: "decode", run: func() (any, error) { return Load[validConfig](Bytes("decode.yml", []byte("unknown: 1\n"))) }, code: CodeDecodeUnknownField},
{name: "source", run: func() (any, error) { return testLoad[validConfig](File(filepath.Join(t.TempDir(), "missing.yml"))) }, code: CodeSourceMissing},
{name: "schema", run: func() (any, error) { return testLoad[invalidSchema](Bytes("schema.yml", []byte("{}\n"))) }, code: CodeSchemaUnsupportedType},
{name: "decode", run: func() (any, error) { return testLoad[validConfig](Bytes("decode.yml", []byte("unknown: 1\n"))) }, code: CodeDecodeUnknownField},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

View File

@@ -8,7 +8,7 @@ import (
func TestMetadataHistoricalPaths(t *testing.T) {
t.Parallel()
result, err := Load[map[string]any](Bytes("base", []byte("retired: {child: 1}\nshape: {'0': base}\n'a/b~': {'': 2}\n")), Options[map[string]any]{
result, err := testLoad[map[string]any](Bytes("base", []byte("retired: {child: 1}\nshape: {'0': base}\n'a/b~': {'': 2}\n")), Options[map[string]any]{
Overrides: []Override{
{Path: Keys("retired"), Value: nil, Flag: "--retired"},
{Path: Keys("shape"), Value: []any{"replacement"}, Flag: "--shape"},
@@ -61,7 +61,7 @@ func TestHistoricalPathsExposeHiddenBaseOwnershipWithoutResolvingBaseSecrets(t *
writeImportFile(t, dir, "first.yml", "definitions: {recap: {prompt: recap}}\n")
writeImportFile(t, dir, "second.yml", "definitions: {recap: {output: recap.md}}\n")
writeImportFile(t, dir, "profiles/testing.yml", "definitions: null\ncredential: "+credential+"\n")
result, err := Load[config](File(root), Options[config]{Imports: true, Profile: "testing"})
result, err := testLoad[config](File(root), Options[config]{Imports: true, Profile: "testing"})
if err != nil {
t.Fatal(err)
}
@@ -232,7 +232,7 @@ func TestResolutionInputKeepsDetachedEntryMetadata(t *testing.T) {
}
func TestQueriesRespectActualHistoricalAndEffectiveContainers(t *testing.T) {
result, err := Load[map[string]any](Bytes("base", []byte("items: [1]\nm: {'0': 2}\nx: 3\nretired: {child: 4}\n")), Options[map[string]any]{Overrides: []Override{{Path: Keys("retired"), Value: "removed", Flag: "--retired"}}})
result, err := testLoad[map[string]any](Bytes("base", []byte("items: [1]\nm: {'0': 2}\nx: 3\nretired: {child: 4}\n")), Options[map[string]any]{Overrides: []Override{{Path: Keys("retired"), Value: "removed", Flag: "--retired"}}})
if err != nil {
t.Fatal(err)
}

View File

@@ -56,7 +56,7 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te
{Path: Keys("settings", "labels"), Value: map[string]any{"cli": "selected"}, Flag: "--label"},
{Path: Keys("settings", "api_key"), Value: "CLI_SECRET", Flag: "--api-key-env"},
}
result, err := loadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
result, err := testLoadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
if name != "CLI_SECRET" {
t.Fatalf("looked up superseded secret name %q", name)
}
@@ -83,7 +83,7 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te
},
})
if err != nil {
t.Fatalf("loadWithSecretLookup() error = %v", err)
t.Fatalf("testLoadWithSecretLookup() error = %v", err)
}
want := overrideLifecycleSettings{
Target: "cli", Labels: map[string]string{"base": "retained", "profile": "retained", "cli": "selected"},
@@ -123,7 +123,7 @@ func TestOverridesSupportDefaultsOnlyMapMergeListReplacementAndNumericKeys(t *te
"int": int8(1), "uint": uint16(2), "float": float32(3.5), "bool": false,
"null": nil, "list": []any{"value"}, "map": map[string]any{"key": "value"},
}
result, err := Load(NoSource(), Options[config]{
result, err := testLoad(NoSource(), Options[config]{
Definition: Definition[config]{Defaults: func() config {
return config{Labels: map[string]string{"kept": "default"}, Items: []int{9}}
}},
@@ -194,7 +194,7 @@ func TestOverrideValidationRejectsPathsOverlapValuesAndProtectedTargets(t *testi
{name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel},
} {
t.Run(test.name, func(t *testing.T) {
result, err := Load(base, Options[config]{Definition: Definition[config]{Version: version}, Overrides: test.overrides})
result, err := testLoad(base, Options[config]{Definition: Definition[config]{Version: version}, Overrides: test.overrides})
if result != nil {
t.Fatalf("result = %#v, want nil", result)
}
@@ -213,7 +213,7 @@ func TestOverrideCannotReplaceSchemaVersionAncestor(t *testing.T) {
type config struct {
Schema schemaConfig `yaml:"schema"`
}
_, err := Load(Bytes("version.yml", []byte("schema: {version: 4}\n")), Options[config]{
_, err := testLoad(Bytes("version.yml", []byte("schema: {version: 4}\n")), Options[config]{
Definition: Definition[config]{Version: &VersionRule{Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil }}},
Overrides: []Override{{Path: Keys("schema"), Value: map[string]any{"other": "value"}, Flag: "--schema"}},
})
@@ -224,7 +224,7 @@ func TestOverrideDecodeFailureUsesFlagSourceWithoutRawValue(t *testing.T) {
type config struct {
Count int `yaml:"count"`
}
result, err := Load(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{
result, err := testLoad(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{
Path: Keys("missing"), Value: "RAW-ARG-CANARY", Flag: "--missing",
}}})
if result != nil {
@@ -249,7 +249,7 @@ func TestOverrideTypeMismatchUsesNormalStrictDecode(t *testing.T) {
type config struct {
Count int `yaml:"count"`
}
_, err := Load(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{
_, err := testLoad(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{
Path: Keys("count"), Value: "not-an-integer", Flag: "--count",
}}})
loadError := requireSecretLoadCode(t, err, CodeDecodeType)
@@ -262,7 +262,7 @@ func TestOverrideEqualReplacementStillAppearsInHistory(t *testing.T) {
type config struct {
Value string `yaml:"value"`
}
result, err := Load(Bytes("equal.yml", []byte("value: same\n")), Options[config]{Overrides: []Override{{Path: Keys("value"), Value: "same", Flag: "--value"}}})
result, err := testLoad(Bytes("equal.yml", []byte("value: same\n")), Options[config]{Overrides: []Override{{Path: Keys("value"), Value: "same", Flag: "--value"}}})
if err != nil {
t.Fatal(err)
}

View File

@@ -40,7 +40,7 @@ nullable: null
value: scalar
`)
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{
result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{
Imports: true,
Profile: "testing",
Definition: Definition[profileTestConfig]{Defaults: func() profileTestConfig {
@@ -101,12 +101,12 @@ func TestLoadProfileSelectionUsesOneFixedYAMLFile(t *testing.T) {
writeImportFile(t, directory, "profiles/default.yml", "settings: {changed: forbidden}\n")
for _, name := range []string{"", "default"} {
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: name})
result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: name})
if err != nil || result.Config.Settings.Changed != "" || len(result.Sources()) != 1 {
t.Fatalf("inactive profile %q = %#v, %v", name, result, err)
}
}
selected, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing"})
selected, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing"})
if err != nil || selected.Config.Settings.Changed != "selected" || len(selected.Sources()) != 2 {
t.Fatalf("selected profile = %#v, %v", selected, err)
}
@@ -150,7 +150,7 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: 4}\n")
writeImportFile(t, directory, "profiles/testing.yml", "schema: {other: allowed}\n")
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing", Definition: Definition[profileTestConfig]{Version: versionRule}})
result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing", Definition: Definition[profileTestConfig]{Version: versionRule}})
if err != nil || result.Config.Schema["other"] != "allowed" {
t.Fatalf("mapping sibling = %#v, %v", result, err)
}
@@ -201,7 +201,7 @@ func TestLoadProfileKeepsBaseFailuresAndFilesystemContainment(t *testing.T) {
t.Run("byte source context", func(t *testing.T) {
directory := t.TempDir()
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: selected}\n")
result, err := Load[profileTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[profileTestConfig]{
result, err := testLoad[profileTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[profileTestConfig]{
Profile: "testing", Filesystem: &FilesystemContext{BaseDir: directory, Root: directory},
})
if err != nil || result.Config.Settings.Changed != "selected" {
@@ -251,7 +251,7 @@ func TestLoadProfileCountsAgainstAggregateBudgets(t *testing.T) {
func requireProfileLoadCode(t *testing.T, source Source, options Options[profileTestConfig], code Code) *LoadError {
t.Helper()
result, err := Load[profileTestConfig](source, options)
result, err := testLoad[profileTestConfig](source, options)
if result != nil || err == nil {
t.Fatalf("result, error = %#v, %v", result, err)
}

View File

@@ -199,7 +199,7 @@ func (o *valueOperation) project(value reflect.Value, descriptor *typeDescriptor
case descriptorSecret:
node, issue := o.node(document.String, path, depth)
if issue == nil {
node.Text = value.Interface().(Secret).EnvName()
node.Text = o.schema.secrets.Read(value).Name
}
return node, issue
case descriptorText:

View File

@@ -359,9 +359,9 @@ func TestCompareDocumentTreesTracksSemanticChanges(t *testing.T) {
func mustSchemaFor[T any](t *testing.T) *schemaDescriptor {
t.Helper()
schema, issue := buildSchema(reflect.TypeFor[T]())
schema, issue := testBuildSchema(reflect.TypeFor[T]())
if issue != nil {
t.Fatalf("buildSchema() issue = %#v", issue)
t.Fatalf("testBuildSchema() issue = %#v", issue)
}
return schema
}

View File

@@ -70,7 +70,7 @@ empty: {}
},
}
result, err := Load[layeredConfig](File(mainPath), Options[layeredConfig]{
result, err := testLoad[layeredConfig](File(mainPath), Options[layeredConfig]{
Imports: true, Profile: "testing", Definition: definition,
})
if err != nil {
@@ -134,7 +134,7 @@ empty: {}
t.Fatalf("canonical resolution history = %#v", lastValue)
}
schema, issue := buildSchema(reflect.TypeFor[layeredConfig]())
schema, issue := testBuildSchema(reflect.TypeFor[layeredConfig]())
if issue != nil {
t.Fatal(issue)
}
@@ -173,7 +173,7 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) {
group.Add(1)
go func() {
defer group.Done()
result, err := Load[layeredConfig](File(mainPath), options)
result, err := testLoad[layeredConfig](File(mainPath), options)
if err != nil {
errors <- err
return
@@ -236,7 +236,7 @@ composite: {first: first}
CLIEqual string `yaml:"cli_equal"`
}
var callbackErr error
result, err := Load[config](File(mainPath), Options[config]{
result, err := testLoad[config](File(mainPath), Options[config]{
Imports: true, Profile: "testing", Overrides: []Override{{Path: Keys("cli_equal"), Value: "same", Flag: "--cli-equal"}},
Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
check := func(ok bool, message string) {
@@ -322,7 +322,7 @@ func TestResolutionInputUsesEntryShapesAcrossTypedAndDynamicValues(t *testing.T)
Dynamic any `yaml:"dynamic"`
}
var callbackErr error
_, err := Load[config](Bytes("shapes.yml", []byte("name: inlined\nmappings: {'-0': key, 'a/b~': escaped}\narray: [zero, one]\ndynamic: {nested: [dynamic]}\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
_, err := testLoad[config](Bytes("shapes.yml", []byte("name: inlined\nmappings: {'-0': key, 'a/b~': escaped}\narray: [zero, one]\ndynamic: {nested: [dynamic]}\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
check := func(path Path, want bool) {
_, found := resolution.InputInfo(path)
if found != want && callbackErr == nil {
@@ -371,7 +371,7 @@ func TestResolutionInputDistinguishesNonFileRootsAndImportedFiles(t *testing.T)
} {
t.Run(test.name, func(t *testing.T) {
var callbackErr error
_, err := Load[config](test.source(), Options[config]{Imports: true, Filesystem: context, Definition: Definition[config]{
_, err := testLoad[config](test.source(), Options[config]{Imports: true, Filesystem: context, Definition: Definition[config]{
Defaults: func() config { return config{Default: "retained"} },
Resolve: func(_ *config, resolution *Resolution) error {
root, _ := resolution.InputInfo(Keys("root"))
@@ -398,7 +398,7 @@ func TestResolutionInputDistinguishesNonFileRootsAndImportedFiles(t *testing.T)
}
var callbackErr error
_, err := Load[config](Bytes("memory://standalone.yml", []byte("root: memory\n")), Options[config]{Definition: Definition[config]{
_, err := testLoad[config](Bytes("memory://standalone.yml", []byte("root: memory\n")), Options[config]{Definition: Definition[config]{
Defaults: func() config { return config{Default: "retained"} },
Resolve: func(_ *config, resolution *Resolution) error {
root, rootFound := resolution.InputInfo(Keys("root"))
@@ -426,7 +426,7 @@ func TestResolutionInputRemainsFixedAcrossRecordedMutations(t *testing.T) {
Values map[string]string `yaml:"values"`
}
var callbackErr error
result, err := Load[config](Bytes("mutations.yml", []byte("existing: original\nvalues: {original: move}\n")), Options[config]{Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
result, err := testLoad[config](Bytes("mutations.yml", []byte("existing: original\nvalues: {original: move}\n")), Options[config]{Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
original, originalFound := resolution.InputInfo(Keys("values", "original"))
_, destinationFound := resolution.InputInfo(Keys("values", "destination"))
existing, existingFound := resolution.InputInfo(Keys("existing"))
@@ -475,7 +475,7 @@ func TestResolutionInputQueriesRemainIndependentAcrossLoads(t *testing.T) {
group.Add(1)
go func() {
defer group.Done()
_, err := Load[config](Bytes("parallel.yml", []byte("value: stable\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
_, err := testLoad[config](Bytes("parallel.yml", []byte("value: stable\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
info, found := resolution.InputInfo(Keys("value"))
source, sourceFound := resolution.Source(info.Origin.Locations[0].SourceID)
if !found || !sourceFound || source.ID != "source-1" {

View File

@@ -85,7 +85,7 @@ sequence: [second, first]
return nil
},
}
result, err := Load[renderConfig](File(mainPath), Options[renderConfig]{Definition: definition, Profile: "testing"})
result, err := testLoad[renderConfig](File(mainPath), Options[renderConfig]{Definition: definition, Profile: "testing"})
if err != nil {
t.Fatal(err)
}
@@ -160,7 +160,7 @@ func TestSecretValuesStayOutOfLoadFailuresAndWarnings(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.definition.Secret = func(path Path) bool { return path.String() == "/credentials" }
result, err := Load[renderConfig](Bytes("secret-failure.yml", []byte(test.input)), Options[renderConfig]{Definition: test.definition})
result, err := testLoad[renderConfig](Bytes("secret-failure.yml", []byte(test.input)), Options[renderConfig]{Definition: test.definition})
if result != nil || err == nil {
t.Fatalf("result, error = %#v, %v", result, err)
}
@@ -205,7 +205,7 @@ func TestDynamicConcreteTagsRemainSecretThroughHistory(t *testing.T) {
Normalize(r, Keys("derived"), &c.Derived, canary, "derive", path)
return nil
}
result, err := Load(NoSource(), Options[cfg]{Definition: definition})
result, err := testLoad(NoSource(), Options[cfg]{Definition: definition})
if err != nil {
t.Fatal(err)
}
@@ -234,7 +234,7 @@ func TestValidatorCannotChangeDynamicSensitivityWithoutChangingText(t *testing.T
type cfg struct {
Payload any `yaml:"payload"`
}
_, err := Load(Bytes("base", []byte("payload: {token: disposable}\n")), Options[cfg]{Definition: Definition[cfg]{Validate: func(c *cfg, v *Validation) {
_, err := testLoad(Bytes("base", []byte("payload: {token: disposable}\n")), Options[cfg]{Definition: Definition[cfg]{Validate: func(c *cfg, v *Validation) {
c.Payload = sensitive{Token: c.Payload.(map[string]any)["token"].(string)}
}}})
var failure *LoadError

View File

@@ -41,7 +41,7 @@ func TestLoadRunsRecordedResolutionWithMapWriteback(t *testing.T) {
},
}
input := "poll_interval: 1m\nartifacts:\n second: {output: ' second '}\n first: {output: ' first '}\n"
result, err := Load(Bytes("resolution.yml", []byte(input)), Options[config]{Definition: definition})
result, err := testLoad(Bytes("resolution.yml", []byte(input)), Options[config]{Definition: definition})
if err != nil {
t.Fatal(err)
}
@@ -94,7 +94,7 @@ func TestResolutionPreservesExplicitNullAndZeroWhileReplacingAbsentBaseline(t *t
Absent int `yaml:"absent"`
}
five := 5
result, err := Load(Bytes("explicit.yml", []byte("optional: null\ncount: 0\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("explicit.yml", []byte("optional: null\ncount: 0\n")), Options[config]{Definition: Definition[config]{
Defaults: func() config { return config{Optional: &five, Count: 5, Absent: 5} },
Resolve: func(configuration *config, resolution *Resolution) error {
if !resolution.IsExplicit(Keys("optional")) || !resolution.IsExplicit(Keys("count")) || resolution.IsExplicit(Keys("absent")) {
@@ -126,7 +126,7 @@ func TestResolutionCoalescesEqualAndRepeatedHelperIntent(t *testing.T) {
Value int `yaml:"value"`
Other int `yaml:"other"`
}
result, err := Load(NoSource(), Options[config]{Definition: Definition[config]{
result, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{
Defaults: func() config { return config{Value: 5} },
Resolve: func(configuration *config, resolution *Resolution) error {
Default(resolution, Keys("value"), &configuration.Value, 5, "equal derived intent", Keys("other"))
@@ -157,7 +157,7 @@ func TestResolutionContainerRecordCoversSubtreeRemovalAndSnapshotsAreDetached(t
type config struct {
Items map[string]item `yaml:"items"`
}
result, err := Load(Bytes("remove.yml", []byte("items:\n keep: {value: 'yes'}\n remove: {value: 'no'}\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("remove.yml", []byte("items:\n keep: {value: 'yes'}\n remove: {value: 'no'}\n")), Options[config]{Definition: Definition[config]{
Resolve: func(configuration *config, resolution *Resolution) error {
delete(configuration.Items, "remove")
resolution.Record(Keys("items"), ChangeNormalization, "removed disabled entries")
@@ -188,7 +188,7 @@ func TestResolutionSequenceReplacementUpdatesDescendantOrigins(t *testing.T) {
type config struct {
Values []int `yaml:"values"`
}
result, err := Load(Bytes("sequence.yml", []byte("values: [1, 2]\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("sequence.yml", []byte("values: [1, 2]\n")), Options[config]{Definition: Definition[config]{
Resolve: func(configuration *config, resolution *Resolution) error {
Normalize(resolution, Keys("values"), &configuration.Values, []int{3}, "replaced values")
return nil
@@ -237,7 +237,7 @@ func TestResolutionRejectsUnrecordedAndMismatchedMutations(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := Load(Bytes("invalid-resolution.yml", []byte("value: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}})
result, err := testLoad(Bytes("invalid-resolution.yml", []byte("value: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}})
assertLoadDiagnostic(t, result, err, test.code)
})
}
@@ -269,7 +269,7 @@ func TestResolutionRejectsInvalidProjectedValuesAndSuppressesReturnedErrors(t *t
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := Load(Bytes("resolution-errors.yml", []byte("value: 1\nnumber: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}})
result, err := testLoad(Bytes("resolution-errors.yml", []byte("value: 1\nnumber: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}})
loadError := assertLoadDiagnostic(t, result, err, test.code)
for _, diagnostic := range loadError.Diagnostics() {
if strings.Contains(diagnostic.Message, "CANARY") {
@@ -291,7 +291,7 @@ func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
filename := writeImportFile(t, t.TempDir(), "config.yml", "declared: file\noptional: null\n")
var savedField FieldInfo
var savedSource SourceInfo
result, err := Load[config](File(filename), Options[config]{Definition: Definition[config]{
result, err := testLoad[config](File(filename), Options[config]{Definition: Definition[config]{
Defaults: func() config { return config{Default: "retained"} },
Resolve: func(_ *config, resolution *Resolution) error {
for _, path := range []Path{Keys("declared"), Keys("default"), Keys("optional")} {
@@ -339,7 +339,7 @@ func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
t.Fatal("saved resolution records alias a completed result")
}
noSource, err := Load[config](NoSource(), Options[config]{Definition: Definition[config]{
noSource, err := testLoad[config](NoSource(), Options[config]{Definition: Definition[config]{
Defaults: func() config { return config{Default: "retained"} },
Resolve: func(_ *config, resolution *Resolution) error {
info, found := resolution.InputInfo(Keys("default"))
@@ -364,7 +364,7 @@ func TestResolutionContextPanicsAfterCallback(t *testing.T) {
Value int `yaml:"value"`
}
var retained *Resolution
if _, err := Load(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
if _, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
retained = resolution
return nil
}}}); err != nil {
@@ -397,7 +397,7 @@ func TestResolutionEntryQueriesRequireAnActiveCallback(t *testing.T) {
requireClosed(t, func() { (&Resolution{}).Source("source-1") })
var successful *Resolution
if _, err := Load(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
if _, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
successful = resolution
return nil
}}}); err != nil {
@@ -407,7 +407,7 @@ func TestResolutionEntryQueriesRequireAnActiveCallback(t *testing.T) {
requireClosed(t, func() { successful.Source("source-1") })
var failed *Resolution
if _, err := Load(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
if _, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
failed = resolution
return errors.New("resolver failed")
}}}); err == nil {
@@ -424,7 +424,7 @@ func TestValidationContextPanicsAfterVersionCallback(t *testing.T) {
Version string `yaml:"version"`
}
var retained *Validation
if _, err := Load(Bytes("version-context.yml", []byte("version: one\n")), Options[config]{Definition: Definition[config]{
if _, err := testLoad(Bytes("version-context.yml", []byte("version: one\n")), Options[config]{Definition: Definition[config]{
Version: &VersionRule{Path: Keys("version"), Check: func(_ Version, validation *Validation) error {
retained = validation
return nil

View File

@@ -29,8 +29,9 @@ const (
)
type schemaDescriptor struct {
root *typeDescriptor
types map[reflect.Type]*typeDescriptor
root *typeDescriptor
types map[reflect.Type]*typeDescriptor
secrets SecretBinding
}
type typeDescriptor struct {
@@ -65,20 +66,39 @@ type schemaIssue struct {
message string
}
// SecretState is the private operation state for one exact-type secret value.
// It is supplied by the facade and never enters inspection records.
type SecretState struct {
Name string
Value string
Specified bool
Resolved bool
}
// SecretBinding adapts the facade-owned exact secret type for one load.
// Bindings are operation data: callers must supply the type and conversions
// together, and a binding never becomes package-global state.
type SecretBinding struct {
Type reflect.Type
Read func(reflect.Value) SecretState
Make func(SecretState) reflect.Value
}
var (
durationType = reflect.TypeFor[time.Duration]()
secretType = reflect.TypeFor[Secret]()
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
)
func buildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
func buildSchema(root reflect.Type, secrets SecretBinding) (*schemaDescriptor, *schemaIssue) {
secrets.validate()
if root == nil || (root.Kind() != reflect.Struct && !isStringMap(root)) {
return nil, &schemaIssue{code: CodeSchemaRoot, message: "configuration root must be a struct or string-keyed map"}
}
builder := schemaBuilder{
cache: make(map[reflect.Type]*typeDescriptor),
building: make(map[reflect.Type]bool),
secrets: secrets,
}
descriptor, issue := builder.describe(root, Path{})
if issue != nil {
@@ -87,12 +107,13 @@ func buildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
if descriptor.kind != descriptorStruct && descriptor.kind != descriptorMap {
return nil, &schemaIssue{code: CodeSchemaRoot, message: "configuration root must be a structural struct or string-keyed map"}
}
return &schemaDescriptor{root: descriptor, types: builder.cache}, nil
return &schemaDescriptor{root: descriptor, types: builder.cache, secrets: secrets}, nil
}
type schemaBuilder struct {
cache map[reflect.Type]*typeDescriptor
building map[reflect.Type]bool
secrets SecretBinding
}
func (b *schemaBuilder) describe(current reflect.Type, path Path) (*typeDescriptor, *schemaIssue) {
@@ -122,7 +143,7 @@ func (b *schemaBuilder) describe(current reflect.Type, path Path) (*typeDescript
descriptor.kind = descriptorDuration
return descriptor, nil
}
if current == secretType {
if b.secrets.Type != nil && current == b.secrets.Type {
descriptor.kind = descriptorSecret
return descriptor, nil
}
@@ -189,6 +210,15 @@ func (b *schemaBuilder) describe(current reflect.Type, path Path) (*typeDescript
return descriptor, nil
}
func (binding SecretBinding) validate() {
if binding.Type == nil {
return
}
if binding.Read == nil || binding.Make == nil {
panic("configkit engine: incomplete secret binding")
}
}
func (b *schemaBuilder) describeFields(descriptor *typeDescriptor, current reflect.Type, path Path) *schemaIssue {
for i := 0; i < current.NumField(); i++ {
field := current.Field(i)

View File

@@ -68,9 +68,9 @@ func TestBuildSchemaDescribesSupportedTypes(t *testing.T) {
private chan int
}
schema, issue := buildSchema(reflect.TypeFor[config]())
schema, issue := testBuildSchema(reflect.TypeFor[config]())
if issue != nil {
t.Fatalf("buildSchema() issue = %#v", issue)
t.Fatalf("testBuildSchema() issue = %#v", issue)
}
root := schema.root
checks := map[string]descriptorKind{
@@ -105,7 +105,7 @@ func TestBuildSchemaDescribesSupportedTypes(t *testing.T) {
if root.inlineMap == nil || root.inlineMap.descriptor.kind != descriptorMap {
t.Fatalf("inline map = %#v", root.inlineMap)
}
if mapped, mapIssue := buildSchema(reflect.TypeFor[map[string]nested]()); mapIssue != nil || mapped.root.kind != descriptorMap {
if mapped, mapIssue := testBuildSchema(reflect.TypeFor[map[string]nested]()); mapIssue != nil || mapped.root.kind != descriptorMap {
t.Fatalf("string-keyed root map = %#v, issue=%#v", mapped, mapIssue)
}
if _, exists := root.fieldByName["ignored"]; exists {
@@ -116,7 +116,7 @@ func TestBuildSchemaDescribesSupportedTypes(t *testing.T) {
func TestBuildSchemaSupportsRecursiveDescriptorsWithoutSharingLoads(t *testing.T) {
t.Parallel()
first, issue := buildSchema(reflect.TypeFor[recursiveConfig]())
first, issue := testBuildSchema(reflect.TypeFor[recursiveConfig]())
if issue != nil {
t.Fatal(issue.message)
}
@@ -128,7 +128,7 @@ func TestBuildSchemaSupportsRecursiveDescriptorsWithoutSharingLoads(t *testing.T
if children.kind != descriptorMap || children.element != first.root {
t.Fatal("recursive map did not refer to the existing descriptor")
}
second, issue := buildSchema(reflect.TypeFor[recursiveConfig]())
second, issue := testBuildSchema(reflect.TypeFor[recursiveConfig]())
if issue != nil {
t.Fatal(issue.message)
}
@@ -207,7 +207,7 @@ func TestBuildSchemaRejectsInvalidRootsTypesTagsAndBindings(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, issue := buildSchema(test.typeOf); issue == nil || issue.code != test.code {
if _, issue := testBuildSchema(test.typeOf); issue == nil || issue.code != test.code {
t.Fatalf("issue = %#v, want %s", issue, test.code)
}
})
@@ -221,7 +221,7 @@ func TestBuildSchemaDoesNotExecuteApplicationMethods(t *testing.T) {
Value panicText `yaml:"value"`
Structural yamlHookStruct `yaml:"structural"`
}
if _, issue := buildSchema(reflect.TypeFor[config]()); issue != nil {
if _, issue := testBuildSchema(reflect.TypeFor[config]()); issue != nil {
t.Fatal(issue.message)
}
}
@@ -261,7 +261,7 @@ func TestResolveSchemaPathUsesContainerSemantics(t *testing.T) {
Dynamic any `yaml:"dynamic"`
Extras map[string]string `yaml:",inline"`
}
schema, issue := buildSchema(reflect.TypeFor[config]())
schema, issue := testBuildSchema(reflect.TypeFor[config]())
if issue != nil {
t.Fatal(issue.message)
}
@@ -310,7 +310,7 @@ func TestInlineMapPointersFailBeforeDefaults(t *testing.T) {
}
for _, nonNil := range []bool{false, true} {
called := false
_, err := Load(NoSource(), Options[cfg]{Definition: Definition[cfg]{Defaults: func() cfg {
_, err := testLoad(NoSource(), Options[cfg]{Definition: Definition[cfg]{Defaults: func() cfg {
called = true
if nonNil {
m := map[string]int{"value": 1}
@@ -330,13 +330,13 @@ func TestInlineMapPointersFailBeforeDefaults(t *testing.T) {
type recursive struct {
Extra pointer `yaml:",inline"`
}
if _, err := Load[recursive](NoSource()); err == nil {
if _, err := testLoad[recursive](NoSource()); err == nil {
t.Fatal("recursive inline pointer was accepted")
}
type double struct {
Extra **map[string]int `yaml:",inline"`
}
if _, err := Load[double](NoSource()); err == nil {
if _, err := testLoad[double](NoSource()); err == nil {
t.Fatal("double inline map pointer was accepted")
}
}

View File

@@ -0,0 +1,107 @@
package engine
import (
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"testing"
)
// Secret is a test-local facade secret. Production engine code must not depend
// on this type; each test load supplies the binding below explicitly.
type Secret struct {
name string
value string
specified bool
resolved bool
}
func SecretEnv(name string) Secret {
return Secret{name: name, specified: true}
}
func (s Secret) EnvName() string {
return s.name
}
func (s Secret) Value() string {
if !s.resolved {
return ""
}
return s.value
}
func (s Secret) IsResolved() bool {
return s.resolved
}
func (s Secret) Format(state fmt.State, _ rune) {
_, _ = io.WriteString(state, "configkit.SecretEnv("+strconv.Quote(s.name)+")")
}
func (s Secret) MarshalJSON() ([]byte, error) {
return json.Marshal(s.name)
}
func testSecretBinding() SecretBinding {
return SecretBinding{
Type: reflect.TypeFor[Secret](),
Read: func(value reflect.Value) SecretState {
secret := value.Interface().(Secret)
return SecretState{Name: secret.name, Value: secret.value, Specified: secret.specified, Resolved: secret.resolved}
},
Make: func(state SecretState) reflect.Value {
return reflect.ValueOf(Secret{name: state.Name, value: state.Value, specified: state.Specified, resolved: state.Resolved})
},
}
}
func testLoad[T any](source Source, supplied ...Options[T]) (*Result[T], error) {
return Load(source, testSecretBinding(), supplied...)
}
func testLoadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
return loadWithSecretLookup(source, testSecretBinding(), lookup, supplied...)
}
func testBuildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
return buildSchema(root, testSecretBinding())
}
func TestSecretBindingsRemainExactAndIndependent(t *testing.T) {
t.Parallel()
type otherSecret struct {
name string
}
other := SecretBinding{
Type: reflect.TypeFor[otherSecret](),
Read: func(value reflect.Value) SecretState {
return SecretState{Name: value.Interface().(otherSecret).name}
},
Make: func(state SecretState) reflect.Value {
return reflect.ValueOf(otherSecret{name: state.Name})
},
}
type config struct {
First Secret `yaml:"first"`
Second otherSecret `yaml:"second"`
}
firstSchema, firstIssue := buildSchema(reflect.TypeFor[config](), testSecretBinding())
if firstIssue != nil {
t.Fatal(firstIssue)
}
secondSchema, secondIssue := buildSchema(reflect.TypeFor[config](), other)
if secondIssue != nil {
t.Fatal(secondIssue)
}
firstField := firstSchema.root.fields[firstSchema.root.fieldByName["first"]].descriptor
firstOther := firstSchema.root.fields[firstSchema.root.fieldByName["second"]].descriptor
secondField := secondSchema.root.fields[secondSchema.root.fieldByName["first"]].descriptor
secondOther := secondSchema.root.fields[secondSchema.root.fieldByName["second"]].descriptor
if firstField.kind != descriptorSecret || firstOther.kind == descriptorSecret || secondField.kind == descriptorSecret || secondOther.kind != descriptorSecret {
t.Fatalf("secret descriptor kinds = %v, %v, %v, %v", firstField.kind, firstOther.kind, secondField.kind, secondOther.kind)
}
}

View File

@@ -2,60 +2,12 @@ package engine
import (
"cmp"
"encoding/json"
"fmt"
"io"
"reflect"
"slices"
"strconv"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
// Secret is an opaque reference to an environment variable. Its name is public
// configuration; its resolved value is available only through Value.
type Secret struct {
name string
value string
specified bool
resolved bool
}
// SecretEnv constructs an unresolved environment-backed secret reference.
// Invalid names are reported if the reference survives a configuration load.
func SecretEnv(name string) Secret {
return Secret{name: name, specified: true}
}
// EnvName returns the public environment-variable name.
func (s Secret) EnvName() string {
return s.name
}
// Value returns the resolved credential, or an empty string when unresolved.
func (s Secret) Value() string {
if !s.resolved {
return ""
}
return s.value
}
// IsResolved reports whether this reference resolved to a nonempty value during
// its configuration load.
func (s Secret) IsResolved() bool {
return s.resolved
}
// Format renders only the public reference name for every formatting verb.
func (s Secret) Format(state fmt.State, _ rune) {
_, _ = io.WriteString(state, "configkit.SecretEnv("+strconv.Quote(s.name)+")")
}
// MarshalJSON emits only the public environment-variable name.
func (s Secret) MarshalJSON() ([]byte, error) {
return json.Marshal(s.name)
}
type secretLookup func(string) (string, bool)
type secretLookupResult struct {
@@ -80,28 +32,28 @@ func resolveSecretReferences(root reflect.Value, schema *schemaDescriptor, input
if lookup == nil {
lookup = func(string) (string, bool) { return "", false }
}
resolveSecretValue(root, schema.root, Path{}, false, input, lookup, &result)
resolveSecretValue(root, schema.root, schema.secrets, Path{}, false, input, lookup, &result)
result.diagnostics = canonicalDiagnostics(result.diagnostics)
return result
}
func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Path, optional bool, input *document.Node, lookup secretLookup, result *secretResolutionResult) {
func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, secrets SecretBinding, path Path, optional bool, input *document.Node, lookup secretLookup, result *secretResolutionResult) {
if descriptor.kind == descriptorPointer {
if value.IsNil() {
return
}
resolveSecretValue(value.Elem(), descriptor.element, path, descriptor.element.kind == descriptorSecret, input, lookup, result)
resolveSecretValue(value.Elem(), descriptor.element, secrets, path, descriptor.element.kind == descriptorSecret, input, lookup, result)
return
}
switch descriptor.kind {
case descriptorSecret:
reference := value.Interface().(Secret)
name := reference.EnvName()
reference := secrets.Read(value)
name := reference.Name
if name == "" {
code := CodeSecretRequired
message := "required secret reference is absent"
if optional || reference.specified {
if optional || reference.Specified {
code = CodeSecretInvalidName
message = "secret environment name is empty"
}
@@ -125,7 +77,7 @@ func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Pa
result.diagnostics = append(result.diagnostics, secretDiagnostic(CodeSecretEmpty, path, "secret environment variable is empty", input))
return
}
value.Set(reflect.ValueOf(Secret{name: name, value: found.value, specified: true, resolved: true}))
value.Set(secrets.Make(SecretState{Name: name, Value: found.value, Specified: true, Resolved: true}))
result.references[path.String()] = name
case descriptorStruct:
for _, member := range descriptor.members {
@@ -133,7 +85,7 @@ func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Pa
if !member.inline {
memberPath = path.Key(member.name)
}
resolveSecretValue(value.Field(member.index), member.descriptor, memberPath, false, input, lookup, result)
resolveSecretValue(value.Field(member.index), member.descriptor, secrets, memberPath, false, input, lookup, result)
}
case descriptorMap:
if value.IsNil() {
@@ -144,7 +96,7 @@ func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Pa
for _, key := range keys {
entry := reflect.New(value.Type().Elem()).Elem()
entry.Set(value.MapIndex(key))
resolveSecretValue(entry, descriptor.element, path.Key(key.String()), false, input, lookup, result)
resolveSecretValue(entry, descriptor.element, secrets, path.Key(key.String()), false, input, lookup, result)
value.SetMapIndex(key, entry)
}
case descriptorSlice, descriptorArray:
@@ -152,7 +104,7 @@ func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Pa
return
}
for index := 0; index < value.Len(); index++ {
resolveSecretValue(value.Index(index), descriptor.element, path.Index(index), false, input, lookup, result)
resolveSecretValue(value.Index(index), descriptor.element, secrets, path.Index(index), false, input, lookup, result)
}
}
}
@@ -165,25 +117,28 @@ func secretDiagnostic(code Code, path Path, message string, input *document.Node
return diagnostic
}
func changedSecretStates(root reflect.Value, descriptor *typeDescriptor, lookups map[string]secretLookupResult) []Path {
func changedSecretStates(root reflect.Value, schema *schemaDescriptor, lookups map[string]secretLookupResult) []Path {
var changed []Path
collectChangedSecretStates(root, descriptor, Path{}, lookups, &changed)
if schema == nil || schema.root == nil {
return nil
}
collectChangedSecretStates(root, schema.root, schema.secrets, Path{}, lookups, &changed)
slices.SortFunc(changed, func(left, right Path) int { return cmp.Compare(left.String(), right.String()) })
return changed
}
func collectChangedSecretStates(value reflect.Value, descriptor *typeDescriptor, path Path, lookups map[string]secretLookupResult, changed *[]Path) {
func collectChangedSecretStates(value reflect.Value, descriptor *typeDescriptor, secrets SecretBinding, path Path, lookups map[string]secretLookupResult, changed *[]Path) {
if descriptor.kind == descriptorPointer {
if !value.IsNil() {
collectChangedSecretStates(value.Elem(), descriptor.element, path, lookups, changed)
collectChangedSecretStates(value.Elem(), descriptor.element, secrets, path, lookups, changed)
}
return
}
switch descriptor.kind {
case descriptorSecret:
reference := value.Interface().(Secret)
expected, ok := lookups[reference.EnvName()]
if ok && (!expected.present || expected.value == "" || !reference.resolved || reference.value != expected.value) {
reference := secrets.Read(value)
expected, ok := lookups[reference.Name]
if ok && (!expected.present || expected.value == "" || !reference.Resolved || reference.Value != expected.value) {
*changed = append(*changed, path.clone())
}
case descriptorStruct:
@@ -192,7 +147,7 @@ func collectChangedSecretStates(value reflect.Value, descriptor *typeDescriptor,
if !member.inline {
memberPath = path.Key(member.name)
}
collectChangedSecretStates(value.Field(member.index), member.descriptor, memberPath, lookups, changed)
collectChangedSecretStates(value.Field(member.index), member.descriptor, secrets, memberPath, lookups, changed)
}
case descriptorMap:
if value.IsNil() {
@@ -201,14 +156,14 @@ func collectChangedSecretStates(value reflect.Value, descriptor *typeDescriptor,
keys := value.MapKeys()
slices.SortFunc(keys, func(left, right reflect.Value) int { return cmp.Compare(left.String(), right.String()) })
for _, key := range keys {
collectChangedSecretStates(value.MapIndex(key), descriptor.element, path.Key(key.String()), lookups, changed)
collectChangedSecretStates(value.MapIndex(key), descriptor.element, secrets, path.Key(key.String()), lookups, changed)
}
case descriptorSlice, descriptorArray:
if descriptor.kind == descriptorSlice && value.IsNil() {
return
}
for index := 0; index < value.Len(); index++ {
collectChangedSecretStates(value.Index(index), descriptor.element, path.Index(index), lookups, changed)
collectChangedSecretStates(value.Index(index), descriptor.element, secrets, path.Index(index), lookups, changed)
}
}
}

View File

@@ -51,7 +51,7 @@ sequence: [SHARED_SECRET, SEQUENCE_SECRET]
return value, present
}
validationCalled := false
result, err := loadWithSecretLookup(Bytes("secrets.yml", []byte(input)), lookup, Options[secretReferenceConfig]{
result, err := testLoadWithSecretLookup(Bytes("secrets.yml", []byte(input)), lookup, Options[secretReferenceConfig]{
Definition: Definition[secretReferenceConfig]{Validate: func(configuration *secretReferenceConfig, validation *Validation) {
validationCalled = true
if configuration.Required.Value() != "shared-value" || !configuration.Required.IsResolved() ||
@@ -62,7 +62,7 @@ sequence: [SHARED_SECRET, SEQUENCE_SECRET]
}},
})
if err != nil {
t.Fatalf("loadWithSecretLookup() error = %v", err)
t.Fatalf("testLoadWithSecretLookup() error = %v", err)
}
if !validationCalled || result.Config.Optional == nil || result.Config.Pointers["nil"] != nil {
t.Fatalf("resolved config = %#v", result.Config)
@@ -94,7 +94,7 @@ func TestSecretReferenceErrorsAreSafeAndPathSpecific(t *testing.T) {
if lookup == nil {
lookup = func(string) (string, bool) { return "", false }
}
_, err := loadWithSecretLookup(Bytes("secret-errors.yml", []byte(test.input)), lookup, Options[secretReferenceConfig]{})
_, err := testLoadWithSecretLookup(Bytes("secret-errors.yml", []byte(test.input)), lookup, Options[secretReferenceConfig]{})
loadError := requireSecretLoadCode(t, err, test.code)
diagnostic := loadError.Diagnostics()[0]
if diagnostic.Path.String() != "/required" || strings.Contains(fmt.Sprintf("%v %#v", err, err), "CREDENTIAL-CANARY") {
@@ -107,9 +107,9 @@ func TestSecretReferenceErrorsAreSafeAndPathSpecific(t *testing.T) {
type config struct {
Optional *Secret `yaml:"optional"`
}
_, err := loadWithSecretLookup(Bytes("optional.yml", []byte("optional: ''\n")), func(string) (string, bool) { return "", false }, Options[config]{})
_, err := testLoadWithSecretLookup(Bytes("optional.yml", []byte("optional: ''\n")), func(string) (string, bool) { return "", false }, Options[config]{})
requireSecretLoadCode(t, err, CodeSecretInvalidName)
result, err := loadWithSecretLookup(Bytes("optional.yml", []byte("optional: null\n")), func(string) (string, bool) { return "", false }, Options[config]{})
result, err := testLoadWithSecretLookup(Bytes("optional.yml", []byte("optional: null\n")), func(string) (string, bool) { return "", false }, Options[config]{})
if err != nil || result.Config.Optional != nil {
t.Fatalf("nil optional result = %#v, %v", result, err)
}
@@ -120,7 +120,7 @@ func TestSecretReferenceErrorsAreSafeAndPathSpecific(t *testing.T) {
Secrets []Secret `yaml:"secrets"`
}
lookups := 0
_, err := loadWithSecretLookup(Bytes("missing-twice.yml", []byte("secrets: [MISSING_SHARED, MISSING_SHARED]\n")), func(string) (string, bool) {
_, err := testLoadWithSecretLookup(Bytes("missing-twice.yml", []byte("secrets: [MISSING_SHARED, MISSING_SHARED]\n")), func(string) (string, bool) {
lookups++
return "", false
}, Options[config]{})
@@ -138,7 +138,7 @@ func TestResolutionSelectsEffectiveSecretNameBeforeLookup(t *testing.T) {
}
lookedUp := []string{}
validated := ""
result, err := loadWithSecretLookup(Bytes("resolved-secret.yml", []byte("name: SELECTED_SECRET\nsecret: OLD_SECRET\n")), func(name string) (string, bool) {
result, err := testLoadWithSecretLookup(Bytes("resolved-secret.yml", []byte("name: SELECTED_SECRET\nsecret: OLD_SECRET\n")), func(name string) (string, bool) {
lookedUp = append(lookedUp, name)
return "resolved-value", true
}, Options[config]{Definition: Definition[config]{
@@ -151,7 +151,7 @@ func TestResolutionSelectsEffectiveSecretNameBeforeLookup(t *testing.T) {
},
}})
if err != nil {
t.Fatalf("loadWithSecretLookup() error = %v", err)
t.Fatalf("testLoadWithSecretLookup() error = %v", err)
}
if !reflect.DeepEqual(lookedUp, []string{"SELECTED_SECRET"}) || validated != "resolved-value" || result.Config.Secret.EnvName() != "SELECTED_SECRET" {
t.Fatalf("lookups = %#v, validated = %q, config = %#v", lookedUp, validated, result.Config)
@@ -172,7 +172,7 @@ func TestResolutionInputQueriesDoNotReadOrResolveSecrets(t *testing.T) {
reader := &resolutionQueryReader{input: strings.NewReader("value: source\nsecret: QUERY_SECRET\n")}
lookups := 0
var callbackErr error
result, err := loadWithSecretLookup(Reader("query.yml", reader), func(name string) (string, bool) {
result, err := testLoadWithSecretLookup(Reader("query.yml", reader), func(name string) (string, bool) {
lookups++
if name != "QUERY_SECRET" {
t.Fatalf("secret lookup name = %q", name)
@@ -210,7 +210,7 @@ func TestValidationCannotReplaceResolvedSecretWithSameName(t *testing.T) {
type config struct {
Secret Secret `yaml:"secret"`
}
_, err := loadWithSecretLookup(Bytes("validator.yml", []byte("secret: VALIDATOR_SECRET\n")), func(string) (string, bool) {
_, err := testLoadWithSecretLookup(Bytes("validator.yml", []byte("secret: VALIDATOR_SECRET\n")), func(string) (string, bool) {
return "CREDENTIAL-CANARY", true
}, Options[config]{Definition: Definition[config]{Validate: func(configuration *config, validation *Validation) {
configuration.Secret = SecretEnv(configuration.Secret.EnvName())
@@ -225,13 +225,13 @@ func TestResolvedDefaultsAreRebuiltAndLaterLoadsRelookup(t *testing.T) {
type config struct {
Secret Secret `yaml:"secret"`
}
first, err := loadWithSecretLookup(NoSource(), func(string) (string, bool) { return "first-value", true }, Options[config]{
first, err := testLoadWithSecretLookup(NoSource(), func(string) (string, bool) { return "first-value", true }, Options[config]{
Definition: Definition[config]{Defaults: func() config { return config{Secret: SecretEnv("DEFAULT_SECRET")} }},
})
if err != nil {
t.Fatal(err)
}
second, err := loadWithSecretLookup(NoSource(), func(string) (string, bool) { return "second-value", true }, Options[config]{
second, err := testLoadWithSecretLookup(NoSource(), func(string) (string, bool) { return "second-value", true }, Options[config]{
Definition: Definition[config]{Defaults: func() config { return first.Config }},
})
if err != nil {
@@ -243,14 +243,14 @@ func TestResolvedDefaultsAreRebuiltAndLaterLoadsRelookup(t *testing.T) {
t.Setenv("CONFIGKIT_RELOOKUP_SECRET", "one")
options := Options[config]{Definition: Definition[config]{Defaults: func() config { return config{Secret: SecretEnv("CONFIGKIT_RELOOKUP_SECRET")} }}}
realFirst, err := Load(NoSource(), options)
realFirst, err := testLoad(NoSource(), options)
if err != nil {
t.Fatal(err)
}
if err := os.Setenv("CONFIGKIT_RELOOKUP_SECRET", "two"); err != nil {
t.Fatal(err)
}
realSecond, err := Load(NoSource(), options)
realSecond, err := testLoad(NoSource(), options)
if err != nil {
t.Fatal(err)
}
@@ -276,7 +276,7 @@ func TestSecretSafeFormattingJSONProjectionAndHistory(t *testing.T) {
type config struct {
Secret Secret `yaml:"secret"`
}
result, err := loadWithSecretLookup(Bytes("safe-secret.yml", []byte("secret: SAFE_SECRET_NAME\n")), func(string) (string, bool) {
result, err := testLoadWithSecretLookup(Bytes("safe-secret.yml", []byte("secret: SAFE_SECRET_NAME\n")), func(string) (string, bool) {
return credential, true
}, Options[config]{})
if err != nil {
@@ -317,7 +317,7 @@ func TestBootstrapExistingEmptyWinsAndReferenceFails(t *testing.T) {
type config struct {
Secret Secret `yaml:"secret"`
}
_, err = Load[config](Bytes("blocked.yml", []byte("secret: "+name+"\n")))
_, err = testLoad[config](Bytes("blocked.yml", []byte("secret: "+name+"\n")))
requireSecretLoadCode(t, err, CodeSecretEmpty)
if value, present := os.LookupEnv(name); !present || value != "" {
t.Fatalf("environment changed to %q, %v", value, present)
@@ -339,7 +339,7 @@ func TestDynamicValuesDoNotEstablishSecretReferences(t *testing.T) {
}
}
lookups := 0
result, err := loadWithSecretLookup(Bytes("dynamic.yml", []byte("dynamic: ORDINARY_STRING\n")), func(string) (string, bool) {
result, err := testLoadWithSecretLookup(Bytes("dynamic.yml", []byte("dynamic: ORDINARY_STRING\n")), func(string) (string, bool) {
lookups++
return "unexpected", true
}, Options[config]{})
@@ -354,9 +354,9 @@ func TestLoadDoesNotMutateSecretEnvironment(t *testing.T) {
type config struct {
Secret Secret `yaml:"secret"`
}
result, err := Load[config](Bytes("unchanged.yml", []byte("secret: "+name+"\n")))
result, err := testLoad[config](Bytes("unchanged.yml", []byte("secret: "+name+"\n")))
if err != nil || result.Config.Secret.Value() != "original" {
t.Fatalf("Load() = %#v, %v", result, err)
t.Fatalf("testLoad() = %#v, %v", result, err)
}
if got := os.Getenv(name); got != "original" {
t.Fatalf("environment = %q", got)

View File

@@ -26,7 +26,7 @@ func TestNonregularSourcesDoNotBlock(t *testing.T) {
case "profile":
opts.Profile = "fifo"
}
_, err := Load(source, opts)
_, err := testLoad(source, opts)
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("expected safe load error, got %v", err)

View File

@@ -110,7 +110,7 @@ func runValidation[T any](configuration *T, validator func(*T, *Validation), sch
return canonicalDiagnostics(state.diagnostics)
}
state.diagnostics = append(state.diagnostics, validationValueMutations(before, after)...)
for _, path := range changedSecretStates(reflect.ValueOf(*configuration), schema.root, lookups) {
for _, path := range changedSecretStates(reflect.ValueOf(*configuration), schema, lookups) {
state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(path))
}
return canonicalDiagnostics(state.diagnostics)

View File

@@ -14,7 +14,7 @@ func TestValidationCollectsSchemaAwareReportsAndNearestLocations(t *testing.T) {
Missing string `yaml:"missing"`
Dynamic map[string]any `yaml:"dynamic"`
}
result, err := Load(Bytes("validation.yml", []byte("primary: configured\ndynamic: {present: 1}\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("validation.yml", []byte("primary: configured\ndynamic: {present: 1}\n")), Options[config]{Definition: Definition[config]{
Validate: func(_ *config, validation *Validation) {
validation.Error(Keys("missing"), "is required", Keys("primary"))
validation.Error(Keys("dynamic", "missing"), "dynamic entry is required")
@@ -58,7 +58,7 @@ func TestValidationRejectsUnknownStructPaths(t *testing.T) {
type config struct {
Known string `yaml:"known"`
}
result, err := Load(Bytes("validation-path.yml", []byte("known: value\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("validation-path.yml", []byte("known: value\n")), Options[config]{Definition: Definition[config]{
Validate: func(_ *config, validation *Validation) {
validation.Error(Keys("unknown"), "must be configured")
},
@@ -73,7 +73,7 @@ func TestValidationMutationFailsWithoutPublishingResult(t *testing.T) {
Value int `yaml:"value"`
Items map[string]int `yaml:"items"`
}
result, err := Load(Bytes("mutation.yml", []byte("value: 1\nitems: {one: 1}\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("mutation.yml", []byte("value: 1\nitems: {one: 1}\n")), Options[config]{Definition: Definition[config]{
Validate: func(configuration *config, _ *Validation) {
configuration.Value = 2
configuration.Items["one"] = 2
@@ -127,7 +127,7 @@ func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
validation.Warning(Keys("value"), "validation warning")
},
}
result, err := Load(Bytes("order.yml", []byte("version: one\n")), Options[config]{Definition: definition})
result, err := testLoad(Bytes("order.yml", []byte("version: one\n")), Options[config]{Definition: definition})
if err != nil {
t.Fatal(err)
}
@@ -148,7 +148,7 @@ func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
Resolve: func(_ *config, _ *Resolution) error { events = append(events, "resolve"); return nil },
Validate: func(_ *config, _ *Validation) { events = append(events, "validate") },
}
result, err := Load(Bytes("order-error.yml", []byte("version: one\n")), Options[config]{Definition: definition})
result, err := testLoad(Bytes("order-error.yml", []byte("version: one\n")), Options[config]{Definition: definition})
assertLoadDiagnostic(t, result, err, CodeVersionError)
if !reflect.DeepEqual(events, []string{"version"}) {
t.Fatalf("events = %#v", events)
@@ -157,7 +157,7 @@ func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
t.Run("resolution error stops validation", func(t *testing.T) {
var validated bool
result, err := Load(Bytes("resolution-order.yml", []byte("value: 1\nversion: one\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("resolution-order.yml", []byte("value: 1\nversion: one\n")), Options[config]{Definition: Definition[config]{
Resolve: func(_ *config, resolution *Resolution) error {
resolution.Error(Keys("value"), "cannot resolve")
return nil
@@ -172,7 +172,7 @@ func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
t.Run("decode error stops application hooks", func(t *testing.T) {
var resolved, validated bool
result, err := Load(Bytes("decode-order.yml", []byte("value: wrong\nversion: one\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("decode-order.yml", []byte("value: wrong\nversion: one\n")), Options[config]{Definition: Definition[config]{
Resolve: func(_ *config, _ *Resolution) error { resolved = true; return nil },
Validate: func(_ *config, _ *Validation) { validated = true },
}})
@@ -190,7 +190,7 @@ func TestCompleteSingleSourceLifecycleKeepsMetadataAgreement(t *testing.T) {
Input int `yaml:"input"`
Derived int `yaml:"derived"`
}
result, err := Load(Bytes("complete.yml", []byte("input: 3\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad(Bytes("complete.yml", []byte("input: 3\n")), Options[config]{Definition: Definition[config]{
Defaults: func() config { return config{Derived: 1} },
Resolve: func(configuration *config, resolution *Resolution) error {
Default(resolution, Keys("derived"), &configuration.Derived, configuration.Input*2, "derived from input", Keys("input"))

View File

@@ -34,10 +34,10 @@ type valueOperation struct {
}
func (o *valueOperation) dynamicDescriptor(current reflect.Type, path Path) (*typeDescriptor, *projectionIssue) {
if current == secretType {
if o.schema.secrets.Type != nil && current == o.schema.secrets.Type {
return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration values cannot establish secret-reference semantics"}
}
builder := schemaBuilder{cache: o.schema.types, building: make(map[reflect.Type]bool)}
builder := schemaBuilder{cache: o.schema.types, building: make(map[reflect.Type]bool), secrets: o.schema.secrets}
descriptor, issue := builder.describe(current, path)
if issue != nil {
return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration value has an unsupported type"}

View File

@@ -15,7 +15,7 @@ func TestVersionPathsShareCanonicalSequenceSemantics(t *testing.T) {
t.Fatal(err)
}
var checked Version
result, err := Load[config](Bytes("version.yml", []byte("versions: ["+value+"]\n")), Options[config]{Definition: Definition[config]{
result, err := testLoad[config](Bytes("version.yml", []byte("versions: ["+value+"]\n")), Options[config]{Definition: Definition[config]{
Version: &VersionRule{Path: path, Check: func(v Version, _ *Validation) error { checked = v; return nil }},
}})
want := Version{}
@@ -49,7 +49,7 @@ func TestVersionPathsPreserveLiteralMappingKeysAndSegmentIntent(t *testing.T) {
if err != nil {
t.Fatal(err)
}
result, err := Load[map[string]string](Bytes("version.yml", []byte("'"+key+"': 4\n")), Options[map[string]string]{Definition: Definition[map[string]string]{
result, err := testLoad[map[string]string](Bytes("version.yml", []byte("'"+key+"': 4\n")), Options[map[string]string]{Definition: Definition[map[string]string]{
Version: &VersionRule{Path: path, Check: func(v Version, _ *Validation) error {
if !v.Present || v.Text != "4" {
t.Errorf("checked = %#v", v)
@@ -77,7 +77,7 @@ func TestVersionPathsPreserveLiteralMappingKeysAndSegmentIntent(t *testing.T) {
type config struct {
Versions any `yaml:"versions"`
}
_, err := Load[config](Bytes("version.yml", []byte(test.input)), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: test.path, Check: func(v Version, validation *Validation) error {
_, err := testLoad[config](Bytes("version.yml", []byte(test.input)), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: test.path, Check: func(v Version, validation *Validation) error {
if v.Present {
t.Errorf("wrong segment kind found version: %#v", v)
}