Add facade lifecycle wrappers

This commit is contained in:
2026-09-12 16:16:11 +00:00
parent b8935e77bd
commit da307c9f49
8 changed files with 458 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ package configkit_test
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"
@@ -22,7 +23,12 @@ func TestFacadeValuesRemainRootOwned(t *testing.T) {
for _, candidate := range []reflect.Type{
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](),
reflect.TypeFor[configkit.LoadError](),
reflect.TypeFor[configkit.LoadError](), reflect.TypeFor[configkit.Metadata](),
reflect.TypeFor[configkit.Resolution](), reflect.TypeFor[configkit.Validation](),
reflect.TypeFor[configkit.Version](), reflect.TypeFor[configkit.VersionRule](),
reflect.TypeFor[configkit.FilesystemContext](), reflect.TypeFor[configkit.Override](),
reflect.TypeFor[configkit.Definition[struct{}]](), reflect.TypeFor[configkit.Options[struct{}]](),
reflect.TypeFor[configkit.Result[struct{}]](),
} {
if candidate.PkgPath() != packagePath {
t.Fatalf("type %s belongs to %q", candidate, candidate.PkgPath())
@@ -41,3 +47,106 @@ func TestFacadeValuesRemainRootOwned(t *testing.T) {
t.Fatalf("zero LoadError = %q", got)
}
}
func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
t.Setenv("FACADE_SECRET_NAME", "credential")
type configuration struct {
Version string `yaml:"version"`
Input int `yaml:"input"`
Derived int `yaml:"derived"`
Secret configkit.Secret `yaml:"secret"`
Labels map[string]string `yaml:"labels"`
}
var retainedResolution *configkit.Resolution
var retainedValidation *configkit.Validation
versionCalls, defaultsCalls, resolveCalls, validateCalls := 0, 0, 0, 0
definition := configkit.Definition[configuration]{
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(version configkit.Version, validation *configkit.Validation) error {
versionCalls++
retainedValidation = validation
if !version.Present || version.Text != "1" {
validation.Error(configkit.Keys("version"), "unsupported version")
}
return nil
}},
Defaults: func() configuration {
defaultsCalls++
return configuration{Derived: 1, Labels: map[string]string{"default": "kept"}}
},
Resolve: func(configuration *configuration, resolution *configkit.Resolution) error {
resolveCalls++
retainedResolution = resolution
if !resolution.IsExplicit(configkit.Keys("input")) {
resolution.Error(configkit.Keys("input"), "input is required")
}
input, ok := resolution.InputInfo(configkit.Keys("input"))
if !ok || !input.Explicit {
resolution.Error(configkit.Keys("input"), "missing input provenance")
}
source, ok := resolution.Source("source-1")
if !ok || source.Label != "facade.yml" {
resolution.Error(configkit.Keys("input"), "missing source")
}
configkit.Default(resolution, configkit.Keys("derived"), &configuration.Derived, configuration.Input*2, "derived from input", configkit.Keys("input"))
return nil
},
Validate: func(configuration *configuration, validation *configkit.Validation) {
validateCalls++
retainedValidation = validation
if !configuration.Secret.IsResolved() || configuration.Secret.Value() != "credential" {
validation.Error(configkit.Keys("secret"), "secret did not resolve")
}
},
}
if versionCalls != 0 || defaultsCalls != 0 || resolveCalls != 0 || validateCalls != 0 {
t.Fatal("callbacks ran while constructing options")
}
result, err := configkit.Load(configkit.Bytes("facade.yml", []byte("version: 1\ninput: 3\nsecret: FACADE_SECRET_NAME\n")), configkit.Options[configuration]{Definition: definition})
if err != nil {
t.Fatal(err)
}
if versionCalls != 1 || defaultsCalls != 1 || resolveCalls != 1 || validateCalls != 1 || result.Config.Derived != 6 {
t.Fatalf("callback counts/configuration = %d/%d/%d/%d %#v", versionCalls, defaultsCalls, resolveCalls, validateCalls, result.Config)
}
derived, ok := result.Metadata().Lookup(configkit.Keys("derived"))
if !ok || derived.Explicit || derived.Origin.Operation != configkit.ChangeDerivedDefault {
t.Fatalf("derived metadata = %#v, %v", derived, ok)
}
if sources := result.Sources(); len(sources) != 1 || sources[0].Label != "facade.yml" {
t.Fatalf("sources = %#v", sources)
}
if result.Diagnostics() != nil {
t.Fatalf("diagnostics = %#v", result.Diagnostics())
}
for _, context := range []func(){
func() { retainedResolution.IsExplicit(configkit.Keys("input")) },
func() { retainedValidation.Warning(configkit.Keys("input"), "too late") },
} {
func() {
defer func() {
if recover() == nil {
t.Fatal("retained callback context remained active")
}
}()
context()
}()
}
}
func TestFacadeLoadReturnsFacadeLoadError(t *testing.T) {
t.Parallel()
type configuration struct {
Value int `yaml:"value"`
}
result, err := configkit.Load[configuration](configkit.Bytes("invalid.yml", []byte("value: wrong\n")))
if result != nil || err == nil {
t.Fatalf("result, err = %#v, %v", result, err)
}
var loadError *configkit.LoadError
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeDecodeType {
t.Fatalf("facade error = %#v", err)
}
}