Files
ConfigKit/docs/config.md

146 lines
7.9 KiB
Markdown

# Configuration Input Contract
This document owns ConfigKit's currently implemented source-input and YAML syntax
contracts. The parser and owned document representation are private; a public typed
loading lifecycle has not been implemented yet.
## 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()` | Represents an explicit absence of input and creates no source record. Its defaults-only lifecycle behavior will become available with `Load`. |
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.
## 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 do not yet decode documents. Document decoding will reuse this graph as
its implementation lands.
## 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`.