use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{DataStruct, DeriveInput, Field, Fields, Ident, parse_quote};
use super::derive_attr::{CtxDerives, parse_ctx_derives};
use super::field::{CtxField, CtxTarget, ScopeFields, bind_lifetimes_to_ctx, contains_lifetime};
use super::validate::validate_generics;
pub(crate) fn expand_struct_item(
input: &DeriveInput,
data: &DataStruct,
ctx_ident: &Ident,
) -> syn::Result<TokenStream2> {
let derives = parse_ctx_derives(input)?;
let Fields::Named(fields) = &data.fields else {
return Err(syn::Error::new_spanned(
input,
"`#[derive(CtxError)]` 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(CtxError)]` requires at least one context field (a field not marked `#[source]`/`#[from]` or `#[location]`)",
));
}
validate_generics(input, "CtxError", scope.context_fields.iter())?;
let vis = &input.vis;
let has_ctx_lifetime = scope.context_fields.iter().any(|cf| match &cf.target {
CtxTarget::Borrow(_) => true,
CtxTarget::From(target) => contains_lifetime(target),
CtxTarget::Field => false,
});
let (impl_generics, error_type_generics, where_clause) = input.generics.split_for_impl();
let mut ctx_generics_lifetime = input.generics.clone();
if has_ctx_lifetime {
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_ctx_lifetime,
&impl_generics,
where_clause,
&ctx_impl_generics,
&derives,
);
let new_impl = expand_struct_new(
ctx_ident,
&scope.context_fields,
has_ctx_lifetime,
&impl_generics,
&error_type_generics,
where_clause,
&ctx_impl_generics,
&ctx_type_generics,
);
let ctx_error_impl = expand_struct_ctx_error(
&input.ident,
ctx_ident,
scope.source_field,
scope.source_is_option,
scope.location_field,
&scope.context_fields,
has_ctx_lifetime,
&impl_generics,
&error_type_generics,
where_clause,
&ctx_type_generics,
);
Ok(quote! {
#ctx_struct
#new_impl
#ctx_error_impl
})
}
#[allow(clippy::too_many_arguments)]
fn expand_struct(
ctx_ident: &Ident,
context_fields: &Vec<CtxField<'_>>,
vis: &syn::Visibility,
has_ctx_lifetime: bool,
impl_generics: &syn::ImplGenerics<'_>,
where_clause: Option<&syn::WhereClause>,
ctx_impl_generics: &syn::ImplGenerics<'_>,
derives: &CtxDerives,
) -> TokenStream2 {
let ctx_fields = context_fields.iter().map(|cf| {
let fvis = cf.vis();
let ident = cf.ident();
match &cf.target {
CtxTarget::Borrow(target) => quote! { #fvis #ident: &'ctx #target },
CtxTarget::From(target) => {
let target = bind_lifetimes_to_ctx(target);
quote! { #fvis #ident: #target }
}
CtxTarget::Field => {
let ty = cf.ty();
quote! { #fvis #ident: #ty }
}
}
});
let struct_generics = if has_ctx_lifetime {
quote! { #ctx_impl_generics }
} else {
quote! { #impl_generics }
};
let derive_attr = derives.derive_attr();
quote! {
#[allow(dead_code)]
#derive_attr
#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_ctx_lifetime: 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();
match &cf.target {
CtxTarget::Borrow(target) => quote! { #ident: &'ctx #target },
CtxTarget::From(target) => {
let target = bind_lifetimes_to_ctx(target);
quote! { #ident: impl Into<#target> }
}
CtxTarget::Field => {
let ty = cf.ty();
quote! { #ident: impl Into<#ty> }
}
}
});
let new_body = context_fields.iter().map(|cf| {
let ident = cf.ident();
if let CtxTarget::Borrow(_) = &cf.target {
quote! { #ident: #ident, }
} else {
quote! { #ident: #ident.into(), }
}
});
let impl_generics_for_new = if has_ctx_lifetime {
quote! { #ctx_impl_generics }
} else {
quote! { #impl_generics }
};
let type_generics_for_new = if has_ctx_lifetime {
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_ctx_error(
error_ident: &Ident,
ctx_ident: &Ident,
source_field: Option<&Field>,
source_is_option: bool,
location_field: Option<&Field>,
context_fields: &[CtxField<'_>],
has_ctx_lifetime: 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 let CtxTarget::Field = &cf.target {
quote! { #ident: ctx.#ident, }
} else {
quote! { #ident: ctx.#ident.into(), }
}
});
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;
if source_is_option {
quote! { #ident: ::std::option::Option::Some(source), }
} else {
quote! { #ident: source, }
}
});
let gat_type_alias = if has_ctx_lifetime {
quote! { #ctx_ident #ctx_type_generics }
} else {
quote! { #ctx_ident #error_type_generics }
};
quote! {
#[automatically_derived]
impl #impl_generics ::dterror::CtxError 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
}
}
}
}
}
}