use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, spanned::Spanned, Attribute, Data, DeriveInput, Fields, Ident, Path};
#[derive(Default)]
struct FieldAttrs {
rename: Option<Ident>,
rename_span: Option<Span>,
transform_fn: Option<Path>,
skip: bool,
into_flag: bool,
}
enum FieldAction {
Skip,
Transform(Path),
Into,
Direct,
}
fn decide_action(a: &FieldAttrs) -> FieldAction {
if a.skip {
FieldAction::Skip
} else if let Some(ref f) = a.transform_fn {
FieldAction::Transform(f.clone())
} else if a.into_flag {
FieldAction::Into
} else {
FieldAction::Direct
}
}
#[proc_macro_derive(DtoFrom, attributes(dto))]
pub fn dto_from_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let target_struct = &input.ident;
let source_ty = match find_source_type(&input.attrs) {
Ok(path) => path,
Err(e) => return e.to_compile_error().into(),
};
let generics = input.generics.clone();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(named) => &named.named,
_ => {
return syn::Error::new_spanned(
&input.ident,
"DtoFrom only supports named-field structs.",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(&input.ident, "DtoFrom only supports structs.")
.to_compile_error()
.into();
}
};
let field_map = fields.iter().map(|f| {
let ident = f.ident.as_ref().expect("named fields guaranteed");
let cfg = match extract_dto_field_attrs(&f.attrs) {
Ok(c) => c,
Err(e) => return e.to_compile_error(),
};
let src_ident = cfg.rename.clone().unwrap_or_else(|| ident.clone());
let access_span = cfg.rename_span.unwrap_or_else(|| ident.span());
generate_field_mapping(ident, &src_ident, &cfg, access_span)
});
let owned_impl = quote! {
impl #impl_generics From<#source_ty> for #target_struct #ty_generics #where_clause {
fn from(source: #source_ty) -> Self {
Self { #(#field_map,)* }
}
}
};
TokenStream::from(quote! { #owned_impl })
}
fn generate_field_mapping(
ident: &Ident,
source_ident: &Ident,
a: &FieldAttrs,
access_span: Span,
) -> proc_macro2::TokenStream {
match decide_action(a) {
FieldAction::Skip => {
quote! { #ident: Default::default() }
}
FieldAction::Transform(ref f) => {
quote_spanned! { access_span => #ident: #f(source.#source_ident) }
}
FieldAction::Into => {
quote_spanned! { access_span => #ident: ::core::convert::Into::into(source.#source_ident) }
}
FieldAction::Direct => {
quote_spanned! { access_span => #ident: source.#source_ident }
}
}
}
fn extract_dto_field_attrs(attrs: &[Attribute]) -> syn::Result<FieldAttrs> {
let mut cfg = FieldAttrs::default();
let mut seen_rename = false;
let mut seen_transform = false;
let mut seen_skip = false;
let mut seen_into = false;
for attr in attrs {
if !attr.path().is_ident("dto") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
let lit = meta.value()?.parse::<syn::LitStr>()?;
if lit.value().trim().is_empty() {
return Err(syn::Error::new(lit.span(), "`rename` cannot be empty"));
}
if seen_rename {
return Err(syn::Error::new(lit.span(), "duplicate `rename`"));
}
seen_rename = true;
cfg.rename_span = Some(lit.span());
cfg.rename = Some(Ident::new(&lit.value(), lit.span()));
} else if meta.path.is_ident("transform_fn") {
if seen_transform {
return Err(syn::Error::new(
meta.path.span(),
"duplicate `transform_fn`",
));
}
seen_transform = true;
let val = meta.value()?;
cfg.transform_fn = Some(val.parse()?);
} else if meta.path.is_ident("skip") {
if seen_skip {
return Err(syn::Error::new(meta.path.span(), "duplicate `skip`"));
}
seen_skip = true;
cfg.skip = true;
} else if meta.path.is_ident("into") {
if seen_into {
return Err(syn::Error::new(meta.path.span(), "duplicate `into`"));
}
seen_into = true;
cfg.into_flag = true;
} else {
return Err(syn::Error::new(
meta.path.span(),
"unknown #[dto(...)] key; expected one of: rename, transform_fn, skip, into",
));
}
Ok(())
})?;
}
if cfg.skip && (cfg.rename.is_some() || cfg.transform_fn.is_some() || cfg.into_flag) {
return Err(syn::Error::new(
Span::call_site(),
"`#[dto(skip)]` cannot be combined with `rename`, `transform_fn`, or `into`",
));
}
if cfg.transform_fn.is_some() && cfg.into_flag {
return Err(syn::Error::new(
Span::call_site(),
"`#[dto(transform_fn = ...)]` conflicts with `#[dto(into)]`",
));
}
Ok(cfg)
}
fn find_source_type(attrs: &[Attribute]) -> syn::Result<Path> {
let mut result: Option<Path> = None;
let mut seen_from = false;
for attr in attrs {
if !attr.path().is_ident("dto") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("from") {
if seen_from {
return Err(syn::Error::new(
meta.path.span(),
"duplicate `from` on struct",
));
}
let path: Path = meta.value()?.parse()?;
result = Some(path);
seen_from = true;
} else {
return Err(syn::Error::new(
meta.path.span(),
"unknown struct-level #[dto(...)] key; expected `from`",
));
}
Ok(())
})?;
}
result.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"Expected `#[dto(from = Type)]` on the struct.",
)
})
}