#![warn(missing_docs)]
#![deny(unsafe_code)]
extern crate self as figue;
pub use figue_attrs::*;
use figue_attrs as args;
#[macro_use]
mod macros;
#[cfg(feature = "arbitrary")]
pub mod arbitrary_checks;
pub(crate) mod builder;
pub(crate) mod color;
pub mod completions;
pub(crate) mod config_format;
pub(crate) mod config_value;
pub(crate) mod config_value_parser;
pub(crate) mod diagnostics;
pub(crate) mod driver;
pub(crate) mod dump;
pub(crate) mod enum_conflicts;
pub(crate) mod env_subst;
pub(crate) mod error;
pub(crate) mod extract;
pub(crate) mod help;
pub(crate) mod json_schema;
pub(crate) mod layers;
pub(crate) mod merge;
pub(crate) mod missing;
pub(crate) mod path;
pub(crate) mod provenance;
pub(crate) mod reflection;
pub(crate) mod schema;
pub(crate) mod span;
pub(crate) mod span_registry;
pub(crate) mod suggest;
pub mod to_args;
pub(crate) mod value_builder;
use facet_core::Facet;
pub use crate::completions::{Shell, generate_completions_for_shape};
#[cfg(feature = "arbitrary")]
pub use arbitrary_checks::{
ArbitraryCheckError, TestToArgsConsistencyConfig, TestToArgsRoundTrip,
assert_to_args_consistency, assert_to_args_roundtrip,
};
pub use builder::builder;
pub use config_format::{ConfigFormat, ConfigFormatError, JsonFormat, JsoncFormat};
pub use config_value::ConfigValue;
pub use config_value_parser::{ConfigValueDeserializeError, from_config_value};
pub use driver::{Driver, DriverError, DriverOutcome, DriverOutput, DriverReport};
pub use error::{ArgsErrorKind, ArgsErrorWithInput};
pub use extract::{ExtractError, ExtractMissingField};
pub use help::{
HelpConfig, generate_help, generate_help_for_shape, generate_html_help,
generate_html_help_for_shape, open_html_help_file, write_html_help_to_temp_file,
};
pub use json_schema::{JsonSchemaError, JsonSchemaFile, generate_json_schemas, write_json_schemas};
pub use layers::env::MockEnv;
pub use layers::file::FormatRegistry;
pub use to_args::{
ToArgs, ToArgsError, to_args_string, to_args_string_with_current_exe, to_os_args,
};
#[derive(Debug)]
pub enum FromStrError {
Parse(ConfigFormatError),
Deserialize(ConfigValueDeserializeError),
}
impl core::fmt::Display for FromStrError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FromStrError::Parse(e) => write!(f, "config parse error: {e}"),
FromStrError::Deserialize(e) => write!(f, "config deserialize error: {e}"),
}
}
}
impl core::error::Error for FromStrError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
FromStrError::Parse(e) => Some(e),
FromStrError::Deserialize(e) => Some(e),
}
}
}
pub fn from_str_with_format<T, F>(contents: &str, format: &F) -> Result<T, FromStrError>
where
T: Facet<'static>,
F: ConfigFormat,
{
let value = format.parse(contents).map_err(FromStrError::Parse)?;
from_config_value::<T>(&value).map_err(FromStrError::Deserialize)
}
pub fn from_str<T: Facet<'static>>(contents: &str) -> Result<T, FromStrError> {
from_str_with_format(contents, &JsoncFormat)
}
pub fn from_std_args<T: Facet<'static>>() -> DriverOutcome<T> {
let args: Vec<String> = std::env::args().skip(1).collect();
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
from_slice(&args_ref)
}
pub fn from_slice<T: Facet<'static>>(args: &[&str]) -> DriverOutcome<T> {
use crate::driver::{Driver, DriverError, DriverOutcome};
let config = match builder::<T>() {
Ok(b) => b
.cli(|cli| cli.args(args.iter().map(|s| s.to_string())))
.build(),
Err(e) => return DriverOutcome::err(DriverError::Builder { error: e }),
};
Driver::new(config).run()
}
#[derive(facet::Facet, Default, Debug)]
pub struct FigueBuiltins {
#[facet(args::named, args::short = 'h', args::help, default)]
pub help: bool,
#[facet(args::named, default)]
pub html_help: bool,
#[facet(args::named, args::short = 'V', args::version, default)]
pub version: bool,
#[facet(args::named, args::completions, default)]
pub completions: Option<Shell>,
#[facet(args::named, args::export_jsonschemas, args::label = "DIR", default)]
pub export_jsonschemas: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::help::generate_help;
use crate::schema::Schema;
#[derive(facet::Facet)]
struct ArgsWithBuiltins {
#[facet(args::positional)]
input: String,
#[facet(flatten)]
builtins: FigueBuiltins,
}
#[test]
fn test_figue_builtins_flatten_in_schema() {
let schema = Schema::from_shape(ArgsWithBuiltins::SHAPE);
assert!(schema.is_ok(), "Schema should build: {:?}", schema.err());
}
#[test]
fn test_figue_builtins_in_help() {
let help = generate_help::<ArgsWithBuiltins>(&HelpConfig::default());
assert!(
help.contains("--[no-]help"),
"help should contain --[no-]help"
);
assert!(help.contains("-h"), "help should contain -h");
assert!(
help.contains("--[no-]html-help"),
"help should contain --[no-]html-help"
);
assert!(
help.contains("--[no-]version"),
"help should contain --[no-]version"
);
assert!(help.contains("-V"), "help should contain -V");
assert!(
help.contains("--completions"),
"help should contain --completions"
);
assert!(
help.contains("<bash,zsh,fish>"),
"help should show enum variants for --completions: {}",
help
);
}
#[test]
fn test_figue_builtins_special_fields_detected() {
let schema = Schema::from_shape(ArgsWithBuiltins::SHAPE).unwrap();
let special = schema.special();
assert!(special.help.is_some(), "help should be detected");
assert_eq!(special.help.as_ref().unwrap(), &vec!["help".to_string()]);
assert!(special.html_help.is_some(), "html_help should be detected");
assert_eq!(
special.html_help.as_ref().unwrap(),
&vec!["html_help".to_string()]
);
assert!(special.version.is_some(), "version should be detected");
assert_eq!(
special.version.as_ref().unwrap(),
&vec!["version".to_string()]
);
assert!(
special.completions.is_some(),
"completions should be detected"
);
assert_eq!(
special.completions.as_ref().unwrap(),
&vec!["completions".to_string()]
);
assert!(
special.export_jsonschemas.is_some(),
"export_jsonschemas should be detected"
);
assert_eq!(
special.export_jsonschemas.as_ref().unwrap(),
&vec!["export_jsonschemas".to_string()]
);
}
#[derive(facet::Facet)]
struct ArgsWithRenamedHelp {
#[facet(args::named, args::help, rename = "print-docs")]
show_help: bool,
#[facet(args::named, args::version, rename = "show-version")]
show_ver: bool,
}
#[test]
fn test_special_fields_renamed() {
let schema = Schema::from_shape(ArgsWithRenamedHelp::SHAPE).unwrap();
let special = schema.special();
assert!(
special.help.is_some(),
"help should be detected via attribute"
);
assert_eq!(
special.help.as_ref().unwrap(),
&vec!["print-docs".to_string()],
"path should use effective name"
);
assert!(
special.version.is_some(),
"version should be detected via attribute"
);
assert_eq!(
special.version.as_ref().unwrap(),
&vec!["show-version".to_string()],
"path should use effective name"
);
}
#[derive(facet::Facet)]
struct DeepInner {
#[facet(args::named, args::help, default)]
help: bool,
}
#[derive(facet::Facet)]
struct DeepMiddle {
#[facet(flatten)]
inner: DeepInner,
}
#[derive(facet::Facet)]
struct ArgsWithDeepFlatten {
#[facet(args::positional)]
input: String,
#[facet(flatten)]
middle: DeepMiddle,
}
#[test]
fn test_special_fields_deeply_flattened() {
let schema = Schema::from_shape(ArgsWithDeepFlatten::SHAPE).unwrap();
let special = schema.special();
assert!(
special.help.is_some(),
"help should be detected in deeply flattened struct"
);
assert_eq!(
special.help.as_ref().unwrap(),
&vec!["help".to_string()],
"flattened fields appear at top level"
);
}
}