use std::{collections::HashMap, hash::RandomState};
use crate::{
Attr,
schema::{
ArgKind, ArgLevelSchema, ArgSchema, ConfigEnumSchema, ConfigEnumVariantSchema,
ConfigFieldGroupSchema, ConfigFieldSchema, ConfigStructSchema, ConfigValueSchema,
ConfigVecSchema, Docs, LeafKind, LeafSchema, ScalarType, Schema, SpecialFields, Subcommand,
ValueSchema,
error::{SchemaError, SchemaErrorContext},
},
};
use facet::{
Def, EnumType, Facet, Field, ScalarType as FacetScalarType, Shape, StructKind, Type, UserType,
Variant,
};
use heck::ToKebabCase;
use indexmap::IndexMap;
impl Schema {
pub(crate) fn from_shape(shape: &'static Shape) -> Result<Self, SchemaError> {
let struct_type = match &shape.ty {
Type::User(UserType::Struct(s)) => *s,
_ => {
return Err(SchemaError::new(
SchemaErrorContext::root(shape),
"top-level shape must be a struct",
));
}
};
let ctx_root = SchemaErrorContext::root(shape);
let config_fields = discover_config_fields(struct_type.fields, &ctx_root, false)?;
let (args, special) = arg_level_from_fields_with_special(struct_type.fields, &ctx_root)?;
let mut configs = Vec::new();
for (field, field_ctx) in config_fields {
let shape = field.shape();
let optional_root = matches!(shape.def, Def::Option(_));
let config_shape = match shape.def {
Def::Option(opt) => opt.t,
_ => shape,
};
let env_prefix = extract_env_prefix(field);
configs.push(config_struct_schema_from_shape(
config_shape,
&field_ctx,
docs_from_lines(field.doc),
Some(field.effective_name().to_string()),
env_prefix,
optional_root,
field.is_flattened(),
)?);
}
let docs = docs_from_lines(shape.doc);
Ok(Schema {
docs,
args,
configs,
special,
})
}
}
fn has_any_args_attr(field: &Field) -> bool {
field.has_attr(Some("args"), "positional")
|| field.has_attr(Some("args"), "named")
|| field.has_attr(Some("args"), "subcommand")
|| field.has_attr(Some("args"), "config")
|| field.has_attr(Some("args"), "short")
|| field.has_attr(Some("args"), "counted")
|| field.has_attr(Some("args"), "env_prefix")
|| field.is_flattened()
}
fn extract_env_prefix(field: &Field) -> Option<String> {
let attr = field.get_attr(Some("args"), "env_prefix")?;
let parsed = attr.get_as::<crate::Attr>()?;
if let crate::Attr::EnvPrefix(prefix_opt) = parsed {
prefix_opt.map(|s| s.to_string())
} else {
None
}
}
fn discover_config_fields(
fields: &'static [Field],
ctx: &SchemaErrorContext,
inside_subcommand: bool,
) -> Result<Vec<(&'static Field, SchemaErrorContext)>, SchemaError> {
let mut config_fields = Vec::new();
for field in fields {
let field_ctx = ctx.with_field(field.name);
if is_config_field(field) {
if inside_subcommand {
return Err(SchemaError::new(
field_ctx,
"#[facet(args::config)] inside a subcommand variant is not supported",
)
.with_primary_label("place this config field on the outermost args struct"));
}
config_fields.push((field, field_ctx));
continue;
}
if field.has_attr(Some("args"), "env_prefix") {
return Err(SchemaError::new(
field_ctx,
format!(
"field `{}` uses args::env_prefix without args::config",
field.name
),
));
}
if field.is_flattened() {
let inner_shape = field.shape();
let Type::User(UserType::Struct(struct_type)) = inner_shape.ty else {
return Err(SchemaError::new(
field_ctx,
format!("flattened field `{}` must be a struct", field.name),
));
};
config_fields.extend(discover_config_fields(
struct_type.fields,
&field_ctx,
inside_subcommand,
)?);
}
if field.has_attr(Some("args"), "subcommand") {
let field_shape = field.shape();
let (enum_shape, enum_type) = match field_shape.def {
Def::Option(opt) => match opt.t.ty {
Type::User(UserType::Enum(enum_type)) => (opt.t, enum_type),
_ => continue,
},
_ => match field_shape.ty {
Type::User(UserType::Enum(enum_type)) => (field_shape, enum_type),
_ => continue,
},
};
for variant in enum_type.variants {
let variant_ctx =
SchemaErrorContext::root(enum_shape).with_variant(variant_cli_name(variant));
let variant_fields = variant_fields_for_schema(variant);
discover_config_fields(variant_fields, &variant_ctx, true)?;
}
}
}
Ok(config_fields)
}
fn extract_env_aliases(field: &Field) -> Vec<String> {
let mut aliases = Vec::new();
for field_attr in field.attributes {
if field_attr.ns == Some("args") && field_attr.key == "env_alias" {
if let Some(s) = field_attr.get_as::<&str>() {
aliases.push(s.to_string());
}
}
}
aliases
}
fn has_env_subst(field: &Field) -> bool {
field.has_attr(Some("args"), "env_subst")
}
fn has_env_subst_all(shape: &'static Shape) -> bool {
shape
.attributes
.iter()
.any(|attr| attr.ns == Some("args") && attr.key == "env_subst_all")
}
fn extract_label(field: &Field) -> Option<String> {
if let Some(attr) = field.get_attr(Some("args"), "label") {
if let Some(parsed) = attr.get_as::<Attr>()
&& let Attr::Label(s) = parsed
{
return Some(s.to_string());
}
if let Some(s) = attr.get_as::<&str>() {
return Some(s.to_string());
}
}
None
}
fn extract_field_default(field: &Field) -> Option<crate::config_value::ConfigValue> {
let default_source = field.default.as_ref()?;
let shape = field.shape();
match crate::config_value_parser::serialize_default_to_config_value(default_source, shape) {
Ok(config_value) => {
if matches!(config_value, crate::config_value::ConfigValue::Null(_)) {
if let Some(value) = from_trait_vec_default(default_source, shape) {
return Some(value);
}
tracing::debug!(
field = field.name,
"extract_field_default: serialized to null, skipping"
);
None
} else {
tracing::debug!(
field = field.name,
?config_value,
"extract_field_default: successfully extracted default"
);
Some(config_value)
}
}
Err(e) => {
if let Some(value) = from_trait_vec_default(default_source, shape) {
return Some(value);
}
tracing::debug!(
field = field.name,
error = %e,
"extract_field_default: failed to serialize default"
);
None
}
}
}
fn from_trait_vec_default(
default_source: &facet_core::DefaultSource,
shape: &'static Shape,
) -> Option<crate::config_value::ConfigValue> {
if !matches!(default_source, facet_core::DefaultSource::FromTrait)
|| !matches!(shape.def, Def::List(_))
{
return None;
}
Some(crate::config_value::ConfigValue::Array(
crate::config_value::Sourced {
value: Vec::new(),
span: None,
provenance: Some(crate::provenance::Provenance::Default),
},
))
}
fn docs_from_lines(lines: &'static [&'static str]) -> Docs {
if lines.is_empty() {
return Docs::default();
}
let summary = lines
.first()
.map(|line| line.trim().to_string())
.filter(|s| !s.is_empty());
let details = if lines.len() > 1 {
let mut buf = String::new();
for line in &lines[1..] {
if !buf.is_empty() {
buf.push('\n');
}
buf.push_str(line.trim());
}
if buf.is_empty() { None } else { Some(buf) }
} else {
None
};
Docs { summary, details }
}
fn scalar_kind_from_shape(shape: &'static Shape) -> Option<ScalarType> {
match shape.scalar_type()? {
FacetScalarType::Bool => Some(ScalarType::Bool),
FacetScalarType::Str
| FacetScalarType::String
| FacetScalarType::CowStr
| FacetScalarType::Char => Some(ScalarType::String),
FacetScalarType::F32 | FacetScalarType::F64 => Some(ScalarType::Float),
FacetScalarType::U8
| FacetScalarType::U16
| FacetScalarType::U32
| FacetScalarType::U64
| FacetScalarType::U128
| FacetScalarType::USize
| FacetScalarType::I8
| FacetScalarType::I16
| FacetScalarType::I32
| FacetScalarType::I64
| FacetScalarType::I128
| FacetScalarType::ISize => Some(ScalarType::Integer),
_ => None,
}
}
fn enum_variants(enum_type: EnumType) -> Vec<String> {
enum_type.variants.iter().map(variant_cli_name).collect()
}
fn variant_cli_name(variant: &Variant) -> String {
variant.effective_name().to_kebab_case()
}
fn leaf_schema_from_shape(
shape: &'static Shape,
_ctx: &SchemaErrorContext,
) -> Result<LeafSchema, SchemaError> {
if let Some(scalar) = scalar_kind_from_shape(shape) {
return Ok(LeafSchema {
kind: LeafKind::Scalar(scalar),
shape,
});
}
match &shape.ty {
Type::User(UserType::Enum(enum_type)) => Ok(LeafSchema {
kind: LeafKind::Enum {
variants: enum_variants(*enum_type),
},
shape,
}),
_ => Ok(LeafSchema {
kind: LeafKind::Scalar(ScalarType::Other),
shape,
}),
}
}
fn value_schema_from_shape(
shape: &'static Shape,
ctx: &SchemaErrorContext,
) -> Result<ValueSchema, SchemaError> {
match shape.def {
Def::Option(opt) => Ok(ValueSchema::Option {
value: Box::new(value_schema_from_shape(opt.t, ctx)?),
shape,
}),
Def::List(list) => Ok(ValueSchema::Vec {
element: Box::new(value_schema_from_shape(list.t, ctx)?),
shape,
}),
_ => match &shape.ty {
Type::User(UserType::Struct(_)) => Ok(ValueSchema::Struct {
fields: config_struct_schema_from_shape(
shape,
ctx,
Docs::default(),
None,
None,
false,
false,
)?,
shape,
}),
_ => Ok(ValueSchema::Leaf(leaf_schema_from_shape(shape, ctx)?)),
},
}
}
fn config_value_schema_from_shape(
shape: &'static Shape,
ctx: &SchemaErrorContext,
) -> Result<ConfigValueSchema, SchemaError> {
match shape.def {
Def::Option(opt) => Ok(ConfigValueSchema::Option {
value: Box::new(config_value_schema_from_shape(opt.t, ctx)?),
shape,
}),
Def::List(list) => Ok(ConfigValueSchema::Vec(ConfigVecSchema {
element: Box::new(config_value_schema_from_shape(list.t, ctx)?),
shape,
})),
_ => match &shape.ty {
Type::User(UserType::Struct(_)) => {
Ok(ConfigValueSchema::Struct(config_struct_schema_from_shape(
shape,
ctx,
Docs::default(),
None,
None,
false,
false,
)?))
}
Type::User(UserType::Enum(enum_type)) => Ok(ConfigValueSchema::Enum(
config_enum_schema_from_shape(shape, *enum_type, ctx)?,
)),
_ => Ok(ConfigValueSchema::Leaf(leaf_schema_from_shape(shape, ctx)?)),
},
}
}
fn value_schema_from_config_value_schema(value: &ConfigValueSchema) -> ValueSchema {
match value {
ConfigValueSchema::Leaf(leaf) => ValueSchema::Leaf(leaf.clone()),
ConfigValueSchema::Option { value, shape } => ValueSchema::Option {
value: Box::new(value_schema_from_config_value_schema(value)),
shape,
},
ConfigValueSchema::Vec(vec_schema) => ValueSchema::Vec {
element: Box::new(value_schema_from_config_value_schema(vec_schema.element())),
shape: vec_schema.shape(),
},
ConfigValueSchema::Struct(struct_schema) => ValueSchema::Struct {
fields: struct_schema.clone(),
shape: struct_schema.shape(),
},
ConfigValueSchema::Enum(enum_schema) => ValueSchema::Leaf(LeafSchema {
kind: LeafKind::Enum {
variants: enum_schema
.variants()
.keys()
.map(|name| name.to_kebab_case())
.collect(),
},
shape: enum_schema.shape(),
}),
}
}
fn value_schema_is_multiple(value: &ValueSchema) -> bool {
match value {
ValueSchema::Option { value, .. } => value_schema_is_multiple(value),
ValueSchema::Vec { .. } => true,
_ => false,
}
}
fn config_enum_schema_from_shape(
shape: &'static Shape,
enum_type: facet::EnumType,
ctx: &SchemaErrorContext,
) -> Result<ConfigEnumSchema, SchemaError> {
let mut variants: IndexMap<String, ConfigEnumVariantSchema, RandomState> = IndexMap::default();
for variant in enum_type.variants {
let variant_ctx = ctx.with_variant(variant.name.to_string());
let docs = docs_from_lines(variant.doc);
let mut fields: IndexMap<String, ConfigFieldSchema, RandomState> = IndexMap::default();
for field in variant.data.fields {
let field_ctx = variant_ctx.with_field(field.name);
let field_docs = docs_from_lines(field.doc);
let sensitive = field.flags.contains(facet_core::FieldFlags::SENSITIVE);
let env_aliases = extract_env_aliases(field);
let env_subst = has_env_subst(field);
let value = config_value_schema_from_shape(field.shape(), &field_ctx)?;
let default = extract_field_default(field);
fields.insert(
field.effective_name().to_string(),
ConfigFieldSchema {
docs: field_docs,
sensitive,
env_aliases,
env_subst,
value,
default,
},
);
}
variants.insert(
variant.effective_name().to_string(),
ConfigEnumVariantSchema { docs, fields },
);
}
Ok(ConfigEnumSchema { shape, variants })
}
fn config_struct_schema_from_shape(
shape: &'static Shape,
ctx: &SchemaErrorContext,
docs: Docs,
field_name: Option<String>,
env_prefix: Option<String>,
optional_root: bool,
flattened_root: bool,
) -> Result<ConfigStructSchema, SchemaError> {
config_struct_schema_from_shape_inner(
shape,
ctx,
docs,
ConfigStructBuildOptions {
field_name,
env_prefix,
optional_root,
flattened_root,
path_prefix: Vec::new(),
parent_env_subst_all: false,
},
)
}
struct ConfigStructBuildOptions {
field_name: Option<String>,
env_prefix: Option<String>,
optional_root: bool,
flattened_root: bool,
path_prefix: Vec<String>,
parent_env_subst_all: bool,
}
fn config_struct_schema_from_shape_inner(
shape: &'static Shape,
ctx: &SchemaErrorContext,
docs: Docs,
options: ConfigStructBuildOptions,
) -> Result<ConfigStructSchema, SchemaError> {
let ConfigStructBuildOptions {
field_name,
env_prefix,
optional_root,
flattened_root,
path_prefix,
parent_env_subst_all,
} = options;
let struct_type = match &shape.ty {
Type::User(UserType::Struct(s)) => *s,
_ => {
return Err(SchemaError::new(
ctx.clone(),
"config field must be a struct",
));
}
};
let this_env_subst_all = has_env_subst_all(shape);
let apply_env_subst_to_children = parent_env_subst_all || this_env_subst_all;
let mut fields_map: IndexMap<String, ConfigFieldSchema, RandomState> = IndexMap::default();
let mut field_groups = Vec::new();
for field in struct_type.fields {
let field_ctx = ctx.with_field(field.name);
if field.is_flattened() {
let inner_shape = field.shape();
let _inner_struct = match &inner_shape.ty {
Type::User(UserType::Struct(s)) => *s,
_ => {
return Err(SchemaError::new(
field_ctx,
format!("flattened config field `{}` must be a struct", field.name),
));
}
};
let mut new_prefix = path_prefix.clone();
new_prefix.push(field.effective_name().to_string());
let inner = config_struct_schema_from_shape_inner(
inner_shape,
&field_ctx,
Docs::default(),
ConfigStructBuildOptions {
field_name: None,
env_prefix: None,
optional_root: false,
flattened_root: false,
path_prefix: new_prefix,
parent_env_subst_all: apply_env_subst_to_children,
},
)?;
field_groups.push(ConfigFieldGroupSchema {
name: field.effective_name().to_string(),
docs: docs_from_lines(field.doc),
fields: inner.fields.clone(),
field_groups: inner.field_groups.clone(),
});
for (name, field_schema) in inner.fields {
if fields_map.contains_key(&name) {
return Err(SchemaError::new(
field_ctx.clone(),
format!(
"duplicate config field `{}` (from flattened field `{}`)",
name, field.name
),
));
}
fields_map.insert(name, field_schema);
}
continue;
}
let docs = docs_from_lines(field.doc);
let sensitive = field.flags.contains(facet_core::FieldFlags::SENSITIVE);
let env_aliases = extract_env_aliases(field);
let value = config_value_schema_from_shape(field.shape(), &field_ctx)?;
let default = extract_field_default(field);
let env_subst = has_env_subst(field) || apply_env_subst_to_children;
let effective_name = field.effective_name().to_string();
fields_map.insert(
effective_name,
ConfigFieldSchema {
docs,
sensitive,
env_aliases,
env_subst,
value,
default,
},
);
}
check_env_alias_conflicts(&fields_map, ctx)?;
Ok(ConfigStructSchema {
docs,
field_name,
env_prefix,
optional_root,
flattened_root,
shape,
fields: fields_map,
field_groups,
})
}
fn check_env_alias_conflicts(
fields: &IndexMap<String, ConfigFieldSchema, RandomState>,
ctx: &SchemaErrorContext,
) -> Result<(), SchemaError> {
use std::collections::HashMap;
let mut alias_to_field: HashMap<&str, &str> = HashMap::new();
for (field_name, field_schema) in fields.iter() {
for alias in field_schema.env_aliases() {
if let Some(existing_field) = alias_to_field.get(alias.as_str()) {
return Err(SchemaError::new(
ctx.clone(),
format!(
"env alias `{}` is used by both `{}` and `{}`",
alias, existing_field, field_name
),
));
}
alias_to_field.insert(alias.as_str(), field_name.as_str());
}
}
Ok(())
}
fn short_from_field(field: &Field) -> Option<char> {
field
.get_attr(Some("args"), "short")
.and_then(|attr| attr.get_as::<Attr>())
.and_then(|attr| {
if let Attr::Short(c) = attr {
c.or_else(|| field.effective_name().chars().next())
} else {
None
}
})
}
fn short_from_variant(variant: &Variant) -> Option<char> {
variant
.get_attr(Some("args"), "short")
.and_then(|attr| attr.get_as::<Attr>())
.and_then(|attr| {
if let Attr::Short(c) = attr {
c.or_else(|| variant.effective_name().chars().next())
} else {
None
}
})
}
fn variant_fields_for_schema(variant: &Variant) -> &'static [Field] {
let fields = variant.data.fields;
if is_flattened_tuple_variant(variant) {
let inner_shape = fields[0].shape();
if let Type::User(UserType::Struct(struct_type)) = inner_shape.ty {
return struct_type.fields;
}
}
fields
}
fn is_flattened_tuple_variant(variant: &Variant) -> bool {
let fields = variant.data.fields;
if variant.data.kind == StructKind::TupleStruct && fields.len() == 1 {
let inner_shape = fields[0].shape();
matches!(inner_shape.ty, Type::User(UserType::Struct(_)))
} else {
false
}
}
fn arg_level_from_fields(
fields: &'static [Field],
ctx: &SchemaErrorContext,
) -> Result<ArgLevelSchema, SchemaError> {
let (args, _special) = arg_level_from_fields_with_prefix(fields, ctx, Vec::new())?;
Ok(args)
}
fn arg_level_from_fields_with_special(
fields: &'static [Field],
ctx: &SchemaErrorContext,
) -> Result<(ArgLevelSchema, SpecialFields), SchemaError> {
arg_level_from_fields_with_prefix(fields, ctx, Vec::new())
}
fn arg_level_from_fields_with_prefix(
fields: &'static [Field],
ctx: &SchemaErrorContext,
path_prefix: Vec<String>,
) -> Result<(ArgLevelSchema, SpecialFields), SchemaError> {
let mut args: IndexMap<String, ArgSchema, RandomState> = IndexMap::default();
let mut subcommands: IndexMap<String, Subcommand, RandomState> = IndexMap::default();
let mut subcommand_field_name: Option<String> = None;
let mut subcommand_optional: bool = false;
let mut special = SpecialFields::default();
let mut seen_long: HashMap<String, SchemaErrorContext> = HashMap::new();
let mut seen_short: HashMap<char, SchemaErrorContext> = HashMap::new();
let mut seen_subcommands: HashMap<String, SchemaErrorContext> = HashMap::new();
let mut seen_subcommand_short: HashMap<char, SchemaErrorContext> = HashMap::new();
let mut first_subcommand_field: Option<SchemaErrorContext> = None;
for field in fields {
let field_ctx = ctx.with_field(field.name);
if is_config_field(field) {
if field.is_flattened() {
let shape = field.shape();
let optional_root = matches!(shape.def, Def::Option(_));
let config_shape = match shape.def {
Def::Option(opt) => opt.t,
_ => shape,
};
let config_field_name = field.effective_name().to_string();
let config_schema = config_struct_schema_from_shape(
config_shape,
&field_ctx,
docs_from_lines(field.doc),
Some(config_field_name.clone()),
extract_env_prefix(field),
optional_root,
true,
)?;
for (name, config_field_schema) in config_schema.fields() {
let long = name.to_kebab_case();
if let Some(existing_ctx) = seen_long.get(&long) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!("duplicate flag `--{long}` (from flattened config root)"),
)
.with_primary_label(format!("`--{long}` first defined here"))
.with_label(field_ctx.clone(), "flattened config root defined here"));
}
if args.contains_key(name) {
return Err(SchemaError::new(
field_ctx.clone(),
format!("duplicate argument `{name}` (from flattened config root)"),
)
.with_primary_label("flattened config root defined here"));
}
seen_long.insert(long, field_ctx.clone());
let value = value_schema_from_config_value_schema(config_field_schema.value());
let arg = ArgSchema {
name: name.clone(),
insertion_path: vec![config_field_name.clone(), name.clone()],
docs: config_field_schema.docs().clone(),
kind: ArgKind::Named {
short: None,
counted: false,
},
multiple: value_schema_is_multiple(&value),
value,
label: None,
required: false,
default: None,
};
args.insert(name.clone(), arg);
}
}
continue;
}
if field.is_flattened() {
let inner_shape = field.shape();
let struct_type = match &inner_shape.ty {
Type::User(UserType::Struct(s)) => *s,
_ => {
return Err(SchemaError::new(
field_ctx,
format!("flattened field `{}` must be a struct", field.name),
));
}
};
let (inner, inner_special) = arg_level_from_fields_with_prefix(
struct_type.fields,
&field_ctx,
path_prefix.clone(),
)?;
if inner_special.help.is_some() {
special.help = inner_special.help;
}
if inner_special.html_help.is_some() {
special.html_help = inner_special.html_help;
}
if inner_special.version.is_some() {
special.version = inner_special.version;
}
if inner_special.completions.is_some() {
special.completions = inner_special.completions;
}
if inner_special.export_jsonschemas.is_some() {
special.export_jsonschemas = inner_special.export_jsonschemas;
}
for (name, arg) in inner.args {
if let Some(existing_ctx) = seen_long.get(&name) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!("duplicate flag `--{}` (from flattened field)", name),
)
.with_primary_label("first defined here")
.with_label(field_ctx.clone(), "flattened here"));
}
seen_long.insert(name.clone(), field_ctx.clone());
if let ArgKind::Named { short: Some(c), .. } = &arg.kind {
if let Some(existing_ctx) = seen_short.get(c) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!("duplicate flag `-{}` (from flattened field)", c),
)
.with_primary_label("first defined here")
.with_label(field_ctx.clone(), "flattened here"));
}
seen_short.insert(*c, field_ctx.clone());
}
args.insert(name, arg);
}
for (name, sub) in inner.subcommands {
if let Some(existing_ctx) = seen_subcommands.get(&name) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!("duplicate subcommand `{}` (from flattened field)", name),
)
.with_primary_label("first defined here")
.with_label(field_ctx.clone(), "flattened here"));
}
seen_subcommands.insert(name.clone(), field_ctx.clone());
subcommands.insert(name, sub);
}
if inner.subcommand_field_name.is_some() {
if first_subcommand_field.is_some() {
return Err(SchemaError::new(
field_ctx,
"multiple subcommand fields via flatten",
));
}
first_subcommand_field = Some(field_ctx.clone());
subcommand_field_name = inner.subcommand_field_name;
subcommand_optional = inner.subcommand_optional;
}
continue;
}
if !has_any_args_attr(field) {
return Err(SchemaError::new(
field_ctx,
format!(
"field `{}` is missing a #[facet(args::...)] annotation",
field.name
),
));
}
if field.has_attr(Some("args"), "env_prefix") && !field.has_attr(Some("args"), "config") {
return Err(SchemaError::new(
field_ctx,
format!(
"field `{}` uses args::env_prefix without args::config",
field.name
),
));
}
let is_positional = field.has_attr(Some("args"), "positional");
let is_subcommand = field.has_attr(Some("args"), "subcommand");
if field.has_attr(Some("args"), "short") && is_positional {
return Err(SchemaError::new(
field_ctx,
"#[facet(args::positional)] is not compatible with #[facet(args::short)]",
)
.with_primary_label("has both attributes"));
}
if is_counted_field(field) && !is_supported_counted_type(field.shape()) {
return Err(SchemaError::new(
field_ctx,
format!(
"field `{}` marked as counted must be an integer",
field.name
),
));
}
if is_subcommand {
if let Some(first_ctx) = &first_subcommand_field {
return Err(SchemaError::new(
first_ctx.clone(),
"only one field may be marked with #[facet(args::subcommand)] at this level",
)
.with_primary_label("first marked here")
.with_label(field_ctx, "also marked here"));
}
first_subcommand_field = Some(field_ctx.clone());
subcommand_field_name = Some(field.name.to_string());
let field_shape = field.shape();
let (enum_shape, enum_type, is_optional) = match field_shape.def {
Def::Option(opt) => match opt.t.ty {
Type::User(UserType::Enum(enum_type)) => (opt.t, enum_type, true),
_ => {
return Err(SchemaError::new(
field_ctx,
format!(
"field `{}` marked as subcommand must be an enum",
field.name
),
));
}
},
_ => match field_shape.ty {
Type::User(UserType::Enum(enum_type)) => (field_shape, enum_type, false),
_ => {
return Err(SchemaError::new(
field_ctx,
format!(
"field `{}` marked as subcommand must be an enum",
field.name
),
));
}
},
};
subcommand_optional = is_optional;
for variant in enum_type.variants {
let cli_name = variant_cli_name(variant);
let effective_name = variant.effective_name().to_string();
let docs = docs_from_lines(variant.doc);
let short = short_from_variant(variant);
let variant_fields = variant_fields_for_schema(variant);
let variant_ctx =
SchemaErrorContext::root(enum_shape).with_variant(cli_name.clone());
let args_schema = arg_level_from_fields(variant_fields, &variant_ctx)?;
let is_flattened_tuple = is_flattened_tuple_variant(variant);
if let Some(short) = short {
if let Some(existing_ctx) = seen_subcommand_short.get(&short) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!("duplicate subcommand short alias `{short}`"),
)
.with_primary_label(format!("`{short}` first defined here"))
.with_label(variant_ctx.clone(), "defined again here"));
}
if let Some(existing_ctx) = seen_subcommands.get(&short.to_string()) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!(
"subcommand short alias `{short}` conflicts with existing subcommand name"
),
)
.with_primary_label("conflicting name defined here")
.with_label(variant_ctx.clone(), "conflicting short alias defined here"));
}
seen_subcommand_short.insert(short, variant_ctx.clone());
}
let sub = Subcommand {
name: cli_name.clone(),
effective_name: effective_name.clone(),
docs,
short,
args: args_schema,
is_flattened_tuple,
shape: enum_shape,
};
if let Some(existing_ctx) = seen_subcommands.get(&cli_name) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!("duplicate subcommand name `{cli_name}`"),
)
.with_primary_label("first defined here")
.with_label(variant_ctx, "defined again here"));
}
seen_subcommands.insert(cli_name.clone(), variant_ctx.clone());
subcommands.insert(effective_name, sub);
}
continue;
}
let short = if field.has_attr(Some("args"), "short") {
short_from_field(field)
} else {
None
};
let counted = field.has_attr(Some("args"), "counted");
let kind = if is_positional {
ArgKind::Positional
} else {
ArgKind::Named { short, counted }
};
let value = value_schema_from_shape(field.shape(), &field_ctx)?;
if matches!(value, ValueSchema::Struct { .. }) {
return Err(SchemaError::new(
field_ctx.clone(),
"struct fields in args must use #[facet(flatten)]",
)
.with_primary_label("this field is a struct type")
.with_label(
field_ctx.clone(),
"add #[facet(flatten)] to include its fields at this level",
));
}
#[allow(clippy::nonminimal_bool)]
let required = {
let shape = field.shape();
!matches!(shape.def, Def::Option(_))
&& !field.has_default()
&& !shape.is_shape(bool::SHAPE)
&& !(counted && is_supported_counted_type(shape))
};
let multiple = counted || matches!(field.shape().def, Def::List(_));
if !is_positional {
let long = field.effective_name().to_kebab_case();
if let Some(existing_ctx) = seen_long.get(&long) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!("duplicate flag `--{long}`"),
)
.with_primary_label(format!("`--{long}` first defined here"))
.with_label(field_ctx.clone(), "defined again here"));
}
seen_long.insert(long.clone(), field_ctx.clone());
if let Some(c) = short {
if let Some(existing_ctx) = seen_short.get(&c) {
return Err(SchemaError::new(
existing_ctx.clone(),
format!("duplicate flag `-{c}`"),
)
.with_primary_label(format!("`-{c}` first defined here"))
.with_label(field_ctx.clone(), "defined again here"));
}
seen_short.insert(c, field_ctx.clone());
}
}
let docs = docs_from_lines(field.doc);
let effective_name = field.effective_name().to_string();
let default = extract_field_default(field);
let mut field_path = path_prefix.clone();
field_path.push(effective_name.clone());
if field.has_attr(Some("args"), "help") {
special.help = Some(field_path.clone());
}
if field.has_attr(Some("args"), "html_help") || field.name == "html_help" {
special.html_help = Some(field_path.clone());
}
if field.has_attr(Some("args"), "version") {
special.version = Some(field_path.clone());
}
if field.has_attr(Some("args"), "completions") {
special.completions = Some(field_path.clone());
}
if field.has_attr(Some("args"), "export_jsonschemas") {
special.export_jsonschemas = Some(field_path.clone());
}
let arg = ArgSchema {
name: effective_name.clone(),
insertion_path: vec![effective_name.clone()],
docs,
kind,
value,
label: extract_label(field),
required,
multiple,
default,
};
if args.contains_key(&effective_name) {
return Err(SchemaError::new(
field_ctx.clone(),
format!("duplicate argument `{effective_name}`"),
)
.with_primary_label("defined again here"));
}
args.insert(effective_name, arg);
}
Ok((
ArgLevelSchema {
args,
subcommands,
subcommand_field_name,
subcommand_optional,
},
special,
))
}
fn is_counted_field(field: &facet_core::Field) -> bool {
field.has_attr(Some("args"), "counted")
}
const fn is_supported_counted_type(shape: &'static facet_core::Shape) -> bool {
use facet_core::{NumericType, PrimitiveType, Type};
matches!(
shape.ty,
Type::Primitive(PrimitiveType::Numeric(NumericType::Integer { .. }))
)
}
fn is_config_field(field: &facet_core::Field) -> bool {
field.has_attr(Some("args"), "config")
}