usage_config/lib.rs
1//! Layered configuration resolution for CLIs that describe their settings in a usage spec.
2//!
3//! Every CLI in the jdx fleet has written this by hand, and every copy has rotted
4//! differently: hk declares eighteen `sources.cli` bindings and reads five, pitchfork
5//! documents a CLI layer it does not have, fnox's module doc describes a config-file layer
6//! that does not exist, and mise hand-copies thirteen flags into its settings in a
7//! forty-nine-line function. The drift is not carelessness — it is what happens when the
8//! declaration of a setting and the code that resolves it are two separate things that have
9//! to be kept in step by hand.
10//!
11//! Here they are one thing. `#[derive(usage::Config)]` reads the settings struct and emits a
12//! [`Registry`] of consts beside it; this crate resolves values against it. Nothing here
13//! parses KDL, so a CLI carries a resolver rather than a spec parser.
14//!
15//! # What it guarantees
16//!
17//! - **One merge.** Provenance is the output of the only merge there is, so `config explain`
18//! cannot describe a resolution that did not happen — which a second, parallel merge
19//! function written for the purpose can.
20//! - **Fixed precedence.** cli > env > files, nearest first > user > machine > declared
21//! defaults. Which layers a CLI has is its own business; their order is not.
22//! - **Scope is enforced, not remembered.** A `scope="global"` setting refuses an untrusted
23//! place in the merge, not in each layer, because a check every layer has to make is one a
24//! new layer will forget. The question is [`Trust`], not "was it a file": a pkl file or a git
25//! config inside a checkout is exactly as much a thing a repository carries as `hk.toml` is,
26//! and a kind usage does not recognize gets the least trusting answer until its layer says
27//! otherwise.
28//! - **Warnings, not output.** Nothing here prints. An unknown key, a value of the wrong
29//! type, a deprecated setting: all returned, for the CLI to render when its logging is up.
30//! - **Lifecycle gates are explicit.** `deprecated_warn_at` and `deprecated_remove_at` act
31//! against the running CLI version supplied to [`resolve_with_context`]. This crate's own
32//! package version is never assumed.
33//!
34//! # Example
35//!
36//! ```
37//! use usage_config::{resolve, Const, EnvLayer, Layers, PropMeta, Registry, Ty, Value};
38//!
39//! // Normally generated from the settings struct by `#[derive(usage::Config)]`.
40//! static PROPS: &[PropMeta] = &[PropMeta {
41//! envs: &["MYCLI_JOBS"],
42//! default: Some(Const::Int(4)),
43//! ..PropMeta::new("jobs", Ty::Uint)
44//! }];
45//! const REGISTRY: Registry = Registry::new(PROPS);
46//!
47//! // The environment is described rather than reached for, so a test never touches the process.
48//! // `EnvLayer::from_process` is what a CLI uses.
49//! let env = EnvLayer::new([("MYCLI_JOBS".to_string(), "8".to_string())]);
50//! let resolved = resolve(REGISTRY, Layers::new().then(&env))?;
51//!
52//! assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
53//! // And where it came from is the variable the user set, not "the environment".
54//! assert_eq!(
55//! resolved.origin_key("jobs").unwrap().describe(),
56//! "MYCLI_JOBS",
57//! );
58//! # Ok::<(), usage_config::LayerError>(())
59//! ```
60
61pub mod cli;
62pub mod env;
63pub mod explain;
64#[cfg(any(feature = "toml", feature = "json", feature = "yaml"))]
65pub mod files;
66pub mod layer;
67pub mod props;
68pub mod read;
69pub mod registry;
70pub mod resolve;
71pub mod source;
72pub mod spec;
73pub mod ty;
74pub mod value;
75
76pub use cli::CliLayer;
77pub use env::EnvLayer;
78pub use explain::explain;
79#[cfg(any(feature = "toml", feature = "json", feature = "yaml"))]
80pub use files::{FileLayer, Format};
81pub use layer::{Entry, Layer, LayerCtx, LayerError, LayerOutput, Warning, WarningKind};
82pub use props::{concat_prop_specs, concat_props, Props};
83pub use read::{Fold, FromValue, ReadError, ReadErrorKind, ReadErrors};
84pub use registry::{Lookup, Merge, PropId, PropMeta, Registry, Scope};
85pub use resolve::{resolve, resolve_with_context, Layers, ResolutionContext, Resolved};
86pub use source::{FileScope, Origin, SourceKind, Trust};
87pub use spec::{spec_kdl, spec_kdl_with, ConfigSpec, PropSpec, SpecFile, SpecSource};
88pub use ty::{Parser, Ty, TypeError};
89pub use value::{Const, Value};