use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{Data, DataEnum, DataStruct, DeriveInput, Field, Fields, Ident, Variant, parse_quote};
use super::derive_attr::{CtxDerives, parse_ctx_derives};
use super::expand_enum::{check_unique_constructor_names, parse_constructor_override};
use super::field::{CtxField, CtxTarget, bind_lifetimes_to_ctx, contains_lifetime};
use super::naming::to_snek_case;
use super::validate::validate_generics;
const DERIVE_NAME: &str = "FromContext";
struct KindVariant<'a> {
variant: &'a Variant,
fields: Vec<CtxField<'a>>,
ctor_name: Ident,
}
pub(crate) fn expand_kind_item(input: &DeriveInput) -> syn::Result<TokenStream2> {
let derives = parse_ctx_derives(input)?;
let ctx_ident = format_ident!("{}Ctx", input.ident);
match &input.data {
Data::Struct(data) => expand_struct(input, data, &ctx_ident, &derives),
Data::Enum(data) => expand_enum(input, data, &ctx_ident, &derives),
Data::Union(data) => Err(syn::Error::new_spanned(
data.union_token,
"`#[derive(FromContext)]` does not support unions",
)),
}
}
fn classify_fields<'a>(
fields: impl IntoIterator<Item = &'a Field>,
) -> syn::Result<Vec<CtxField<'a>>> {
fields.into_iter().map(CtxField::parse).collect()
}
fn has_ctx_lifetime<'a>(fields: impl IntoIterator<Item = &'a CtxField<'a>>) -> bool {
fields.into_iter().any(|cf| match &cf.target {
CtxTarget::Borrow(_) => true,
CtxTarget::From(target) => contains_lifetime(target),
CtxTarget::Field => false,
})
}
fn stored_ty(cf: &CtxField) -> TokenStream2 {
match &cf.target {
CtxTarget::Borrow(target) => quote! { &'ctx #target },
CtxTarget::From(target) => {
let target = bind_lifetimes_to_ctx(target);
quote! { #target }
}
CtxTarget::Field => {
let ty = cf.ty();
quote! { #ty }
}
}
}
fn ctor_arg(cf: &CtxField) -> TokenStream2 {
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> }
}
}
}
fn from_arg_ty(
ctx_ident: &Ident,
input: &DeriveInput,
has_ctx_lifetime: bool,
kind_type_generics: &syn::TypeGenerics<'_>,
) -> TokenStream2 {
if has_ctx_lifetime {
let type_params = input.generics.type_params().map(|param| ¶m.ident);
quote! { #ctx_ident < '_ , #(#type_params),* > }
} else {
quote! { #ctx_ident #kind_type_generics }
}
}
fn from_context_impl(
kind_ident: &Ident,
ctx_ident: &Ident,
has_ctx_lifetime: bool,
impl_generics: &syn::ImplGenerics<'_>,
kind_type_generics: &syn::TypeGenerics<'_>,
where_clause: Option<&syn::WhereClause>,
ctx_type_generics: &syn::TypeGenerics<'_>,
) -> TokenStream2 {
let gat_type_alias = if has_ctx_lifetime {
quote! { #ctx_ident #ctx_type_generics }
} else {
quote! { #ctx_ident #kind_type_generics }
};
quote! {
#[automatically_derived]
impl #impl_generics ::dterror::FromContext for #kind_ident #kind_type_generics #where_clause {
type Ctx<'ctx> = #gat_type_alias;
}
}
}
fn expand_struct(
input: &DeriveInput,
data: &DataStruct,
ctx_ident: &Ident,
derives: &CtxDerives,
) -> 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 context_fields = classify_fields(&fields.named)?;
if context_fields.is_empty() {
return Err(syn::Error::new_spanned(
input,
"`#[derive(FromContext)]` requires at least one field",
));
}
validate_generics(input, DERIVE_NAME, &context_fields)?;
let kind_ident = &input.ident;
let vis = &input.vis;
let has_ctx_lifetime = has_ctx_lifetime(&context_fields);
let (impl_generics, kind_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_fields = context_fields.iter().map(|cf| {
let fvis = cf.vis();
let ident = cf.ident();
let stored = stored_ty(cf);
quote! { #fvis #ident: #stored }
});
let struct_generics = if has_ctx_lifetime {
quote! { #ctx_impl_generics }
} else {
quote! { #impl_generics }
};
let derive_attr = derives.derive_attr();
let ctx_struct = quote! {
#[allow(dead_code)]
#derive_attr
#vis struct #ctx_ident #struct_generics #where_clause {
#(#ctx_fields,)*
}
};
let new_args = context_fields.iter().map(ctor_arg);
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 (new_impl_generics, new_type_generics) = if has_ctx_lifetime {
(&ctx_impl_generics, &ctx_type_generics)
} else {
(&impl_generics, &kind_type_generics)
};
let new_impl = quote! {
impl #new_impl_generics #ctx_ident #new_type_generics #where_clause {
pub fn new(#(#new_args),*) -> Self {
Self {
#(#new_body)*
}
}
}
};
let from_inits = context_fields.iter().map(|cf| {
let ident = cf.ident();
if let CtxTarget::Field = &cf.target {
quote! { #ident: value.#ident, }
} else {
quote! { #ident: value.#ident.into(), }
}
});
let from_arg = from_arg_ty(ctx_ident, input, has_ctx_lifetime, &kind_type_generics);
let from_impl = quote! {
#[automatically_derived]
impl #impl_generics ::core::convert::From<#from_arg> for #kind_ident #kind_type_generics #where_clause {
fn from(value: #from_arg) -> Self {
Self {
#(#from_inits)*
}
}
}
};
let trait_impl = from_context_impl(
kind_ident,
ctx_ident,
has_ctx_lifetime,
&impl_generics,
&kind_type_generics,
where_clause,
&ctx_type_generics,
);
Ok(quote! {
#ctx_struct
#new_impl
#from_impl
#trait_impl
})
}
fn get_variants<'a>(input: &DeriveInput, data: &'a DataEnum) -> syn::Result<Vec<KindVariant<'a>>> {
let mut kind_variants = vec![];
for variant in &data.variants {
match &variant.fields {
Fields::Unit => kind_variants.push(KindVariant {
variant,
fields: Vec::new(),
ctor_name: parse_constructor_override(variant)?
.unwrap_or_else(|| to_snek_case(&variant.ident)),
}),
Fields::Unnamed(_) => {
return Err(syn::Error::new_spanned(
variant,
"`#[derive(FromContext)]` only supports variants with named fields",
));
}
Fields::Named(fields) => kind_variants.push(KindVariant {
variant,
fields: classify_fields(&fields.named)?,
ctor_name: parse_constructor_override(variant)?
.unwrap_or_else(|| to_snek_case(&variant.ident)),
}),
}
}
if kind_variants.is_empty() {
return Err(syn::Error::new_spanned(
input,
"`#[derive(FromContext)]` requires at least one variant",
));
}
Ok(kind_variants)
}
fn expand_enum_variant(kv: &KindVariant<'_>) -> TokenStream2 {
let vident = &kv.variant.ident;
if kv.fields.is_empty() {
return quote! { #vident };
}
let ctx_fields = kv.fields.iter().map(|cf| {
let fvis = cf.vis();
let ident = cf.ident();
let stored = stored_ty(cf);
quote! { #fvis #ident: #stored }
});
quote! { #vident { #(#ctx_fields,)* } }
}
fn expand_enum_variant_fn(kv: &KindVariant<'_>) -> TokenStream2 {
let vident = &kv.variant.ident;
let name = &kv.ctor_name;
if kv.fields.is_empty() {
let doc = format!("Construct the `{vident}` context variant.");
return quote! {
#[doc = #doc]
pub fn #name() -> Self {
Self::#vident
}
};
}
let args = kv.fields.iter().map(ctor_arg);
let body = kv.fields.iter().map(|cf| {
let ident = cf.ident();
if let CtxTarget::Borrow(_) = &cf.target {
quote! { #ident: #ident, }
} else {
quote! { #ident: #ident.into(), }
}
});
let doc = format!(
"Construct the `{vident}` context variant from one argument per field, in declaration order."
);
quote! {
#[doc = #doc]
pub fn #name(#(#args),*) -> Self {
Self::#vident {
#(#body)*
}
}
}
}
fn expand_enum(
input: &DeriveInput,
data: &DataEnum,
ctx_ident: &Ident,
derives: &CtxDerives,
) -> syn::Result<TokenStream2> {
let kind_variants = get_variants(input, data)?;
check_unique_constructor_names(kind_variants.iter().map(|kv| (&kv.ctor_name, kv.variant)))?;
let context_fields = kind_variants.iter().flat_map(|kv| kv.fields.iter());
validate_generics(input, DERIVE_NAME, context_fields)?;
let kind_ident = &input.ident;
let vis = &input.vis;
let has_ctx_lifetime = has_ctx_lifetime(kind_variants.iter().flat_map(|kv| kv.fields.iter()));
let (impl_generics, kind_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 variants = kind_variants.iter().map(expand_enum_variant);
let enum_generics = if has_ctx_lifetime {
quote! { #ctx_impl_generics }
} else {
quote! { #impl_generics }
};
let derive_attr = derives.derive_attr();
let ctx_enum = quote! {
#[allow(dead_code)]
#derive_attr
#vis enum #ctx_ident #enum_generics #where_clause {
#(#variants,)*
}
};
let fns = kind_variants.iter().map(expand_enum_variant_fn);
let (ctor_impl_generics, ctor_type_generics) = if has_ctx_lifetime {
(&ctx_impl_generics, &ctx_type_generics)
} else {
(&impl_generics, &kind_type_generics)
};
let constructors = quote! {
impl #ctor_impl_generics #ctx_ident #ctor_type_generics #where_clause {
#(#fns)*
}
};
let arms = kind_variants.iter().map(|kv| {
let vident = &kv.variant.ident;
if kv.fields.is_empty() {
return quote! {
#ctx_ident::#vident => #kind_ident::#vident,
};
}
let bindings = kv.fields.iter().map(CtxField::ident);
let inits = kv.fields.iter().map(|cf| {
let ident = cf.ident();
if let CtxTarget::Field = &cf.target {
quote! { #ident: #ident, }
} else {
quote! { #ident: #ident.into(), }
}
});
quote! {
#ctx_ident::#vident { #(#bindings),* } => #kind_ident::#vident {
#(#inits)*
},
}
});
let from_arg = from_arg_ty(ctx_ident, input, has_ctx_lifetime, &kind_type_generics);
let from_impl = quote! {
#[automatically_derived]
impl #impl_generics ::core::convert::From<#from_arg> for #kind_ident #kind_type_generics #where_clause {
fn from(value: #from_arg) -> Self {
match value {
#(#arms)*
}
}
}
};
let trait_impl = from_context_impl(
kind_ident,
ctx_ident,
has_ctx_lifetime,
&impl_generics,
&kind_type_generics,
where_clause,
&ctx_type_generics,
);
Ok(quote! {
#ctx_enum
#constructors
#from_impl
#trait_impl
})
}