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}