Skip to main content

teamy_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,
206    TestToArgsConsistencyConfig,
207    TestToArgsRoundTrip,
208    assert_to_args_consistency,
209    assert_to_args_roundtrip,
210};
211pub use builder::builder;
212pub use config_format::{ConfigFormat, ConfigFormatError, JsonFormat};
213pub use config_value::ConfigValue;
214pub use driver::{Driver, DriverError, DriverOutcome, DriverOutput, DriverReport};
215pub use error::{ArgsErrorKind, ArgsErrorWithInput};
216pub use extract::{ExtractError, ExtractMissingField};
217pub use help::{HelpConfig, generate_help, generate_help_for_shape};
218pub use layers::env::MockEnv;
219pub use layers::file::FormatRegistry;
220pub use to_args::{
221    ToArgs, ToArgsError, to_args_string, to_args_string_with_current_exe, to_os_args,
222};
223
224/// Parse command-line arguments from `std::env::args()`.
225///
226/// This is a convenience function for CLI-only parsing (no env vars, no config files).
227/// For layered configuration, use [`builder`] instead.
228///
229/// Returns a [`DriverOutcome`] which handles `--help`, `--version`, and errors gracefully.
230/// Use `.unwrap()` for automatic exit handling, or `.into_result()` for manual control.
231///
232/// # Example
233///
234/// ```rust,no_run
235/// use facet::Facet;
236/// use figue::{self as args, FigueBuiltins};
237///
238/// #[derive(Facet)]
239/// struct Args {
240///     #[facet(args::positional)]
241///     input: String,
242///
243///     #[facet(flatten)]
244///     builtins: FigueBuiltins,
245/// }
246///
247/// let args: Args = figue::from_std_args().unwrap();
248/// println!("Processing: {}", args.input);
249/// ```
250pub fn from_std_args<T: Facet<'static>>() -> DriverOutcome<T> {
251    let args: Vec<String> = std::env::args().skip(1).collect();
252    let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
253    from_slice(&args_ref)
254}
255
256/// Parse command-line arguments from a slice.
257///
258/// This is a convenience function for CLI-only parsing (no env vars, no config files).
259/// For layered configuration, use [`builder`] instead.
260///
261/// This function is particularly useful for testing, as you can provide arguments
262/// directly without modifying `std::env::args()`.
263///
264/// # Example
265///
266/// ```rust
267/// use facet::Facet;
268/// use figue::{self as args, FigueBuiltins};
269///
270/// #[derive(Facet, Debug)]
271/// struct Args {
272///     /// Enable verbose mode
273///     #[facet(args::named, args::short = 'v', default)]
274///     verbose: bool,
275///
276///     /// Input file
277///     #[facet(args::positional)]
278///     input: String,
279///
280///     #[facet(flatten)]
281///     builtins: FigueBuiltins,
282/// }
283///
284/// // Parse with long flag
285/// let args: Args = figue::from_slice(&["--verbose", "file.txt"]).unwrap();
286/// assert!(args.verbose);
287/// assert_eq!(args.input, "file.txt");
288///
289/// // Parse with short flag
290/// let args: Args = figue::from_slice(&["-v", "file.txt"]).unwrap();
291/// assert!(args.verbose);
292///
293/// // Parse without optional flag
294/// let args: Args = figue::from_slice(&["file.txt"]).unwrap();
295/// assert!(!args.verbose);
296/// ```
297///
298/// # Errors
299///
300/// Returns an error (via [`DriverOutcome`]) if:
301/// - Required arguments are missing
302/// - Unknown flags are provided
303/// - Type conversion fails (e.g., "abc" for a number)
304/// - `--help`, `--version`, or `--completions` is requested (success exit)
305pub fn from_slice<T: Facet<'static>>(args: &[&str]) -> DriverOutcome<T> {
306    use crate::driver::{Driver, DriverError, DriverOutcome};
307
308    let config = match builder::<T>() {
309        Ok(b) => b
310            .cli(|cli| cli.args(args.iter().map(|s| s.to_string())))
311            .build(),
312        Err(e) => return DriverOutcome::err(DriverError::Builder { error: e }),
313    };
314
315    Driver::new(config).run()
316}
317
318/// Standard CLI builtins that can be flattened into your Args struct.
319///
320/// This provides the standard `--help`, `--version`, and `--completions` flags
321/// that most CLI applications need. Flatten it into your Args struct:
322///
323/// ```rust
324/// use figue::{self as args, FigueBuiltins};
325/// use facet::Facet;
326///
327/// #[derive(Facet, Debug)]
328/// struct Args {
329///     /// Your actual arguments
330///     #[facet(args::positional)]
331///     input: String,
332///
333///     /// Standard CLI options
334///     #[facet(flatten)]
335///     builtins: FigueBuiltins,
336/// }
337///
338/// // The builtins are automatically available
339/// let args: Args = figue::from_slice(&["myfile.txt"]).unwrap();
340/// assert_eq!(args.input, "myfile.txt");
341/// assert!(!args.builtins.help);
342/// assert!(!args.builtins.version);
343/// ```
344///
345/// The driver automatically handles these fields:
346/// - `--help` / `-h`: Shows help and exits with code 0
347/// - `--version` / `-V`: Shows version and exits with code 0
348/// - `--completions <SHELL>`: Generates shell completions and exits with code 0
349///
350/// # Setting the Version
351///
352/// By default, `--version` displays "unknown" because figue cannot automatically
353/// capture your crate's version at compile time. To display your crate's version,
354/// configure it via the builder:
355///
356/// ```rust,no_run
357/// use figue::{self as args, builder, Driver, FigueBuiltins};
358/// use facet::Facet;
359///
360/// #[derive(Facet)]
361/// struct Args {
362///     #[facet(args::positional)]
363///     input: String,
364///
365///     #[facet(flatten)]
366///     builtins: FigueBuiltins,
367/// }
368///
369/// let config = figue::builder::<Args>()
370///     .unwrap()
371///     .cli(|cli| cli.args(std::env::args().skip(1)))
372///     .help(|h| h
373///         .program_name(env!("CARGO_PKG_NAME"))
374///         .version(env!("CARGO_PKG_VERSION")))
375///     .build();
376///
377/// let args: Args = figue::Driver::new(config).run().unwrap();
378/// // use args...
379/// ```
380///
381/// The `env!("CARGO_PKG_VERSION")` macro is evaluated at *your* crate's compile time,
382/// capturing the correct version from your `Cargo.toml`.
383///
384/// # Handling Help and Version Manually
385///
386/// If you need to handle these cases yourself (e.g., for custom formatting),
387/// use `into_result()` instead of `unwrap()`:
388///
389/// ```rust
390/// use figue::{self as args, FigueBuiltins, DriverError};
391/// use facet::Facet;
392///
393/// #[derive(Facet)]
394/// struct Args {
395///     #[facet(args::positional, default)]
396///     input: Option<String>,
397///
398///     #[facet(flatten)]
399///     builtins: FigueBuiltins,
400/// }
401///
402/// let result = figue::from_slice::<Args>(&["--help"]).into_result();
403/// match result {
404///     Err(DriverError::Help { text }) => {
405///         assert!(text.contains("--help"));
406///     }
407///     _ => panic!("expected help"),
408/// }
409/// ```
410#[derive(facet::Facet, Default, Debug)]
411pub struct FigueBuiltins {
412    /// Show help message and exit.
413    #[facet(args::named, args::short = 'h', args::help, default)]
414    pub help: bool,
415
416    /// Show version and exit.
417    #[facet(args::named, args::short = 'V', args::version, default)]
418    pub version: bool,
419
420    /// Generate shell completions.
421    #[facet(args::named, args::completions, default)]
422    pub completions: Option<Shell>,
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use crate::help::generate_help;
429    use crate::schema::Schema;
430
431    #[derive(facet::Facet)]
432    struct ArgsWithBuiltins {
433        /// Input file
434        #[facet(args::positional)]
435        input: String,
436
437        /// Standard options
438        #[facet(flatten)]
439        builtins: FigueBuiltins,
440    }
441
442    #[test]
443    fn test_figue_builtins_flatten_in_schema() {
444        let schema = Schema::from_shape(ArgsWithBuiltins::SHAPE);
445        assert!(schema.is_ok(), "Schema should build: {:?}", schema.err());
446    }
447
448    #[test]
449    fn test_figue_builtins_in_help() {
450        let help = generate_help::<ArgsWithBuiltins>(&HelpConfig::default());
451        assert!(help.contains("--help"), "help should contain --help");
452        assert!(help.contains("-h"), "help should contain -h");
453        assert!(help.contains("--version"), "help should contain --version");
454        assert!(help.contains("-V"), "help should contain -V");
455        assert!(
456            help.contains("--completions"),
457            "help should contain --completions"
458        );
459        assert!(
460            help.contains("<bash,zsh,fish>"),
461            "help should show enum variants for --completions: {}",
462            help
463        );
464    }
465
466    #[test]
467    fn test_figue_builtins_special_fields_detected() {
468        let schema = Schema::from_shape(ArgsWithBuiltins::SHAPE).unwrap();
469        let special = schema.special();
470
471        // With flatten, fields appear at top level - path is just ["help"]
472        assert!(special.help.is_some(), "help should be detected");
473        assert_eq!(special.help.as_ref().unwrap(), &vec!["help".to_string()]);
474
475        // Version at top level
476        assert!(special.version.is_some(), "version should be detected");
477        assert_eq!(
478            special.version.as_ref().unwrap(),
479            &vec!["version".to_string()]
480        );
481
482        // Completions at top level
483        assert!(
484            special.completions.is_some(),
485            "completions should be detected"
486        );
487        assert_eq!(
488            special.completions.as_ref().unwrap(),
489            &vec!["completions".to_string()]
490        );
491    }
492
493    // ========================================================================
494    // Tests: Special fields with custom names and nesting
495    // ========================================================================
496
497    /// Special fields can be renamed - detection works via attribute, not field name
498    #[derive(facet::Facet)]
499    struct ArgsWithRenamedHelp {
500        /// Print documentation and exit
501        #[facet(args::named, args::help, rename = "print-docs")]
502        show_help: bool,
503
504        /// Show program version
505        #[facet(args::named, args::version, rename = "show-version")]
506        show_ver: bool,
507    }
508
509    #[test]
510    fn test_special_fields_renamed() {
511        let schema = Schema::from_shape(ArgsWithRenamedHelp::SHAPE).unwrap();
512        let special = schema.special();
513
514        // Detection is by ATTRIBUTE (crate::help), not field name.
515        // The path uses the EFFECTIVE name (after rename).
516        assert!(
517            special.help.is_some(),
518            "help should be detected via attribute"
519        );
520        assert_eq!(
521            special.help.as_ref().unwrap(),
522            &vec!["print-docs".to_string()],
523            "path should use effective name"
524        );
525
526        assert!(
527            special.version.is_some(),
528            "version should be detected via attribute"
529        );
530        assert_eq!(
531            special.version.as_ref().unwrap(),
532            &vec!["show-version".to_string()],
533            "path should use effective name"
534        );
535    }
536
537    /// Deeply nested special fields (flatten inside flatten)
538    #[derive(facet::Facet)]
539    struct DeepInner {
540        #[facet(args::named, args::help, default)]
541        help: bool,
542    }
543
544    #[derive(facet::Facet)]
545    struct DeepMiddle {
546        #[facet(flatten)]
547        inner: DeepInner,
548    }
549
550    #[derive(facet::Facet)]
551    struct ArgsWithDeepFlatten {
552        #[facet(args::positional)]
553        input: String,
554
555        #[facet(flatten)]
556        middle: DeepMiddle,
557    }
558
559    #[test]
560    fn test_special_fields_deeply_flattened() {
561        let schema = Schema::from_shape(ArgsWithDeepFlatten::SHAPE).unwrap();
562        let special = schema.special();
563
564        // With flatten, all fields bubble up to top level - path is just ["help"]
565        assert!(
566            special.help.is_some(),
567            "help should be detected in deeply flattened struct"
568        );
569        assert_eq!(
570            special.help.as_ref().unwrap(),
571            &vec!["help".to_string()],
572            "flattened fields appear at top level"
573        );
574    }
575}