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; Overlay selects one fixed overlay; // Overrides contains only values explicitly supplied by an application CLI. type Options[T any] struct { Definition Definition[T] Imports bool Overlay string Filesystem *FilesystemContext Overrides []Override SourceVersionMode sourceVersionMode } // FilesystemContext supplies filesystem roots for imports and overlays. 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 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 } // Loaded is a short-lived generic carrier for a successful configuration load. // Inspection is frozen independently of Config and does not retain application values. type Loaded[T any] struct { Config T Inspection *Inspection } // Inspection owns frozen load snapshots and detached query state. type Inspection struct { metadata Metadata sources []SourceInfo contexts map[string]ResourceContext diagnostics []Diagnostic baseline *document.Node input *document.Node inputOrigins map[string]Origin final *document.Node immutableLookup *documentLookup resolution []resolutionSnapshot resolutionReplay []resolutionSnapshot applications []historyApplication inheritance []inheritanceOperation secrets map[string]bool secretReferences map[string]string } // Metadata returns detached effective presence, origin, and history records. func (r *Inspection) Metadata() Metadata { if r == nil { return Metadata{} } return cloneMetadata(r.metadata) } // Sources returns a detached list of sources used by the load. func (r *Inspection) Sources() []SourceInfo { if r == nil { return nil } return cloneSourceInfos(r.sources) } // SourceContext returns a passive filesystem view for an acquired source. // Unknown IDs and sources without a filesystem context return false. func (r *Inspection) SourceContext(id string) (ResourceContext, bool) { if r == nil { return ResourceContext{}, false } context, ok := r.contexts[id] return context, ok } // Diagnostics returns detached nonfatal diagnostics from the load. func (r *Inspection) 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 overlay. It accepts at most one Options value and // returns no result when any error is reported. func Load[T any](source Source, secrets SecretBinding, supplied ...Options[T]) (*Loaded[T], error) { return loadWithSecretLookup(source, secrets, os.LookupEnv, supplied...) } func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup secretLookup, supplied ...Options[T]) (*Loaded[T], error) { options, diagnostics := selectOptions(source, supplied) if len(diagnostics) != 0 { return nil, (&diagnosticAccumulator{diagnostics: diagnostics}).loadError() } request := loadRequest[T]{ main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem}, definition: options.Definition, overlay: options.Overlay, overrides: options.Overrides, ordinary: true, sourceVersionMode: options.SourceVersionMode, effectiveVersion: effectiveSourceVersionRule(options.Definition.Version, Path{}, options.SourceVersionMode), } return executeLoadRequest(request, secrets, lookup) } func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup) (*Loaded[T], error) { accumulator := diagnosticAccumulator{} accounting := &importAccounting{} mainOptions := Options[T]{Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem} mainOptions.SourceVersionMode = request.sourceVersionMode mainOptions.Definition.Version = request.protectionVersion() mainOptions.Overrides = request.overrides if request.ordinary { mainOptions.Overlay = request.overlay } var schema *schemaDescriptor if request.sourceVersionMode != sourceVersionModeDefault { var schemaIssue *schemaIssue schema, schemaIssue = buildSchema(reflect.TypeFor[T](), secrets) if schemaIssue != nil { accumulator.add(diagnosticFromSchemaIssue(schemaIssue)) return nil, accumulator.loadError() } } versionState := &sourceVersionState{ localRule: request.definition.Version, effectiveRule: request.protectionVersion(), mode: request.sourceVersionMode, } input, diagnostics := assembleBaseInputTrackedWithVersion(request.main.source, mainOptions, request.main.destination, accounting, nil, nil, versionState) accumulator.sources = input.sources accumulator.add(versionState.diagnostics...) reportedVersionDiagnostics := len(versionState.diagnostics) accumulator.add(diagnostics...) if hasErrors(diagnostics) { return nil, accumulator.loadError() } if schema == nil { var schemaIssue *schemaIssue schema, schemaIssue = buildSchema(reflect.TypeFor[T](), secrets) if schemaIssue != nil { accumulator.add(diagnosticFromSchemaIssue(schemaIssue)) return nil, accumulator.loadError() } } if request.sourceVersionMode == sourceVersionModeDefault { versionDiagnostics := checkVersion(request.definition.Version, input.document) accumulator.add(versionDiagnostics...) if hasErrors(versionDiagnostics) { return nil, accumulator.loadError() } } if request.ordinary { input, diagnostics = applyInputLayers(request.main.source, mainOptions, input) } else { input, diagnostics = assembleLayeredInput(request, input, accounting) } accumulator.add(versionState.diagnostics[reportedVersionDiagnostics:]...) accumulator.sources = input.sources accumulator.add(diagnostics...) if len(diagnostics) != 0 { return nil, accumulator.loadError() } if request.ordinary && input.parsed.source != nil { baseSources := make([]SourceInfo, 0, len(input.sources)) for _, source := range input.sources { if source.Layer == LayerBase { baseSources = append(baseSources, source) } } if len(baseSources) != 0 && input.contexts[baseSources[0].ID].FS == nil { contexts, diagnostic := contextsForAcquiredSources(request.main.source, request.main.filesystem, input.parsed.source, baseSources) if diagnostic != nil { accumulator.add(*diagnostic) return nil, accumulator.loadError() } mergeResourceContexts(&input.contexts, contexts) } } if input.document != nil && containsInheritanceDirective(input.document) { composition, problems := compositionForInput(input) if len(problems) != 0 { accumulator.add(problems...) return nil, accumulator.loadError() } expanded, problems := expandInheritanceWithOrigins(composition.root, composition.origins, defaultProjectionLimits()) if len(problems) != 0 { accumulator.add(problems...) return nil, accumulator.loadError() } if problem := inheritanceVersionDiagnostic(request.protectionVersion(), expanded); problem != nil { accumulator.add(*problem) return nil, accumulator.loadError() } input.composition = composition input.inheritance = expanded input.document = expanded.root } decodeDefinition := request.definition decodeDefinition.Version = request.protectionVersion() decoded, diagnostics := decodeLoadInput(schema, decodeDefinition, input) accumulator.add(diagnostics...) if len(diagnostics) != 0 { return nil, accumulator.loadError() } resolved, diagnostics := resolveLoadInput(schema, request.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, input.inheritance) { 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 request.definition.Validate != nil { diagnostics = runValidation(&configuration, request.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, request.definition, resolved, secretResolution.references, &accumulator) }