mod_template_macros 0.3.0

Creating macros that define modules from template modules
Documentation
use std::collections::HashMap;

use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
use quote::ToTokens;
use syn::Meta;

use super::token_stream_or_syn_error::TokenStreamOrSynError;

pub type Substituter<'a> = dyn Fn(Meta) -> syn::Result<TokenStream> + 'a;

pub fn substitute_attributes(
    input: TokenStream,
    attr_map: &mut HashMap<String, Box<Substituter>>,
) -> Result<TokenStream, syn::Error> {
    let mut input = input.into_iter();
    let mut output = TokenStreamOrSynError::new();

    'iteration: loop {
        let Some(tt) = input.next() else {
            break output.into_result();
        };

        'process: {
            if let TokenTree::Punct(ref punct) = tt {
                if punct.as_char() != '#' {
                    break 'process;
                }
                let Some(tt_expect_group) = input.next() else {
                    break 'process;
                };
                let TokenTree::Group(ref group) = tt_expect_group else {
                    output.extend([tt, tt_expect_group]);
                    continue 'iteration;
                };
                if group.delimiter() != Delimiter::Bracket {
                    output.extend([tt, tt_expect_group]);
                    continue 'iteration;
                }

                let Some(substituted) = try_substitute_attribute(group.stream(), attr_map) else {
                    output.extend([tt, tt_expect_group]);
                    continue 'iteration;
                };
                match substituted {
                    Ok(substituted) => output.extend(substituted),
                    Err(err) => output.error_combine(err),
                }
                continue 'iteration;
            } else if let TokenTree::Group(ref group) = tt {
                let inner_output = substitute_attributes(group.stream(), attr_map);
                match inner_output {
                    Ok(inner_output) => output.append(Group::new(group.delimiter(), inner_output)),
                    Err(inner_err) => output.error_combine(inner_err),
                }

                continue 'iteration;
            }
        }

        output.append(tt);
    }
}

fn try_substitute_attribute(
    meta: TokenStream,
    attr_map: &mut HashMap<String, Box<Substituter>>,
) -> Option<syn::Result<TokenStream>> {
    let meta: Meta = syn::parse2(meta).ok()?;

    let path = meta.path().into_token_stream().to_string();
    let Some(ref substituter) = attr_map.get(&path) else {
        return None;
    };

    Some(substituter(meta))
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use crate::utils::substitute_attributes::Substituter;

    use super::substitute_attributes;

    #[test]
    fn basic() {
        let input = quote::quote! {
            #[foo]
            mod a_mod {
                #[foo]
                #[not_in_map]
                #[bar]
                struct a_struct {
                    #[baz(#[zab])]
                    a_field: i32,
                }
                impl a_struct {
                    #[bar]
                    #[foo]
                    fn an_fn() {}
                }
            }
        };

        let mut attr_map: HashMap<String, Box<Substituter>> = HashMap::new();
        attr_map.insert("foo".to_string(), Box::new(|_| Ok(quote::quote!(#[oof]))));
        attr_map.insert(
            "bar".to_string(),
            Box::new(|_| Ok(quote::quote!(#[bar_1] #[bar_2]))),
        );
        attr_map.insert(
            "baz".to_string(),
            Box::new(|meta| Ok(meta.require_list().unwrap().tokens.clone())),
        );

        let expected = quote::quote! {
            #[oof]
            mod a_mod {
                #[oof]
                #[not_in_map]
                #[bar_1]
                #[bar_2]
                struct a_struct {
                    #[zab]
                    a_field: i32,
                }
                impl a_struct {
                    #[bar_1]
                    #[bar_2]
                    #[oof]
                    fn an_fn() {}
                }
            }
        };

        let actual = substitute_attributes(input, &mut attr_map);

        assert_eq!(
            expected.to_string(),
            actual.expect("there should not be errors").to_string()
        )
    }

    mod errors {
        use super::*;

        fn before_each() -> (HashMap<String, Box<Substituter<'static>>>,) {
            let mut attr_map: HashMap<String, Box<Substituter>> = HashMap::new();
            attr_map.insert(
                "errored".to_string(),
                Box::new(|meta| {
                    let error_span = meta.path().segments.last().unwrap().ident.span();
                    Err(syn::Error::new(error_span, "errored"))
                }),
            );

            (attr_map,)
        }

        #[test]
        fn single() {
            let (mut attr_map,) = before_each();

            let input = quote::quote! {
                #[errored]
                mod foo {}
            };

            let actual = substitute_attributes(input, &mut attr_map);

            let actual_error = actual.expect_err("should have an error");

            assert_eq!(actual_error.to_string(), "errored");
        }

        #[test]
        fn multiple() {
            let (mut attr_map,) = before_each();

            let input = quote::quote! {
                #[errored]
                mod foo {
                    #[errored]
                    fn bar() {}
                }
            };

            let actual = substitute_attributes(input, &mut attr_map);

            // TODO: how to assert error message of combined syn::Error?
            let _actual_error = actual.expect_err("should have an error");
        }
    }
}