Layered configuration resolution for CLIs that describe their settings in a usage spec.
Every CLI in the jdx fleet has written this by hand, and every copy has rotted
differently: hk declares eighteen sources.cli bindings and reads five, pitchfork
documents a CLI layer it does not have, fnox's module doc describes a config-file layer
that does not exist, and mise hand-copies thirteen flags into its settings in a
forty-nine-line function. The drift is not carelessness — it is what happens when the
declaration of a setting and the code that resolves it are two separate things that have
to be kept in step by hand.
Here they are one thing. #[derive(usage::Config)] reads the settings struct and emits a
[Registry] of consts beside it; this crate resolves values against it. Nothing here
parses KDL, so a CLI carries a resolver rather than a spec parser.
What it guarantees
- One merge. Provenance is the output of the only merge there is, so
config explaincannot describe a resolution that did not happen — which a second, parallel merge function written for the purpose can. - Fixed precedence. cli > env > files, nearest first > user > machine > declared defaults. Which layers a CLI has is its own business; their order is not.
- Scope is enforced, not remembered. A
scope="global"setting refuses an untrusted place in the merge, not in each layer, because a check every layer has to make is one a new layer will forget. The question is [Trust], not "was it a file": a pkl file or a git config inside a checkout is exactly as much a thing a repository carries ashk.tomlis, and a kind usage does not recognize gets the least trusting answer until its layer says otherwise. - Warnings, not output. Nothing here prints. An unknown key, a value of the wrong type, a deprecated setting: all returned, for the CLI to render when its logging is up.
- Lifecycle gates are explicit.
deprecated_warn_atanddeprecated_remove_atact against the running CLI version supplied to [resolve_with_context]. This crate's own package version is never assumed.
Example
use ;
// Normally generated from the settings struct by `#[derive(usage::Config)]`.
static PROPS: & = &;
const REGISTRY: Registry = new;
// The environment is described rather than reached for, so a test never touches the process.
// `EnvLayer::from_process` is what a CLI uses.
let env = new;
let resolved = resolve?;
assert_eq!;
// And where it came from is the variable the user set, not "the environment".
assert_eq!;
# Ok::