package configkit 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) }