rustrust-macros

Rust macro to generate endpoint paths


I'm trying to create a Rust macro that generates endpoints like "/api/v2/stats/<token>".

I thought it would be cool to make it like warp path does it: warp::path!("sum" / u32 / u32). But in warp, they do not need to support token trees with dots, i.e. expressions, and retrieve their values...

What I got so far is this:

macro_rules! path {
    () => {};
    ($next:tt $($tail:tt)*) => {{
        println!(stringify!($next));
        path!($($tail)*);
    }};
}

fn main() {
    struct Data {
        event: String,
        token: String,
    }
    let data = Data {
        event: String::from("stats"),
        token: String::from("a1b2c3d4"),
    };
    path!("/api/v2" / data.event / data.token)
}

playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=babf87265cc6060fc1695019e30e38bf

This shows what the macro is seeing:

"/api/v2"
/
data
.
event
/
data
.
token

I know token trees can be reinterpreted as expressions later, so there should be a way to keep tt's in tail, split slashes from "anything else", and get those as expressions to retrieve their values, but I'm not seeing how...
How could I make it return the String "/api/v2/stats/a1b2c3d4"?

EDIT: As requested, here are more examples of inputs and expected outputs:

struct Conf<'a> {env: &'a str};
let conf = Conf { env: "dev" };
let subsystem = "stats";

path!("/"); // root: "/"
path!("/api/v1" / data.event / "results"); // "/api/v1/stats/results"
path!("/api/v2/errors" / conf.env / subsystem); // "/api/v2/errors/dev/stats"

EDIT: I kind of did it with expressions, which is not that expressive, more a workaround, but it works:

macro_rules! path {
    ($($path:expr),+) => {{
        let mut s = [$($path),+].into_iter()
            .flat_map(|p| [p, "/"])
            .collect::<String>();
        s.pop();
        s
    }}
}

Limitations: it only accepts commas, the path parts have to be &str so I have to reference them manually, and most of all, this could be better represented as a function, but it is something to start with:

let result_url = path!("/api/v2", &data.event, &data.token);

Thank you!


Solution

  • You can use a tt muncher to achieve this:

    macro_rules! path {
        (@munch / ) => {
            String::from("/")
        };
        (@munch / $part:literal $(/)* ) => {
            format!("/{}", $part)
        };
        (@munch / $part:literal / $($tail:tt)* ) => {
            format!("/{}{}", $part, path!(@munch / $($tail)*))
        };
        (@munch / $($parts:ident).+ $(/)* ) => {
            format!("/{}", & $($parts).+)
        };
        (@munch / $($parts:ident).+ / $($tail:tt)* ) => {
            format!("/{}{}", & $($parts).+, path!(@munch / $($tail)*))
        };
        (/ $($input:tt)*) => {
            path!(@munch / $($input)*)
        };
    }
    

    playground

    Currently this produces nested format! calls. In order to avoid that you'll probably also need to use an accumulator. This kinda stuff interests me so I'm working on a version with an accumulator.

    Edit: And here's the accumulator version

    macro_rules! path {
        (/) => {
            String::from("/")
        };
        (/ $($input:tt)*) => {
            path!(@munch { / $($input)* } => ())
        };
    
        (@munch { / $part:literal $(/)* } => ($($accum:expr),*)) => {
            path!(@done ($( $accum, )* $part))
        };
        (@munch { / $part:literal / $($tail:tt)* } => ($($accum:expr),*)) => {
            path!(@munch { / $($tail)* } => ($( $accum, )* $part ))
        };
        
        (@munch { / $($parts:ident).+ $(/)* } => ($($accum:expr),*)) => {
            path!(@done ($( $accum, )* & $($parts).+ ))
        };
        (@munch { / $($parts:ident).+ / $($tail:tt)* } => ($($accum:expr),*)) => {
            path!(@munch { / $($tail)* } => ($( $accum, )* & $($parts).+ ))
        };
    
        (@replace_expr $_t:tt => $sub:expr) => { $sub };
        (@done ($($accum:expr),*)) => {
            format!(
                concat!($( path!(@replace_expr ($accum) => "/{}"), )*),
                $( $accum, )*
            )
        };
    }
    

    playground

    Edit2: per your request, another version which uses two accumulators to support a leading literal

    macro_rules! path {
        (/) => {
            String::from("/")
        };
        (/ $($input:tt)*) => {
            path!(@munch { / $($input)* } -> () : ())
        };
        ($part:literal $(/)*) => {
            String::from($part)
        };
        ($part:literal $($input:tt)*) => {
            path!(@munch { $($input)* } -> ("{}") : ($part))
        };
    
        (@munch { / $part:literal $(/)* } -> ($($fmt_accum:literal),*) : ($($args_accum:expr),*)) => {
            path!(@done ($( $fmt_accum, )* "/{}") : ($( $args_accum, )* $part))
        };
        (@munch { / $part:literal / $($tail:tt)* } -> ($($fmt_accum:literal),*) : ($($args_accum:expr),*)) => {
            path!(@munch { / $($tail)* } -> ($( $fmt_accum, )* "/{}") : ($( $args_accum, )* $part ))
        };
        
        (@munch { / $($parts:ident).+ $(/)* } -> ($($fmt_accum:literal),*) : ($($args_accum:expr),*)) => {
            path!(@done ($( $fmt_accum, )* "/{}") : ($( $args_accum, )* & $($parts).+ ))
        };
        (@munch { / $($parts:ident).+ / $($tail:tt)* } -> ($($fmt_accum:literal),*) : ($($args_accum:expr),*)) => {
            path!(@munch { / $($tail)* } -> ($( $fmt_accum, )* "/{}") : ($( $args_accum, )* & $($parts).+ ))
        };
    
        (@done ($($fmt_accum:literal),*) : ($($args_accum:expr),*)) => {
            format!(
                concat!($( $fmt_accum, )*),
                $( $args_accum, )*
            )
        };
    }
    

    playground