structural-typing-macros 0.1.7

Procedural macros for structural-typing crate
Documentation
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()
}