Skip to main content

Crate usage_config

Crate usage_config 

Source
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 explain cannot 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 as hk.toml is, 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_at and deprecated_remove_at act against the running CLI version supplied to resolve_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 config block.
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.