Expand description
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 isTrust, 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 toresolve_with_context. This crate’s own package version is never assumed.
§Example
use usage_config::{resolve, Const, EnvLayer, Layers, PropMeta, Registry, Ty, Value};
// Normally generated from the settings struct by `#[derive(usage::Config)]`.
static PROPS: &[PropMeta] = &[PropMeta {
envs: &["MYCLI_JOBS"],
default: Some(Const::Int(4)),
..PropMeta::new("jobs", Ty::Uint)
}];
const REGISTRY: Registry = Registry::new(PROPS);
// 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 = EnvLayer::new([("MYCLI_JOBS".to_string(), "8".to_string())]);
let resolved = resolve(REGISTRY, Layers::new().then(&env))?;
assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
// And where it came from is the variable the user set, not "the environment".
assert_eq!(
resolved.origin_key("jobs").unwrap().describe(),
"MYCLI_JOBS",
);Re-exports§
pub use cli::CliLayer;pub use env::EnvLayer;pub use explain::explain;pub use layer::Entry;pub use layer::Layer;pub use layer::LayerCtx;pub use layer::LayerError;pub use layer::LayerOutput;pub use layer::Warning;pub use layer::WarningKind;pub use props::concat_prop_specs;pub use props::concat_props;pub use props::Props;pub use read::Fold;pub use read::FromValue;pub use read::ReadError;pub use read::ReadErrorKind;pub use read::ReadErrors;pub use registry::Lookup;pub use registry::Merge;pub use registry::PropId;pub use registry::PropMeta;pub use registry::Registry;pub use registry::Scope;pub use resolve::resolve;pub use resolve::resolve_with_context;pub use resolve::Layers;pub use resolve::ResolutionContext;pub use resolve::Resolved;pub use source::FileScope;pub use source::Origin;pub use source::SourceKind;pub use source::Trust;pub use spec::spec_kdl;pub use spec::spec_kdl_with;pub use spec::ConfigSpec;pub use spec::PropSpec;pub use spec::SpecFile;pub use spec::SpecSource;pub use ty::Parser;pub use ty::Ty;pub use ty::TypeError;pub use value::Const;pub use value::Value;
Modules§
- cli
- The command line as a layer.
- env
- The environment as a layer.
- explain
- Why a setting has the value it has.
- layer
- Where values come from, as an interface.
- props
- What
#[derive(usage::Config)]generates, and how flattened groups compose. - read
- A resolution read as the types a settings struct holds.
- registry
- The settings a CLI has, as a generated table.
- resolve
- One merge, and the provenance is its output.
- source
- Where a value came from.
- spec
- A registry written back out as the spec’s
configblock. - ty
- The type a setting was declared with, and reading a raw string as it.
- value
- What a setting holds, at runtime and as a declared default.