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.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, 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)} 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 }