code-product-lib 0.4.1

macro producing multiple expansions
Documentation
#![doc = include_str!("../README.md")]

use proc_macro2::TokenStream;

#[cfg(test)]
use quote::quote;

mod product;
use product::Product;
mod token;

/// The scope mode for the product expansion. This defines how scopes are handled at the top
/// level.
pub enum ScopeMode {
    /// Expand products in all scopes, including the root scope.
    Expand,
    /// Expand products in sub-scopes but not in the root scope. Thus one has to insert at
    /// least one manual scope with `${  }` to expand products.
    SubExpand,
    /// Insert scopes automatically and close/expand them after a ending brace or a
    /// semicolon. Then start a new scope This is convenient when a lot structs or impls shall
    /// be independently expanded.
    ///
    /// # Example
    ///
    /// ```text
    /// impl Foo for $((First)(Second)) { }
    /// impl Bar for $((Third)(Fourth)) { }
    /// ```
    ///
    /// will treat each impl item as it is surrounded by it own scope and expand to:
    ///
    /// ```text
    /// impl Foo for First { }
    /// impl Foo for Second { }
    /// impl Bar for Third { }
    /// impl Bar for Fourth { }
    /// ```
    AutoBraceSemi,
    // when you read this, you prolly want to add another mode. Just do and send a PR.
}

/// Expands a product syntax into a `TokenStream`.
///
/// # Examples
///
/// ## Complete expansion
///
/// ```
/// # use quote::quote;
/// # use code_product_lib::*;
///
/// let input = quote! {
///     impl Foo for $((This)(That)) { }
/// };
///
/// let output = expand_products(input, ScopeMode::Expand);
/// assert_eq!(
///     output.to_string(),
///     "impl Foo for This { } \
///      impl Foo for That { }");
/// ```
///
/// ## Sub-expand
///
/// ```
/// # use quote::quote;
/// # use code_product_lib::*;
///
/// let input = quote! {
///     let toplevel = thing ;
///     ${ impl Foo for $((This)(That)) { } }
///     ${ impl Bar for $((This)(That)) { } }
/// };
///
/// let output = expand_products(input, ScopeMode::SubExpand);
/// assert_eq!(
///     output.to_string(),
///     "let toplevel = thing ; \
///      impl Foo for This { } \
///      impl Foo for That { } \
///      impl Bar for This { } \
///      impl Bar for That { }"
/// );
/// ```
///
/// ## Auto-brace-semi
///
/// ```
/// # use quote::quote;
/// # use code_product_lib::*;
///
/// let input = quote! {
///     impl Foo for $((This)(That)) { }
///     impl Bar for $((This)(That)) { }
/// };
///
/// let output = expand_products(input, ScopeMode::AutoBraceSemi);
/// assert_eq!(
///     output.to_string(),
///     "impl Foo for This { } \
///      impl Foo for That { } \
///      impl Bar for This { } \
///      impl Bar for That { }"
/// );
/// ```
pub fn expand_products(input: TokenStream, mode: ScopeMode) -> TokenStream {
    Product::new(input).parse(mode).expand()
}

#[cfg(test)]
macro_rules! assert_expansion {
    ($expand_mode:ident {$($in:tt)*} == {$($out:tt)*}) => {
        let input = quote! { $($in)* };
        let output = expand_products(input, ScopeMode::$expand_mode);
        let expected = quote! { $($out)* };
        assert_eq!(output.to_string(), expected.to_string());
    };
    ($($in:tt)*) => {
        assert_expansion!(Expand {$($in)*} == {$($in)*});
    };
    (SubExpand $($in:tt)*) => {
        assert_expansion!(SubExpand {$($in)*} == {$($in)*});
    };
}

#[test]
fn smoke() {
    assert_expansion! {
        #[newtype]
        pub struct MyNewtype<T>(T);
    };
}

#[test]
fn smoke_sub() {
    assert_expansion! {
        noexpand
        #[newtype]
        pub struct MyNewtype<T>(T);
    };
}

#[test]
fn test_tt_groups_parsed_recursive() {
    let input = quote! {
        {{$$}}
    };

    let output = expand_products(input, ScopeMode::Expand);
    assert_eq!(output.to_string(), "{ { $ } }");
}

#[test]
fn test_scopes() {
    assert_expansion! {
        Expand {
            foo ${bar ${baz}}
        } == {
            foo bar baz
        }
    };
}

#[test]
fn test_sub_scopes() {
    assert_expansion! {
        SubExpand {
            foo ${bar ${baz}}
        } == {
            foo bar baz
        }
    };
}

#[test]
fn expand_at_escaping() {
    assert_expansion! {
        Expand {
            $$ ${$$ foo}
        } == {
            $ $ foo
        }
    };
}

#[test]
#[should_panic]
fn single_at_is_invalid() {
    assert_expansion! {
        Expand {
            $
        } == {
            $
        }
    };
}

#[test]
fn noexpand_leaves_at_verbatim() {
    assert_expansion! {
        SubExpand {
            $ ${$$}
        } == {
            $ $
        }
    };
}

#[test]
fn simple_expand_unnamed() {
    assert_expansion! {
        Expand {
            $((foo))
        } == {
            foo
        }
    };
}

#[test]
fn simple_expand_named() {
    assert_expansion! {
        Expand {
            $(bar: (foo))
            $bar
        } == {
            foo
        }
    };
}

#[test]
fn simple_expand_named_visible() {
    assert_expansion! {
        Expand {
            $($bar: (foo))
        } == {
            foo
        }
    };
}

#[test]
fn empty_defintions_expand() {
    assert_expansion! {
        Expand {
            $(()()())
            $((foo)(bar)) ;
        } == {
            foo ;
            foo ;
            foo ;
            bar ;
            bar ;
            bar ;
        }
    };
}