186 lines
6.2 KiB
Go
186 lines
6.2 KiB
Go
package engine
|
|
|
|
import (
|
|
"os"
|
|
"reflect"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
// 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 {
|
|
state *resolutionState
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Result is a successful independently owned configuration load.
|
|
type Result[T any] struct {
|
|
Config T
|
|
|
|
metadata Metadata
|
|
sources []SourceInfo
|
|
diagnostics []Diagnostic
|
|
baseline *document.Node
|
|
input *document.Node
|
|
inputOrigins map[string]Origin
|
|
final *document.Node
|
|
resolution []resolutionSnapshot
|
|
applications []historyApplication
|
|
secrets map[string]bool
|
|
secretReferences map[string]string
|
|
}
|
|
|
|
// Metadata returns detached effective presence, origin, and history records.
|
|
func (r *Result[T]) Metadata() Metadata {
|
|
if r == nil {
|
|
return Metadata{}
|
|
}
|
|
return cloneMetadata(r.metadata)
|
|
}
|
|
|
|
// Sources returns a detached list of sources used by the load.
|
|
func (r *Result[T]) Sources() []SourceInfo {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return cloneSourceInfos(r.sources)
|
|
}
|
|
|
|
// Diagnostics returns detached nonfatal diagnostics from the load.
|
|
func (r *Result[T]) Diagnostics() []Diagnostic {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return cloneDiagnostics(r.diagnostics)
|
|
}
|
|
|
|
// 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 loadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
|
|
accumulator := diagnosticAccumulator{}
|
|
options, diagnostics := selectOptions(source, supplied)
|
|
accumulator.add(diagnostics...)
|
|
if len(diagnostics) != 0 {
|
|
return nil, accumulator.loadError()
|
|
}
|
|
|
|
input, diagnostics := assembleBaseInput(source, options)
|
|
accumulator.sources = input.sources
|
|
accumulator.add(diagnostics...)
|
|
if len(diagnostics) != 0 {
|
|
return nil, accumulator.loadError()
|
|
}
|
|
schema, schemaIssue := buildSchema(reflect.TypeFor[T]())
|
|
if schemaIssue != nil {
|
|
accumulator.add(diagnosticFromSchemaIssue(schemaIssue))
|
|
return nil, accumulator.loadError()
|
|
}
|
|
versionDiagnostics := checkVersion(options.Definition.Version, input.document)
|
|
accumulator.add(versionDiagnostics...)
|
|
if hasErrors(versionDiagnostics) {
|
|
return nil, accumulator.loadError()
|
|
}
|
|
input, diagnostics = applyInputLayers(source, options, input)
|
|
accumulator.sources = input.sources
|
|
accumulator.add(diagnostics...)
|
|
if len(diagnostics) != 0 {
|
|
return nil, accumulator.loadError()
|
|
}
|
|
|
|
decoded, diagnostics := decodeLoadInput(schema, options.Definition, input)
|
|
accumulator.add(diagnostics...)
|
|
if len(diagnostics) != 0 {
|
|
return nil, accumulator.loadError()
|
|
}
|
|
resolved, diagnostics := resolveLoadInput(schema, options.Definition, input, decoded)
|
|
accumulator.add(diagnostics...)
|
|
if hasErrors(diagnostics) {
|
|
return nil, accumulator.loadError()
|
|
}
|
|
if !historyNodesWithinLimit(decoded.baseline, decoded.final, resolved.final, resolved.applications, resolved.snapshots) {
|
|
accumulator.add(Diagnostic{Code: CodeExplainLimitExceeded, Severity: SeverityError, Message: "retained configuration history exceeds the node limit"})
|
|
return nil, accumulator.loadError()
|
|
}
|
|
|
|
// Detach resolver-borrowed values before reference mutation or validation.
|
|
configuration, ownershipIssue := detachLoadConfiguration(schema, resolved.configuration, cloneUnresolvedSecrets)
|
|
if ownershipIssue != nil {
|
|
accumulator.add(*ownershipIssue)
|
|
return nil, accumulator.loadError()
|
|
}
|
|
secretResolution := resolveSecretReferences(reflect.ValueOf(&configuration).Elem(), schema, input.document, lookup)
|
|
accumulator.add(secretResolution.diagnostics...)
|
|
if hasErrors(secretResolution.diagnostics) {
|
|
return nil, accumulator.loadError()
|
|
}
|
|
if options.Definition.Validate != nil {
|
|
diagnostics = runValidation(&configuration, options.Definition.Validate, schema, resolved.final, input.document, secretResolution.lookups)
|
|
accumulator.add(diagnostics...)
|
|
if hasErrors(diagnostics) {
|
|
return nil, accumulator.loadError()
|
|
}
|
|
}
|
|
resolved.configuration = configuration
|
|
// Publication detaches equal-valued validator replacements, preserving the
|
|
// validated secret state, then finalizes metadata and sensitivity.
|
|
return publishLoadResult(schema, options.Definition, resolved, secretResolution.references, &accumulator)
|
|
}
|