392 lines
16 KiB
Go
392 lines
16 KiB
Go
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
|
|
// SequenceIdentities declares the scalar fields used to combine records and
|
|
// resolve record inheritance at selected sequence destinations. Registration
|
|
// errors are reported before source acquisition. Before successful return,
|
|
// matching final sequences are also checked, including values from defaults,
|
|
// CLI overrides, and application resolution. Schema and validation govern
|
|
// path existence and shape; later caller mutations are outside this guarantee.
|
|
SequenceIdentities []SequenceIdentity
|
|
}
|
|
|
|
// PlacementMode selects how an input mapping is placed at its Destination.
|
|
// The zero value, PlacementModeMapping, preserves ordinary mapping placement.
|
|
// Unknown values and sequence-item placement at the root fail option validation.
|
|
type PlacementMode uint8
|
|
|
|
const (
|
|
// PlacementModeMapping places a source mapping at Destination.
|
|
PlacementModeMapping PlacementMode = iota
|
|
// PlacementModeSequenceItem wraps a source mapping as one item at Destination.
|
|
PlacementModeSequenceItem
|
|
)
|
|
|
|
// SourceVersionMode controls schema-version declarations across one complete
|
|
// load. The zero value preserves the existing main-only effective-path check
|
|
// and its protected-path behavior. Enabled modes check participating documents
|
|
// at source-local Definition.Version.Path coordinates and consume accepted
|
|
// non-main declarations. See the shared source-version contract in docs/config.md.
|
|
type SourceVersionMode uint8
|
|
|
|
const (
|
|
// SourceVersionModeDefault preserves the existing main-version check and
|
|
// rejects version-path contributions from imported, underlay, and overlay
|
|
// payloads.
|
|
SourceVersionModeDefault SourceVersionMode = iota
|
|
// SourceVersionModeIfPresent checks each participating document that has a
|
|
// declaration. A missing main declaration still reaches the checker; an
|
|
// absent non-main declaration skips it. Accepted non-main declarations are
|
|
// consumed before composition.
|
|
SourceVersionModeIfPresent
|
|
// SourceVersionModeRequired requires a declaration in each participating
|
|
// document. NoSource retains the existing absent-main checker behavior.
|
|
// Accepted non-main declarations are consumed before composition.
|
|
SourceVersionModeRequired
|
|
)
|
|
|
|
// ImportDefaults supplies load-wide defaults for enabled import declarations.
|
|
// Each field applies independently and an explicit declaration value takes
|
|
// precedence. Nil boolean fields are unspecified and leave the library defaults
|
|
// in effect: Required false, Follow true, Hidden true, and Recursive false.
|
|
// Pattern applies to directory searches; nil leaves filenames unrestricted,
|
|
// while a non-nil empty slice selects none. Defaults apply to main, underlay,
|
|
// and transitive imports, not selected overlays. Load calls snapshot the
|
|
// pointed-to booleans and Pattern slice before acquiring sources.
|
|
type ImportDefaults struct {
|
|
// Required defaults absence handling. For path, true requires every named
|
|
// file; for dir and glob, true requires at least one selected file.
|
|
Required *bool
|
|
// Follow controls discovered symbolic links for directory and glob search.
|
|
Follow *bool
|
|
// Hidden controls discovery of hidden files and directories.
|
|
Hidden *bool
|
|
// Recursive controls descendant traversal for directory searches.
|
|
Recursive *bool
|
|
// Pattern replaces the library's unrestricted filename matching for directory
|
|
// searches unless a declaration supplies its own pattern.
|
|
Pattern []string
|
|
}
|
|
|
|
// Options contains the application definition and per-load input selections.
|
|
// Imports enables traversal of declarations in a selected main document; an
|
|
// absent source has no main declarations to traverse. Overlay selects one fixed
|
|
// overlay. Overrides contain only values explicitly supplied by an application CLI.
|
|
// Enabled source-version modes interpret Definition.Version.Path in each
|
|
// document's local coordinates. Placement projects checks and main protection
|
|
// through the input destination; item placement adds the source's item
|
|
// coordinate to that projection. See docs/config.md for the full contract.
|
|
type Options[T any] struct {
|
|
Definition Definition[T]
|
|
// Destination places the ordinary source beneath literal mapping keys. Its
|
|
// zero value means the application root. Invalid paths fail before acquisition.
|
|
Destination Path
|
|
// PlacementMode defaults to mapping placement; unknown values and root item
|
|
// placement fail before acquisition.
|
|
PlacementMode PlacementMode
|
|
Imports bool
|
|
// ImportDefaults supplies one load-wide default policy for every enabled
|
|
// declaration, including transitive imports. Each field is independently
|
|
// overridden by an explicit declaration value.
|
|
ImportDefaults ImportDefaults
|
|
Overlay string
|
|
Filesystem *FilesystemContext
|
|
Overrides []Override
|
|
SourceVersionMode SourceVersionMode
|
|
}
|
|
|
|
// FilesystemContext supplies roots for imports and overlays. With FS nil,
|
|
// BaseDir and Root use absolute host paths. For NoSource, Root is required and
|
|
// BaseDir may be omitted to use Root as the import base. With FS set, BaseDir
|
|
// and Root use slash-relative paths in that namespace ("." denotes its root).
|
|
// File sources derive BaseDir from the selected file; a native file's explicit
|
|
// Root limits its scope.
|
|
type FilesystemContext struct {
|
|
BaseDir string
|
|
Root string
|
|
FS *Filesystem
|
|
}
|
|
|
|
// 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.
|
|
// Default source-version mode interprets Path as the existing effective path;
|
|
// enabled modes interpret it in each participating document's local coordinates.
|
|
// At the selected path, strings and strict decimal integers supply their exact
|
|
// spelling as Version.Text. See docs/config.md for the complete contract.
|
|
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 overlay. After source precedence, built-in sibling
|
|
// `extends` relationships resolve before defaults and typed decoding. 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...)
|
|
return fromEngineLoaded(loaded, err)
|
|
}
|
|
|
|
func fromEngineLoaded[T any](loaded *engine.Loaded[T], err error) (*Result[T], error) {
|
|
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 direct or inherited input defined path.
|
|
func (r *Resolution) IsExplicit(path Path) bool {
|
|
return r.engineContext().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.engineContext().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.engineContext().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.engineContext().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.engineContext().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.engineContext().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.engineContext(), 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.engineContext(), 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.engineContext().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.engineContext().Warning(toEnginePath(path), message, toEnginePaths(related)...)
|
|
}
|
|
|
|
// Pass nil through so the engine remains the owner of the callback guard.
|
|
func (r *Resolution) engineContext() *engine.Resolution {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return r.value
|
|
}
|
|
|
|
func (v *Validation) engineContext() *engine.Validation {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return v.value
|
|
}
|
|
|
|
func toEngineOptions[T any](options Options[T]) engine.Options[T] {
|
|
result := engine.Options[T]{
|
|
Definition: toEngineDefinition(options.Definition),
|
|
Imports: options.Imports,
|
|
Overlay: options.Overlay,
|
|
SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)),
|
|
}
|
|
result = engine.OptionsWithPlacement(result, toEnginePath(options.Destination.clone()), uint8(options.PlacementMode))
|
|
result = engine.OptionsWithImportDefaults(result, toEngineImportDefaults(options.ImportDefaults))
|
|
if options.Filesystem != nil {
|
|
result.Filesystem = toEngineFilesystemContext(options.Filesystem)
|
|
}
|
|
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 toEngineFilesystemContext(context *FilesystemContext) *engine.FilesystemContext {
|
|
if context == nil {
|
|
return nil
|
|
}
|
|
result := &engine.FilesystemContext{BaseDir: context.BaseDir, Root: context.Root}
|
|
if context.FS != nil {
|
|
filesystem := context.FS.value
|
|
result.FS = &filesystem
|
|
}
|
|
return result
|
|
}
|
|
|
|
func toEngineDefinition[T any](definition Definition[T]) engine.Definition[T] {
|
|
result := engine.Definition[T]{Defaults: definition.Defaults}
|
|
if definition.SequenceIdentities != nil {
|
|
registrations := make([]engine.SequenceIdentityRegistrationInput, len(definition.SequenceIdentities))
|
|
for index, identity := range definition.SequenceIdentities {
|
|
registrations[index] = toEngineSequenceIdentity(identity)
|
|
}
|
|
result = engine.DefinitionWithSequenceIdentityRegistrations(result, registrations)
|
|
}
|
|
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)}
|
|
if rule.Check != nil {
|
|
result.Version.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
|
|
}
|