xmlity-derive 0.0.9

Derive proc-macros for xmlity.
Documentation
use proc_macro2::Span;
use syn::{parse_quote, Expr, Ident, Item, Stmt};

use crate::{
    common::FieldIdent,
    options::{
        records::fields::{
            AttributeOpts, ChildOpts, FieldAttributeGroupOpts, FieldOpts, FieldValueGroupOpts,
        },
        FieldWithOpts, WithExpandedNameExt,
    },
    DeriveError, DeriveResult,
};
use quote::{quote, ToTokens};

use super::{
    builders::{SerializeAttributeBuilderExt, SerializeBuilderExt},
    serialize::SingleChildSerializeElementBuilder,
    serialize_attribute::SimpleSerializeAttributeBuilder,
};

pub fn attribute_field_serializer(
    access_ident: impl ToTokens,
    field_ident: &FieldIdent,
    item_type: &syn::Type,
    field_ident_to_expr: impl Fn(&FieldIdent) -> syn::Expr,
    opts: AttributeOpts,
) -> DeriveResult<proc_macro2::TokenStream> {
    let value_expr = field_ident_to_expr(field_ident);

    let skip_serializing_if_expr = opts.skip_serializing_if(&value_expr);

    let (prefix, serialize_expr) = match opts {
        AttributeOpts::Declared(opts) => {
            let wrapper_ident = Ident::new("__W", Span::call_site());

            let wrapper = SimpleSerializeAttributeBuilder {
                ident: &wrapper_ident,
                generics: &syn::Generics::default(),
                expanded_name: opts
                    .expanded_name(&field_ident.to_named_ident().to_string())
                    .into_owned(),
                preferred_prefix: opts.preferred_prefix,
                enforce_prefix: opts.enforce_prefix,
                item_type,
            };

            let definition = wrapper.struct_definition();
            let trait_impl = wrapper.serialize_attribute_trait_impl()?;

            let serialize_expr = wrapper.value_expression(&value_expr);
            let wrapped_serialize_expr: Expr = parse_quote!(&#serialize_expr);

            (
                vec![Item::from(definition), Item::from(trait_impl)],
                wrapped_serialize_expr,
            )
        }
        AttributeOpts::Deferred(_opts) => (vec![], value_expr),
    };

    let serialize_attribute_stmt: Stmt = parse_quote!(
        ::xmlity::ser::SerializeAttributes::serialize_attribute(#access_ident, #serialize_expr)?;
    );

    let serialize_attribute_expr = if let Some(skip_serializing_if_expr) = skip_serializing_if_expr
    {
        parse_quote! {
            if !#skip_serializing_if_expr {
                #serialize_attribute_stmt
            }
        }
    } else {
        serialize_attribute_stmt
    };

    Ok(quote! {
        {
            #(#prefix)*
            #serialize_attribute_expr
        }
    })
}

pub fn attribute_group_fields_serializer(
    access_ident: impl ToTokens,
    fields: impl IntoIterator<Item = FieldWithOpts<FieldIdent, FieldAttributeGroupOpts>>,
    field_ident_to_expr: impl Fn(&FieldIdent) -> syn::Expr,
) -> DeriveResult<proc_macro2::TokenStream> {
    let fields = fields
    .into_iter()
    .map(|var_field| {
        let field_ident = &var_field.field_ident;

        match var_field.options {
            FieldAttributeGroupOpts::Attribute(opts) => {
                attribute_field_serializer(
                    &access_ident,
                    field_ident,
                    &var_field.field_type,
                    &field_ident_to_expr,
                    opts,
                )
            },
            FieldAttributeGroupOpts::Group(_) => {
                let ser_value = field_ident_to_expr(field_ident);

                Ok(quote! {
                    ::xmlity::ser::SerializationGroup::serialize_attributes(#ser_value, #access_ident)?;
                })
            },
        }
    }).collect::<DeriveResult<Vec<_>>>()?;

    Ok(quote! {
        #(#fields)*
    })
}

pub fn element_field_serializer(
    access_ident: impl ToTokens,
    field_ident: &FieldIdent,
    item_type: &syn::Type,
    field_ident_to_expr: impl Fn(&FieldIdent) -> syn::Expr,
    opts: ChildOpts,
) -> DeriveResult<proc_macro2::TokenStream> {
    let value_expr = field_ident_to_expr(field_ident);

    let (prefix, serialize_expr, skip_serializing_if_expr): (Vec<_>, _, _) = match opts {
        ChildOpts::Value(value_opts) => {
            let skip_serializing_if_expr =
                value_opts.skip_serializing_if::<Expr>(parse_quote!(&#value_expr));

            (Vec::new(), value_expr, skip_serializing_if_expr)
        }
        ChildOpts::Element(opts) => {
            let skip_serializing_if_expr =
                opts.skip_serializing_if::<Expr>(parse_quote!(&#value_expr));

            let wrapper_ident = Ident::new("__W", Span::call_site());

            let wrapper = SingleChildSerializeElementBuilder {
                ident: &wrapper_ident,
                expanded_name: opts
                    .expanded_name(field_ident.to_named_ident().to_string().as_str())
                    .into_owned(),
                preferred_prefix: opts.preferred_prefix,
                enforce_prefix: opts.enforce_prefix,
                item_type,
                group: opts.group,
                skip_serializing_if: None,
            };

            let definition = wrapper.struct_definition();
            let trait_impl = wrapper.serialize_trait_impl()?;

            let serialize_expr = wrapper.value_expression(&value_expr);
            let wrapped_serialize_expr = parse_quote! {&#serialize_expr};

            (
                vec![Item::from(definition), Item::from(trait_impl)],
                wrapped_serialize_expr,
                skip_serializing_if_expr,
            )
        }
    };

    let serialize_element_stmt: Stmt = parse_quote!(
        ::xmlity::ser::SerializeSeq::serialize_element(#access_ident, #serialize_expr)?;
    );

    let serialize_element_expr = if let Some(skip_serializing_if_expr) = skip_serializing_if_expr {
        parse_quote! {
            if !#skip_serializing_if_expr {
                #serialize_element_stmt
            }
        }
    } else {
        serialize_element_stmt
    };

    Ok(quote! {
        {
            #(#prefix)*
            #serialize_element_expr
        }
    })
}

pub fn element_group_fields_serializer(
    access_ident: impl ToTokens,
    fields: impl IntoIterator<Item = FieldWithOpts<FieldIdent, FieldValueGroupOpts>>,
    field_ident_to_expr: impl Fn(&FieldIdent) -> syn::Expr,
) -> DeriveResult<proc_macro2::TokenStream> {
    let fields = fields
    .into_iter()
    .map::<DeriveResult<_>, _>(|var_field| {
        let field_ident = &var_field.field_ident;

        match var_field.options {
            FieldValueGroupOpts::Value(opts) => {
                element_field_serializer(&access_ident, field_ident, &var_field.field_type, |a| field_ident_to_expr(a), opts)
            },
            FieldValueGroupOpts::Group(_) => {
                let ser_value = field_ident_to_expr(field_ident);

                Ok(quote! {
                    ::xmlity::ser::SerializationGroup::serialize_children(#ser_value, #access_ident)?;
                })
            },
        }
    }).collect::<Result<Vec<_>, _>>()?;

    Ok(quote! {
        #(#fields)*
    })
}

pub fn element_fields_serializer(
    access_ident: impl ToTokens,
    fields: impl IntoIterator<Item = FieldWithOpts<FieldIdent, ChildOpts>>,
    field_ident_to_expr: impl Fn(&FieldIdent) -> syn::Expr,
) -> DeriveResult<proc_macro2::TokenStream> {
    let fields = fields
        .into_iter()
        .map(|var_field| {
            element_field_serializer(
                &access_ident,
                &var_field.field_ident,
                &var_field.field_type,
                |a| field_ident_to_expr(a),
                var_field.options,
            )
        })
        .collect::<Result<Vec<_>, _>>()?;

    Ok(quote! {
        #(#fields)*
    })
}

pub fn fields(
    ast: &syn::DeriveInput,
) -> Result<Vec<FieldWithOpts<FieldIdent, FieldOpts>>, DeriveError> {
    let syn::Data::Struct(syn::DataStruct { fields, .. }) = &ast.data else {
        unreachable!()
    };

    match fields {
        syn::Fields::Named(fields) => fields
            .named
            .iter()
            .map(|f| {
                Ok(FieldWithOpts {
                    field_ident: FieldIdent::Named(f.ident.clone().expect("Named struct")),
                    options: FieldOpts::from_field(f)?,
                    field_type: f.ty.clone(),
                })
            })
            .collect::<Result<Vec<_>, _>>(),
        syn::Fields::Unnamed(fields) => fields
            .unnamed
            .iter()
            .enumerate()
            .map(|(i, f)| {
                Ok(FieldWithOpts {
                    field_ident: FieldIdent::Indexed(syn::Index::from(i)),
                    options: FieldOpts::from_field(f)?,
                    field_type: f.ty.clone(),
                })
            })
            .collect::<Result<Vec<_>, _>>(),
        syn::Fields::Unit => Ok(vec![]),
    }
}

pub fn attribute_group_fields(
    fields: Vec<FieldWithOpts<FieldIdent, FieldOpts>>,
) -> Result<Vec<FieldWithOpts<FieldIdent, FieldAttributeGroupOpts>>, DeriveError> {
    Ok(fields
        .into_iter()
        .filter_map(|field| {
            field.map_options_opt(|opt| match opt {
                FieldOpts::Attribute(opts) => Some(FieldAttributeGroupOpts::Attribute(opts)),
                FieldOpts::Group(opts) => Some(FieldAttributeGroupOpts::Group(opts)),
                FieldOpts::Value(_) => None,
            })
        })
        .collect())
}

pub fn element_group_fields(
    fields: Vec<FieldWithOpts<FieldIdent, FieldOpts>>,
) -> Result<Vec<FieldWithOpts<FieldIdent, FieldValueGroupOpts>>, DeriveError> {
    Ok(fields
        .into_iter()
        .filter_map(|field| {
            field.map_options_opt(|opt| match opt {
                FieldOpts::Value(opts) => Some(FieldValueGroupOpts::Value(opts)),
                FieldOpts::Group(opts) => Some(FieldValueGroupOpts::Group(opts)),
                FieldOpts::Attribute(_) => None,
            })
        })
        .collect())
}

pub fn element_fields(
    fields: Vec<FieldWithOpts<FieldIdent, FieldOpts>>,
) -> Result<Vec<FieldWithOpts<FieldIdent, ChildOpts>>, DeriveError> {
    Ok(fields
        .into_iter()
        .filter_map(|field| {
            field.map_options_opt(|opt| match opt {
                FieldOpts::Value(opts) => Some(opts),
                FieldOpts::Group(_) | FieldOpts::Attribute(_) => None,
            })
        })
        .collect())
}