Looping over string literals with a for_each macro?

Hello there,
I would like to write tests involving format strings. I want to test a parser parsing different formats of currencies:

#[test]
fn parse_euro() {
    macro_rules! test { // helper macro constructing a test case
        ($fmt:expr, $u:expr) => {
            assert_eq!(Euro::parse(&format!($fmt, $u)).unwrap().1,
            Euro::from($u));
        }
    };
    // loop over all test cases
    // (all combinations of formats and values)
    for fmt in &["{}€", "{} Euro", "€{}"] {
        for amt in vec![1, 32, 1823, 0, 39, 99999999] {
            test!(fmt, amt);
        }
    }
}

The problem here is that format! requires that its format string (fmt) is a string literal. How can I declare an array of string literals and loop over it e.g. have a macro generate code for each string literal? Is there a standard macro for this?

Thanks

Edit:
I think I am looking for something like this:

macro_rules! for_each {
    ($id:pat in $($fmt:expr),*{ $sub:expr }) => {
        $(
            replace!($id, $fmt, $sub);
        )*
    }
};

Which I can use like this:

for_each!(fmt in "{}€", "{} Euro", "€{}" {
    for amt in vec![1, 32, 1823, 0, 39, 99999999] {
        test!(fmt, amt);
    }
});

But I don`t know how to replace a pattern/token (fmt in the example) with a string literal in an expression..

What you're trying to do is possible but tricky. Fortunately, I have in the past written a general tool which can be used to do this:

Playground: Rust Playground

#[test]
fn parse_euro() {
    cartesian!{
        ["{}€" "{} Euro" "€{}"]
        [{1} {32} {1823} {0} {39} {99999999}]
        for_each!($fmt:tt {$n:expr})
        => {
            assert_eq!(
                Euro::parse(&format!($fmt, $n)).unwrap().1,
                Euro::from($n),
            );
        }
    }
}

The way it works is by defining a callback macro (solving the problem of how to replace the fmt token), and repeatedly calling that macro. In addition to that, it accepts multiple sets of tokens and forms a cartesian product of them (support for this was essential because it cannot be used recursively due to technical limitations), so we can also include amt in the loop. So at some intermediate point in the expansion, it looks like this:

macro_rules! mac {
    ($fmt:tt {$n:expr}) => {
        assert_eq!(
            Euro::parse(&format!($fmt, $n)).unwrap().1,
            Euro::from($n),
        );
    };
}

mac!{ "{}€" {1} }
mac!{ "{}€" {32} }
mac!{ "{}€" {1823} }
// ...
mac!{ "€{}" {39} }
mac!{ "€{}" {99999999} }

Here's the part that defines the callback:

/// Higher-order macro that iterates over a cartesian product.
///
/// Useful for generating impls involving opaque traits of the sort
/// sometimes seen used as bounds in public APIs.
///
/// It takes a number of groups of token trees and a suitable definition
/// for a callback macro, and it calls the macro with one token tree from
/// each group in order.
macro_rules! cartesian {
    (
        $([$($groups:tt)*])*
        for_each!($($mac_match:tt)*)
        => {$($mac_body:tt)*}$(;)*
    )
    => {
        // (this fixed name gets overwritten on each use.  Technically, using a fixed name
        //  makes this macro not "re-entrant", in the sense that you can't call it in a nested
        //  fashion... but there is no reason to do so, because the macro has a monolithic design)
        macro_rules! __cartesian__user_macro {
            ($($mac_match)*) => {$($mac_body)*};
        }
        cartesian__!{ @product::next($([$($groups)*])*) -> (__cartesian__user_macro!()) }
    };
}

and this generates all the calls to the callback:

#[doc(hidden)]
macro_rules! cartesian__ {
    (@product::next([$($token:tt)+] $($rest:tt)*) -> $cb:tt)
    => { cartesian__!{ @product::unpack([$($token)+] $($rest)*) -> $cb } };

    // base case; direct product of no arguments
    (@product::next() -> ($mac:ident!($($args:tt)*)))
    => {$mac!{$($args)*}};

    // Each direct product in the invocation incurs a fixed number of recursions
    //  as we replicate the macro.  First, we must smash anything we want to replicate
    //  into a single tt that can be matched without repetitions.  Do this to `rest`.
    (@product::unpack([$($token:tt)*] $($rest:tt)*) -> $cb:tt)
    => {cartesian__!{ @product::unpack_2([$($token)*] [$($rest)*]) -> $cb }};

    // Replicate macro for each token.
    (@product::unpack_2([$($token:tt)*] $rest:tt) -> $cb:tt)
    => { $( cartesian__!{ @product::unpack_3($token $rest) -> $cb } )* };

    // Expand the unparsed arguments back to normal;
    // add the token into the macro call
    (@product::unpack_3($token:tt [$($rest:tt)*]) -> ($mac:ident!($($args:tt)*)))
    => {cartesian__!{ @product::next($($rest)*) -> ($mac!($($args)*$token)) }};
}

There are also different approaches to this particular problem. There is the strfmt crate for doing string formatting on template strings constructed at runtime. You could also try iterating over formatting functions instead of format templates:

#[test]
fn parse_euro() {
    let fmts: &[fn(u32) -> String] = &[
        |x| format!("{}€", x),
        |x| format!("{} Euro", x),
        |x| format!("€{}", x),
    ];

    // loop over all test cases
    // (all combinations of formats and values)
    for fmt in fmts {
        for amt in vec![1, 32, 1823, 0, 39, 99999999] {
            assert_eq!(
                Euro::parse(&fmt(amt)).unwrap().1,
                Euro::from(amt),
            );
        }
    }
}
3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.