Skip to main content

usage_rs/
lib.rs

1//! The facade for building compiled Rust CLIs with usage.
2//!
3//! Depend on `usage-rs` under the short crate name `usage`. That is the one package an
4//! application needs: derive macros, the argv runtime, help, and clap-shaped errors ship in the
5//! defaults. Completions stay behind a feature; low-level adopters that want only the binding
6//! runtime keep depending on `usage-argv` directly.
7//!
8//! ```toml
9//! [dependencies]
10//! usage = { package = "usage-rs", version = "6" }
11//! ```
12//!
13//! What happens after a parse can come from the same declaration: a command implements [`Run`],
14//! the subcommand enum says `#[usage(run)]`, and the `match` that routes argv to the code
15//! carrying it out is generated rather than written. [`RunWith`] under `#[usage(run_with)]` hands
16//! each command shared state, and [`RunAsync`] / [`RunAsyncWith`] under `#[usage(run_async)]` /
17//! `#[usage(run_async_with)]` are the async pair. Nothing about any of it reaches the spec.
18//!
19//! Enable portable expression validation only when a CLI declares `validate` rules:
20//!
21//! ```toml
22//! usage = { package = "usage-rs", version = "6", features = ["validation"] }
23//! ```
24//!
25//! ```
26//! use usage_rs as usage;
27//! # #[cfg(feature = "spec")]
28//! use usage::Cli;
29//!
30//! # #[cfg(not(feature = "spec"))]
31//! # fn main() {}
32//! # #[cfg(feature = "spec")]
33//! # fn main() {
34//! #[derive(Cli)]
35//! #[usage(bin = "ex")]
36//! struct Ex {
37//!     #[usage(long, value_hint = usage::ValueHint::FilePath)]
38//!     file: Option<std::path::PathBuf>,
39//! }
40//!
41//! let argv = [std::ffi::OsStr::new("--file"), std::ffi::OsStr::new("input.txt")];
42//! let ex = Ex::parse_from(&argv).expect("valid command line");
43//! assert_eq!(ex.file.as_deref(), Some(std::path::Path::new("input.txt")));
44//! # }
45//! ```
46
47#![forbid(unsafe_code)]
48
49// Generated absolute paths must also work if a derive is used inside this crate. Integration
50// targets already receive this name through Cargo; the library target needs the self alias.
51extern crate self as usage_rs;
52
53pub use usage_argv as argv;
54pub use usage_argv::*;
55#[cfg(feature = "config")]
56pub use usage_config as config;
57#[cfg(feature = "config")]
58pub use usage_derive::Config;
59#[cfg(feature = "spec")]
60pub use usage_derive::{ArgGroup, Args, Cli, Subcommands, ValueEnum};
61#[cfg(feature = "test")]
62pub use usage_test as test;
63#[cfg(feature = "validation")]
64pub use usage_validation as validation;
65#[cfg(feature = "response-files")]
66pub mod response;
67
68#[cfg(all(test, feature = "spec"))]
69mod tests {
70    #[derive(crate::Cli)]
71    #[usage(bin = "internal")]
72    struct Internal {}
73
74    #[cfg(feature = "validation")]
75    #[derive(Debug, crate::Cli)]
76    #[usage(bin = "validated")]
77    struct Validated {
78        #[usage(
79            long,
80            validate = "int(value) >= 1 && int(value) <= 65535",
81            validate_error = "must be a valid port"
82        )]
83        port: Option<u16>,
84    }
85
86    #[cfg(feature = "validation")]
87    #[derive(Debug, crate::Args)]
88    struct ValidatedArgs {
89        #[usage(long, validate = "value == 'ok'", validate_error = "must be ok")]
90        token: Option<String>,
91    }
92
93    #[cfg(feature = "validation")]
94    #[derive(Debug, crate::Cli)]
95    #[usage(bin = "validated-args")]
96    struct ValidatedArgsCli {
97        #[usage(flatten)]
98        args: ValidatedArgs,
99    }
100
101    #[test]
102    fn derives_resolve_the_facade_from_inside_the_facade() {
103        assert_eq!(Internal::spec().bin, Some("internal"));
104    }
105
106    #[cfg(feature = "config")]
107    #[derive(crate::Config)]
108    struct InternalSettings {
109        /// How many jobs to run at once
110        #[usage(env = "INTERNAL_JOBS", default = 4)]
111        jobs: u64,
112    }
113
114    #[cfg(feature = "config")]
115    #[test]
116    fn the_config_derive_resolves_the_facade_from_inside_the_facade() {
117        let resolved = crate::config::resolve(
118            InternalSettings::SETTINGS_REGISTRY,
119            crate::config::Layers::new(),
120        )
121        .expect("resolves");
122        let settings = InternalSettings::read(&resolved).expect("reads");
123        assert_eq!(settings.jobs, 4);
124        assert!(InternalSettings::spec_kdl().contains(r#"prop "jobs""#));
125    }
126
127    #[cfg(feature = "validation")]
128    #[test]
129    fn derives_evaluate_portable_validation_expressions() {
130        let valid = [
131            ::std::ffi::OsStr::new("--port"),
132            ::std::ffi::OsStr::new("9229"),
133        ];
134        assert_eq!(Validated::parse_from(&valid).unwrap().port, Some(9229));
135
136        let invalid = [
137            ::std::ffi::OsStr::new("--port"),
138            ::std::ffi::OsStr::new("0"),
139        ];
140        let crate::Error::InvalidValue(error) = Validated::parse_from(&invalid).unwrap_err() else {
141            panic!("expected invalid value");
142        };
143        assert_eq!(error.reason, "must be a valid port");
144
145        let invalid_args = [
146            ::std::ffi::OsStr::new("--token"),
147            ::std::ffi::OsStr::new("bad"),
148        ];
149        let crate::Error::InvalidValue(error) =
150            ValidatedArgsCli::parse_from(&invalid_args).unwrap_err()
151        else {
152            panic!("expected invalid value from flattened Args");
153        };
154        assert_eq!(error.reason, "must be ok");
155
156        let valid_args = [
157            ::std::ffi::OsStr::new("--token"),
158            ::std::ffi::OsStr::new("ok"),
159        ];
160        assert_eq!(
161            ValidatedArgsCli::parse_from(&valid_args)
162                .unwrap()
163                .args
164                .token
165                .as_deref(),
166            Some("ok")
167        );
168
169        let kdl = Validated::to_kdl();
170        assert!(
171            kdl.contains(r#"validate="int(value) >= 1 && int(value) <= 65535""#),
172            "{kdl}"
173        );
174    }
175}