use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Ident};
use crate::codegen::generics_utils::impl_generics_with_f;
use crate::parsing::StructInfo;
pub fn generate(info: &StructInfo) -> syn::Result<(Option<TokenStream>, Option<TokenStream>)> {
let has_deserialize = info.derives.iter().any(|d| d == "Deserialize");
if !has_deserialize {
return Ok((None, None));
}
validate_serde_attributes(info)?;
let helper_struct = generate_helper_struct(info);
let try_from_impl = generate_try_from_impl(info)?;
Ok((Some(helper_struct), Some(try_from_impl)))
}
fn validate_serde_attributes(info: &StructInfo) -> syn::Result<()> {
for attr in &info.other_attrs {
if !attr.path().is_ident("serde") {
continue;
}
let attr_clone = attr.clone();
let mut has_error = None;
let _ = attr_clone.parse_nested_meta(|meta| {
let path = meta.path.get_ident().map(|i| i.to_string());
match path.as_deref() {
Some("default") => {
has_error = Some(syn::Error::new_spanned(
attr,
"structural typing does not support #[serde(default)] on containers",
));
}
Some("transparent") => {
has_error = Some(syn::Error::new_spanned(
attr,
"structural typing does not support #[serde(transparent)]",
));
}
_ => {}
}
Ok(())
});
if let Some(err) = has_error {
return Err(err);
}
}
for field in &info.fields {
for attr in &field.attrs {
if !attr.path().is_ident("serde") {
continue;
}
let attr_clone = attr.clone();
let mut has_error = None;
let _ = attr_clone.parse_nested_meta(|meta| {
let path = meta.path.get_ident().map(|i| i.to_string());
match path.as_deref() {
Some("default") => {
has_error = Some(syn::Error::new_spanned(
attr,
"structural typing does not support #[serde(default)] on fields",
));
}
Some("skip") | Some("skip_deserializing") => {
has_error = Some(syn::Error::new_spanned(
attr,
"structural typing does not support #[serde(skip)] or #[serde(skip_deserializing)]",
));
}
Some("flatten") => {
has_error = Some(syn::Error::new_spanned(
attr,
"structural typing does not support #[serde(flatten)]",
));
}
_ => {}
}
Ok(())
});
if let Some(err) = has_error {
return Err(err);
}
}
}
Ok(())
}
fn generate_helper_struct(info: &StructInfo) -> TokenStream {
let helper_name = helper_struct_name(&info.name);
let params = &info.generics.params;
let where_clause = &info.generics.where_clause;
let fields = info.fields.iter().map(|field| {
let name = &field.name;
let ty = &field.ty;
let preserved_attrs = filter_deserialize_attrs(&field.attrs);
quote! {
#(#preserved_attrs)*
#[serde(default)]
pub #name: Option<#ty>
}
});
let preserved_struct_attrs = filter_container_serde_attrs(&info.other_attrs);
let field_types: Vec<_> = info.fields.iter().map(|field| {
let ty = &field.ty;
quote! { Option<#ty> }
}).collect();
quote! {
#[::derive_where::derive_where(Deserialize; #(#field_types),*)]
#[doc(hidden)]
#(#preserved_struct_attrs)*
pub(super) struct #helper_name<#params> #where_clause {
#(#fields),*
}
}
}
fn generate_try_from_impl(info: &StructInfo) -> syn::Result<TokenStream> {
let name = &info.name;
let module_name = &info.module_name;
let helper_name = helper_struct_name(&info.name);
let (impl_generics, user_type_args) = impl_generics_with_f(&info.generics, module_name);
let (impl_generics, _, where_clause) = impl_generics.split_for_impl();
let (_, user_ty_generics, _) = info.generics.split_for_impl();
let field_conversions = info.fields.iter().map(|field| {
let field_name = &field.name;
let field_name_str = field_name.to_string();
quote! {
#field_name: value.#field_name
.try_extract()
.map_err(|_| missing_field(#field_name_str))?
.0
}
});
let where_bounds = info.fields.iter().map(|field| {
let field_name = &field.name;
let field_ty = &field.ty;
quote! {
Option<#field_ty>: ::structural_typing::extract::TryExtract<<<F as #module_name::Fields>::#field_name as ::structural_typing::presence::Presence>::Output<#field_ty>, #field_ty>
}
});
Ok(quote! {
impl #impl_generics ::core::convert::TryFrom<#module_name::#helper_name #user_ty_generics> for #name<#(#user_type_args,)* F>
#where_clause
where
#(#where_bounds),*
{
type Error = String;
fn try_from(value: #module_name::#helper_name #user_ty_generics) -> ::core::result::Result<Self, Self::Error> {
use ::structural_typing::extract::TryExtract;
let missing_field = |field: &'static str| -> String {
format!("missing field `{}`", field)
};
Ok(Self {
#(#field_conversions),*
})
}
}
})
}
fn helper_struct_name(struct_name: &Ident) -> Ident {
Ident::new(
&format!("__{}Deserialize", struct_name),
struct_name.span(),
)
}
pub fn helper_path(module_name: &Ident, struct_name: &Ident, generics: &syn::Generics) -> String {
let helper_name = helper_struct_name(struct_name);
if generics.params.is_empty() {
format!("{}::{}", module_name, helper_name)
} else {
let params: Vec<String> = generics.params.iter().map(|param| {
match param {
syn::GenericParam::Type(type_param) => type_param.ident.to_string(),
syn::GenericParam::Lifetime(lifetime_param) => lifetime_param.lifetime.to_string(),
syn::GenericParam::Const(const_param) => const_param.ident.to_string(),
}
}).collect();
format!("{}::{}<{}>", module_name, helper_name, params.join(", "))
}
}
fn filter_deserialize_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
attrs
.iter()
.filter(|attr| {
if !attr.path().is_ident("serde") {
return false;
}
let mut should_keep = true;
let _ = attr.parse_nested_meta(|meta| {
if let Some(ident) = meta.path.get_ident() {
let ident_str = ident.to_string();
if ident_str == "skip_serializing"
|| ident_str == "serialize_with"
{
should_keep = false;
}
}
Ok(())
});
should_keep
})
.cloned()
.collect()
}
fn filter_container_serde_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
attrs
.iter()
.filter(|attr| attr.path().is_ident("serde"))
.cloned()
.collect()
}
pub fn filter_serialize_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
attrs
.iter()
.filter(|attr| {
if !attr.path().is_ident("serde") {
return false;
}
let mut should_keep = true;
let _ = attr.parse_nested_meta(|meta| {
if let Some(ident) = meta.path.get_ident() {
let ident_str = ident.to_string();
if ident_str == "skip_deserializing"
|| ident_str == "deserialize_with"
|| ident_str == "alias"
{
should_keep = false;
}
}
Ok(())
});
should_keep
})
.cloned()
.collect()
}