Skip to main content

figue/
lib.rs

1#![warn(missing_docs)]
2#![deny(unsafe_code)]
3// Allow deprecated during transition to new driver-based API
4//! # figue - Layered Configuration for Rust
5//!
6//! figue provides type-safe, layered configuration parsing with support for:
7//! - **CLI arguments** - Standard command-line argument parsing
8//! - **Environment variables** - Configure apps via environment
9//! - **Config files** - JSON, and more formats via plugins
10//! - **Defaults from code** - Compile-time defaults
11//!
12//! Built on [facet](https://docs.rs/facet) reflection, figue uses derive macros
13//! to generate parsers at compile time with zero runtime reflection overhead.
14//!
15//! ## Quick Start
16//!
17//! For simple CLI-only parsing, use [`from_slice`] or [`from_std_args`]:
18//!
19//! ```rust
20//! use facet::Facet;
21//! use figue::{self as args, FigueBuiltins};
22//!
23//! #[derive(Facet, Debug)]
24//! struct Args {
25//!     /// Enable verbose output
26//!     #[facet(args::named, args::short = 'v', default)]
27//!     verbose: bool,
28//!
29//!     /// Input file to process
30//!     #[facet(args::positional)]
31//!     input: String,
32//!
33//!     /// Standard CLI options (--help, --version, --completions)
34//!     #[facet(flatten)]
35//!     builtins: FigueBuiltins,
36//! }
37//!
38//! // Parse from a slice (useful for testing)
39//! let args: Args = figue::from_slice(&["--verbose", "input.txt"]).unwrap();
40//! assert!(args.verbose);
41//! assert_eq!(args.input, "input.txt");
42//! ```
43//!
44//! ## Layered Configuration
45//!
46//! For applications that need config files and environment variables, use the
47//! [`builder`] API with [`Driver`]:
48//!
49//! ```rust
50//! use facet::Facet;
51//! use figue::{self as args, builder, Driver};
52//!
53//! #[derive(Facet, Debug)]
54//! struct Args {
55//!     /// Application configuration
56//!     #[facet(args::config, args::env_prefix = "MYAPP")]
57//!     config: AppConfig,
58//! }
59//!
60//! #[derive(Facet, Debug)]
61//! struct AppConfig {
62//!     /// Server port
63//!     #[facet(default = 8080)]
64//!     port: u16,
65//!
66//!     /// Server host
67//!     #[facet(default = "localhost")]
68//!     host: String,
69//! }
70//!
71//! // Build layered configuration
72//! let config = builder::<Args>()
73//!     .unwrap()
74//!     .cli(|cli| cli.args(["--config.port", "3000"]))
75//!     .build();
76//!
77//! let output = Driver::new(config).run().into_result().unwrap();
78//! assert_eq!(output.value.config.port, 3000);
79//! assert_eq!(output.value.config.host, "localhost"); // from default
80//! ```
81//!
82//! ## Subcommands
83//!
84//! figue supports subcommands via enum types:
85//!
86//! ```rust
87//! use facet::Facet;
88//! use figue::{self as args, FigueBuiltins};
89//!
90//! #[derive(Facet, Debug)]
91//! struct Cli {
92//!     #[facet(args::subcommand)]
93//!     command: Command,
94//!
95//!     #[facet(flatten)]
96//!     builtins: FigueBuiltins,
97//! }
98//!
99//! #[derive(Facet, Debug)]
100//! #[repr(u8)]
101//! enum Command {
102//!     /// Build the project
103//!     Build {
104//!         /// Build in release mode
105//!         #[facet(args::named, args::short = 'r')]
106//!         release: bool,
107//!     },
108//!     /// Run the project
109//!     Run {
110//!         /// Arguments to pass through
111//!         #[facet(args::positional)]
112//!         args: Vec<String>,
113//!     },
114//! }
115//!
116//! let cli: Cli = figue::from_slice(&["build", "--release"]).unwrap();
117//! match cli.command {
118//!     Command::Build { release } => assert!(release),
119//!     Command::Run { .. } => unreachable!(),
120//! }
121//! ```
122//!
123//! ## Attribute Reference
124//!
125//! figue uses `#[facet(...)]` attributes to configure parsing behavior:
126//!
127//! | Attribute | Description |
128//! |-----------|-------------|
129//! | `args::positional` | Mark field as positional argument |
130//! | `args::named` | Mark field as named flag (--flag) |
131//! | `args::short = 'x'` | Add short flag (-x) |
132//! | `args::counted` | Count occurrences (-vvv = 3) |
133//! | `args::subcommand` | Mark field as subcommand selector |
134//! | `args::config` | Mark field as layered config struct |
135//! | `args::env_prefix = "X"` | Set env var prefix for config |
136//! | `args::help` | Mark as help flag (exits with code 0) |
137//! | `args::version` | Mark as version flag (exits with code 0) |
138//! | `args::completions` | Mark as shell completions flag |
139//! | `flatten` | Flatten nested struct fields |
140//! | `default` / `default = x` | Provide default value |
141//! | `rename = "x"` | Rename field in CLI/config |
142//! | `sensitive` | Redact field in debug output |
143//!
144//! ## Entry Points
145//!
146//! - [`from_std_args`] - Parse from `std::env::args()` (CLI-only)
147//! - [`from_slice`] - Parse from a string slice (CLI-only, good for testing)
148//! - [`builder`] - Start building layered configuration (CLI + env + files)
149//!
150//! For most CLI applications, start with [`FigueBuiltins`] flattened into your
151//! args struct to get `--help`, `--version`, and `--completions` for free.
152
153extern crate self as figue;
154
155// Re-export attribute macros from figue-attrs.
156// This allows users to write `#[facet(figue::named)]` or `use figue as args; #[facet(args::named)]`
157pub use figue_attrs::*;
158
159// Alias for internal use - allows `#[facet(args::named)]` syntax
160use figue_attrs as args;
161
162#[macro_use]
163mod macros;
164
165/// Arbitrary-based helper assertions for consumer roundtrip tests.
166#[cfg(feature = "arbitrary")]
167pub mod arbitrary_checks;
168pub(crate) mod builder;
169pub(crate) mod color;
170pub mod completions;
171pub(crate) mod config_format;
172pub(crate) mod config_value;
173pub(crate) mod config_value_parser;
174pub(crate) mod diagnostics;
175pub(crate) mod driver;
176pub(crate) mod dump;
177pub(crate) mod enum_conflicts;
178pub(crate) mod env_subst;
179pub(crate) mod error;
180pub(crate) mod extract;
181pub(crate) mod help;
182pub(crate) mod layers;
183pub(crate) mod merge;
184pub(crate) mod missing;
185pub(crate) mod path;
186pub(crate) mod provenance;
187pub(crate) mod reflection;
188pub(crate) mod schema;
189pub(crate) mod span;
190pub(crate) mod span_registry;
191pub(crate) mod suggest;
192/// Convert typed CLI values back into command-line arguments.
193pub mod to_args;
194pub(crate) mod value_builder;
195
196use facet_core::Facet;
197
198// ==========================================
199// PUBLIC INTERFACE
200// ==========================================
201
202pub use crate::completions::{Shell, generate_completions_for_shape};
203#[cfg(feature = "arbitrary")]
204pub use arbitrary_checks::{
205    ArbitraryCheckError, TestToArgsConsistencyConfig, TestToArgsRoundTrip,
206    assert_to_args_consistency, assert_to_args_roundtrip,
207};
208pub use builder::builder;
209pub use config_format::{ConfigFormat, ConfigFormatError, JsonFormat};
210pub use config_value::ConfigValue;
211pub use driver::{Driver, DriverError, DriverOutcome, DriverOutput, DriverReport};
212pub use error::{ArgsErrorKind, ArgsErrorWithInput};
213pub use extract::{ExtractError, ExtractMissingField};
214pub use help::{HelpConfig, generate_help, generate_help_for_shape};
215pub use layers::env::MockEnv;
216pub use layers::file::FormatRegistry;
217pub use to_args::{
218    ToArgs, ToArgsError, to_args_string, to_args_string_with_current_exe, to_os_args,
219};
220
221/// Parse command-line arguments from `std::env::args()`.
222///
223/// This is a convenience function for CLI-only parsing (no env vars, no config files).
224/// For layered configuration, use [`builder`] instead.
225///
226/// Returns a [`DriverOutcome`] which handles `--help`, `--version`, and errors gracefully.
227/// Use `.unwrap()` for automatic exit handling, or `.into_result()` for manual control.
228///
229/// # Example
230///
231/// ```rust,no_run
232/// use facet::Facet;
233/// use figue::{self as args, FigueBuiltins};
234///
235/// #[derive(Facet)]
236/// struct Args {
237///     #[facet(args::positional)]
238///     input: String,
239///
240///     #[facet(flatten)]
241///     builtins: FigueBuiltins,
242/// }
243///
244/// let args: Args = figue::from_std_args().unwrap();
245/// println!("Processing: {}", args.input);
246/// ```
247pub fn from_std_args<T: Facet<'static>>() -> DriverOutcome<T> {
248    let args: Vec<String> = std::env::args().skip(1).collect();
249    let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
250    from_slice(&args_ref)
251}
252
253/// Parse command-line arguments from a slice.
254///
255/// This is a convenience function for CLI-only parsing (no env vars, no config files).
256/// For layered configuration, use [`builder`] instead.
257///
258/// This function is particularly useful for testing, as you can provide arguments
259/// directly without modifying `std::env::args()`.
260///
261/// # Example
262///
263/// ```rust
264/// use facet::Facet;
265/// use figue::{self as args, FigueBuiltins};
266///
267/// #[derive(Facet, Debug)]
268/// struct Args {
269///     /// Enable verbose mode
270///     #[facet(args::named, args::short = 'v', default)]
271///     verbose: bool,
272///
273///     /// Input file
274///     #[facet(args::positional)]
275///     input: String,
276///
277///     #[facet(flatten)]
278///     builtins: FigueBuiltins,
279/// }
280///
281/// // Parse with long flag
282/// let args: Args = figue::from_slice(&["--verbose", "file.txt"]).unwrap();
283/// assert!(args.verbose);
284/// assert_eq!(args.input, "file.txt");
285///
286/// // Parse with short flag
287/// let args: Args = figue::from_slice(&["-v", "file.txt"]).unwrap();
288/// assert!(args.verbose);
289///
290/// // Parse without optional flag
291/// let args: Args = figue::from_slice(&["file.txt"]).unwrap();
292/// assert!(!args.verbose);
293/// ```
294///
295/// # Errors
296///
297/// Returns an error (via [`DriverOutcome`]) if:
298/// - Required arguments are missing
299/// - Unknown flags are provided
300/// - Type conversion fails (e.g., "abc" for a number)
301/// - `--help`, `--version`, or `--completions` is requested (success exit)
302pub fn from_slice<T: Facet<'static>>(args: &[&str]) -> DriverOutcome<T> {
303    use crate::driver::{Driver, DriverError, DriverOutcome};
304
305    let config = match builder::<T>() {
306        Ok(b) => b
307            .cli(|cli| cli.args(args.iter().map(|s| s.to_string())))
308            .build(),
309        Err(e) => return DriverOutcome::err(DriverError::Builder { error: e }),
310    };
311
312    Driver::new(config).run()
313}
314
315/// Standard CLI builtins that can be flattened into your Args struct.
316///
317/// This provides the standard `--help`, `--version`, and `--completions` flags
318/// that most CLI applications need. Flatten it into your Args struct:
319///
320/// ```rust
321/// use figue::{self as args, FigueBuiltins};
322/// use facet::Facet;
323///
324/// #[derive(Facet, Debug)]
325/// struct Args {
326///     /// Your actual arguments
327///     #[facet(args::positional)]
328///     input: String,
329///
330///     /// Standard CLI options
331///     #[facet(flatten)]
332///     builtins: FigueBuiltins,
333/// }
334///
335/// // The builtins are automatically available
336/// let args: Args = figue::from_slice(&["myfile.txt"]).unwrap();
337/// assert_eq!(args.input, "myfile.txt");
338/// assert!(!args.builtins.help);
339/// assert!(!args.builtins.version);
340/// ```
341///
342/// The driver automatically handles these fields:
343/// - `--help` / `-h`: Shows help and exits with code 0
344/// - `--version` / `-V`: Shows version and exits with code 0
345/// - `--completions <SHELL>`: Generates shell completions and exits with code 0
346///
347/// # Setting the Version
348///
349/// By default, `--version` displays "unknown" because figue cannot automatically
350/// capture your crate's version at compile time. To display your crate's version,
351/// configure it via the builder:
352///
353/// ```rust,no_run
354/// use figue::{self as args, builder, Driver, FigueBuiltins};
355/// use facet::Facet;
356///
357/// #[derive(Facet)]
358/// struct Args {
359///     #[facet(args::positional)]
360///     input: String,
361///
362///     #[facet(flatten)]
363///     builtins: FigueBuiltins,
364/// }
365///
366/// let config = figue::builder::<Args>()
367///     .unwrap()
368///     .cli(|cli| cli.args(std::env::args().skip(1)))
369///     .help(|h| h
370///         .program_name(env!("CARGO_PKG_NAME"))
371///         .version(env!("CARGO_PKG_VERSION")))
372///     .build();
373///
374/// let args: Args = figue::Driver::new(config).run().unwrap();
375/// // use args...
376/// ```
377///
378/// The `env!("CARGO_PKG_VERSION")` macro is evaluated at *your* crate's compile time,
379/// capturing the correct version from your `Cargo.toml`.
380///
381/// # Handling Help and Version Manually
382///
383/// If you need to handle these cases yourself (e.g., for custom formatting),
384/// use `into_result()` instead of `unwrap()`:
385///
386/// ```rust
387/// use figue::{self as args, FigueBuiltins, DriverError};
388/// use facet::Facet;
389///
390/// #[derive(Facet)]
391/// struct Args {
392///     #[facet(args::positional, default)]
393///     input: Option<String>,
394///
395///     #[facet(flatten)]
396///     builtins: FigueBuiltins,
397/// }
398///
399/// let result = figue::from_slice::<Args>(&["--help"]).into_result();
400/// match result {
401///     Err(DriverError::Help { text }) => {
402///         assert!(text.contains("--help"));
403///     }
404///     _ => panic!("expected help"),
405/// }
406/// ```
407#[derive(facet::Facet, Default, Debug)]
408pub struct FigueBuiltins {
409    /// Show help message and exit.
410    #[facet(args::named, args::short = 'h', args::help, default)]
411    pub help: bool,
412
413    /// Show version and exit.
414    #[facet(args::named, args::short = 'V', args::version, default)]
415    pub version: bool,
416
417    /// Generate shell completions.
418    #[facet(args::named, args::completions, default)]
419    pub completions: Option<Shell>,
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use crate::help::generate_help;
426    use crate::schema::Schema;
427
428    #[derive(facet::Facet)]
429    struct ArgsWithBuiltins {
430        /// Input file
431        #[facet(args::positional)]
432        input: String,
433
434        /// Standard options
435        #[facet(flatten)]
436        builtins: FigueBuiltins,
437    }
438
439    #[test]
440    fn test_figue_builtins_flatten_in_schema() {
441        let schema = Schema::from_shape(ArgsWithBuiltins::SHAPE);
442        assert!(schema.is_ok(), "Schema should build: {:?}", schema.err());
443    }
444
445    #[test]
446    fn test_figue_builtins_in_help() {
447        let help = generate_help::<ArgsWithBuiltins>(&HelpConfig::default());
448        assert!(help.contains("--help"), "help should contain --help");
449        assert!(help.contains("-h"), "help should contain -h");
450        assert!(help.contains("--version"), "help should contain --version");
451        assert!(help.contains("-V"), "help should contain -V");
452        assert!(
453            help.contains("--completions"),
454            "help should contain --completions"
455        );
456        assert!(
457            help.contains("<bash,zsh,fish>"),
458            "help should show enum variants for --completions: {}",
459            help
460        );
461    }
462
463    #[test]
464    fn test_figue_builtins_special_fields_detected() {
465        let schema = Schema::from_shape(ArgsWithBuiltins::SHAPE).unwrap();
466        let special = schema.special();
467
468        // With flatten, fields appear at top level - path is just ["help"]
469        assert!(special.help.is_some(), "help should be detected");
470        assert_eq!(special.help.as_ref().unwrap(), &vec!["help".to_string()]);
471
472        // Version at top level
473        assert!(special.version.is_some(), "version should be detected");
474        assert_eq!(
475            special.version.as_ref().unwrap(),
476            &vec!["version".to_string()]
477        );
478
479        // Completions at top level
480        assert!(
481            special.completions.is_some(),
482            "completions should be detected"
483        );
484        assert_eq!(
485            special.completions.as_ref().unwrap(),
486            &vec!["completions".to_string()]
487        );
488    }
489
490    // ========================================================================
491    // Tests: Special fields with custom names and nesting
492    // ========================================================================
493
494    /// Special fields can be renamed - detection works via attribute, not field name
495    #[derive(facet::Facet)]
496    struct ArgsWithRenamedHelp {
497        /// Print documentation and exit
498        #[facet(args::named, args::help, rename = "print-docs")]
499        show_help: bool,
500
501        /// Show program version
502        #[facet(args::named, args::version, rename = "show-version")]
503        show_ver: bool,
504    }
505
506    #[test]
507    fn test_special_fields_renamed() {
508        let schema = Schema::from_shape(ArgsWithRenamedHelp::SHAPE).unwrap();
509        let special = schema.special();
510
511        // Detection is by ATTRIBUTE (crate::help), not field name.
512        // The path uses the EFFECTIVE name (after rename).
513        assert!(
514            special.help.is_some(),
515            "help should be detected via attribute"
516        );
517        assert_eq!(
518            special.help.as_ref().unwrap(),
519            &vec!["print-docs".to_string()],
520            "path should use effective name"
521        );
522
523        assert!(
524            special.version.is_some(),
525            "version should be detected via attribute"
526        );
527        assert_eq!(
528            special.version.as_ref().unwrap(),
529            &vec!["show-version".to_string()],
530            "path should use effective name"
531        );
532    }
533
534    /// Deeply nested special fields (flatten inside flatten)
535    #[derive(facet::Facet)]
536    struct DeepInner {
537        #[facet(args::named, args::help, default)]
538        help: bool,
539    }
540
541    #[derive(facet::Facet)]
542    struct DeepMiddle {
543        #[facet(flatten)]
544        inner: DeepInner,
545    }
546
547    #[derive(facet::Facet)]
548    struct ArgsWithDeepFlatten {
549        #[facet(args::positional)]
550        input: String,
551
552        #[facet(flatten)]
553        middle: DeepMiddle,
554    }
555
556    #[test]
557    fn test_special_fields_deeply_flattened() {
558        let schema = Schema::from_shape(ArgsWithDeepFlatten::SHAPE).unwrap();
559        let special = schema.special();
560
561        // With flatten, all fields bubble up to top level - path is just ["help"]
562        assert!(
563            special.help.is_some(),
564            "help should be detected in deeply flattened struct"
565        );
566        assert_eq!(
567            special.help.as_ref().unwrap(),
568            &vec!["help".to_string()],
569            "flattened fields appear at top level"
570        );
571    }
572}