248 lines
14 KiB
Markdown
248 lines
14 KiB
Markdown
# Configuration Input Contract
|
|
|
|
This document owns ConfigKit's currently implemented source-input, YAML syntax,
|
|
schema, typed-decoding, loading, metadata, and ownership contracts. The parser,
|
|
owned document representation, and decoder remain private behind the public loading
|
|
lifecycle.
|
|
|
|
## Sources
|
|
|
|
ConfigKit exposes opaque sources through these constructors:
|
|
|
|
| Constructor | Current contract |
|
|
| --- | --- |
|
|
| `File(path)` | Requires a nonempty path, opens a regular file for each consumption, and closes only the library-owned file handle. Missing, inaccessible, unreadable, directory, and other nonregular inputs fail. |
|
|
| `Bytes(label, data)` | Requires a nonempty, non-secret diagnostic label and snapshots bytes during construction. Later caller mutation cannot affect the source. |
|
|
| `Reader(label, input)` | Requires a nonempty, non-secret label and non-nil reader. The reader is claimed once across all copies of the source, consumed but never closed, and cannot be reused. |
|
|
| `NoSource()` | Selects defaults-only loading, creates no source record, and marks no paths explicit. |
|
|
|
|
The zero `Source` is invalid. Constructor argument failures are retained inside the
|
|
opaque source and become safe structured diagnostics when the source is consumed.
|
|
File sources can observe file changes between consumptions; byte sources cannot.
|
|
|
|
Each source is limited to 8 MiB. A load will be limited to 64 MiB across all source
|
|
and override input when those layers are implemented. Reads request at most the
|
|
remaining allowance plus one byte so excess data is rejected rather than silently
|
|
truncated. Data returned together with a read error is not accepted as partial input.
|
|
|
|
## Single-source loading
|
|
|
|
`Load[T](source, options...)` accepts zero or one `Options[T]`. The configuration
|
|
root `T` must satisfy the application schema contract below. With no options it
|
|
starts from the Go zero value. A `Definition[T].Defaults` factory, when supplied,
|
|
runs exactly once after successful source parsing, schema construction, and optional
|
|
schema-version checking. Its result is detached before input is applied.
|
|
|
|
The current lifecycle is:
|
|
|
|
```text
|
|
validate options → parse one source → reject reserved directives → build schema
|
|
→ check the optional application version → create and clone defaults
|
|
→ strictly decode → snapshot configuration and metadata
|
|
```
|
|
|
|
`File`, `Bytes`, and `Reader` each contribute one base source with the deterministic
|
|
ID `source-1`. `NoSource` skips parsing, supplies an absent version to a configured
|
|
version checker, contributes no source, and continues through defaults, decode, and
|
|
projection. A missing file remains an error; it is never interpreted as no input.
|
|
|
|
The current `Options` boundary accepts a defaults factory and `VersionRule`.
|
|
Imports, active profiles, filesystem context, overrides, resolver and semantic
|
|
validator callbacks, and dynamic secret classification return an explicit
|
|
`options.unavailable` error because their lifecycle stages are not implemented.
|
|
The reserved profile spelling `default` selects no active profile. `NoSource`
|
|
additionally rejects imports, active profiles, and filesystem context as invalid
|
|
combinations. Input containing a reserved `_configkit` mapping key is rejected while
|
|
imports are disabled.
|
|
|
|
Every source, options, schema, version, defaults, or decode failure returns a nil
|
|
result and a `*LoadError`. Successful results contain caller-owned `Config`, detached
|
|
`Metadata`, a defensive source list, and defensive nonfatal diagnostics. Mutating
|
|
the returned configuration or inspection records cannot alter ConfigKit's private
|
|
snapshot. ConfigKit performs no logging and exposes no effective writer yet.
|
|
|
|
### Application schema versions
|
|
|
|
`VersionRule` declares a semantic path and non-nil checker. Before defaults or typed
|
|
decoding, the checker receives a `Version` preserving absent, explicit null, or
|
|
string input. Any other input kind is a `version.type` error and the checker does not
|
|
run. ConfigKit does not interpret the text or impose a universal version path.
|
|
|
|
The checker reports safe, value-free errors and warnings through `Validation.Error`
|
|
and `Validation.Warning`; ConfigKit resolves primary and related paths to exact or
|
|
nearest input locations. Reported errors fail loading. If the checker returns an
|
|
error without reporting one, ConfigKit emits a generic `version.hook_failed`
|
|
diagnostic and suppresses the returned error text. Warnings alone are retained on a
|
|
successful result. Callback panics remain programming failures.
|
|
|
|
### Base/default metadata
|
|
|
|
`Result.Metadata` describes every final canonical path. Exact surviving input paths
|
|
are explicit and have a base origin; omitted paths have a default origin. An explicit
|
|
parent mapping does not make omitted children explicit. Null state and available
|
|
source coordinates are retained. `History` records the Go-zero or application
|
|
default baseline followed by exact base definitions, including equal-valued input.
|
|
All metadata queries and returned nested records are detached.
|
|
|
|
## YAML documents
|
|
|
|
The private YAML boundary currently accepts exactly one nonempty document whose root
|
|
is a mapping. Comments and block or flow collections are supported. It rejects:
|
|
|
|
- empty input or an empty document;
|
|
- every additional document, including an empty one;
|
|
- duplicate decoded mapping keys;
|
|
- non-string mapping keys;
|
|
- explicit tags, anchors, aliases, and merge keys;
|
|
- a quoted or plain mapping key equal to `<<`; and
|
|
- input exceeding the fixed depth or node limits.
|
|
|
|
Mapping key comparison is exact: it performs no case folding or Unicode
|
|
normalization. The private document tree retains declaration order, exact validated
|
|
number spelling, source identifiers, and separate key/value line and column
|
|
locations. It contains no upstream parser nodes or types.
|
|
|
|
Root depth is zero. Input depth is limited to 256 and each parsed input tree to
|
|
1,000,000 nodes, counting mapping keys as input nodes. The upstream parser also has
|
|
a private calibrated depth guard; passing that guard never bypasses ConfigKit's tree
|
|
limits.
|
|
|
|
## Scalars
|
|
|
|
Scalar meaning is determined by ConfigKit rather than upstream coercion:
|
|
|
|
| Input form | Meaning |
|
|
| --- | --- |
|
|
| Quoted, literal, or folded scalar | String |
|
|
| Plain `true` or `false` | Boolean |
|
|
| Plain `null`, `~`, or an empty mapping value | Null |
|
|
| JSON decimal number | Number, retaining its exact spelling |
|
|
| Date/time-looking or other ordinary plain text | String |
|
|
|
|
JSON numbers permit an optional minus, a zero or nonzero decimal integer without a
|
|
leading zero, an optional fraction with at least one digit, and an optional decimal
|
|
exponent. Hexadecimal, octal, digit separators, leading plus, leading/trailing-dot
|
|
forms, incomplete exponents, and non-finite values are rejected. Legacy boolean
|
|
words (`yes`, `no`, `on`, and `off`) and non-lowercase variants of the reserved
|
|
boolean/null words are rejected unless quoted.
|
|
|
|
Mapping keys use the same scalar classification and must classify as strings. Quote
|
|
keys that otherwise classify as a boolean, number, or null.
|
|
|
|
## Application schema binding
|
|
|
|
ConfigKit builds a private reflection descriptor graph for each schema operation;
|
|
it keeps no global mutable descriptor cache. A configuration root must be a struct
|
|
or string-keyed map. Nested supported shapes are:
|
|
|
|
- booleans, strings, signed and unsigned integers, and finite floating-point types;
|
|
- `time.Duration` and paired `encoding.TextMarshaler`/`TextUnmarshaler` scalars;
|
|
- pointers, structs, string-keyed maps, slices, fixed arrays, and `any`; and
|
|
- recursive type definitions whose eventual application values are acyclic.
|
|
|
|
Named scalar types retain their Go identity. `uintptr`, complex numbers, functions,
|
|
channels, unsafe pointers, non-string map keys, and nonempty interfaces are not
|
|
configuration types. A custom text scalar must provide both directions on its value
|
|
or pointer method set. Building a descriptor only inspects method sets; it never
|
|
executes application methods or parser-specific hooks.
|
|
|
|
Exported struct fields use an explicit `yaml` name or the lowercase Go field name.
|
|
Unexported fields and `yaml:"-"` are ignored. `omitempty` is accepted but does not
|
|
change binding or future projection. An explicit `yaml:",inline"` flattens a struct
|
|
or pointer-to-struct, or designates at most one string-keyed inline map for otherwise
|
|
unmatched names. Ambiguous names, named inline fields, multiple inline maps, and
|
|
unsupported or malformed tag options are schema errors. Embedded fields are ordinary
|
|
named fields unless explicitly inlined. `configkit:"secret"` is the only current
|
|
ConfigKit field tag and records sensitivity for later lifecycle work.
|
|
|
|
Schema path traversal uses these same bindings. Explicit key segments address
|
|
mappings, explicit index segments address sequences, and parsed numeric pointer
|
|
tokens become indices only in a sequence. Sequence indices cannot have leading
|
|
zeroes; fixed-array indices are bounds checked. Dynamic `any` and inline-map entries
|
|
remain schema-valid for application-defined descendants.
|
|
|
|
Descriptors are reused by typed decoding, projection, and cloning so those operations
|
|
cannot acquire different field bindings.
|
|
|
|
## Defaults and typed decoding
|
|
|
|
The private decoder starts from an independently owned clone of the application's
|
|
typed defaults and applies only explicitly present input nodes. Defaults therefore
|
|
never become explicit, while equal-valued input remains explicit. Successful decode
|
|
produces detached typed state, baseline and final canonical trees, an input-presence
|
|
index, and default/input origins for the later public loading lifecycle.
|
|
|
|
| Configured input | Effect on the typed baseline |
|
|
| --- | --- |
|
|
| Absent field | Retain the default |
|
|
| Explicit false, zero, or empty string | Replace the default |
|
|
| Partial mapping or string-keyed map | Recurse and retain omitted default children or entries |
|
|
| Empty mapping | Retain existing mapping children |
|
|
| Sequence, including an empty sequence | Replace the entire sequence |
|
|
| Pointer object | Recurse into a non-nil default; otherwise allocate an object whose omitted children have ordinary Go zero values |
|
|
| Allowed null | Replace the value with nil and suppress its default |
|
|
|
|
Null is allowed only for pointers, maps, slices, and dynamic `any` values. It is an
|
|
error for scalars, structs, and arrays. New map entries and sequence elements begin
|
|
from their Go zero values; ConfigKit does not invent entry-level defaults.
|
|
|
|
Decoding is strict at every typed depth. Unknown struct fields, type mismatches,
|
|
invalid nulls, numeric overflow, and arrays with the wrong length are errors at the
|
|
original semantic path and source coordinates. String-keyed maps accept arbitrary
|
|
keys but preserve the strict schema of their values; inline maps accept unmatched
|
|
keys according to their element descriptor. Multiple independent field failures are
|
|
collected without publishing the partially decoded object.
|
|
|
|
Booleans and strings are not coerced. Integer destinations require integer syntax
|
|
and a value in the destination type's range. Float destinations accept finite JSON
|
|
numbers representable by their destination precision. Dynamic numbers become
|
|
`int64` for integer syntax and `float64` for fractional or exponent syntax, with
|
|
range and finiteness checks. Named scalar types retain their identity. Durations
|
|
decode only from strings accepted by `time.ParseDuration`; integers are rejected.
|
|
Paired text scalars decode only from strings through `encoding.TextUnmarshaler`, and
|
|
codec failures are classified without exposing application error text or input
|
|
values.
|
|
|
|
This decoder is available only through `Load`; ConfigKit exposes no generic document
|
|
conversion API.
|
|
|
|
## Typed projection and ownership
|
|
|
|
ConfigKit can internally project a schema-valid application value directly into its
|
|
owned canonical tree without YAML serialization. Projection includes every bound
|
|
field, including ordinary zero values; `omitempty` has no effect. Struct fields keep
|
|
binding order, dynamic map keys sort by decoded string, and sequences keep their
|
|
order. Integers retain exact base-10 values, finite floats use their destination
|
|
precision, durations use `time.Duration.String`, and paired text scalars use their
|
|
canonical marshaled text.
|
|
|
|
Nil pointers, maps, slices, and dynamic values project as null. Non-nil empty maps
|
|
and slices remain empty containers, preserving the distinction. Recursive acyclic
|
|
objects are supported. Cyclic values, non-finite floats, unsupported dynamic concrete
|
|
types, invalid codec output, and depth/node-limit violations fail with safe schema
|
|
classifications that omit codec error text.
|
|
|
|
The same descriptor graph creates detached typed copies. Pointers, maps, slices,
|
|
arrays, nested dynamic values, and shared references are recursively copied; shared
|
|
pointer identity is not preserved as configuration meaning. Paired text scalars are
|
|
rebuilt by unmarshaling their canonical text into fresh values, detaching private
|
|
backing storage. Ignored and unexported struct fields are zeroed rather than copied.
|
|
|
|
Private structural comparison operates on canonical trees. Mapping additions,
|
|
removals, and descendant replacements retain semantic paths. A sequence is atomic:
|
|
any sequence change reports the sequence path and never invents correspondence
|
|
between old and new indices. Cross-kind and null/container changes replace the
|
|
affected path.
|
|
|
|
These mechanisms currently support later decoding, resolution auditing, metadata,
|
|
and rendering. They are not a public generic conversion or cloning API.
|
|
|
|
## Safe failures
|
|
|
|
Source failures use stable `source.*` codes and YAML failures use stable `yaml.*`
|
|
codes declared by the root package. Diagnostics contain safe classifications,
|
|
semantic paths when applicable, and available source coordinates. Raw parser,
|
|
conversion, reader-error, and input text is not forwarded. Source labels and
|
|
diagnostic messages are application-visible and therefore must never contain
|
|
secrets; terminal control characters are escaped by `WriteDiagnostics`.
|