use std::{
collections::{HashMap, HashSet},
iter,
};
use proc_macro2::Ident;
use quote::{quote, quote_spanned};
use syn::{
Attribute, Data, DataEnum, DataStruct, DeriveInput, Expr, Field, Fields, GenericArgument,
Index, Lit, LitStr, Member, Path, PathArguments, Token, Type, TypePath, ext::IdentExt,
parse::ParseStream, spanned::Spanned,
};
pub(crate) fn wrap_in_option(val: Option<proc_macro2::TokenStream>) -> proc_macro2::TokenStream {
if let Some(val) = val {
quote!(::core::option::Option::Some(#val))
} else {
quote!(::core::option::Option::None)
}
}
fn parse_docs(attrs: &[Attribute]) -> String {
let doc_lines = attrs.iter().filter_map(|attr| {
if attr.meta.path().is_ident("doc") {
let name_value = attr.meta.require_name_value().ok()?;
let Expr::Lit(doc_literal) = &name_value.value else {
return None;
};
match &doc_literal.lit {
Lit::Str(doc_literal) => Some(doc_literal.value()),
_ => None,
}
} else {
None
}
});
let mut docs = String::new();
for line in doc_lines {
let line = line.trim();
if line.is_empty() {
if !docs.is_empty() {
docs.push('\n');
}
} else {
if !docs.is_empty() && !docs.ends_with(|ch: char| ch.is_ascii_whitespace()) {
docs.push(' ');
}
docs.push_str(line);
}
}
docs
}
#[derive(Debug)]
pub(crate) struct Validation {
pub(crate) expr: Expr,
pub(crate) description: Option<Expr>,
}
impl Validation {
fn new(input: ParseStream<'_>) -> syn::Result<Self> {
let content;
syn::parenthesized!(content in input);
let expr = content.parse()?;
let description = if content.is_empty() {
None
} else {
content.parse::<Token![,]>()?;
Some(content.parse()?)
};
Ok(Self { expr, description })
}
}
#[derive(Debug)]
pub(crate) struct ConfigVariantAttrs {
pub(crate) rename: Option<LitStr>,
pub(crate) aliases: Vec<LitStr>,
pub(crate) default: bool,
pub(crate) help: String,
}
impl ConfigVariantAttrs {
fn new(attrs: &[Attribute]) -> syn::Result<Self> {
let config_attrs = attrs.iter().filter(|attr| attr.path().is_ident("config"));
let mut rename = None;
let mut aliases = vec![];
let mut default = false;
for attr in config_attrs {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
rename = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("alias") {
aliases.push(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("default") {
default = true;
Ok(())
} else {
Err(meta.error("Unsupported attribute"))
}
})?;
}
Ok(Self {
rename,
aliases,
default,
help: parse_docs(attrs),
})
}
}
#[derive(Debug)]
pub(crate) enum DefaultValue {
DefaultTrait,
Path(Path),
Expr(Expr),
}
impl DefaultValue {
pub(crate) fn instance(&self, span: proc_macro2::Span) -> proc_macro2::TokenStream {
match self {
Self::DefaultTrait => quote_spanned!(span=> ::core::default::Default::default()),
Self::Path(path) => quote_spanned!(span=> #path()),
Self::Expr(expr) => quote_spanned!(span=> #expr),
}
}
fn fallback_fn(&self, span: proc_macro2::Span) -> proc_macro2::TokenStream {
match self {
Self::DefaultTrait => {
quote_spanned!(span=> ::core::default::Default::default)
}
Self::Path(def_fn) => quote!(#def_fn),
Self::Expr(expr) => quote_spanned!(span=> || #expr),
}
}
}
#[derive(Debug)]
pub(crate) struct Alias {
pub(crate) lit: LitStr,
pub(crate) is_deprecated: bool,
}
#[derive(Debug, Default)]
pub(crate) struct ConfigFieldAttrs {
pub(crate) rename: Option<LitStr>,
pub(crate) aliases: Vec<Alias>,
pub(crate) default: Option<DefaultValue>,
pub(crate) example: Option<Expr>,
pub(crate) fallback: Option<Expr>,
pub(crate) flatten: bool,
pub(crate) nest: bool,
pub(crate) is_secret: bool,
pub(crate) with: Option<Expr>,
pub(crate) deserialize_if: Option<Validation>,
pub(crate) validations: Vec<Validation>,
}
impl ConfigFieldAttrs {
#[allow(clippy::too_many_lines)]
fn new(attrs: &[Attribute], is_option: bool) -> syn::Result<Self> {
let config_attrs = attrs.iter().filter(|attr| attr.path().is_ident("config"));
let mut rename = None;
let mut aliases = vec![];
let mut default = None;
let mut example = None;
let mut fallback = None;
let mut nested_span = None;
let mut flatten_span = None;
let mut with = None;
let mut secret_span = None;
let mut deserialize_if = None;
let mut validations = vec![];
for attr in config_attrs {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
rename = Some(meta.value()?.parse::<LitStr>()?);
Ok(())
} else if meta.path.is_ident("alias") {
aliases.push(Alias {
lit: meta.value()?.parse()?,
is_deprecated: false,
});
Ok(())
} else if meta.path.is_ident("deprecated") {
aliases.push(Alias {
lit: meta.value()?.parse()?,
is_deprecated: true,
});
Ok(())
} else if meta.path.is_ident("default") {
default = Some(if meta.input.peek(syn::Token![=]) {
DefaultValue::Path(meta.value()?.parse()?)
} else {
DefaultValue::DefaultTrait
});
Ok(())
} else if meta.path.is_ident("default_t") {
default = Some(DefaultValue::Expr(meta.value()?.parse()?));
Ok(())
} else if meta.path.is_ident("example") {
example = Some(meta.value()?.parse::<Expr>()?);
Ok(())
} else if meta.path.is_ident("fallback") {
fallback = Some(meta.value()?.parse::<Expr>()?);
Ok(())
} else if meta.path.is_ident("flatten") {
flatten_span = Some(meta.path.span());
Ok(())
} else if meta.path.is_ident("nest") {
nested_span = Some(meta.path.span());
Ok(())
} else if meta.path.is_ident("secret") {
secret_span = Some(meta.path.span());
Ok(())
} else if meta.path.is_ident("with") {
with = Some(meta.value()?.parse::<Expr>()?);
Ok(())
} else if meta.path.is_ident("validate") {
validations.push(Validation::new(meta.input)?);
Ok(())
} else if meta.path.is_ident("deserialize_if") {
deserialize_if = Some(Validation::new(meta.input)?);
Ok(())
} else {
Err(meta.error("Unsupported attribute"))
}
})?;
}
if let (Some(nested_span), Some(_)) = (nested_span, flatten_span) {
let msg = "cannot specify both `nest` and `flatten` for config";
return Err(syn::Error::new(nested_span, msg));
}
let flatten = flatten_span.is_some();
let nest = flatten_span.is_some() || nested_span.is_some();
if let (Some(with), true) = (&with, nest) {
let msg = "cannot specify `with` for a `nest`ed / `flatten`ed configuration";
return Err(syn::Error::new(with.span(), msg));
}
if let (Some(fallback), true) = (&fallback, nest) {
let msg = "cannot specify `fallback` for a `nest`ed / `flatten`ed configuration";
return Err(syn::Error::new(fallback.span(), msg));
}
if let (Some(filter), true) = (&deserialize_if, nest) {
let msg = "cannot specify `filter` for a `nest`ed / `flatten`ed configuration";
return Err(syn::Error::new(filter.expr.span(), msg));
}
if let (Some(deserialize_if), false) = (&deserialize_if, is_option) {
let msg = "`deserialize_if` can only be specified for `Option`s";
return Err(syn::Error::new(deserialize_if.expr.span(), msg));
}
if let (Some(flatten_span), Some(_)) = (flatten_span, &rename) {
let msg = "`rename` attribute is useless for flattened configs; did you mean to make a config nested?";
return Err(syn::Error::new(flatten_span, msg));
}
if let (Some(flatten_span), true) = (flatten_span, is_option) {
let msg = "cannot make `flatten`ed config optional; did you mean to make it nested?";
return Err(syn::Error::new(flatten_span, msg));
}
if let (Some(flatten_span), false) = (flatten_span, aliases.is_empty()) {
let msg = "aliases for flattened configs are not supported yet; did you mean to make a config nested?";
return Err(syn::Error::new(flatten_span, msg));
}
if let (Some(secret_span), true) = (secret_span, nest) {
let msg = "only params can be marked as secret, sub-configs cannot";
return Err(syn::Error::new(secret_span, msg));
}
Ok(Self {
rename,
aliases,
default,
example,
fallback,
flatten,
nest,
with,
deserialize_if,
validations,
is_secret: secret_span.is_some(),
})
}
}
#[derive(Debug)]
pub(crate) struct ConfigField {
pub(crate) attrs: ConfigFieldAttrs,
pub(crate) docs: String,
pub(crate) name: Member,
pub(crate) ty: Type,
}
impl ConfigField {
fn new(raw: &Field) -> syn::Result<Self> {
Self::new_inner(raw, false)
}
fn from_newtype_variant(raw: &Field) -> syn::Result<Self> {
let mut this = Self::new_inner(raw, true)?;
this.attrs.flatten = true;
this.attrs.nest = true;
Ok(this)
}
fn new_inner(raw: &Field, support_unnamed: bool) -> syn::Result<Self> {
let name = if let Some(name) = raw.ident.clone() {
Member::Named(name)
} else if support_unnamed {
Member::Unnamed(Index {
index: 0,
span: raw.ty.span(),
})
} else {
let message = "Only named fields are supported";
return Err(syn::Error::new_spanned(raw, message));
};
let ty = raw.ty.clone();
let attrs = ConfigFieldAttrs::new(&raw.attrs, Self::is_option(&ty))?;
Ok(Self {
attrs,
docs: parse_docs(&raw.attrs),
name,
ty,
})
}
pub(crate) fn from_tag(
cr: &proc_macro2::TokenStream,
tag: &LitStr,
variants: impl Iterator<Item = String>,
default: Option<&str>,
) -> Self {
let default_opt = wrap_in_option(default.map(|val| quote!(#val)));
let with = syn::parse_quote! {
#cr::de::_private::TagDeserializer::new(&[#(#variants,)*], #default_opt)
};
Self {
attrs: ConfigFieldAttrs {
default: default.map(|s| {
DefaultValue::Expr(syn::parse_quote_spanned!(tag.span() => #s.into()))
}),
with: Some(with),
..ConfigFieldAttrs::default()
},
docs: "Tag for the enum config".to_owned(),
name: Ident::new(&tag.value(), tag.span()).into(),
ty: syn::parse_quote_spanned!(tag.span()=> &'static str),
}
}
pub(crate) fn is_option(ty: &Type) -> bool {
Self::unwrap_option(ty).is_some()
}
pub(crate) fn unwrap_option(ty: &Type) -> Option<&Type> {
let Type::Path(TypePath { path, .. }) = ty else {
return None;
};
if path.segments.len() != 1 {
return None;
}
let segment = &path.segments[0];
if segment.ident != "Option" {
return None;
}
let PathArguments::AngleBracketed(angle_bracketed) = &segment.arguments else {
return None;
};
if angle_bracketed.args.len() != 1 {
return None;
}
match &angle_bracketed.args[0] {
GenericArgument::Type(ty) => Some(ty),
_ => None,
}
}
pub(crate) fn param_name(&self) -> String {
self.attrs.rename.as_ref().map_or_else(
|| match &self.name {
Member::Named(ident) => ident.unraw().to_string(),
Member::Unnamed(idx) => idx.index.to_string(),
},
LitStr::value,
)
}
pub(crate) fn name_span(&self) -> proc_macro2::Span {
match &self.name {
Member::Named(ident) => ident.span(),
Member::Unnamed(_) => self.ty.span(),
}
}
pub(crate) fn default_fn(&self) -> Option<proc_macro2::TokenStream> {
let name_span = self.name_span();
self.attrs
.default
.as_ref()
.map(|default| default.fallback_fn(name_span))
}
}
#[derive(Debug, Clone, Copy)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum RenameRule {
LowerCase,
UpperCase,
CamelCase,
SnakeCase,
ScreamingSnakeCase,
KebabCase,
ScreamingKebabCase,
}
impl RenameRule {
fn parse(s: &str) -> Result<Self, &'static str> {
Ok(match s {
"lowercase" => Self::LowerCase,
"UPPERCASE" => Self::UpperCase,
"camelCase" => Self::CamelCase,
"snake_case" => Self::SnakeCase,
"SCREAMING_SNAKE_CASE" => Self::ScreamingSnakeCase,
"kebab-case" => Self::KebabCase,
"SCREAMING-KEBAB-CASE" => Self::ScreamingKebabCase,
_ => {
return Err(
"Invalid case specified; should be one of: lowercase, UPPERCASE, camelCase, \
snake_case, SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE",
);
}
})
}
fn transform(self, ident: &str) -> String {
debug_assert!(ident.is_ascii()); let (spacing_char, scream) = match self {
Self::LowerCase => return ident.to_ascii_lowercase(),
Self::UpperCase => return ident.to_ascii_uppercase(),
Self::CamelCase => return ident[..1].to_ascii_lowercase() + &ident[1..],
Self::SnakeCase => ('_', false),
Self::ScreamingSnakeCase => ('_', true),
Self::KebabCase => ('-', false),
Self::ScreamingKebabCase => ('-', true),
};
let mut output = String::with_capacity(ident.len());
for (i, ch) in ident.char_indices() {
if i > 0 && ch.is_ascii_uppercase() {
output.push(spacing_char);
}
output.push(if scream {
ch.to_ascii_uppercase()
} else {
ch.to_ascii_lowercase()
});
}
output
}
}
#[derive(Debug)]
pub(crate) struct ConfigContainerAttrs {
pub(crate) cr: Option<Path>,
pub(crate) rename_all: Option<RenameRule>,
pub(crate) tag: Option<LitStr>,
pub(crate) validations: Vec<Validation>,
pub(crate) derive_default: bool,
}
impl ConfigContainerAttrs {
fn new(attrs: &[Attribute], is_struct: bool) -> syn::Result<Self> {
let config_attrs = attrs.iter().filter(|attr| attr.path().is_ident("config"));
let mut cr = None;
let mut rename_all = None;
let mut tag = None;
let mut validations = vec![];
let mut derive_default = false;
for attr in config_attrs {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("crate") {
cr = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("rename_all") {
let rule: LitStr = meta.value()?.parse()?;
let parsed = RenameRule::parse(&rule.value())
.map_err(|msg| syn::Error::new(rule.span(), msg))?;
rename_all = Some((rule, parsed));
Ok(())
} else if meta.path.is_ident("tag") {
tag = Some(meta.value()?.parse::<LitStr>()?);
Ok(())
} else if meta.path.is_ident("validate") {
validations.push(Validation::new(meta.input)?);
Ok(())
} else if meta.path.is_ident("derive") {
let content;
syn::parenthesized!(content in meta.input);
let tr: Ident = content.parse()?;
if tr == "Default" {
derive_default = true;
} else {
let msg = "Can only derive(Default) yet";
return Err(syn::Error::new(tr.span(), msg));
}
Ok(())
} else {
Err(meta.error("Unsupported attribute"))
}
})?;
}
if is_struct {
if let Some((rename_all, _)) = &rename_all {
let msg = "`rename_all` attribute must not be used on struct configs";
return Err(syn::Error::new(rename_all.span(), msg));
}
if let Some(tag) = &tag {
let msg = "`tag` attribute must not be used on struct configs";
return Err(syn::Error::new(tag.span(), msg));
}
}
Ok(Self {
cr,
rename_all: rename_all.map(|(_, parsed)| parsed),
tag,
validations,
derive_default,
})
}
}
#[derive(Debug)]
pub(crate) struct ConfigEnumVariant {
pub(crate) attrs: ConfigVariantAttrs,
pub(crate) name: Ident,
pub(crate) fields: Vec<ConfigField>,
}
impl ConfigEnumVariant {
pub(crate) fn name(&self, rename_rule: Option<RenameRule>) -> String {
self.attrs.rename.as_ref().map_or_else(
|| {
let name = self.name.to_string();
if let Some(rule) = rename_rule {
rule.transform(&name)
} else {
name
}
},
LitStr::value,
)
}
pub(crate) fn expected_variants(
&self,
rename_rule: Option<RenameRule>,
) -> impl Iterator<Item = String> + '_ {
iter::once(self.name(rename_rule)).chain(self.attrs.aliases.iter().map(LitStr::value))
}
}
#[derive(Debug)]
pub(crate) enum ConfigContainerFields {
Struct(Vec<ConfigField>),
Enum {
tag: LitStr,
variants: Vec<ConfigEnumVariant>,
},
}
impl ConfigContainerFields {
pub(crate) fn all_fields(&self) -> Vec<(usize, &ConfigField)> {
match self {
Self::Struct(fields) => fields.iter().map(|field| (0, field)).collect(),
Self::Enum { variants, .. } => variants
.iter()
.enumerate()
.flat_map(|(variant_idx, variant)| {
variant.fields.iter().map(move |field| (variant_idx, field))
})
.collect(),
}
}
}
#[derive(Debug)]
pub(crate) struct ConfigContainer {
pub(crate) attrs: ConfigContainerAttrs,
pub(crate) name: Ident,
pub(crate) help: String,
pub(crate) fields: ConfigContainerFields,
}
impl ConfigContainer {
pub(crate) fn new(raw: &DeriveInput) -> syn::Result<Self> {
if raw.generics.type_params().count() != 0
|| raw.generics.const_params().count() != 0
|| raw.generics.lifetimes().count() != 0
{
let message = "generics are not supported";
return Err(syn::Error::new_spanned(&raw.generics, message));
}
let attrs = ConfigContainerAttrs::new(&raw.attrs, matches!(&raw.data, Data::Struct(_)))?;
let fields = match &raw.data {
Data::Struct(data) => ConfigContainerFields::Struct(Self::extract_struct_fields(data)?),
Data::Enum(data) => Self::extract_enum_fields(data, &attrs)?,
Data::Union(_) => {
let message = "#[derive(DescribeConfig)] can only be placed on structs or enums";
return Err(syn::Error::new_spanned(raw, message));
}
};
let name = raw.ident.clone();
Ok(Self {
attrs,
name,
help: parse_docs(&raw.attrs),
fields,
})
}
fn extract_struct_fields(data: &DataStruct) -> syn::Result<Vec<ConfigField>> {
data.fields.iter().map(ConfigField::new).collect()
}
fn extract_enum_fields(
data: &DataEnum,
container_attrs: &ConfigContainerAttrs,
) -> syn::Result<ConfigContainerFields> {
let mut variants = vec![];
let mut merged_fields_by_name = HashMap::new();
let mut variants_with_aliases = HashSet::new();
let mut has_default_variant = false;
for variant in &data.variants {
let attrs = ConfigVariantAttrs::new(&variant.attrs)?;
let (name, name_span) = attrs.rename.as_ref().map_or_else(
|| (variant.ident.to_string(), variant.ident.span()),
|lit| (lit.value(), lit.span()),
);
if !variants_with_aliases.insert(name) {
let msg = "Tag value is redefined";
return Err(syn::Error::new(name_span, msg));
}
for alias in &attrs.aliases {
if !variants_with_aliases.insert(alias.value()) {
let msg = "Tag value is redefined";
return Err(syn::Error::new(alias.span(), msg));
}
}
if attrs.default {
if has_default_variant {
let msg = "Only one variant can be marked as default";
return Err(syn::Error::new(variant.ident.span(), msg));
}
has_default_variant = true;
}
let mut variant_fields = vec![];
match &variant.fields {
Fields::Named(fields) => {
for field in &fields.named {
let new_field = ConfigField::new(field)?;
if let Some(prev_ty) =
merged_fields_by_name.insert(new_field.param_name(), &field.ty)
&& *prev_ty != new_field.ty
{
let msg = "Parameter with this name and another type is already defined in another enum variant; \
this may lead to unexpected config merge results and thus not supported";
return Err(syn::Error::new_spanned(field, msg));
}
variant_fields.push(new_field);
}
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() >= 2 {
let msg = "Variants with >=2 unnamed fields are not supported";
return Err(syn::Error::new(variant.ident.span(), msg));
} else if fields.unnamed.len() == 1 {
let field = fields.unnamed.first().unwrap();
variant_fields.push(ConfigField::from_newtype_variant(field)?);
}
}
Fields::Unit => { }
}
variants.push(ConfigEnumVariant {
attrs,
name: variant.ident.clone(),
fields: variant_fields,
});
}
let has_fields = variants.iter().any(|variant| !variant.fields.is_empty());
if !has_fields {
let msg = "Cannot use an enum without fields as a config; this is useless";
return Err(syn::Error::new_spanned(&data.variants, msg));
}
let tag = container_attrs.tag.clone().ok_or_else(|| {
let msg = "Only tagged enums are supported as configs. Please add #[config(tag = ..)] to the enum";
syn::Error::new_spanned(&data.variants, msg)
})?;
if merged_fields_by_name.contains_key(&tag.value()) {
let msg = "Tag name coincides with an existing param name";
return Err(syn::Error::new(tag.span(), msg));
}
Ok(ConfigContainerFields::Enum { tag, variants })
}
pub(crate) fn cr(&self, span: proc_macro2::Span) -> proc_macro2::TokenStream {
if let Some(cr) = &self.attrs.cr {
quote_spanned!(span=> #cr)
} else {
quote_spanned!(span=> ::smart_config)
}
}
}