Add facade lifecycle wrappers
This commit is contained in:
3
.codebase-memory/.gitattributes
vendored
Normal file
3
.codebase-memory/.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Auto-generated by codebase-memory-mcp
|
||||||
|
# Prevent merge conflicts on compressed artifact
|
||||||
|
graph.db.zst merge=ours binary
|
||||||
11
.codebase-memory/artifact.json
Normal file
11
.codebase-memory/artifact.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 2,
|
||||||
|
"commit": "b8935e77bd5ac1dc347339966fbaba5de9af5348",
|
||||||
|
"indexed_at": "2026-09-12T16:16:09Z",
|
||||||
|
"project": "home-eric-Workspace-ConfigKit",
|
||||||
|
"nodes": 2447,
|
||||||
|
"edges": 13587,
|
||||||
|
"original_size": 11206656,
|
||||||
|
"compressed_size": 1701304,
|
||||||
|
"compression_level": 3
|
||||||
|
}
|
||||||
BIN
.codebase-memory/graph.db.zst
Normal file
BIN
.codebase-memory/graph.db.zst
Normal file
Binary file not shown.
@@ -382,7 +382,7 @@ lifecycle and callback wrappers to the split engine result.
|
|||||||
|
|
||||||
## Stage 6 — Wire candidate lifecycle and callback wrappers
|
## Stage 6 — Wire candidate lifecycle and callback wrappers
|
||||||
|
|
||||||
Status: not started.
|
Status: complete (2026-09-12).
|
||||||
|
|
||||||
1. Add candidate Definition, Options, VersionRule, Version, FilesystemContext,
|
1. Add candidate Definition, Options, VersionRule, Version, FilesystemContext,
|
||||||
Override, Resolution, and Validation with unchanged public declarations.
|
Override, Resolution, and Validation with unchanged public declarations.
|
||||||
@@ -404,6 +404,23 @@ and detached entry provenance stays fixed. Run
|
|||||||
`go test ./internal/facade ./internal/engine -run 'Load|Resolution|Validation|Version|Metadata|Secret|Callback|Facade'`,
|
`go test ./internal/facade ./internal/engine -run 'Load|Resolution|Validation|Version|Metadata|Secret|Callback|Facade'`,
|
||||||
`go test -race ./internal/facade ./internal/engine`, and the everyday check.
|
`go test -race ./internal/facade ./internal/engine`, and the everyday check.
|
||||||
|
|
||||||
|
### Completion
|
||||||
|
|
||||||
|
Added the temporary facade's root-owned lifecycle declarations, option conversion,
|
||||||
|
live callback wrappers, `Load`, `Result`, and `Metadata`. Callback wrappers carry
|
||||||
|
only the active engine context during callback execution; `Result` stores the
|
||||||
|
published configuration once and keeps its non-generic engine inspection private.
|
||||||
|
All record and path crossings use explicit conversion, and facade loads translate
|
||||||
|
engine load failures to the root-owned `LoadError` type.
|
||||||
|
|
||||||
|
External candidate tests exercise public version, default, resolution, validation,
|
||||||
|
metadata, source, and secret behavior, including inactive retained contexts and
|
||||||
|
typed load errors. Linux validation with Go 1.26.7 passed
|
||||||
|
`go test ./internal/facade ./internal/engine -run 'Load|Resolution|Validation|Version|Metadata|Secret|Callback|Facade'`,
|
||||||
|
`go test -race ./internal/facade ./internal/engine`, and `./scripts/check.sh`.
|
||||||
|
There were no deviations or remaining risks. The next handoff completes the
|
||||||
|
candidate inspection and secrets-bootstrap surface.
|
||||||
|
|
||||||
## Stage 7 — Complete candidate inspection and bootstrap surface
|
## Stage 7 — Complete candidate inspection and bootstrap surface
|
||||||
|
|
||||||
Status: not started.
|
Status: not started.
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ func (e *LoadError) Diagnostics() []Diagnostic {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
values := e.value.Diagnostics()
|
values := e.value.Diagnostics()
|
||||||
|
if values == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
result := make([]Diagnostic, len(values))
|
result := make([]Diagnostic, len(values))
|
||||||
for i := range values {
|
for i := range values {
|
||||||
result[i] = fromEngineDiagnostic(values[i])
|
result[i] = fromEngineDiagnostic(values[i])
|
||||||
@@ -31,6 +34,9 @@ func (e *LoadError) Sources() []SourceInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
values := e.value.Sources()
|
values := e.value.Sources()
|
||||||
|
if values == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
result := make([]SourceInfo, len(values))
|
result := make([]SourceInfo, len(values))
|
||||||
for i := range values {
|
for i := range values {
|
||||||
result[i] = fromEngineSourceInfo(values[i])
|
result[i] = fromEngineSourceInfo(values[i])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package configkit_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -22,7 +23,12 @@ func TestFacadeValuesRemainRootOwned(t *testing.T) {
|
|||||||
for _, candidate := range []reflect.Type{
|
for _, candidate := range []reflect.Type{
|
||||||
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
|
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
|
||||||
reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](),
|
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 {
|
if candidate.PkgPath() != packagePath {
|
||||||
t.Fatalf("type %s belongs to %q", candidate, candidate.PkgPath())
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
254
internal/facade/lifecycle.go
Normal file
254
internal/facade/lifecycle.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Definition contains reusable application rules for a configuration type.
|
||||||
|
// Secret is a pure predicate that can add sensitivity to semantic paths.
|
||||||
|
// Definitions should be treated as read-only after being shared.
|
||||||
|
type Definition[T any] struct {
|
||||||
|
Defaults func() T
|
||||||
|
Resolve func(*T, *Resolution) error
|
||||||
|
Validate func(*T, *Validation)
|
||||||
|
Version *VersionRule
|
||||||
|
Secret func(Path) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options contains the application definition and per-load input selections.
|
||||||
|
// Imports enables explicit path and glob imports; Profile selects one fixed profile
|
||||||
|
// overlay; Overrides contains only values explicitly supplied by an application CLI.
|
||||||
|
type Options[T any] struct {
|
||||||
|
Definition Definition[T]
|
||||||
|
Imports bool
|
||||||
|
Profile string
|
||||||
|
Filesystem *FilesystemContext
|
||||||
|
Overrides []Override
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemContext supplies filesystem roots for imports and profiles.
|
||||||
|
type FilesystemContext struct {
|
||||||
|
BaseDir string
|
||||||
|
Root string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override describes one explicitly supplied application override. Value uses
|
||||||
|
// parser-neutral built-in scalars, nil, []any, or map[string]any; Flag is a safe
|
||||||
|
// display name and must not contain a raw argument or value.
|
||||||
|
type Override struct {
|
||||||
|
Path Path
|
||||||
|
Value any
|
||||||
|
Flag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolution exposes entry-state input provenance, surviving-input presence, and
|
||||||
|
// audited application resolution changes. A resolver must not retain or
|
||||||
|
// concurrently use the context.
|
||||||
|
type Resolution struct{ value *engine.Resolution }
|
||||||
|
|
||||||
|
// VersionRule declares the input path and application checker for a schema version.
|
||||||
|
// At that path, string and decimal-integer input is supplied as Version.Text.
|
||||||
|
type VersionRule struct {
|
||||||
|
Path Path
|
||||||
|
Check func(Version, *Validation) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version preserves the absent, null, and textual states of a schema-version input.
|
||||||
|
// Text is the exact spelling of accepted string or decimal-integer input.
|
||||||
|
type Version struct {
|
||||||
|
Present bool
|
||||||
|
Null bool
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation collects safe application diagnostics during a supported callback.
|
||||||
|
// A callback must not retain the context or use it concurrently.
|
||||||
|
type Validation struct{ value *engine.Validation }
|
||||||
|
|
||||||
|
// Result is a successful independently owned configuration load.
|
||||||
|
type Result[T any] struct {
|
||||||
|
Config T
|
||||||
|
|
||||||
|
inspection *engine.Inspection
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
options := make([]engine.Options[T], len(supplied))
|
||||||
|
for index := range supplied {
|
||||||
|
options[index] = toEngineOptions(supplied[index])
|
||||||
|
}
|
||||||
|
loaded, err := engine.Load(source.toEngine(), secretBinding(), options...)
|
||||||
|
if err != nil {
|
||||||
|
var loadError *engine.LoadError
|
||||||
|
if errors.As(err, &loadError) {
|
||||||
|
return nil, &LoadError{value: loadError}
|
||||||
|
}
|
||||||
|
return nil, &LoadError{}
|
||||||
|
}
|
||||||
|
return &Result[T]{Config: loaded.Config, inspection: loaded.Inspection}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns detached effective presence, origin, and history records.
|
||||||
|
func (r *Result[T]) Metadata() Metadata {
|
||||||
|
if r == nil || r.inspection == nil {
|
||||||
|
return Metadata{}
|
||||||
|
}
|
||||||
|
return Metadata{value: r.inspection.Metadata()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sources returns a detached list of sources used by the load.
|
||||||
|
func (r *Result[T]) Sources() []SourceInfo {
|
||||||
|
if r == nil || r.inspection == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
values := r.inspection.Sources()
|
||||||
|
if values == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]SourceInfo, len(values))
|
||||||
|
for index := range values {
|
||||||
|
result[index] = fromEngineSourceInfo(values[index])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnostics returns detached nonfatal diagnostics from the load.
|
||||||
|
func (r *Result[T]) Diagnostics() []Diagnostic {
|
||||||
|
if r == nil || r.inspection == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
values := r.inspection.Diagnostics()
|
||||||
|
if values == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]Diagnostic, len(values))
|
||||||
|
for index := range values {
|
||||||
|
result[index] = fromEngineDiagnostic(values[index])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExplicit reports whether surviving input explicitly defined path.
|
||||||
|
func (r *Resolution) IsExplicit(path Path) bool { return r.value.IsExplicit(toEnginePath(path)) }
|
||||||
|
|
||||||
|
// InputInfo returns detached entry-state metadata for path. The entry state is
|
||||||
|
// the decoded configuration, including retained static defaults, immediately
|
||||||
|
// before resolution starts. It is fixed while resolution records mutate the
|
||||||
|
// working configuration; unknown or shape-mismatched paths return false.
|
||||||
|
//
|
||||||
|
// InputInfo is valid only during the resolution callback. See the configuration
|
||||||
|
// contract for source and provenance semantics.
|
||||||
|
func (r *Resolution) InputInfo(path Path) (FieldInfo, bool) {
|
||||||
|
value, ok := r.value.InputInfo(toEnginePath(path))
|
||||||
|
return fromEngineFieldInfo(value), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source returns a detached record for a source acquired by this load. Empty or
|
||||||
|
// unknown IDs return false and do not perform input, filesystem, or credential
|
||||||
|
// access. A source path is present only when that acquired source has one.
|
||||||
|
//
|
||||||
|
// Source is valid only during the resolution callback. See the configuration
|
||||||
|
// contract for source and provenance semantics.
|
||||||
|
func (r *Resolution) Source(id string) (SourceInfo, bool) {
|
||||||
|
value, ok := r.value.Source(id)
|
||||||
|
return fromEngineSourceInfo(value), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record declares the intent for a resolver mutation. A container path covers
|
||||||
|
// mutations to its descendants.
|
||||||
|
func (r *Resolution) Record(path Path, kind ChangeKind, reason string, dependencies ...Path) {
|
||||||
|
r.value.Record(toEnginePath(path), engine.ChangeKind(kind), reason, toEnginePaths(dependencies)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error reports a safe, value-free resolution error at path.
|
||||||
|
func (r *Resolution) Error(path Path, message string, related ...Path) {
|
||||||
|
r.value.Error(toEnginePath(path), message, toEnginePaths(related)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning reports a safe, value-free resolution warning at path.
|
||||||
|
func (r *Resolution) Warning(path Path, message string, related ...Path) {
|
||||||
|
r.value.Warning(toEnginePath(path), message, toEnginePaths(related)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default assigns and records value only when path was absent from surviving
|
||||||
|
// input. Static defaults are not considered explicit.
|
||||||
|
func Default[V any](resolution *Resolution, path Path, target *V, value V, reason string, dependencies ...Path) {
|
||||||
|
engine.Default(resolution.value, toEnginePath(path), target, value, reason, toEnginePaths(dependencies)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize assigns and records value regardless of input presence.
|
||||||
|
func Normalize[V any](resolution *Resolution, path Path, target *V, value V, reason string, dependencies ...Path) {
|
||||||
|
engine.Normalize(resolution.value, toEnginePath(path), target, value, reason, toEnginePaths(dependencies)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error reports a value-free application error at path. Related paths are
|
||||||
|
// resolved to source locations when possible.
|
||||||
|
func (v *Validation) Error(path Path, message string, related ...Path) {
|
||||||
|
v.value.Error(toEnginePath(path), message, toEnginePaths(related)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning reports a value-free application warning at path. Related paths are
|
||||||
|
// resolved to source locations when possible.
|
||||||
|
func (v *Validation) Warning(path Path, message string, related ...Path) {
|
||||||
|
v.value.Warning(toEnginePath(path), message, toEnginePaths(related)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toEngineOptions[T any](options Options[T]) engine.Options[T] {
|
||||||
|
result := engine.Options[T]{
|
||||||
|
Definition: toEngineDefinition(options.Definition),
|
||||||
|
Imports: options.Imports,
|
||||||
|
Profile: options.Profile,
|
||||||
|
}
|
||||||
|
if options.Filesystem != nil {
|
||||||
|
result.Filesystem = &engine.FilesystemContext{BaseDir: options.Filesystem.BaseDir, Root: options.Filesystem.Root}
|
||||||
|
}
|
||||||
|
if options.Overrides != nil {
|
||||||
|
result.Overrides = make([]engine.Override, len(options.Overrides))
|
||||||
|
for index := range options.Overrides {
|
||||||
|
result.Overrides[index] = engine.Override{
|
||||||
|
Path: toEnginePath(options.Overrides[index].Path), Value: options.Overrides[index].Value, Flag: options.Overrides[index].Flag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func toEngineDefinition[T any](definition Definition[T]) engine.Definition[T] {
|
||||||
|
result := engine.Definition[T]{Defaults: definition.Defaults}
|
||||||
|
if definition.Resolve != nil {
|
||||||
|
result.Resolve = func(configuration *T, resolution *engine.Resolution) error {
|
||||||
|
return definition.Resolve(configuration, &Resolution{value: resolution})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if definition.Validate != nil {
|
||||||
|
result.Validate = func(configuration *T, validation *engine.Validation) {
|
||||||
|
definition.Validate(configuration, &Validation{value: validation})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if definition.Version != nil {
|
||||||
|
rule := definition.Version
|
||||||
|
result.Version = &engine.VersionRule{Path: toEnginePath(rule.Path), Check: func(version engine.Version, validation *engine.Validation) error {
|
||||||
|
return rule.Check(Version{Present: version.Present, Null: version.Null, Text: version.Text}, &Validation{value: validation})
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
if definition.Secret != nil {
|
||||||
|
result.Secret = func(path engine.Path) bool { return definition.Secret(fromEnginePath(path)) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func toEnginePaths(paths []Path) []engine.Path {
|
||||||
|
if paths == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]engine.Path, len(paths))
|
||||||
|
for index := range paths {
|
||||||
|
result[index] = toEnginePath(paths[index])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
56
internal/facade/metadata.go
Normal file
56
internal/facade/metadata.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||||
|
|
||||||
|
// Metadata provides detached effective presence/origin and chronological history
|
||||||
|
// queries for a load. Its zero value contains no paths.
|
||||||
|
type Metadata struct{ value engine.Metadata }
|
||||||
|
|
||||||
|
// Lookup returns information for an effective path. Unknown and invalid paths
|
||||||
|
// return false.
|
||||||
|
func (m Metadata) Lookup(path Path) (FieldInfo, bool) {
|
||||||
|
value, ok := m.value.Lookup(toEnginePath(path))
|
||||||
|
return fromEngineFieldInfo(value), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExplicit reports whether a surviving input contribution explicitly defined
|
||||||
|
// path.
|
||||||
|
func (m Metadata) IsExplicit(path Path) bool {
|
||||||
|
return m.value.IsExplicit(toEnginePath(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// History returns detached definition and resolution records for path. A record's
|
||||||
|
// Path can be an ancestor when that container operation replaced or removed path.
|
||||||
|
// Removed historical paths can have history even though Lookup returns false.
|
||||||
|
// Unknown and invalid paths return nil.
|
||||||
|
func (m Metadata) History(path Path) []Change {
|
||||||
|
values := m.value.History(toEnginePath(path))
|
||||||
|
if values == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]Change, len(values))
|
||||||
|
for index := range values {
|
||||||
|
result[index] = fromEngineChange(values[index])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoricalPaths returns the paths observed during loading, including effective
|
||||||
|
// paths and paths removed by overlays or recorded resolution. It includes default
|
||||||
|
// paths and the root, but not paths that never existed. The zero Metadata returns
|
||||||
|
// an empty slice.
|
||||||
|
//
|
||||||
|
// Paths are detached, unique by canonical JSON Pointer, and sorted by that
|
||||||
|
// spelling. Like ParsePath results, numeric tokens are resolved contextually;
|
||||||
|
// one pointer can identify a mapping key in an earlier layer and a sequence index
|
||||||
|
// in a later one. Use History to inspect definitions and Lookup to check whether
|
||||||
|
// a path remains effective. Enumeration exposes no configuration values and does
|
||||||
|
// not perform another load or resolve secrets.
|
||||||
|
func (m Metadata) HistoricalPaths() []Path {
|
||||||
|
values := m.value.HistoricalPaths()
|
||||||
|
result := make([]Path, len(values))
|
||||||
|
for index := range values {
|
||||||
|
result[index] = fromEnginePath(values[index])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user