package configkit import ( "errors" "go.maximumdirect.net/configkit/internal/engine" ) // Definition supplies the defaults and application rules used when loading T. // Treat a definition as read-only after sharing it. Sensitive is a pure predicate // that can mark additional configuration paths for hiding in inspection output. type Definition[T any] struct { // Defaults returns starting values for settings omitted from input. Defaults func() T // Resolve derives or normalizes values and records those changes. Resolve func(*T, *Resolution) error // Validate reports application errors and warnings without changing values. Validate func(*T, *Validation) // Version chooses a version path and the application's acceptance rule. Version *VersionRule // Sensitive adds paths to those already hidden by configkit:"secret" tags. Sensitive 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 ( // SourceVersionModeMainOnly preserves the existing main-version check and // rejects version-path contributions from imported, underlay, and overlay // payloads. SourceVersionModeMainOnly 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, FollowSymlinks true, IncludeHidden 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 // FollowSymlinks controls discovered symbolic links for directory and glob search. FollowSymlinks *bool // IncludeHidden controls discovery of hidden files and directories. IncludeHidden *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 and requires filesystem context for NoSource. 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 lets a resolver inspect supplied values and record derived defaults // or normalization. Its input queries describe the state at callback entry, // even after the resolver changes the configuration. Use the context only during // the callback; do not retain it or use it concurrently. 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 lets an application report value-free errors and warnings during // a version or validation callback. Use it only during the callback; do not // retain the context or use it concurrently. type Validation struct{ value *engine.Validation } // Result contains a successful load's configuration and saved inspection state. // The application owns Config and may change it. Inspection continues to describe // the completed load; it does not follow later changes to Config. Separate loads // return independently owned results. type Result[T any] struct { Config T inspection *engine.Inspection } // Load reads the selected input and returns a configuration of type T with // inspection metadata. NoSource deliberately omits the main document. Enabled // imports and one selected overlay participate in loading. Sources combine first, // then sibling extends relationships resolve before defaults and strict typed // decoding. Load accepts at most one Options value and returns a nil result when // any error is reported. See docs/config.md for the complete processing order. 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 provides queries for final input presence, value origins, and history. // Records returned by its queries are copies that cannot change the saved load. func (r *Result[T]) Metadata() Metadata { if r == nil || r.inspection == nil { return Metadata{} } return Metadata{value: r.inspection.Metadata()} } // Sources returns a copied list of sources used by the load. Changing the list // or its nested records cannot change later queries. 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 } // Source returns a detached record for an acquired source in this load. IDs are // local to the load; the lookup does not reacquire the source or access the // filesystem. Empty or unknown IDs return false. func (r *Result[T]) Source(id string) (SourceInfo, bool) { if r == nil || r.inspection == nil { return SourceInfo{}, false } value, ok := r.inspection.Source(id) if !ok { return SourceInfo{}, false } return fromEngineSourceInfo(value), true } // 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)) } // EntryInfo returns detached entry-state metadata for path. The entry state is // the decoded configuration, including retained static defaults and CLI // assignments, immediately before resolution starts. It is fixed while // resolution records mutate the working configuration; unknown or // shape-mismatched paths return false. // // EntryInfo is valid only during the resolution callback. See the configuration // contract for source and provenance semantics. func (r *Resolution) EntryInfo(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 } // RecordDerivedDefault records a derived-default intent for a resolver mutation // already performed by application code. A container path covers mutations to // its descendants. This does not assign a value or suppress work for explicit // input. func (r *Resolution) RecordDerivedDefault(path Path, reason string, dependencies ...Path) { r.engineContext().RecordDerivedDefault(toEnginePath(path), reason, toEnginePaths(dependencies)...) } // RecordNormalization records a normalization intent for a resolver mutation // already performed by application code. A container path covers mutations to // its descendants. This does not assign a value. func (r *Resolution) RecordNormalization(path Path, reason string, dependencies ...Path) { r.engineContext().RecordNormalization(toEnginePath(path), 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)), ImportDefaults: toEngineImportDefaults(options.ImportDefaults), Destination: toEnginePath(options.Destination.clone()), PlacementMode: engine.PlacementMode(options.PlacementMode), } if options.Filesystem != nil { result.Filesystem = toEngineFilesystemContext(options.Filesystem) } result.Overrides = toEngineOverrides(options.Overrides) 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.Sensitive != nil { result.Secret = func(path engine.Path) bool { return definition.Sensitive(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 }