use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::collections::HashMap;
use syn::{Attribute, DataEnum, DeriveInput, Fields, Ident, LitStr, Variant, parse_quote};
use super::field::ScopeFields;
use super::naming::to_snek_case;
use super::validate::validate_generics;
struct CtxVariant<'a> {
variant: &'a Variant,
scope: ScopeFields<'a>,
ctor_name: Ident,
}
pub(crate) fn expand_enum_item(
input: &DeriveInput,
data: &DataEnum,
ctx_ident: &Ident,
) -> syn::Result<TokenStream2> {
let mut ctx_variants: Vec<CtxVariant> = Vec::new();
for variant in &data.variants {
match &variant.fields {
Fields::Unit => {}
Fields::Unnamed(_) => {
return Err(syn::Error::new_spanned(
variant,
"`#[derive(FromContext)]` only supports variants with named fields",
));
}
Fields::Named(fields) => {
let scope = ScopeFields::classify(&fields.named)?;
if scope.context_fields.is_empty() || scope.source_field.is_none() {
continue;
}
let ctor_name = parse_constructor_override(variant)?
.unwrap_or_else(|| to_snek_case(&variant.ident));
ctx_variants.push(CtxVariant {
variant,
scope,
ctor_name,
});
}
}
}
if ctx_variants.is_empty() {
return Err(syn::Error::new_spanned(
input,
"`#[derive(FromContext)]` requires at least one variant with a context field (a field not marked `#[source]`/`#[from]` or `#[location]`) and a `#[source]`/`#[from]` field",
));
}
check_unique_constructor_names(&ctx_variants)?;
let context_fields = ctx_variants
.iter()
.flat_map(|cv| cv.scope.context_fields.iter());
validate_generics(input, context_fields)?;
let vis = &input.vis;
let has_borrowed = ctx_variants
.iter()
.flat_map(|cv| cv.scope.context_fields.iter())
.any(|cf| cf.borrow_target.is_some());
let (impl_generics, error_type_generics, where_clause) = input.generics.split_for_impl();
let mut ctx_generics_lifetime = input.generics.clone();
if has_borrowed {
ctx_generics_lifetime.params.insert(0, parse_quote!('ctx));
}
let (ctx_impl_generics, ctx_type_generics, _) = ctx_generics_lifetime.split_for_impl();
let ctx_enum = expand_enum(
ctx_ident,
&ctx_variants,
vis,
has_borrowed,
&impl_generics,
where_clause,
&ctx_impl_generics,
);
let constructors = expand_enum_constructors(
ctx_ident,
&ctx_variants,
has_borrowed,
&impl_generics,
&error_type_generics,
where_clause,
&ctx_impl_generics,
&ctx_type_generics,
);
let from_context_impl = expand_enum_from_context(
&input.ident,
ctx_ident,
&ctx_variants,
has_borrowed,
&impl_generics,
&error_type_generics,
where_clause,
&ctx_type_generics,
);
Ok(quote! {
#ctx_enum
#constructors
#from_context_impl
})
}
fn parse_constructor_override(variant: &Variant) -> syn::Result<Option<Ident>> {
let context_attrs: Vec<&Attribute> = variant
.attrs
.iter()
.filter(|attr| attr.path().is_ident("context"))
.collect();
if let Some(duplicate) = context_attrs.get(1) {
return Err(syn::Error::new_spanned(
duplicate,
"a variant may carry at most one `#[context(...)]` attribute",
));
}
let mut ctor_name: Option<Ident> = None;
for attr in &context_attrs {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("constructor") {
if ctor_name.is_some() {
return Err(
meta.error("duplicate `#[context(constructor = ...)]` on the same variant")
);
}
let Ok(value) = meta.value() else {
return Err(meta.error(
"`#[context(constructor)]` requires a quoted string name, e.g. \
`#[context(constructor = \"io\")]`",
));
};
let lit: LitStr = match value.parse() {
Ok(lit) => lit,
Err(_) => {
return Err(meta.error(
"`#[context(constructor)]` requires a quoted string name, e.g. \
`#[context(constructor = \"io\")]`",
));
}
};
ctor_name = Some(match lit.parse::<Ident>() {
Ok(ident) => ident,
Err(_) => {
return Err(meta.error(
"constructor name must be a valid identifier, e.g. \
`#[context(constructor = \"io\")]`",
));
}
});
} else {
return Err(meta.error(
"unknown `#[context(...)]` helper on a variant; expected `constructor`",
));
}
Ok(())
})?;
}
Ok(ctor_name)
}
fn check_unique_constructor_names(ctx_variants: &[CtxVariant<'_>]) -> syn::Result<()> {
let mut seen: HashMap<Ident, &Variant> = HashMap::new();
for cv in ctx_variants {
if let Some(first) = seen.get(&cv.ctor_name) {
return Err(syn::Error::new_spanned(
&cv.variant.ident,
format!(
"duplicate constructor name `{}`; variants `{}` and `{}` both generate it",
cv.ctor_name, first.ident, cv.variant.ident
),
));
}
seen.insert(cv.ctor_name.clone(), cv.variant);
}
Ok(())
}
fn expand_enum(
ctx_ident: &Ident,
ctx_variants: &[CtxVariant<'_>],
vis: &syn::Visibility,
has_borrowed: bool,
impl_generics: &syn::ImplGenerics<'_>,
where_clause: Option<&syn::WhereClause>,
ctx_impl_generics: &syn::ImplGenerics<'_>,
) -> TokenStream2 {
let variants = ctx_variants.iter().map(|cv| {
let vident = &cv.variant.ident;
let ctx_fields = cv.scope.context_fields.iter().map(|cf| {
let fvis = cf.vis();
let ident = cf.ident();
if let Some(target) = &cf.borrow_target {
quote! { #fvis #ident: &'ctx #target }
} else {
let ty = cf.ty();
quote! { #fvis #ident: #ty }
}
});
quote! { #vident { #(#ctx_fields,)* } }
});
let enum_generics = if has_borrowed {
quote! { #ctx_impl_generics }
} else {
quote! { #impl_generics }
};
quote! {
#[derive(Clone, Debug, PartialEq)]
#vis enum #ctx_ident #enum_generics #where_clause {
#(#variants,)*
}
}
}
#[allow(clippy::too_many_arguments)]
fn expand_enum_constructors(
ctx_ident: &Ident,
ctx_variants: &[CtxVariant<'_>],
has_borrowed: bool,
impl_generics: &syn::ImplGenerics<'_>,
error_type_generics: &syn::TypeGenerics<'_>,
where_clause: Option<&syn::WhereClause>,
ctx_impl_generics: &syn::ImplGenerics<'_>,
ctx_type_generics: &syn::TypeGenerics<'_>,
) -> TokenStream2 {
let fns = ctx_variants.iter().map(|cv| {
let vident = &cv.variant.ident;
let name = &cv.ctor_name;
let args = cv.scope.context_fields.iter().map(|cf| {
let ident = cf.ident();
if let Some(target) = &cf.borrow_target {
quote! { #ident: &'ctx #target }
} else {
let ty = cf.ty();
quote! { #ident: impl Into<#ty> }
}
});
let body = cv.scope.context_fields.iter().map(|cf| {
let ident = cf.ident();
if cf.borrow_target.is_some() {
quote! { #ident: #ident, }
} else {
quote! { #ident: #ident.into(), }
}
});
let doc = format!("Construct the `{vident}` context variant from one argument per context field, in declaration order.");
quote! {
#[doc = #doc]
pub fn #name(#(#args),*) -> Self {
Self::#vident {
#(#body)*
}
}
}
});
let impl_generics_for_ctor = if has_borrowed {
quote! { #ctx_impl_generics }
} else {
quote! { #impl_generics }
};
let type_generics_for_ctor = if has_borrowed {
quote! { #ctx_type_generics }
} else {
quote! { #error_type_generics }
};
quote! {
impl #impl_generics_for_ctor #ctx_ident #type_generics_for_ctor #where_clause {
#(#fns)*
}
}
}
#[allow(clippy::too_many_arguments)]
fn expand_enum_from_context(
error_ident: &Ident,
ctx_ident: &Ident,
ctx_variants: &[CtxVariant<'_>],
has_borrowed: bool,
impl_generics: &syn::ImplGenerics<'_>,
error_type_generics: &syn::TypeGenerics<'_>,
where_clause: Option<&syn::WhereClause>,
ctx_type_generics: &syn::TypeGenerics<'_>,
) -> TokenStream2 {
let arms = ctx_variants.iter().map(|cv| {
let vident = &cv.variant.ident;
let bindings = cv
.scope
.context_fields
.iter()
.map(crate::field::CtxField::ident);
let inits = cv.scope.context_fields.iter().map(|cf| {
let ident = cf.ident();
if cf.borrow_target.is_some() {
quote! { #ident: #ident.into(), }
} else {
quote! { #ident: #ident, }
}
});
let location_init = cv.scope.location_field.map(|field| {
let ident = &field.ident;
quote! { #ident: location, }
});
let source_init = cv.scope.source_field.map(|field| {
let ident = &field.ident;
quote! { #ident: source, }
});
quote! {
#ctx_ident::#vident { #(#bindings),* } => #error_ident::#vident {
#(#inits)*
#location_init
#source_init
},
}
});
let gat_type_alias = if has_borrowed {
quote! { #ctx_ident #ctx_type_generics }
} else {
quote! { #ctx_ident #error_type_generics }
};
quote! {
impl #impl_generics ::dterror::FromContext for #error_ident #error_type_generics #where_clause {
type Ctx<'ctx> = #gat_type_alias;
fn from_context(
ctx: Self::Ctx<'_>,
location: &'static std::panic::Location<'static>,
source: Box<dyn std::error::Error + Send + Sync + 'static>,
) -> Self {
match ctx {
#(#arms)*
}
}
}
}
}