mod_template_macros 0.3.0

Creating macros that define modules from template modules
Documentation
pub(crate) mod attribute_substitution_declaration;
pub(crate) mod construction_declaration;
pub(crate) mod mbe_header;

use std::collections::HashMap;

pub use self::attribute_substitution_declaration::AttributeSubstitutionDeclaration;
pub use self::construction_declaration::ConstructionDeclaration;
pub use self::mbe_header::MbeHeader;

pub struct AttributeOptions {
    mbe_header: MbeHeader,
    constructions: Vec<ConstructionDeclaration>,
    attribute_substitutions: Vec<AttributeSubstitutionDeclaration>,
}

impl AttributeOptions {
    pub fn mbe_header(&self) -> &MbeHeader {
        &self.mbe_header
    }
    pub fn constructions(&self) -> &Vec<ConstructionDeclaration> {
        &self.constructions
    }
    pub fn attribute_substitutions(&self) -> &Vec<AttributeSubstitutionDeclaration> {
        &self.attribute_substitutions
    }

    pub fn build_type_map(&self) -> HashMap<String, syn::Type> {
        let mut type_map: HashMap<String, syn::Type> = HashMap::new();
        for construction in self.constructions() {
            type_map.insert(
                construction.target_name_ident().to_string(),
                construction.ty().clone(),
            );
        }
        type_map
    }
}

impl syn::parse::Parse for AttributeOptions {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let mbe_header: MbeHeader = input.parse()?;
        if input.is_empty() {
            return Ok(Self {
                mbe_header,
                constructions: vec![],
                attribute_substitutions: vec![],
            });
        }
        let _: syn::Token![;] = input.parse()?;

        let mut constructions: Option<Vec<ConstructionDeclaration>> = None;
        let mut attribute_substitutions: Option<Vec<AttributeSubstitutionDeclaration>> = None;

        loop {
            if input.is_empty() {
                return Ok(Self {
                    mbe_header,
                    constructions: constructions.unwrap_or_default(),
                    attribute_substitutions: attribute_substitutions.unwrap_or_default(),
                });
            }

            let ident: syn::Ident = input.parse()?;
            match &ident.to_string()[..] {
                "constructions" => {
                    if constructions.is_some() {
                        return Err(syn::Error::new(
                            ident.span(),
                            "duplicate constructions block",
                        ));
                    }

                    constructions = Some(construction_declaration::parse(input)?);
                }
                "attribute_substitutions" => {
                    if attribute_substitutions.is_some() {
                        return Err(syn::Error::new(
                            ident.span(),
                            "duplicate attribute-substitutions block",
                        ));
                    }

                    attribute_substitutions =
                        Some(attribute_substitution_declaration::parse(input)?);
                }
                _ => {
                    return Err(syn::Error::new(ident.span(), "unexpected"));
                }
            }

            if !input.is_empty() {
                let _: syn::Token![,] = input.parse()?;
            }
        }
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use proc_macro2::TokenStream;
    use quote::ToTokens;

    use super::{
        attribute_substitution_declaration::tests::AttributeSubstitutionDeclarationForTest,
        construction_declaration::tests::ConstructionDeclarationForTest, AttributeOptions,
    };

    #[derive(Debug, PartialEq, Eq, typed_builder::TypedBuilder)]
    pub struct AttributeOptionsForTest {
        mbe_header: String,
        constructions: Vec<ConstructionDeclarationForTest>,
        attribute_substitutions: Vec<AttributeSubstitutionDeclarationForTest>,
    }
    impl From<AttributeOptions> for AttributeOptionsForTest {
        fn from(value: AttributeOptions) -> Self {
            let AttributeOptions {
                mbe_header,
                constructions,
                attribute_substitutions: attr_subst,
            } = value;

            let mbe_header = mbe_header.into_token_stream().to_string();
            let constructions = constructions.into_iter().map(|x| x.into()).collect();
            let attr_subst = attr_subst.into_iter().map(|x| x.into()).collect();
            Self {
                mbe_header,
                constructions,
                attribute_substitutions: attr_subst,
            }
        }
    }

    #[test]
    fn basic() {
        let input = quote::quote!(
            macro_rules! the_macro_name;
            constructions(FOO -> Foo, BAR -> Bar),
            attribute_substitutions(FOO, BAZ),
        );

        let expected = AttributeOptionsForTest::builder()
            .mbe_header(quote::quote!(macro_rules! the_macro_name).to_string())
            .constructions(vec![
                ConstructionDeclarationForTest::builder()
                    .target_name("FOO".to_string())
                    .ty("Foo".to_string())
                    .build(),
                ConstructionDeclarationForTest::builder()
                    .target_name("BAR".to_string())
                    .ty("Bar".to_string())
                    .build(),
            ])
            .attribute_substitutions(vec![
                AttributeSubstitutionDeclarationForTest::builder()
                    .target_name("FOO".to_string())
                    .build(),
                AttributeSubstitutionDeclarationForTest::builder()
                    .target_name("BAZ".to_string())
                    .build(),
            ])
            .build();

        let actual: AttributeOptions = syn::parse2(input).unwrap();
        let actual: AttributeOptionsForTest = actual.into();

        assert_eq!(actual, expected);
    }

    fn do_test_only_mbe_header(header: TokenStream) {
        let input = quote::quote!(#header);

        let expected = AttributeOptionsForTest::builder()
            .mbe_header(quote::quote!(#header).to_string())
            .constructions(vec![])
            .attribute_substitutions(vec![])
            .build();

        let actual: AttributeOptions = syn::parse2(input).unwrap();
        let actual: AttributeOptionsForTest = actual.into();

        assert_eq!(actual, expected);
    }

    #[test]
    fn only_mbe_header() {
        do_test_only_mbe_header(quote::quote!(macro_rules! the_macro_name));
    }
    #[test]
    fn macro_rules_with_an_attribute() {
        do_test_only_mbe_header(quote::quote!(#[macro_export] macro_rules! the_macro_name));
    }
    #[test]
    fn macro_2_with_pub() {
        do_test_only_mbe_header(quote::quote!(pub macro the_macro_name));
    }

    #[test]
    fn no_constructions_and_without_trailing_comma() {
        let input = quote::quote!(macro_rules! the_macro_name; attribute_substitutions(FOO));

        let expected = AttributeOptionsForTest::builder()
            .mbe_header(quote::quote!(macro_rules! the_macro_name).to_string())
            .constructions(vec![])
            .attribute_substitutions(vec![AttributeSubstitutionDeclarationForTest::builder()
                .target_name("FOO".to_string())
                .build()])
            .build();

        let actual: AttributeOptions = syn::parse2(input).unwrap();
        let actual: AttributeOptionsForTest = actual.into();

        assert_eq!(actual, expected);
    }

    #[test]
    fn no_attribute_substitutions_and_with_no_parameters_in_constructions() {
        let input = quote::quote!(macro_rules! the_macro_name; constructions());

        let expected = AttributeOptionsForTest::builder()
            .mbe_header(quote::quote!(macro_rules! the_macro_name).to_string())
            .constructions(vec![])
            .attribute_substitutions(vec![])
            .build();

        let actual: AttributeOptions = syn::parse2(input).unwrap();
        let actual: AttributeOptionsForTest = actual.into();

        assert_eq!(actual, expected);
    }
}