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!
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)*)
};
}
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, )*
)
};
}
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, )*
)
};
}