use crate::crate_paths::get_reinhardt_crate;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
Attribute, Data, DeriveInput, Fields, Meta, Path, Result, Token, parse::Parser, parse_quote,
punctuated::Punctuated,
};
pub(crate) fn dto_impl(args: TokenStream, mut input: DeriveInput) -> Result<TokenStream> {
if !args.is_empty() {
return Err(syn::Error::new_spanned(
args,
"#[dto] does not accept arguments in this version",
));
}
let reinhardt = get_reinhardt_crate();
let fields = match &mut input.data {
Data::Struct(s) => match &mut s.fields {
Fields::Named(f) => Some(&mut f.named),
Fields::Unnamed(f) => Some(&mut f.unnamed),
Fields::Unit => None,
},
Data::Enum(_) | Data::Union(_) => {
return Err(syn::Error::new_spanned(
&input.ident,
"#[dto] can only be applied to structs",
));
}
};
if let Some(fields) = fields {
for field in fields.iter_mut() {
for attr in field.attrs.iter_mut() {
if attr.path().is_ident("validate") {
*attr = wrap_in_cfg_attr_native(attr);
}
}
}
}
for trait_name in ["Validate", "Schema"] {
if let Some(attr) = find_unconditional_derive(&input.attrs, trait_name)? {
return Err(syn::Error::new_spanned(
attr,
format!(
"#[dto] cannot be combined with unconditional `#[derive({trait_name})]`. \
Remove the derive so #[dto] can emit it as `cfg_attr(native, ...)` for you, \
or replace it with `#[cfg_attr(native, derive({trait_name}))]`."
),
));
}
}
let needs_validate = !has_native_derive(&input.attrs, "Validate")?;
let needs_schema = !has_native_derive(&input.attrs, "Schema")?;
let mut derives: Punctuated<Path, Token![,]> = Punctuated::new();
if needs_validate {
derives.push(parse_quote!(#reinhardt::Validate));
}
if needs_schema {
derives.push(parse_quote!(#reinhardt::rest::openapi::Schema));
}
if !derives.is_empty() {
let new_attr: Attribute = parse_quote!(#[cfg_attr(native, derive(#derives))]);
input.attrs.push(new_attr);
}
let to_schema_import = quote! {
#[cfg(native)]
#[allow(unused_imports)]
use #reinhardt::rest::openapi::ToSchema as _;
};
Ok(quote! {
#input
#to_schema_import
})
}
fn wrap_in_cfg_attr_native(attr: &Attribute) -> Attribute {
let meta = &attr.meta;
parse_quote!(#[cfg_attr(native, #meta)])
}
fn find_unconditional_derive<'a>(
attrs: &'a [Attribute],
trait_name: &str,
) -> Result<Option<&'a Attribute>> {
for attr in attrs {
if !attr.path().is_ident("derive") {
continue;
}
let Meta::List(list) = &attr.meta else {
continue;
};
let derives =
Punctuated::<Path, Token![,]>::parse_terminated.parse2(list.tokens.clone())?;
if derives
.iter()
.any(|p| p.segments.last().is_some_and(|seg| seg.ident == trait_name))
{
return Ok(Some(attr));
}
}
Ok(None)
}
fn has_native_derive(attrs: &[Attribute], trait_name: &str) -> Result<bool> {
for attr in attrs {
if !attr.path().is_ident("cfg_attr") {
continue;
}
let Meta::List(list) = &attr.meta else {
continue;
};
let nested = Punctuated::<Meta, Token![,]>::parse_terminated.parse2(list.tokens.clone())?;
let mut iter = nested.iter();
let Some(first) = iter.next() else {
continue;
};
if !matches!(first, Meta::Path(p) if p.is_ident("native")) {
continue;
}
for inner in iter {
let Meta::List(inner_list) = inner else {
continue;
};
if !inner_list.path.is_ident("derive") {
continue;
}
let derives = Punctuated::<Path, Token![,]>::parse_terminated
.parse2(inner_list.tokens.clone())?;
if derives
.iter()
.any(|p| p.segments.last().is_some_and(|seg| seg.ident == trait_name))
{
return Ok(true);
}
}
}
Ok(false)
}