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}