use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::{ToTokens, quote, quote_spanned};
use syn::{DeriveInput, LitStr, Type, spanned::Spanned};
use crate::utils::{
Alias, ConfigContainer, ConfigContainerFields, ConfigEnumVariant, ConfigField, DefaultValue,
RenameRule, Validation, wrap_in_option,
};
impl DefaultValue {
fn boxed(
this: Option<&Self>,
span: proc_macro2::Span,
ty: &Type,
) -> Option<proc_macro2::TokenStream> {
match this {
None if !ConfigField::is_option(ty) => None,
Some(Self::DefaultTrait) | None => Some(quote_spanned! {span=>
<::std::boxed::Box<#ty> as ::core::default::Default>::default()
}),
Some(Self::Path(path)) => {
Some(quote_spanned!(span=> ::std::boxed::Box::<#ty>::new(#path())))
}
Some(Self::Expr(expr)) => {
Some(quote_spanned!(span=> ::std::boxed::Box::<#ty>::new(#expr)))
}
}
}
}
impl Validation {
fn wrap(&self, cr: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let expr = &self.expr;
if let Some(description) = &self.description {
quote_spanned! {expr.span()=>
#cr::validation::_private::WithDescription::new(#expr, #description)
}
} else {
quote!(#expr)
}
}
}
impl Alias {
fn describe(&self, cr: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let lit = &self.lit;
let maybe_deprecated = self.is_deprecated.then(|| quote!(.deprecated()));
quote_spanned!(lit.span()=> (#lit, #cr::metadata::AliasOptions::new()#maybe_deprecated))
}
}
impl ConfigField {
fn deserializer(&self, cr: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let mut deserializer = if let Some(with) = &self.attrs.with {
if Self::is_option(&self.ty) {
quote_spanned!(with.span()=> #cr::de::Optional::map(#with))
} else {
quote!(#with)
}
} else {
let ty = &self.ty;
quote_spanned!(ty.span()=> <#ty as #cr::de::WellKnown>::DE)
};
if self.attrs.is_secret {
deserializer = quote!(#cr::de::Secret(#deserializer));
}
if let Some(deserialize_if) = &self.attrs.deserialize_if {
let ty = Self::unwrap_option(&self.ty).expect("checked during field parsing");
let wrapped = deserialize_if.wrap(cr);
deserializer = quote_spanned! {deserialize_if.expr.span()=>
#cr::de::_private::DeserializeIf::<#ty, _>::new(#deserializer, &(#wrapped))
};
}
if let Some(default_fn) = self.default_fn() {
deserializer = quote!(#cr::de::WithDefault::new(#deserializer, #default_fn));
}
if !self.attrs.validations.is_empty() {
let validations = self.attrs.validations.iter().map(|val| {
let wrapped = val.wrap(cr);
quote_spanned!(val.expr.span()=> &(#wrapped))
});
deserializer =
quote!(#cr::de::_private::Validated::new(#deserializer, &[#(#validations,)*]));
}
deserializer
}
fn validate_names(&self, parent: &ConfigContainer) -> proc_macro2::TokenStream {
let name_span = self.name_span();
let param_name = self.param_name();
let name_validation_span = self.attrs.rename.as_ref().map_or(name_span, LitStr::span);
let cr = parent.cr(name_validation_span);
let name_validation = quote_spanned! {name_validation_span=>
const _: () = #cr::metadata::_private::assert_param_name(#param_name);
};
let aliases = self.attrs.aliases.iter();
let aliases_validation = aliases.map(|alias| {
let alias = &alias.lit;
let cr = parent.cr(alias.span());
quote_spanned! {alias.span()=>
const _: () = #cr::metadata::_private::assert_param_alias(#alias);
}
});
quote! {
#name_validation
#(#aliases_validation)*
}
}
fn describe_param(
&self,
parent: &ConfigContainer,
variant_idx: Option<usize>,
) -> proc_macro2::TokenStream {
let name = &self.name;
let name_span = self.name_span();
let help = &self.docs;
let param_name = self.param_name();
let ty = &self.ty;
let ty_in_code = if let Some(text) = ty.span().source_text() {
quote!(#text)
} else {
quote!(::core::stringify!(#ty))
};
let default_value = DefaultValue::boxed(self.attrs.default.as_ref(), name_span, ty);
let default_value = wrap_in_option(default_value.map(|val| quote!(|| #val)));
let example_value = self.attrs.example.as_ref().map(
|example| quote_spanned!(example.span()=> || ::std::boxed::Box::<#ty>::new(#example)),
);
let example_value = wrap_in_option(example_value);
let fallback = self
.attrs
.fallback
.as_ref()
.map(quote::ToTokens::to_token_stream);
let fallback = wrap_in_option(fallback);
let cr = parent.cr(name_span);
let deserializer = self.deserializer(&cr);
let tag_variant = wrap_in_option(variant_idx.map(|idx| quote!(&TAG_VARIANTS[#idx])));
let aliases = self.attrs.aliases.iter().map(|alias| alias.describe(&cr));
quote_spanned! {name_span=> {
let deserializer = #deserializer;
#cr::metadata::ParamMetadata {
name: #param_name,
aliases: &[#(#aliases,)*],
help: #help,
rust_field_name: ::core::stringify!(#name),
rust_type: #cr::metadata::RustType::of::<#ty>(#ty_in_code),
expecting: #cr::de::_private::extract_expected_types::<#ty, _>(&deserializer),
tag_variant: #tag_variant,
deserializer: &#cr::de::_private::Erased::<#ty, _>::new(deserializer),
default_value: #default_value,
example_value: #example_value,
fallback: #fallback,
}
}}
}
fn describe_nested_config(
&self,
parent: &ConfigContainer,
variant_idx: Option<usize>,
) -> proc_macro2::TokenStream {
let cr = parent.cr(self.name_span());
let name = &self.name;
let aliases = self.attrs.aliases.iter().map(|alias| alias.describe(&cr));
let ty = Self::unwrap_option(&self.ty).unwrap_or(&self.ty);
let config_name = if self.attrs.flatten {
String::new()
} else {
self.param_name()
};
let tag_variant = wrap_in_option(variant_idx.map(|idx| quote!(&TAG_VARIANTS[#idx])));
quote_spanned! {self.name_span()=>
#cr::metadata::NestedConfigMetadata {
name: #config_name,
aliases: &[#(#aliases,)*],
rust_field_name: ::core::stringify!(#name),
tag_variant: #tag_variant,
meta: &<#ty as #cr::DescribeConfig>::DESCRIPTION,
}
}
}
fn visit_nested_config(
&self,
cr: &proc_macro2::TokenStream,
i: usize,
binding: Option<Ident>,
) -> proc_macro2::TokenStream {
let is_optional = Self::is_option(&self.ty);
let field = &self.name;
let binding = binding.map_or_else(|| quote!(&self.#field), ToTokens::into_token_stream);
if is_optional {
quote_spanned! {field.span()=>
visitor.visit_nested_opt_config(
#i,
::core::option::Option::as_ref(#binding)
.map(|config| config as &dyn #cr::visit::VisitConfig),
)
}
} else {
quote_spanned!(field.span()=> visitor.visit_nested_config(#i, #binding))
}
}
}
impl ConfigEnumVariant {
fn describe(
&self,
cr: &proc_macro2::TokenStream,
rename_rule: Option<RenameRule>,
) -> proc_macro2::TokenStream {
let name = self.name(rename_rule);
let rust_name = &self.name;
let aliases = self.attrs.aliases.iter();
let help = &self.attrs.help;
quote_spanned! {self.name.span()=>
#cr::metadata::ConfigVariant {
name: #name,
aliases: &[#(#aliases,)*],
rust_name: ::core::stringify!(#rust_name),
help: #help,
}
}
}
fn visit_match_arm(
&self,
cr: &proc_macro2::TokenStream,
variant_idx: usize,
start_param_offset: &mut usize,
start_config_offset: &mut usize,
) -> proc_macro2::TokenStream {
let name = &self.name;
let mut bindings = vec![];
let mut params = vec![];
let mut nested_configs = vec![];
for (i, field) in self.fields.iter().enumerate() {
let is_config = field.attrs.nest;
let field_name = &field.name;
let field_binding = quote::format_ident!("__{i}");
bindings.push(quote_spanned!(field_name.span()=> #field_name: #field_binding));
if is_config {
nested_configs.push(field.visit_nested_config(
cr,
*start_config_offset,
Some(field_binding),
));
*start_config_offset += 1;
} else {
params.push(quote_spanned! {field_name.span()=>
visitor.visit_param(#start_param_offset, #field_binding)
});
*start_param_offset += 1;
}
}
quote_spanned! {name.span()=>
Self::#name { #(#bindings,)* .. } => {
visitor.visit_tag(#variant_idx);
#(#params;)*
#(#nested_configs;)*
}
}
}
}
impl ConfigContainer {
fn derive_visit_config(&self) -> proc_macro2::TokenStream {
let name = &self.name;
let cr = self.cr(name.span());
let visit_impl = match &self.fields {
ConfigContainerFields::Struct(fields) => {
let params = fields.iter().filter(|field| !field.attrs.nest);
let params = params.enumerate().map(|(i, field)| {
let field = &field.name;
quote_spanned!(field.span()=> visitor.visit_param(#i, &self.#field))
});
let nested_configs = fields.iter().filter(|field| field.attrs.nest);
let nested_configs = nested_configs
.enumerate()
.map(|(i, field)| field.visit_nested_config(&cr, i, None));
quote!(#(#params;)* #(#nested_configs;)*)
}
ConfigContainerFields::Enum { variants, .. } => {
let mut start_param_offset = 0;
let mut start_nested_config_offset = 0;
let match_arms = variants.iter().enumerate().map(|(variant_idx, variant)| {
variant.visit_match_arm(
&cr,
variant_idx,
&mut start_param_offset,
&mut start_nested_config_offset,
)
});
quote! {
match self {
#(#match_arms)*
}
}
}
};
quote! {
impl #cr::visit::VisitConfig for #name {
fn visit_config(&self, visitor: &mut dyn #cr::visit::ConfigVisitor) {
#visit_impl
}
}
}
}
fn erase_validation(
validation: &Validation,
cr: &proc_macro2::TokenStream,
ty: &impl quote::ToTokens,
) -> proc_macro2::TokenStream {
let validation = validation.wrap(cr);
quote! {
&#cr::validation::_private::ErasedValidation::<#ty, _>::new(#validation)
}
}
fn derive_describe_config(&self) -> proc_macro2::TokenStream {
let name = &self.name;
let cr = self.cr(name.span());
let name_str = name.to_string();
let help = &self.help;
let all_fields = self.fields.all_fields();
let validations = all_fields
.iter()
.filter(|(_, field)| !field.attrs.flatten)
.map(|(_, field)| field.validate_names(self));
let is_enum = matches!(&self.fields, ConfigContainerFields::Enum { .. });
let params = all_fields
.iter()
.filter(|(_, field)| !field.attrs.nest)
.map(|(variant_idx, field)| {
field.describe_param(self, is_enum.then_some(*variant_idx))
});
let mut params: Vec<_> = params.collect();
let mut tag_variants_const = None;
let mut tag_description = None;
if let ConfigContainerFields::Enum { tag, variants } = &self.fields {
let (default_variant_idx, default) = variants
.iter()
.enumerate()
.find_map(|(i, variant)| {
variant
.attrs
.default
.then(|| (i, variant.name(self.attrs.rename_all)))
})
.unzip();
let expected_variants = variants
.iter()
.flat_map(|variant| variant.expected_variants(self.attrs.rename_all));
let tag_span = tag.span();
let tag = ConfigField::from_tag(&cr, tag, expected_variants, default.as_deref());
let tag_index = params.len();
params.push(tag.describe_param(self, None));
let tag_variants = variants
.iter()
.map(|variant| variant.describe(&cr, self.attrs.rename_all));
tag_variants_const = Some(quote_spanned! {tag_span=>
const TAG_VARIANTS: &[#cr::metadata::ConfigVariant] = &[#(#tag_variants,)*];
});
let default_variant =
wrap_in_option(default_variant_idx.map(|i| quote!(&TAG_VARIANTS[#i])));
tag_description = Some(quote_spanned! {tag_span=>
#cr::metadata::ConfigTag {
param: &PARAMS[#tag_index],
variants: TAG_VARIANTS,
default_variant: #default_variant,
}
});
}
let tag_description = wrap_in_option(tag_description);
let nested_configs = all_fields.iter().filter_map(|(variant_idx, field)| {
if field.attrs.nest {
return Some(field.describe_nested_config(self, is_enum.then_some(*variant_idx)));
}
None
});
let config_validations = self
.attrs
.validations
.iter()
.map(|val| Self::erase_validation(val, &cr, &name));
quote! {
impl #cr::DescribeConfig for #name {
const DESCRIPTION: #cr::metadata::ConfigMetadata = {
const PARAMS: &[#cr::metadata::ParamMetadata] = &[#(#params,)*];
#tag_variants_const
#cr::metadata::ConfigMetadata {
ty: #cr::metadata::RustType::of::<#name>(#name_str),
help: #help,
params: PARAMS,
tag: #tag_description,
nested_configs: &[#(#nested_configs,)*],
deserializer: |ctx| {
use #cr::metadata::_private::DeserializeBoxedConfig as _;
let receiver = &::core::marker::PhantomData::<#name>;
receiver.deserialize_boxed_config(ctx)
},
visitor: #cr::metadata::_private::box_config_visitor::<Self>(),
validations: &[#(#config_validations,)*],
}
};
}
#(#validations)*
const _: () = <#name as #cr::DescribeConfig>::DESCRIPTION.assert_valid();
}
}
fn default_fields(fields: &[ConfigField]) -> syn::Result<Vec<proc_macro2::TokenStream>> {
let fields = fields.iter().map(|field| {
let name = &field.name;
let name_span = field.name.span();
let field_instance = if let Some(default) = &field.attrs.default {
default.instance(name_span)
} else if ConfigField::is_option(&field.ty) {
quote_spanned!(field.ty.span()=> ::core::option::Option::None)
} else if field.attrs.nest {
quote_spanned!(field.ty.span()=> ::core::default::Default::default())
} else {
let msg = "Cannot derive(Default): field does not have a default value";
return Err(syn::Error::new(name_span, msg));
};
Ok(quote_spanned!(name_span=> #name: #field_instance))
});
fields.collect()
}
fn derive_default(&self) -> syn::Result<proc_macro2::TokenStream> {
let instance = match &self.fields {
ConfigContainerFields::Struct(fields) => {
let fields = Self::default_fields(fields)?;
quote!(Self { #(#fields,)* })
}
ConfigContainerFields::Enum { variants, .. } => {
let default_variant = variants
.iter()
.find(|variant| variant.attrs.default)
.ok_or_else(|| {
let msg = "Cannot derive(Default): enum does not have a variant marked with #[config(default)]";
syn::Error::new(self.name.span(), msg)
})?;
let fields = Self::default_fields(&default_variant.fields)?;
let variant_name = &default_variant.name;
quote!(Self::#variant_name { #(#fields,)* })
}
};
let name = &self.name;
Ok(quote! {
impl ::core::default::Default for #name {
fn default() -> Self {
#instance
}
}
})
}
fn derive_all(&self) -> syn::Result<proc_macro2::TokenStream> {
let visit_impl = self.derive_visit_config();
let describe_impl = self.derive_describe_config();
let default_impl = if self.attrs.derive_default {
Some(self.derive_default()?)
} else {
None
};
Ok(quote!(#visit_impl #describe_impl #default_impl))
}
}
pub(crate) fn impl_describe_config(input: TokenStream) -> TokenStream {
let input: DeriveInput = syn::parse(input).unwrap();
let trait_impl = match ConfigContainer::new(&input) {
Ok(trait_impl) => trait_impl,
Err(err) => return err.into_compile_error().into(),
};
match trait_impl.derive_all() {
Ok(derived) => derived.into(),
Err(err) => err.into_compile_error().into(),
}
}