a3s-boot-macros 0.1.2

Attribute macros for a3s-boot
Documentation
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Attribute, Ident, LitBool, LitStr, Result, Token};

use super::is_attribute_named;

pub(in crate::controller) fn take_controller_serialization_attrs(
    attrs: &[Attribute],
) -> (
    Vec<Attribute>,
    ControllerSerializationAttrs,
    Vec<syn::Error>,
) {
    let mut clean_attrs = Vec::new();
    let mut serialization = ControllerSerializationAttrs::default();
    let mut errors = Vec::new();

    for attr in attrs {
        if !is_attribute_named(attr, "serialize") {
            clean_attrs.push(attr.clone());
            continue;
        }

        match attr.parse_args::<SerializationSpec>() {
            Ok(spec) => {
                if serialization.spec.is_some() {
                    errors.push(syn::Error::new_spanned(
                        attr,
                        "controller impl blocks can use at most one #[serialize] attribute",
                    ));
                } else {
                    serialization.spec = Some(spec);
                }
            }
            Err(error) => errors.push(error),
        }
    }

    (clean_attrs, serialization, errors)
}

pub(in crate::controller) fn take_route_serialization_attrs(
    attrs: &[Attribute],
) -> (Vec<Attribute>, Option<SerializationSpec>, Vec<syn::Error>) {
    let mut clean_attrs = Vec::new();
    let mut spec = None;
    let mut errors = Vec::new();

    for attr in attrs {
        if !is_attribute_named(attr, "serialize") {
            clean_attrs.push(attr.clone());
            continue;
        }

        match attr.parse_args::<SerializationSpec>() {
            Ok(parsed) => {
                if spec.is_some() {
                    errors.push(syn::Error::new_spanned(
                        attr,
                        "route methods can use at most one #[serialize] attribute",
                    ));
                } else {
                    spec = Some(parsed);
                }
            }
            Err(error) => errors.push(error),
        }
    }

    (clean_attrs, spec, errors)
}

#[derive(Default)]
pub(in crate::controller) struct ControllerSerializationAttrs {
    spec: Option<SerializationSpec>,
}

impl ControllerSerializationAttrs {
    pub(in crate::controller) fn tokens(&self) -> Vec<proc_macro2::TokenStream> {
        self.spec.iter().map(SerializationSpec::token).collect()
    }
}

#[derive(Clone, Default)]
pub(in crate::controller) struct SerializationSpec {
    include_fields: Vec<LitStr>,
    exclude_fields: Vec<LitStr>,
    skip_null_fields: bool,
}

impl SerializationSpec {
    pub(in crate::controller) fn token(&self) -> proc_macro2::TokenStream {
        let mut options = quote!(::a3s_boot::SerializationOptions::new());

        if !self.include_fields.is_empty() {
            let fields = &self.include_fields;
            options = quote! {
                (#options).include_fields([#(#fields),*])
            };
        }

        if !self.exclude_fields.is_empty() {
            let fields = &self.exclude_fields;
            options = quote! {
                (#options).exclude_fields([#(#fields),*])
            };
        }

        if self.skip_null_fields {
            options = quote! {
                (#options).skip_null_fields()
            };
        }

        quote!(with_serialization(#options))
    }
}

impl Parse for SerializationSpec {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        let mut spec = Self::default();

        while !input.is_empty() {
            let name = input.parse::<Ident>()?;
            let key = name.to_string();
            match key.as_str() {
                "include" => {
                    input.parse::<Token![=]>()?;
                    spec.include_fields.extend(parse_lit_str_array(input)?);
                }
                "exclude" => {
                    input.parse::<Token![=]>()?;
                    spec.exclude_fields.extend(parse_lit_str_array(input)?);
                }
                "skip_null" => {
                    if input.peek(Token![=]) {
                        input.parse::<Token![=]>()?;
                        spec.skip_null_fields = input.parse::<LitBool>()?.value;
                    } else {
                        spec.skip_null_fields = true;
                    }
                }
                _ => {
                    return Err(syn::Error::new_spanned(
                        name,
                        "expected `include`, `exclude`, or `skip_null`",
                    ));
                }
            }

            if input.is_empty() {
                break;
            }
            input.parse::<Token![,]>()?;
        }

        Ok(spec)
    }
}

fn parse_lit_str_array(input: ParseStream<'_>) -> Result<Vec<LitStr>> {
    let content;
    syn::bracketed!(content in input);
    Ok(Punctuated::<LitStr, Token![,]>::parse_terminated(&content)?
        .into_iter()
        .collect())
}