Document and demonstrate native and layered configuration loading
This commit is contained in:
26
README.md
26
README.md
@@ -5,11 +5,13 @@ An opinionated Go library for applications that use YAML configuration files.
|
|||||||
Linux is release-validated; macOS support is best-effort. See the
|
Linux is release-validated; macOS support is best-effort. See the
|
||||||
[platform support policy](docs/policy/release.md#platform-support) for details.
|
[platform support policy](docs/policy/release.md#platform-support) for details.
|
||||||
|
|
||||||
ConfigKit's initial loader accepts one strict YAML
|
ConfigKit's simple `Load` accepts one strict YAML source (or explicit
|
||||||
source (or explicit defaults-only input), optionally traverses explicit path and glob
|
defaults-only input). `LoadLayered` additionally combines explicit lower-priority
|
||||||
imports, strictly composes their disjoint definitions, applies typed defaults, and
|
source groups with an independently selected main input. Sources can be host
|
||||||
can overlay one selected fixed overlay and check an application schema version. It
|
files, native `fs.FS` files, labeled bytes or readers, or snapshotted mappings.
|
||||||
returns detached metadata and safe diagnostics.
|
Enabled path and glob imports compose strictly within their group; precedence
|
||||||
|
between groups combines mappings recursively. Both entry points return a complete,
|
||||||
|
validated configuration with detached metadata and safe diagnostics.
|
||||||
Applications may perform audited defaulting and normalization through a resolver,
|
Applications may perform audited defaulting and normalization through a resolver,
|
||||||
then report domain errors and warnings through read-only semantic validation. Secret
|
then report domain errors and warnings through read-only semantic validation. Secret
|
||||||
tags and a path predicate drive deterministic redacted effective YAML. Typed secret
|
tags and a path predicate drive deterministic redacted effective YAML. Typed secret
|
||||||
@@ -50,19 +52,19 @@ for completion and ownership details.
|
|||||||
Within a configuration tier, independent contributions compose strictly: shared
|
Within a configuration tier, independent contributions compose strictly: shared
|
||||||
mappings combine, while overlapping non-mapping definitions are errors. Between
|
mappings combine, while overlapping non-mapping definitions are errors. Between
|
||||||
precedence tiers, mappings combine recursively, retaining omitted lower-priority
|
precedence tiers, mappings combine recursively, retaining omitted lower-priority
|
||||||
children; other values replace lower-priority values. Today, the main document
|
children; other values replace lower-priority values. The main document and its
|
||||||
and its imports form the strictly composed base, with a selected overlay and
|
imports form one strictly composed tier. Explicit underlay groups sit below it
|
||||||
explicit CLI overrides above it and typed defaults below it. The
|
and above typed defaults; a selected overlay and explicit CLI overrides sit
|
||||||
[configuration contract](docs/config.md#strict-document-composition) owns the
|
above it. The [configuration contract](docs/config.md#strict-document-composition)
|
||||||
detailed combination rules and input-specific constraints.
|
owns the detailed combination rules and input-specific constraints.
|
||||||
|
|
||||||
The [architecture goals](docs/policy/architecture.md#success-criteria) explain
|
The [architecture goals](docs/policy/architecture.md#success-criteria) explain
|
||||||
these priorities and how genuine new needs can justify extending the shared
|
these priorities and how genuine new needs can justify extending the shared
|
||||||
model. The [deferred scope](docs/roadmap/deferred.md) records deliberate exclusions.
|
model. The [deferred scope](docs/roadmap/deferred.md) records deliberate exclusions.
|
||||||
|
|
||||||
The [configuration contract](docs/config.md) describes implemented behavior. The
|
The [configuration contract](docs/config.md) describes implemented behavior. The
|
||||||
[consumer adoption guide](docs/consumers/README.md) provides maintained Narratio-,
|
[consumer adoption guide](docs/consumers/README.md) provides maintained simple
|
||||||
Notarius-, WeatherReporter-, and Seriatim-shaped examples and migration guidance.
|
native-filesystem and layered examples alongside the migration shapes.
|
||||||
See the [development guide](docs/development.md) for repository orientation and the
|
See the [development guide](docs/development.md) for repository orientation and the
|
||||||
[release procedure](docs/release.md) for maintainer-only candidate and publication
|
[release procedure](docs/release.md) for maintainer-only candidate and publication
|
||||||
steps.
|
steps.
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
Proposed draft for review. The
|
Proposed draft for review. The
|
||||||
[filesystem sources and underlays roadmap](../roadmap/sources-and-layering.md)
|
[filesystem sources and underlays roadmap](../roadmap/sources-and-layering.md)
|
||||||
owns implementation status, detailed target behavior, and remaining API choices.
|
records the target and implementation progress; the
|
||||||
|
[configuration contract](../config.md#layered-loading) owns current behavior.
|
||||||
|
|
||||||
This extends [ADR 0001](0001-separate-composition-and-overlay.md) and preserves
|
This extends [ADR 0001](0001-separate-composition-and-overlay.md) and preserves
|
||||||
its distinction between strict composition and intentional overlay.
|
its distinction between strict composition and intentional overlay.
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
|
|
||||||
Proposed draft for review. The
|
Proposed draft for review. The
|
||||||
[configuration contract](../config.md#completion-and-resource-lifetime) describes
|
[configuration contract](../config.md#completion-and-resource-lifetime) describes
|
||||||
the implemented loading lifecycle. The
|
the implemented loading lifecycle and
|
||||||
[source-backed underlays roadmap](../roadmap/sources-and-layering.md) owns its
|
[source resource contexts](../config.md#source-resource-contexts).
|
||||||
application to the planned source and resource-context APIs.
|
|
||||||
|
|
||||||
## Date
|
## Date
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ have index zero. Explicit empty groups and `NoSource` consume no source ID.
|
|||||||
Byte, source, node, and glob-entry budgets are shared across the load, while
|
Byte, source, node, and glob-entry budgets are shared across the load, while
|
||||||
the existing per-source and import-depth bounds still apply.
|
the existing per-source and import-depth bounds still apply.
|
||||||
|
|
||||||
|
The maintained [layered consumer example](consumers/README.md#native-filesystem-and-layered-input)
|
||||||
|
shows a main host file, two underlay tiers, embedded defaults, a mapping source,
|
||||||
|
and explicit resource access without a second configuration pass.
|
||||||
|
|
||||||
### Source resource contexts
|
### Source resource contexts
|
||||||
|
|
||||||
`Result.SourceContext(id)` and callback-scoped `Resolution.SourceContext(id)`
|
`Result.SourceContext(id)` and callback-scoped `Resolution.SourceContext(id)`
|
||||||
@@ -165,6 +169,12 @@ contextless synthetic inputs have no resource context. To open a relative
|
|||||||
resource, join `BaseDir` with the application resource name using slash-path
|
resource, join `BaseDir` with the application resource name using slash-path
|
||||||
operations and open that path through `FS`.
|
operations and open that path through `FS`.
|
||||||
|
|
||||||
|
For a resource-valued field, query `result.Metadata().Lookup(fieldPath)`, inspect
|
||||||
|
its origin locations, choose a source ID only when the application's policy can
|
||||||
|
identify the intended contributor, then call `result.SourceContext(id)`. The
|
||||||
|
context belongs to that source even when the field survived from a lower-priority
|
||||||
|
underlay. A composite origin has no automatically selected resource base.
|
||||||
|
|
||||||
Native contexts borrow the caller's filesystem within the configured root.
|
Native contexts borrow the caller's filesystem within the configured root.
|
||||||
Host contexts retain a canonical root path, not an open root handle; each
|
Host contexts retain a canonical root path, not an open root handle; each
|
||||||
resource `Open` checks containment and opens a fresh handle that the returned
|
resource `Open` checks containment and opens a fresh handle that the returned
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ validation, schema-version policy, CLI parsing, runtime matching and conversion,
|
|||||||
resource checks, remote acquisition, and service initialization.
|
resource checks, remote acquisition, and service initialization.
|
||||||
|
|
||||||
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
|
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
|
||||||
four sanitized shapes using only ConfigKit and the Go standard library:
|
sanitized shapes using only ConfigKit and the Go standard library:
|
||||||
|
|
||||||
| Shape | Demonstrated fit | Application code deliberately retained |
|
| Shape | Demonstrated fit | Application code deliberately retained |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -15,6 +15,8 @@ four sanitized shapes using only ConfigKit and the Go standard library:
|
|||||||
| Notarius-like | Canonical object bindings, a string-backed schema version accepting string or integer YAML, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
|
| Notarius-like | Canonical object bindings, a string-backed schema version accepting string or integer YAML, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
|
||||||
| WeatherReporter-like | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
|
| WeatherReporter-like | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
|
||||||
| Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
|
| Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
|
||||||
|
| Bundled file | Simple native `fs.FS` source through ordinary `Load` | Selection and lifetime of the application-owned embedded filesystem |
|
||||||
|
| Layered catalog | Embedded defaults with an import, a mapping underlay, an independent host main file, explicit false/zero input, and source-relative resource access | Domain validation and opening/interpreting the template resource |
|
||||||
|
|
||||||
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
||||||
executable examples and ordinary tests. It contains no private application data,
|
executable examples and ordinary tests. It contains no private application data,
|
||||||
@@ -48,6 +50,27 @@ both without redefining their syntax. ConfigKit overlays are configuration overl
|
|||||||
they do not select or replace an application's LLM, model, pipeline, report, or
|
they do not select or replace an application's LLM, model, pipeline, report, or
|
||||||
execution profile concepts.
|
execution profile concepts.
|
||||||
|
|
||||||
|
## Native filesystem and layered input
|
||||||
|
|
||||||
|
[`LoadBundled`](../../examples/consumers/layered.go) keeps the simple `Load` shape:
|
||||||
|
it selects a file in an application-owned embedded filesystem and needs no
|
||||||
|
underlay setup. [`LoadCatalog`](../../examples/consumers/layered.go) uses
|
||||||
|
`LoadLayered` with an imported embedded-defaults group, a separate
|
||||||
|
`Mapping` group, and a host main file supplied by its caller. The groups
|
||||||
|
compose strictly within themselves and recursively across precedence boundaries;
|
||||||
|
the main file's explicit `false` and `0` replace lower values without being
|
||||||
|
mistaken for omissions. Its validator owns allowed modes and resource-name policy.
|
||||||
|
|
||||||
|
[`ReadWelcome`](../../examples/consumers/layered.go) follows the effective field's
|
||||||
|
origin location to a source ID, queries `Result.SourceContext`, and opens the
|
||||||
|
resource relative to that context's `BaseDir`. It does not assume the main file
|
||||||
|
owns a retained underlay value. A composite origin needs an application choice;
|
||||||
|
ConfigKit does not guess a directory. The context query is passive, while later
|
||||||
|
resource reads observe the caller-owned filesystem or a freshly opened host root
|
||||||
|
and may fail independently of the completed configuration load. The
|
||||||
|
[configuration contract](../config.md#source-resource-contexts) owns exact
|
||||||
|
lifetime and precedence rules.
|
||||||
|
|
||||||
Known module catalogs should use canonical object entries with a module identifier
|
Known module catalogs should use canonical object entries with a module identifier
|
||||||
and typed options. A small union struct is sufficient when the available option
|
and typed options. A small union struct is sufficient when the available option
|
||||||
fields are known; validation selects which fields apply to each module. Runtime-
|
fields are known; validation selects which fields apply to each module. Runtime-
|
||||||
@@ -126,6 +149,7 @@ focused behavioral owner:
|
|||||||
| Source/import/selected-overlay pipeline rules and artifact defaults | Narratio-shaped executable example and fixture |
|
| Source/import/selected-overlay pipeline rules and artifact defaults | Narratio-shaped executable example and fixture |
|
||||||
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
|
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
|
||||||
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
|
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
|
||||||
|
| Native filesystem, grouped underlays, programmatic mapping, retained resource context | `Example_nativeFilesystem`, `Example_layeredCatalog`, and their focused consumer tests |
|
||||||
| Results, metadata, diagnostics, effective output, and writer failures | Focused root-package load, provenance, rendering, and explanation tests |
|
| Results, metadata, diagnostics, effective output, and writer failures | Focused root-package load, provenance, rendering, and explanation tests |
|
||||||
|
|
||||||
The package-level import-boundary test covers the consumer examples too: supported
|
The package-level import-boundary test covers the consumer examples too: supported
|
||||||
|
|||||||
@@ -133,12 +133,16 @@ code changes. Use the component overview to find the corresponding implementatio
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| YAML syntax, diagnostics, parser limits | [Adapter tests](../internal/yaml/parse_test.go), [source translation](../internal/engine/parse_test.go) | `go test ./internal/yaml` and `go test ./internal/engine -run '^TestParseSource'` |
|
| YAML syntax, diagnostics, parser limits | [Adapter tests](../internal/yaml/parse_test.go), [source translation](../internal/engine/parse_test.go) | `go test ./internal/yaml` and `go test ./internal/engine -run '^TestParseSource'` |
|
||||||
| File/reader access, imports, globs, containment | [Sources](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [imports](../internal/engine/imports_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
|
| File/reader access, imports, globs, containment | [Sources](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [imports](../internal/engine/imports_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
|
||||||
|
| Native filesystem and mapping sources | [Native acquisition](../internal/engine/native_filesystem_test.go), [filesystem facade](../facade_filesystem_test.go), [mapping facade](../facade_mapping_test.go) | `go test ./internal/engine -run 'NativeFilesystem'` and `go test . -run 'TestFacade'` |
|
||||||
| Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` |
|
| Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` |
|
||||||
|
| Layered loading and source equivalence | [Grouped lifecycle](../internal/engine/load_request_test.go), [layered facade](../facade_layered_test.go), [equivalence](../facade_equivalence_test.go) | `go test ./internal/engine -run '^TestGrouped'`, `go test . -run '^TestFacadeLayered'`, and `go test . -run '^TestEquivalentSourceForms'` |
|
||||||
| Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
|
| Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
|
||||||
| Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` |
|
| Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` |
|
||||||
| Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` |
|
| Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` |
|
||||||
|
| Passive resource contexts | [Engine contexts](../internal/engine/resource_context_test.go), [public contexts](../facade_resource_context_test.go) | `go test ./internal/engine -run 'Resource'` and `go test . -run 'SourceContext'` |
|
||||||
| Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test ./internal/engine -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` |
|
| Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test ./internal/engine -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` |
|
||||||
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` |
|
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [native and layered examples](../examples/consumers/layered_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` |
|
||||||
|
| Assembly and layered scaling evidence | [Assembly benchmarks](../internal/engine/assembly_bench_test.go), [layered benchmarks](../internal/engine/underlay_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkUnderlay' -benchmem`; repeat with `^BenchmarkCompose` and `^BenchmarkOverlay` |
|
||||||
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go), [public boundary](../public_boundary_test.go) | `go test . -run 'TestLocalDocumentationLinks\|TestDependencyImportBoundary\|TestPublicFacadeBoundary'` |
|
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go), [public boundary](../public_boundary_test.go) | `go test . -run 'TestLocalDocumentationLinks\|TestDependencyImportBoundary\|TestPublicFacadeBoundary'` |
|
||||||
| Development and release checker behavior | [Shell regression](../release_check_test.go) | `go test . -run 'TestCheckScripts\|TestDevelopmentCheck'` |
|
| Development and release checker behavior | [Shell regression](../release_check_test.go) | `go test . -run 'TestCheckScripts\|TestDevelopmentCheck'` |
|
||||||
| Guarded release publication and recovery | [Publication regression](../release_publish_test.go) | `go test . -run '^TestReleasePublication'` |
|
| Guarded release publication and recovery | [Publication regression](../release_publish_test.go) | `go test . -run '^TestReleasePublication'` |
|
||||||
|
|||||||
@@ -7,23 +7,26 @@ private state; public signatures and fields do not expose engine types.
|
|||||||
|
|
||||||
Implemented component owners:
|
Implemented component owners:
|
||||||
|
|
||||||
- `internal/engine` owns source acquisition, composition, schema inspection,
|
- `internal/engine` owns host and native-filesystem source acquisition,
|
||||||
decoding, lifecycle coordination, callback auditing, provenance/history,
|
grouped strict composition and recursive precedence assembly, schema
|
||||||
secret resolution and bootstrap, and safe inspection rendering. It never
|
inspection, decoding, lifecycle coordination, callback auditing,
|
||||||
imports the root package.
|
provenance/history, passive source-context retention, secret resolution and
|
||||||
- Root `Source`, `Path`, `Secret`, records, lifecycle callbacks, results, and
|
bootstrap, and safe inspection rendering. It never imports the root package.
|
||||||
bootstrap reports are root-defined wrappers or records. Callback wrappers pass
|
- Root `Source`, `Filesystem`, `Input`, `Underlay`, `Path`, `Secret`, records,
|
||||||
only live engine context during a callback; `Result.Config` is detached and its
|
lifecycle callbacks, results, and bootstrap reports are root-defined wrappers
|
||||||
private inspection remains frozen independently.
|
or records. Callback wrappers pass only live engine context during a callback;
|
||||||
|
`Result.Config` is detached and its private inspection remains frozen
|
||||||
|
independently.
|
||||||
- Root conversion files explicitly cross the path, record, callback, secret, and
|
- Root conversion files explicitly cross the path, record, callback, secret, and
|
||||||
error boundaries. There are no public aliases, anonymous engine embeddings, or
|
error boundaries. There are no public aliases, anonymous engine embeddings, or
|
||||||
public engine bridge methods.
|
public engine bridge methods.
|
||||||
- `internal/document` owns the parser-independent ordered tree and cloning.
|
- `internal/document` owns the parser-independent ordered tree and cloning.
|
||||||
`internal/yaml` is the sole upstream YAML adapter and parser/dumper boundary.
|
`internal/yaml` is the sole upstream YAML adapter and parser/dumper boundary.
|
||||||
- Engine mechanism tests own source, composition, decoding, callback, provenance,
|
- Engine mechanism tests own source, composition, decoding, callback, provenance,
|
||||||
secret, history, rendering, and bootstrap behavior. Root tests own public API
|
source-context, secret, history, rendering, and bootstrap behavior. Root tests
|
||||||
compatibility, facade integration, documentation links, and dependency/public
|
own public API compatibility, facade integration, documentation links, and
|
||||||
boundary checks. Consumer examples remain under `examples/consumers`.
|
dependency/public boundary checks. Consumer examples remain under
|
||||||
|
`examples/consumers`.
|
||||||
|
|
||||||
Dependency direction is root to engine to document/YAML support. The root may
|
Dependency direction is root to engine to document/YAML support. The root may
|
||||||
import only `internal/engine` among project internal production packages; engine
|
import only `internal/engine` among project internal production packages; engine
|
||||||
|
|||||||
@@ -161,9 +161,11 @@ Tier precedence is explicit; traversal order within a tier establishes stable
|
|||||||
reporting, not permission to override. The distinction and its rationale are
|
reporting, not permission to override. The distinction and its rationale are
|
||||||
recorded in [ADR 0001](../adr/0001-separate-composition-and-overlay.md).
|
recorded in [ADR 0001](../adr/0001-separate-composition-and-overlay.md).
|
||||||
|
|
||||||
The main document and its imports currently form the strictly composed base
|
The main document and its imports form a strictly composed tier. Explicit
|
||||||
tier. Input-specific constraints, including CLI path rules and opt-in version
|
underlay groups compose strictly within each group, then combine recursively
|
||||||
ownership, remain part of the configuration contract. Consistent combination
|
from lowest to highest beneath the main tier. Source acquisition kind does not
|
||||||
|
determine its precedence role. Input-specific constraints, including CLI path
|
||||||
|
rules and opt-in version ownership, remain part of the configuration contract. Consistent combination
|
||||||
semantics do not require identical acquisition or lifetime behavior from every
|
semantics do not require identical acquisition or lifetime behavior from every
|
||||||
source kind.
|
source kind.
|
||||||
|
|
||||||
@@ -172,9 +174,10 @@ parser defaults. Processing order and diagnostics must be deterministic for the
|
|||||||
same inputs and options. Invalid input must not produce a successful usable
|
same inputs and options. Invalid input must not produce a successful usable
|
||||||
configuration. Do not run application hooks on invalid typed configuration.
|
configuration. Do not run application hooks on invalid typed configuration.
|
||||||
|
|
||||||
User-controlled precedence is application defaults, composed base, selected
|
User-controlled precedence is application defaults, ordered underlays, composed
|
||||||
overlay, then explicitly supplied CLI overrides. Applications own flag parsing;
|
main input, selected overlay, then explicitly supplied CLI overrides.
|
||||||
ConfigKit owns overlay semantics and provenance. Domain resolution follows these
|
Applications own flag parsing; ConfigKit owns overlay semantics and provenance.
|
||||||
|
Domain resolution follows these
|
||||||
inputs and must remain recorded, not become a hidden override channel.
|
inputs and must remain recorded, not become a hidden override channel.
|
||||||
|
|
||||||
Preserve node-based composition until the effective input is ready for typed
|
Preserve node-based composition until the effective input is ready for typed
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ General environment-variable configuration overlays are not supported. Typed sec
|
|||||||
references and explicit directory bootstrap are the environment convention. Literal
|
references and explicit directory bootstrap are the environment convention. Literal
|
||||||
credential configuration and implicit secret-directory discovery are unsupported.
|
credential configuration and implicit secret-directory discovery are unsupported.
|
||||||
|
|
||||||
The initial release deliberately excludes:
|
The following remain deliberately excluded:
|
||||||
|
|
||||||
- automatic `conf.d` discovery;
|
- automatic `conf.d` discovery;
|
||||||
- remote imports, arbitrary providers, and additional configuration formats;
|
- remote imports, arbitrary providers, and additional configuration formats;
|
||||||
- custom merge strategies, sequence merging, reset operators, and overlay inheritance;
|
- custom merge strategies, sequence merging, and reset operators;
|
||||||
|
- named overlay inheritance, tracked separately in the
|
||||||
|
[PromptKit inheritance request](feature-request-promptkit-inheritance.md);
|
||||||
- parser-specific or custom structural-decoding extension APIs;
|
- parser-specific or custom structural-decoding extension APIs;
|
||||||
- generic dynamic-subtree conversion and module-registry APIs;
|
- generic dynamic-subtree conversion and module-registry APIs;
|
||||||
- automatic schema migrations;
|
- automatic schema migrations;
|
||||||
|
|||||||
@@ -506,6 +506,16 @@ unchanged final tree. Benchmarks run with `-benchmem` and parameterized sizes.
|
|||||||
|
|
||||||
## Stage 10: Complete consumer examples and architecture documentation
|
## Stage 10: Complete consumer examples and architecture documentation
|
||||||
|
|
||||||
|
**Completed 2026-09-29.** Maintained executable examples now cover a simple
|
||||||
|
embedded filesystem source and a layered catalog with imported embedded defaults,
|
||||||
|
a mapping underlay, an independent host main file, explicit false/zero values,
|
||||||
|
domain validation, and resource access through the retained origin's source ID.
|
||||||
|
The configuration contract, public declaration comments, consumer/development
|
||||||
|
guides, README, component overview, architecture/deferred text, roadmap status,
|
||||||
|
and draft ADR status links reflect implemented behavior. Named inheritance remains
|
||||||
|
separate and deferred; the ADRs remain drafts. The everyday checker and full race
|
||||||
|
suite passed. No release preparation or publication was performed.
|
||||||
|
|
||||||
1. Add a maintained simple native-filesystem example and a combined example with
|
1. Add a maintained simple native-filesystem example and a combined example with
|
||||||
embedded defaults, two underlay tiers, independently located main input,
|
embedded defaults, two underlay tiers, independently located main input,
|
||||||
programmatic contributions, and a relative resource from a retained origin.
|
programmatic contributions, and a relative resource from a retained origin.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# Filesystem Sources and Source-Backed Underlays
|
# Filesystem Sources and Source-Backed Underlays
|
||||||
|
|
||||||
Status: agreed target for implementation; not implemented. This document owns
|
Status: implemented; draft ADRs remain pending review. This document preserves
|
||||||
the desired behavior and design rationale. The [implementation plan](implementation.md)
|
the design target and rationale, including its historical baseline. The
|
||||||
owns API signatures, sequential work, and validation commands.
|
[configuration contract](../config.md) owns current behavior, and the
|
||||||
|
[implementation plan](implementation.md) records sequential work and validation.
|
||||||
|
|
||||||
## Purpose and priorities
|
## Purpose and priorities
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ The [named-inheritance request](feature-request-promptkit-inheritance.md) remain
|
|||||||
separate and optional. This roadmap neither implements it nor adds speculative
|
separate and optional. This roadmap neither implements it nor adds speculative
|
||||||
infrastructure for it.
|
infrastructure for it.
|
||||||
|
|
||||||
## Current baseline
|
## Planning baseline (historical)
|
||||||
|
|
||||||
Planning baseline: `ec26591b275bd74f89120bdea69c5f6459dfcafb`, following v0.4.2.
|
Planning baseline: `ec26591b275bd74f89120bdea69c5f6459dfcafb`, following v0.4.2.
|
||||||
Current behavior is owned by the [configuration contract](../config.md), not by
|
Current behavior is owned by the [configuration contract](../config.md), not by
|
||||||
|
|||||||
95
examples/consumers/layered.go
Normal file
95
examples/consumers/layered.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package consumers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed testdata/native-source testdata/layered-bundle
|
||||||
|
var bundledFiles embed.FS
|
||||||
|
|
||||||
|
// BundledConfig is a small configuration loaded directly from an embedded file.
|
||||||
|
type BundledConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBundled demonstrates a native filesystem source without layered options.
|
||||||
|
func LoadBundled() (*configkit.Result[BundledConfig], error) {
|
||||||
|
files, err := fs.Sub(bundledFiles, "testdata/native-source")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return configkit.Load[BundledConfig](configkit.NewFilesystem("bundled", files).File("config.yml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogConfig keeps resource names and domain rules in the application.
|
||||||
|
type CatalogConfig struct {
|
||||||
|
Mode string `yaml:"mode"`
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Workers int `yaml:"workers"`
|
||||||
|
Templates map[string]string `yaml:"templates"`
|
||||||
|
Labels map[string]string `yaml:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCatalog combines two underlay tiers beneath an independently located
|
||||||
|
// host main file. ConfigKit assembles and validates the complete result once.
|
||||||
|
func LoadCatalog(mainPath string) (*configkit.Result[CatalogConfig], error) {
|
||||||
|
files, err := fs.Sub(bundledFiles, "testdata/layered-bundle")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bundle := configkit.NewFilesystem("catalog bundle", files)
|
||||||
|
return configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath)},
|
||||||
|
configkit.LayeredOptions[CatalogConfig]{
|
||||||
|
Underlays: []configkit.Underlay{
|
||||||
|
{Inputs: []configkit.Input{{Source: bundle.File("defaults/config.yml"), Imports: true}}},
|
||||||
|
{Inputs: []configkit.Input{{Source: configkit.Mapping("local policy", map[string]any{
|
||||||
|
"workers": 2,
|
||||||
|
"labels": map[string]any{"tenant": "example"},
|
||||||
|
})}}},
|
||||||
|
},
|
||||||
|
Definition: configkit.Definition[CatalogConfig]{
|
||||||
|
Defaults: func() CatalogConfig { return CatalogConfig{Mode: "demo"} },
|
||||||
|
Validate: validateCatalog,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCatalog(configuration *CatalogConfig, validation *configkit.Validation) {
|
||||||
|
if configuration.Mode != "demo" && configuration.Mode != "production" {
|
||||||
|
validation.Error(configkit.Keys("mode"), "must be demo or production")
|
||||||
|
}
|
||||||
|
if configuration.Workers < 0 {
|
||||||
|
validation.Error(configkit.Keys("workers"), "must be nonnegative")
|
||||||
|
}
|
||||||
|
if name := configuration.Templates["welcome"]; name == "." || !fs.ValidPath(name) {
|
||||||
|
validation.Error(configkit.Keys("templates", "welcome"), "must be a relative resource name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadWelcome follows the effective template field's one declaring source to
|
||||||
|
// its retained resource context. Opening and interpreting the resource remain
|
||||||
|
// application work; this does not reopen configuration or change metadata.
|
||||||
|
func ReadWelcome(result *configkit.Result[CatalogConfig]) (string, error) {
|
||||||
|
if result == nil {
|
||||||
|
return "", errors.New("catalog result is nil")
|
||||||
|
}
|
||||||
|
info, found := result.Metadata().Lookup(configkit.Keys("templates", "welcome"))
|
||||||
|
if !found || len(info.Origin.Locations) != 1 {
|
||||||
|
return "", errors.New("welcome template has no single declaring source")
|
||||||
|
}
|
||||||
|
context, found := result.SourceContext(info.Origin.Locations[0].SourceID)
|
||||||
|
if !found {
|
||||||
|
return "", errors.New("welcome template has no resource context")
|
||||||
|
}
|
||||||
|
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, result.Config.Templates["welcome"]))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
71
examples/consumers/layered_test.go
Normal file
71
examples/consumers/layered_test.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package consumers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_nativeFilesystem() {
|
||||||
|
result, err := consumers.LoadBundled()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(result.Config.Name, result.Config.Enabled)
|
||||||
|
// Output: embedded false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_layeredCatalog() {
|
||||||
|
result, err := consumers.LoadCatalog(fixture("layered-main", "config.yml"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
welcome, err := consumers.ReadWelcome(result)
|
||||||
|
fmt.Println(result.Config.Mode, result.Config.Enabled, result.Config.Workers,
|
||||||
|
result.Config.Labels["origin"], result.Config.Labels["tenant"], strings.TrimSpace(welcome), err)
|
||||||
|
// Output: production false 0 bundle example Welcome from the bundle. <nil>
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogRetainsPresenceAndSourceContext(t *testing.T) {
|
||||||
|
result, err := consumers.LoadCatalog(fixture("layered-main", "config.yml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, key := range []string{"enabled", "workers"} {
|
||||||
|
info, found := result.Metadata().Lookup(configkit.Keys(key))
|
||||||
|
if !found || !info.Explicit || info.Origin.Layer != configkit.LayerBase {
|
||||||
|
t.Fatalf("%s presence = %#v, %v", key, info, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info, found := result.Metadata().Lookup(configkit.Keys("templates", "welcome"))
|
||||||
|
if !found || info.Origin.Layer != configkit.LayerUnderlay || len(info.Origin.Locations) != 1 {
|
||||||
|
t.Fatalf("welcome origin = %#v, %v", info, found)
|
||||||
|
}
|
||||||
|
context, found := result.SourceContext(info.Origin.Locations[0].SourceID)
|
||||||
|
if !found || context.BaseDir != "defaults" {
|
||||||
|
t.Fatalf("welcome context = %#v, %v", context, found)
|
||||||
|
}
|
||||||
|
welcome, err := consumers.ReadWelcome(result)
|
||||||
|
if err != nil || strings.TrimSpace(welcome) != "Welcome from the bundle." {
|
||||||
|
t.Fatalf("welcome = %q, %v", welcome, err)
|
||||||
|
}
|
||||||
|
if result.Config.Labels["tenant"] != "example" || result.Config.Labels["origin"] != "bundle" {
|
||||||
|
t.Fatalf("recursive underlay labels = %#v", result.Config.Labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogValidationRejectsInvalidMode(t *testing.T) {
|
||||||
|
_, err := consumers.LoadCatalog(fixture("layered-main", "invalid.yml"))
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) == 0 ||
|
||||||
|
loadError.Diagnostics()[0].Code != configkit.CodeValidateError ||
|
||||||
|
loadError.Diagnostics()[0].Path.String() != "/mode" {
|
||||||
|
t.Fatalf("invalid mode error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
examples/consumers/testdata/layered-bundle/defaults/config.yml
vendored
Normal file
7
examples/consumers/testdata/layered-bundle/defaults/config.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: ../shared.yml
|
||||||
|
enabled: true
|
||||||
|
workers: 4
|
||||||
|
templates:
|
||||||
|
welcome: messages/welcome.txt
|
||||||
1
examples/consumers/testdata/layered-bundle/defaults/messages/welcome.txt
vendored
Normal file
1
examples/consumers/testdata/layered-bundle/defaults/messages/welcome.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Welcome from the bundle.
|
||||||
2
examples/consumers/testdata/layered-bundle/shared.yml
vendored
Normal file
2
examples/consumers/testdata/layered-bundle/shared.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
labels:
|
||||||
|
origin: bundle
|
||||||
3
examples/consumers/testdata/layered-main/config.yml
vendored
Normal file
3
examples/consumers/testdata/layered-main/config.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mode: production
|
||||||
|
enabled: false
|
||||||
|
workers: 0
|
||||||
1
examples/consumers/testdata/layered-main/invalid.yml
vendored
Normal file
1
examples/consumers/testdata/layered-main/invalid.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
mode: unsupported
|
||||||
2
examples/consumers/testdata/native-source/config.yml
vendored
Normal file
2
examples/consumers/testdata/native-source/config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
name: embedded
|
||||||
|
enabled: false
|
||||||
11
layered.go
11
layered.go
@@ -26,10 +26,13 @@ type LayeredOptions[T any] struct {
|
|||||||
Overrides []Override
|
Overrides []Override
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadLayered acquires the main input first, strictly composes each underlay,
|
// LoadLayered acquires the main input first and strictly composes enabled
|
||||||
// then applies underlays in order beneath main, a selected overlay, and CLI
|
// imports with each input's tier. It strictly composes contributions within
|
||||||
// overrides. It performs defaults, decoding, resolution, and validation once.
|
// each underlay, then recursively combines underlays in listed order beneath
|
||||||
// Zero or one options value is accepted; any error returns a nil result.
|
// main, a selected overlay, and CLI overrides. Defaults, decoding, resolution,
|
||||||
|
// and validation run once on the assembled input. Source IDs follow acquisition
|
||||||
|
// order, not precedence order. Zero or one options value is accepted; any error
|
||||||
|
// returns a nil result.
|
||||||
func LoadLayered[T any](main Input, supplied ...LayeredOptions[T]) (*Result[T], error) {
|
func LoadLayered[T any](main Input, supplied ...LayeredOptions[T]) (*Result[T], error) {
|
||||||
options := make([]engine.LayeredOptions[T], len(supplied))
|
options := make([]engine.LayeredOptions[T], len(supplied))
|
||||||
for index := range supplied {
|
for index := range supplied {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import "io/fs"
|
|||||||
|
|
||||||
// ResourceContext provides a passive rooted filesystem view and a
|
// ResourceContext provides a passive rooted filesystem view and a
|
||||||
// slash-relative BaseDir for one acquired source. FS is borrowed: caller-owned
|
// slash-relative BaseDir for one acquired source. FS is borrowed: caller-owned
|
||||||
// filesystems remain under caller control, and host files are opened afresh.
|
// filesystems remain under caller control, while a host context opens a fresh
|
||||||
|
// contained root for each resource open rather than retaining a loading handle.
|
||||||
// Querying this record performs no I/O. Resource contents may change after
|
// Querying this record performs no I/O. Resource contents may change after
|
||||||
// loading; callers must close files they open through FS.
|
// loading; callers must close files they open through FS.
|
||||||
type ResourceContext struct {
|
type ResourceContext struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user