use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{DataStruct, DeriveInput, Field, Fields, Ident, parse_quote};
use super::field::{CtxField, ScopeFields};
use super::validate::validate_generics;
pub(crate) fn expand_struct_item(
input: &DeriveInput,
data: &DataStruct,
ctx_ident: &Ident,
) -> syn::Result<TokenStream2> {
let Fields::Named(fields) = &data.fields else {
return Err(syn::Error::new_spanned(
input,
"`#[derive(FromContext)]` only supports structs with named fields",
));
};
let scope = ScopeFields::classify(&fields.named)?;
if scope.context_fields.is_empty() {
return Err(syn::Error::new_spanned(
input,
"`#[derive(FromContext)]` requires at least one context field (a field not marked `#[source]`/`#[from]` or `#[location]`)",
));
}
validate_generics(input, scope.context_fields.iter())?;
let vis = &input.vis;
let has_borrowed = 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_struct = expand_struct(
ctx_ident,
&scope.context_fields,
vis,
has_borrowed,
&impl_generics,
where_clause,
&ctx_impl_generics,
);
let new_impl = expand_struct_new(
ctx_ident,
&scope.context_fields,
has_borrowed,
&impl_generics,
&error_type_generics,
where_clause,
&ctx_impl_generics,
&ctx_type_generics,
);
let from_context_impl = expand_struct_from_context(
&input.ident,
ctx_ident,
scope.source_field,
scope.location_field,
&scope.context_fields,
has_borrowed,
&impl_generics,
&error_type_generics,
where_clause,
&ctx_type_generics,
);
Ok(quote! {
#ctx_struct
#new_impl
#from_context_impl
})
}
fn expand_struct(
ctx_ident: &Ident,
context_fields: &Vec<CtxField<'_>>,
vis: &syn::Visibility,
has_borrowed: bool,
impl_generics: &syn::ImplGenerics<'_>,
where_clause: Option<&syn::WhereClause>,
ctx_impl_generics: &syn::ImplGenerics<'_>,
) -> TokenStream2 {
let ctx_fields = 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 }
}
});
let struct_generics = if has_borrowed {
quote! { #ctx_impl_generics }
} else {
quote! { #impl_generics }
};
quote! {
#[derive(Clone, Debug, PartialEq)]
#vis struct #ctx_ident #struct_generics #where_clause {
#(#ctx_fields,)*
}
}
}
#[allow(clippy::too_many_arguments)]
fn expand_struct_new(
ctx_ident: &Ident,
context_fields: &Vec<CtxField<'_>>,
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 new_args = 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 new_body = context_fields.iter().map(|cf| {
let ident = cf.ident();
if cf.borrow_target.is_some() {
quote! { #ident: #ident, }
} else {
quote! { #ident: #ident.into(), }
}
});
let impl_generics_for_new = if has_borrowed {
quote! { #ctx_impl_generics }
} else {
quote! { #impl_generics }
};
let type_generics_for_new = if has_borrowed {
quote! { #ctx_type_generics }
} else {
quote! { #error_type_generics }
};
quote! {
impl #impl_generics_for_new #ctx_ident #type_generics_for_new #where_clause {
pub fn new(#(#new_args),*) -> Self {
Self {
#(#new_body)*
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn expand_struct_from_context(
error_ident: &Ident,
ctx_ident: &Ident,
source_field: Option<&Field>,
location_field: Option<&Field>,
context_fields: &[CtxField<'_>],
has_borrowed: bool,
impl_generics: &syn::ImplGenerics<'_>,
error_type_generics: &syn::TypeGenerics<'_>,
where_clause: Option<&syn::WhereClause>,
ctx_type_generics: &syn::TypeGenerics<'_>,
) -> TokenStream2 {
{
let ctx_field_inits = context_fields.iter().map(|cf| {
let ident = cf.ident();
if cf.borrow_target.is_some() {
quote! { #ident: ctx.#ident.into(), }
} else {
quote! { #ident: ctx.#ident, }
}
});
let location_init = location_field.map(|field| {
let ident = &field.ident;
quote! { #ident: location, }
});
let source_init = source_field.map(|field| {
let ident = &field.ident;
quote! { #ident: source, }
});
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 {
Self {
#(#ctx_field_inits)*
#location_init
#source_init
}
}
}
}
}
}