Add internal engine relocation candidate
This commit is contained in:
185
internal/engine/load.go
Normal file
185
internal/engine/load.go
Normal file
@@ -0,0 +1,185 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user