xmlity-derive 0.0.9

Derive proc-macros for xmlity.
Documentation
use std::borrow::Cow;

use proc_macro2::Span;
use quote::quote;
use syn::{parse_quote, Arm, Data, DataEnum, DeriveInput, Fields, Generics, Ident, Stmt};

use crate::{
    common::{self, value_deconstructor, FieldIdent, RecordInput},
    options::{
        enums::roots::RootValueOpts as EnumRootVolueOpts,
        records::{
            self,
            roots::{RootValueOpts as RecordRootValueOpts, SerializeRootOpts},
        },
    },
    ser::{
        builders::{SerializeBuilder, SerializeBuilderExt},
        common::{element_fields, element_fields_serializer},
    },
    DeriveError,
};

use super::variant::SerializeVariantBuilder;

pub struct RecordSerializeValueBuilder<'a, T: Fn(syn::Expr) -> syn::Expr> {
    input: &'a RecordInput<'a, T>,
    options: Option<&'a RecordRootValueOpts>,
}

impl<'a, T: Fn(syn::Expr) -> syn::Expr> RecordSerializeValueBuilder<'a, T> {
    pub fn new(ast: &'a RecordInput<'a, T>, options: Option<&'a RecordRootValueOpts>) -> Self {
        Self {
            input: ast,
            options,
        }
    }
}

impl<T: Fn(syn::Expr) -> syn::Expr> SerializeBuilder for RecordSerializeValueBuilder<'_, T> {
    fn serialize_fn_body(
        &self,
        serializer_access: &Ident,
        _serializer_type: &syn::Type,
    ) -> Result<Vec<Stmt>, DeriveError> {
        if let Some(serialize_with) = self.options.as_ref().and_then(|a| a.serialize_with()) {
            return Ok(parse_quote! {
                (#serialize_with)(self, #serializer_access)
            });
        }

        let seq_access_ident = Ident::new("__seq_access", proc_macro2::Span::call_site());

        let fields: Vec<_> = match (&self.input.fields, &self.options) {
            (common::StructTypeWithFields::Named(fields), _) => fields
                .iter()
                .cloned()
                .map(|f| f.map_ident(FieldIdent::Named))
                .collect(),
            (common::StructTypeWithFields::Unnamed(fields), _) => fields
                .iter()
                .cloned()
                .map(|f| f.map_ident(FieldIdent::Indexed))
                .collect(),
            (
                common::StructTypeWithFields::Unit,
                Some(RecordRootValueOpts {
                    value: Some(value), ..
                }),
            ) => {
                return Ok(parse_quote! {
                    ::xmlity::Serializer::serialize_text(#serializer_access, #value)
                });
            }
            (common::StructTypeWithFields::Unit, _) => {
                return Ok(parse_quote! {
                    ::xmlity::Serializer::serialize_none(#serializer_access)
                });
            }
        };

        let record_path = self.input.record_path.as_ref();

        let value_deconstructor = value_deconstructor(
            self.input.constructor_path.as_ref(),
            &parse_quote!(&#record_path),
            &self.input.fields,
            self.input.fallable_deconstruction,
        );

        let value_fields = element_fields_serializer(
            quote! {&mut #seq_access_ident},
            element_fields(fields)?,
            |field_ident| {
                let ident_name = field_ident.to_named_ident();
                parse_quote!(#ident_name)
            },
        )?;

        Ok(parse_quote! {
            #(#value_deconstructor)*
            let mut #seq_access_ident = ::xmlity::Serializer::serialize_seq(#serializer_access)?;
            #value_fields
            ::xmlity::ser::SerializeSeq::end(#seq_access_ident)
        })
    }

    fn ident(&self) -> Cow<'_, Ident> {
        Cow::Borrowed(self.input.impl_for_ident.as_ref())
    }

    fn generics(&self) -> Cow<'_, Generics> {
        Cow::Borrowed(self.input.generics.as_ref())
    }
}

pub struct DeriveEnum<'a> {
    ast: &'a syn::DeriveInput,
    opts: Option<&'a EnumRootVolueOpts>,
}

impl<'a> DeriveEnum<'a> {
    pub fn new(ast: &'a syn::DeriveInput, value_opts: Option<&'a EnumRootVolueOpts>) -> Self {
        Self {
            ast,
            opts: value_opts,
        }
    }
}

impl SerializeBuilder for DeriveEnum<'_> {
    fn serialize_fn_body(
        &self,
        serializer_access: &Ident,
        _serializer_type: &syn::Type,
    ) -> Result<Vec<Stmt>, DeriveError> {
        let DeriveInput {
            ident: enum_ident,
            data,
            generics: enum_generics,
            ..
        } = self.ast;

        let Data::Enum(DataEnum { variants, .. }) = &data else {
            unreachable!()
        };

        if let Some(serialize_with) = self.opts.as_ref().and_then(|a| a.serialize_with()) {
            return Ok(parse_quote! {
                (#serialize_with)(self, #serializer_access)
            });
        }

        if variants.is_empty() {
            return Ok(parse_quote! {
                unreachable!("Enum {} has no variants", stringify!(#enum_ident))
            });
        }

        let variants = variants
            .iter()
            .map::<Result<Arm, DeriveError>, _>(|variant| {
                let variant_ident = &variant.ident;

                let record = common::parse_enum_variant_derive_input(
                    enum_ident,
                    enum_generics,
                    variant,
                    variants.len() > 1,
                )?;

                let mut variant_opts = records::roots::SerializeRootOpts::parse(&variant.attrs)?;
                if let SerializeRootOpts::Value(records::roots::RootValueOpts {
                    value: value @ None,
                    ..
                }) = &mut variant_opts
                {
                    let ident_value = self
                        .opts
                        .as_ref()
                        .map(|a| a.rename_all)
                        .unwrap_or_default()
                        .apply_to_variant(&variant.ident.to_string());

                    *value = Some(ident_value);
                }
                if let SerializeRootOpts::None = variant_opts {
                    let ident_value = self
                        .opts
                        .as_ref()
                        .map(|a| a.rename_all)
                        .unwrap_or_default()
                        .apply_to_variant(&variant.ident.to_string());

                    variant_opts = SerializeRootOpts::Value(records::roots::RootValueOpts {
                        value: Some(ident_value),
                        ..Default::default()
                    });
                }

                let sub_value_ident = Ident::new("__sub_value", Span::call_site());

                let matcher = match &variant.fields {
                    Fields::Unit => quote! { #sub_value_ident @ #enum_ident::#variant_ident },
                    Fields::Unnamed(fields) => {
                        let fields = fields.unnamed.iter().map(|_| quote! { _ });
                        quote! { #sub_value_ident @ #enum_ident::#variant_ident(#(#fields),*) }
                    }
                    Fields::Named(_) => {
                        quote! { #sub_value_ident @ #enum_ident::#variant_ident { .. } }
                    }
                };

                let variant_builder = SerializeVariantBuilder::new(&record, &variant_opts);

                let definition = variant_builder.definition();
                let serialize_impl = variant_builder.serialize_trait_impl()?;

                let serialize_expr = variant_builder.serialize_expr(&sub_value_ident);

                Ok(parse_quote!(
                    __inner @ #matcher => {
                        #definition
                        #serialize_impl

                        ::xmlity::Serialize::serialize(&#serialize_expr, #serializer_access)
                    }
                ))
            })
            .collect::<Result<Vec<_>, _>>()?;

        Ok(parse_quote! {
            match self {
                #(#variants)*
            }
        })
    }

    fn ident(&self) -> Cow<'_, Ident> {
        Cow::Borrowed(&self.ast.ident)
    }

    fn generics(&self) -> Cow<'_, Generics> {
        Cow::Borrowed(&self.ast.generics)
    }
}