use super::*;
use crate as args;
use facet::Facet;
use facet_testhelpers::test;
macro_rules! assert_schema_snapshot {
($result:expr) => {{
match $result {
Ok(value) => insta::assert_snapshot!(facet_json::to_string_pretty(&value).unwrap()),
Err(err) => {
let rendered = err.to_string();
let stripped = strip_ansi_escapes::strip(rendered.as_bytes());
let stripped = String::from_utf8_lossy(&stripped);
insta::assert_snapshot!(stripped);
}
}
}};
}
#[derive(Facet)]
struct BasicArgs {
#[facet(args::named, args::short = 'v')]
verbose: bool,
#[facet(args::positional)]
input: String,
#[facet(args::named)]
include: Vec<String>,
#[facet(args::named, args::short = 'q', args::counted)]
quiet: u32,
#[facet(args::subcommand)]
command: Option<Command>,
#[facet(args::config, args::env_prefix = "APP")]
config: Option<AppConfig>,
}
#[derive(Facet)]
#[repr(u8)]
enum Command {
Build(BuildArgs),
#[facet(rename = "clean-all")]
Clean,
}
#[derive(Facet)]
struct BuildArgs {
#[facet(args::named, args::short = 'r')]
release: bool,
}
#[derive(Facet)]
struct AppConfig {
host: String,
port: u16,
}
#[derive(Facet)]
struct MissingArgsAnnotation {
foo: String,
}
#[derive(Facet)]
#[repr(u8)]
enum SubA {
A,
}
#[derive(Facet)]
#[repr(u8)]
enum SubB {
B,
}
#[derive(Facet)]
struct MultipleSubcommands {
#[facet(args::subcommand)]
a: SubA,
#[facet(args::subcommand)]
b: SubB,
}
#[derive(Facet)]
struct SubcommandOnNonEnum {
#[facet(args::subcommand)]
value: String,
}
#[derive(Facet)]
struct CountedOnNonInteger {
#[facet(args::named, args::counted)]
value: bool,
}
#[derive(Facet)]
struct ShortOnPositional {
#[facet(args::positional, args::short = 'p')]
value: String,
}
#[derive(Facet)]
struct EnvPrefixWithoutConfig {
#[facet(args::env_prefix = "APP")]
value: String,
}
#[derive(Facet)]
struct ConflictingLongFlags {
#[facet(args::named, rename = "dup")]
a: bool,
#[facet(args::named, rename = "dup")]
b: bool,
}
#[derive(Facet)]
struct ConflictingShortFlags {
#[facet(args::named, args::short = 'v')]
a: bool,
#[facet(args::named, args::short = 'v')]
b: bool,
}
#[derive(Facet)]
struct BadConfigField {
#[facet(args::config)]
config: String,
}
#[derive(Facet)]
#[repr(u8)]
enum TopLevelEnum {
Foo,
}
#[test]
fn snapshot_schema_basic() {
assert_schema_snapshot!(Schema::from_shape(BasicArgs::SHAPE));
}
#[test]
fn snapshot_schema_top_level_enum() {
assert_schema_snapshot!(Schema::from_shape(TopLevelEnum::SHAPE));
}
#[test]
fn snapshot_schema_missing_args_annotation() {
assert_schema_snapshot!(Schema::from_shape(MissingArgsAnnotation::SHAPE));
}
#[test]
fn snapshot_schema_multiple_subcommands() {
assert_schema_snapshot!(Schema::from_shape(MultipleSubcommands::SHAPE));
}
#[test]
fn snapshot_schema_subcommand_on_non_enum() {
assert_schema_snapshot!(Schema::from_shape(SubcommandOnNonEnum::SHAPE));
}
#[test]
fn snapshot_schema_counted_on_non_integer() {
assert_schema_snapshot!(Schema::from_shape(CountedOnNonInteger::SHAPE));
}
#[test]
fn snapshot_schema_short_on_positional() {
assert_schema_snapshot!(Schema::from_shape(ShortOnPositional::SHAPE));
}
#[test]
fn snapshot_schema_env_prefix_without_config() {
assert_schema_snapshot!(Schema::from_shape(EnvPrefixWithoutConfig::SHAPE));
}
#[test]
fn snapshot_schema_conflicting_long_flags() {
assert_schema_snapshot!(Schema::from_shape(ConflictingLongFlags::SHAPE));
}
#[test]
fn snapshot_schema_conflicting_short_flags() {
assert_schema_snapshot!(Schema::from_shape(ConflictingShortFlags::SHAPE));
}
#[test]
fn snapshot_schema_bad_config_field() {
assert_schema_snapshot!(Schema::from_shape(BadConfigField::SHAPE));
}
#[derive(Facet)]
struct CommonArgs {
#[facet(args::named, args::short = 'v')]
verbose: bool,
#[facet(args::named, args::short = 'q')]
quiet: bool,
}
#[derive(Facet)]
struct ArgsWithFlatten {
#[facet(args::positional)]
input: String,
#[facet(flatten)]
common: CommonArgs,
}
#[test]
fn test_flatten_schema_builds() {
let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).expect("schema should build");
let args = schema.args();
assert!(
args.args.contains_key("verbose"),
"verbose should be in args"
);
assert!(args.args.contains_key("quiet"), "quiet should be in args");
assert!(args.args.contains_key("input"), "input should be in args");
}
#[derive(Facet)]
struct OutputArgs {
#[facet(args::named, args::short = 'f')]
format: Option<String>,
}
#[derive(Facet)]
struct ExtendedCommonArgs {
#[facet(flatten)]
common: CommonArgs,
#[facet(flatten)]
output: OutputArgs,
}
#[derive(Facet)]
struct ArgsWithNestedFlatten {
#[facet(args::positional)]
input: String,
#[facet(flatten)]
extended: ExtendedCommonArgs,
}
#[derive(Facet)]
struct ConflictingFlattenArgs {
#[facet(args::named, args::short = 'v')]
version: bool,
#[facet(flatten)]
common: CommonArgs, }
#[test]
fn test_flatten_conflict_detected() {
let result = Schema::from_shape(ConflictingFlattenArgs::SHAPE);
assert!(result.is_err(), "should detect duplicate -v flag");
let err = result.unwrap_err().to_string();
assert!(
err.contains("duplicate") || err.contains("-v"),
"error should mention duplicate: {}",
err
);
}
#[derive(Facet)]
struct CommonConfig {
log_level: Option<String>,
debug: bool,
}
#[derive(Facet)]
struct DatabaseConfig {
host: String,
port: u16,
}
#[derive(Facet)]
struct ConfigWithFlatten {
name: String,
#[facet(flatten)]
common: CommonConfig,
}
#[derive(Facet)]
struct ArgsWithFlattenedConfig {
#[facet(args::positional)]
input: String,
#[facet(args::config)]
config: ConfigWithFlatten,
}
#[test]
fn test_config_flatten_schema_builds() {
let schema = Schema::from_shape(ArgsWithFlattenedConfig::SHAPE).expect("schema should build");
let config = schema.config().expect("should have config");
let fields = config.fields();
assert_eq!(fields.len(), 3, "should have 3 fields after flatten");
assert!(fields.contains_key("name"), "should have name field");
assert!(
fields.contains_key("log_level"),
"should have log_level from flattened common"
);
assert!(
fields.contains_key("debug"),
"should have debug from flattened common"
);
}
#[derive(Facet)]
struct ExtendedConfig {
#[facet(flatten)]
common: CommonConfig,
#[facet(flatten)]
database: DatabaseConfig,
}
#[derive(Facet)]
struct ConfigWithNestedFlatten {
app_name: String,
#[facet(flatten)]
extended: ExtendedConfig,
}
#[derive(Facet)]
struct ArgsWithNestedFlattenConfig {
#[facet(args::positional)]
input: String,
#[facet(args::config)]
config: ConfigWithNestedFlatten,
}
#[test]
fn test_config_nested_flatten_schema_builds() {
let schema =
Schema::from_shape(ArgsWithNestedFlattenConfig::SHAPE).expect("schema should build");
let config = schema.config().expect("should have config");
let fields = config.fields();
assert_eq!(fields.len(), 5, "should have 5 fields after nested flatten");
assert!(fields.contains_key("app_name"), "should have app_name");
assert!(fields.contains_key("log_level"), "should have log_level");
assert!(fields.contains_key("debug"), "should have debug");
assert!(fields.contains_key("host"), "should have host");
assert!(fields.contains_key("port"), "should have port");
}
#[derive(Facet)]
struct ConflictingConfigA {
name: String,
}
#[derive(Facet)]
struct ConflictingConfigB {
name: String, }
#[derive(Facet)]
struct ConfigWithConflictingFlatten {
#[facet(flatten)]
a: ConflictingConfigA,
#[facet(flatten)]
b: ConflictingConfigB,
}
#[derive(Facet)]
struct ArgsWithConflictingConfigFlatten {
#[facet(args::positional)]
input: String,
#[facet(args::config)]
config: ConfigWithConflictingFlatten,
}
#[test]
fn test_config_flatten_conflict_detected() {
let result = Schema::from_shape(ArgsWithConflictingConfigFlatten::SHAPE);
assert!(result.is_err(), "should detect duplicate config field");
let err = result.unwrap_err().to_string();
assert!(
err.contains("duplicate") || err.contains("name"),
"error should mention duplicate: {}",
err
);
}
#[derive(Facet)]
struct NestedOptions {
#[facet(args::named)]
verbose: bool,
}
#[derive(Facet)]
struct ArgsWithUnflattenedStruct {
#[facet(args::named)]
options: NestedOptions, }
#[derive(Facet)]
#[facet(transparent)]
struct TransparentPattern(String);
#[derive(Facet)]
struct ArgsWithTransparentNewtype {
#[facet(args::named)]
pattern: TransparentPattern,
}
#[test]
fn test_struct_field_without_flatten_is_error() {
let result = Schema::from_shape(ArgsWithUnflattenedStruct::SHAPE);
assert!(result.is_err(), "struct field without flatten should error");
let err = result.unwrap_err().to_string();
assert!(
err.contains("flatten"),
"error should mention flatten: {}",
err
);
}
#[test]
fn test_transparent_newtype_arg_is_allowed() {
let schema = Schema::from_shape(ArgsWithTransparentNewtype::SHAPE)
.expect("transparent newtype args should be treated like their inner leaf type");
assert!(
schema.args().args.contains_key("pattern"),
"transparent newtype field should appear as a regular named arg"
);
}
#[derive(Facet)]
struct ConfigWithConflictingAliases {
#[facet(args::env_alias = "DATABASE_URL")]
db_url: String,
#[facet(args::env_alias = "DATABASE_URL")]
connection_string: String,
}
#[derive(Facet)]
struct ArgsWithConflictingAliases {
#[facet(args::config)]
config: ConfigWithConflictingAliases,
}
#[test]
fn test_env_alias_conflict_detected() {
let result = Schema::from_shape(ArgsWithConflictingAliases::SHAPE);
assert!(result.is_err(), "should detect duplicate env alias");
let err = result.unwrap_err().to_string();
assert!(
err.contains("DATABASE_URL") && err.contains("db_url") && err.contains("connection_string"),
"error should mention the alias and both fields: {}",
err
);
}