core.configuration — Universal configuration subsystem
A unified surface for parsing, serialising, validating, converting, merging, and interpolating configuration in any supported format.
Architecture summary
┌──────────────────────────────────────────────────────────┐
│ Layer 4 — this module: Configuration<T>::load* │
├──────────────────────────────────────────────────────────┤
│ Layer 3 — schema, path, merge, env, convert │
├──────────────────────────────────────────────────────────┤
│ Layer 2 — format protocol + registry, error │
├──────────────────────────────────────────────────────────┤
│ Layer 1 — format adapters (toml, yaml, ini, ...) │
├──────────────────────────────────────────────────────────┤
│ Layer 0 — value (ConfigValue ADT — categorical hub) │
└──────────────────────────────────────────────────────────┘
Categorical position: this module is the slice category
over ConfigValue whose objects are typed records T and whose
morphisms are refinement-preserving load/save adapters. The
Configuration<T> type is the universal closure: any T admitting
@derive(ConfigSchema) is automatically loadable + saveable across
every registered format with the same call-site shape.
Layout
| File | What's in it |
|---|---|
mod.vr | re-exports + Configuration<T>::load* user API |
value.vr | ConfigValue ADT — categorical hub for all formats |
format.vr | Format protocol + format registry |
convert.vr | ConfigValue ↔ Verum types conversion |
error.vr | ConfigError taxonomy |
toml.vr | TOML adapter (Cargo / pyproject / etc. ergonomics) |
ConfigValue — the categorical hub
// Every variant is `Config`-prefixed, and there are fourteen: the four
// TOML date/time shapes are distinct arms, and bytes, durations and a
// tagged wrapper are part of the model.
public type ConfigValue is
ConfigNull
| ConfigBool(Bool)
| ConfigInt(Int)
| ConfigFloat(Float)
| ConfigString(Text)
| ConfigBytes(List<Byte>)
| ConfigOffsetDateTime(Rfc3339Time)
| ConfigLocalDateTime(Rfc3339Time)
| ConfigLocalDate(Rfc3339Time)
| ConfigLocalTime(Rfc3339Time)
| ConfigDuration(Duration)
| ConfigArray(List<ConfigValue>)
| ConfigTable(Map<Text, ConfigValue>)
| ConfigTagged(Text, Heap<ConfigValue>);
Every supported format (TOML, YAML, JSON, INI, env, …) parses
into ConfigValue and serialises from ConfigValue. Cross-format
conversion is then free composition: parse one format to
ConfigValue, serialise back to another.
Format protocol
public type Format is protocol {
fn name(&self) -> Text; // "toml", "yaml", ...
fn extensions(&self) -> List<Text>; // ["toml"]
fn parse(&self, source: &Text)
-> Result<ConfigValue, ConfigError>;
fn serialise(&self, value: &ConfigValue, opts: &SerialiseOptions)
-> Result<Text, ConfigError>;
};
Adapters implement this protocol; the registry resolves by
name or extension.
Typed Configuration<T>
public type Configuration<T> is { value: T };
public fn load_str<T: ConfigSchema>(
source: &Text,
format: &Text,
) -> Result<Configuration<T>, ConfigError>;
public fn load_file<T: ConfigSchema>(
path: &Text,
) -> Result<Configuration<T>, ConfigError>; // format inferred from extension
public fn save_str<T: ConfigSchema>(
config: &Configuration<T>,
format: &Text,
) -> Result<Text, ConfigError>;
public fn save_file<T: ConfigSchema>(
config: &Configuration<T>,
path: &Text,
) -> Result<(), ConfigError>;
@derive(ConfigSchema) on any record type generates the
ConfigValue ↔ T conversion. Refinement types are preserved
across the round-trip: port: Int { > 0 && < 65536 } rejects
out-of-range values at load time with a structured ConfigError
whose kind is ConfigErrorKind.RefinementViolation, carrying the
field path in path and the human-readable reason in message.
Error taxonomy
ConfigError is a RECORD carrying one ConfigErrorKind, not a sum of
payload-bearing variants. The position fields are always present, so
every error can point at the source whether or not it has a semantic
path:
public type ConfigError is {
format_id: FormatId,
kind: ConfigErrorKind,
line: Int, // 1-indexed
column: Int, // 1-indexed, byte-counted (sources are UTF-8)
byte_offset: Int, // 0-indexed
message: Text,
path: Maybe<Text>, // set for SEMANTIC errors only
};
The category is the kind, and there are 32 of them, grouped by the
phase that raises them:
| Group | Kinds |
|---|---|
| Syntactic | UnexpectedEof UnexpectedChar InvalidEscape InvalidNumber InvalidUnicode InvalidDateTime InvalidDuration InvalidIndent InvalidEncoding TrailingGarbage |
| Structural | EmptyKey DuplicateKey KeyAfterValue RedefineSuperTable TypeMismatchInArray UnclosedBlock |
| Limits | DepthLimit StringTooLong TableTooLarge ArrayTooLarge TotalSizeExceeded |
| Typed loader | SchemaViolation RefinementViolation UnknownField ValueOutOfRange |
| References | InvalidReference CircularReference |
| Conversion | LossyConversion UnsupportedFeature |
| I/O | IoFileNotFound IoPermissionDenied IoOther |
path is Maybe<Text> and is populated for SEMANTIC errors — the
typed-loader and reference groups. A syntax error has a line and a
column but no path through the config tree, because the tree did not
parse. Handle Maybe.None when inspecting path on a parse failure.
Transcribed from core/configuration/error.vr.
Merge + interpolation
public fn merge(base: ConfigValue, overlay: ConfigValue) -> ConfigValue;
public fn interpolate(value: &mut ConfigValue, env: &Map<Text, Text>)
-> Result<(), ConfigError>;
Merge is deep-recursive: Table keys union recursively, Array
overlays replace, scalars overlay-wins. Interpolation expands
${ENV_VAR} / ${ENV_VAR:-default} syntax against the supplied
env map.
Status
| File | Status |
|---|---|
mod.vr | undocumented — full load/save surface — no conformance suite yet |
value.vr | unverified — 8-variant ADT — core-tests/configuration/value |
format.vr | undocumented — protocol + registry — no conformance suite yet |
convert.vr | unverified — ConfigValue ↔ T (incl. refinement preservation) — core-tests/configuration/convert |
error.vr | regression-only — full taxonomy — core-tests/configuration/error |
toml.vr | undocumented — parse/serialise round-trip — no conformance suite yet |
YAML / JSON / INI / env-var adapters are tracked as future work.
The Format protocol's open-registry design means landing a new
adapter is purely additive — zero upstream code change.
Composition example
The top-level load_text / dump_text functions in mod.vr take text
and a FormatId, such as format_id_toml() or format_id_yaml() from
core/configuration/error.vr. Read file contents separately:
mount core.configuration.{load_text, dump_text};
mount core.configuration.error.{format_id_toml, format_id_yaml};
mount core.io.fs;
// Load TOML, save the same data as YAML.
let toml_text = fs.read_to_string(&path)?;
let cfg = load_text(&toml_text, &format_id_toml())?;
let yaml = dump_text(&cfg, &format_id_yaml())?;
For bulk environment enumeration: unlike the JSON case on
cookbook → shell scripting, this one
does exist — core.base.env.vars() returns an iterator of every
(Text, Text) pair in the process environment:
mount core.base.env.{vars};
for (key, value) in vars() {
// fold into your own overlay however your merge strategy wants it
}
There is no ready-made configuration.from_env::<T>() bridge from
that iterator into a typed overlay, and no Configuration<T>::load*
convenience wrapping path, parsing and merging into one call. Application
code assembles the defaults, file and environment overlays from these primitives.
The categorical foundation (slice over ConfigValue) ensures
these compositions are associative + identity-preserving — the
result of merging defaults+user+env is independent of grouping.