Я пытаюсь создать макрос Rust, который генерирует конечные точки, такие как "/api/v2/stats/<token>".
Я подумал, что было бы круто сделать так, как это делает warp path: warp::path!("sum" / u32 / u32). Но в варпе им не нужно поддерживать деревья токенов с точками, то есть выражениями, и извлекать их значения...
То, что я получил до сих пор, это:
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)
}
Это показывает, что видит макрос:
"/api/v2"
/
data
.
event
/
data
.
token
Я знаю, что деревья токенов могут быть переинтерпретированы как выражения позже, поэтому должен быть способ сохранить tt в хвосте, отделить косые черты от «чего-либо еще» и получить их как выражения для получения их значений, но я не вижу, как это сделать. . Как мне заставить его вернуть строку "/api/v2/stats/a1b2c3d4"?
Дополнительные примеры входных данных и ожидаемых результатов:
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"
Обновлено: я как бы сделал это с выражениями, которые не такие выразительные, скорее обходной путь, но он работает:
macro_rules! path {
($($path:expr),+) => {{
let mut s = [$($path),+].into_iter().flat_map(|p| [p, "/"]).collect::<String>();
s.pop();
s
}}
}
let result_url = path!("/api/v2", &data.event, &data.token);
Спасибо!
Я нашел возможный обходной путь, используя запятые и выражения, но это еще не то, что мне хотелось бы...
Вы также можете потребовать круглые скобки для нелитералов во входных данных макроса: path!("/api/v2" / (data.event) / (data.token))
На самом деле вам нужно будет сделать что-то подобное, поскольку вам придется анализировать data.event как выражение, за которым грамматически не может следовать /. Это потребуется для поддержки таких вещей, как data.token.to_uppercase().
Хорошая идея, спасибо @PeterHall! Но я все же думаю, что должен быть способ интерпретировать их как выражения только на самом последнем шаге, как в ТТ-мунчере...
Возможно, было бы проще использовать пользовательскую структуру, которая реализует Div<Rhs: Display> для добавления элементов пути, подобно pathlib Python.
Проблема в том, что произвольное выражение может содержать /, поэтому вам нужно устранить неоднозначность, если вы не хотите ограничиваться только цепочкой методов доступа к полям с . .
Для этого вы можете использовать tt muncher:
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)*)
};
}
Детская площадка
В настоящее время это производит вложенные вызовы format!. Чтобы избежать этого, вам, вероятно, также потребуется использовать аккумулятор. Меня это интересует, поэтому я работаю над версией с аккумулятором.
Редактировать: А вот версия аккумулятора
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: по вашему запросу другая версия, которая использует два аккумулятора для поддержки ведущего литерала
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, )*
)
};
}
Детская площадка
Вау, потрясающе @PitaJ!! Вложенные format! можно просто заменить на format_args! вызовы с format!("{}", path!()) на основном уровне. Я просто считаю, что мнемоника не всегда начинается с одной косой черты, но я попытался удалить это и не смог...
Интересно, что вам даже не нужно было приводить части к выражениям, чтобы получить их значения! ДО этого я не знал, что можно "собирать" идентификаторы с точками, и код будет работать, очень круто!!
@rsalmei Проверьте редактирование с накопительной версией
Просто потрясающе @PitaJ! Я должен изучить это досконально, потому что, признаюсь, я не могу понять это, хотя... И вишенка на торте, можно ли иметь path!("/api... вместо path!(/ "api...?
Должно быть возможно. Дай мне попробовать
Вероятно, сложнее следовать, но меньше правил, поэтому немного меньше рекурсии: play.rust-lang.org/…
ВАУ, это потрясающе @eggyal! Я также хотел бы тщательно изучить вашу версию, потому что я не могу ее понять.... Довольно круто!
Макросы Rust - это сам по себе язык, на самом деле огромный, и я рад, что здесь есть два мастера макросов!! Спасибо PitaJ и Эггьял.
@rsalmei см. редактирование с последней версией. Если он вас удовлетворит, я добавлю несколько комментариев, чтобы объяснить, что происходит.
Да! Это даже больше, чем я ожидал @PitaJ! Вы сделали так, чтобы он поддерживал ОБА ведущую косую черту и литерал.... Это действительно здорово, я очень благодарен!
О, @PitaJ, пожалуйста, связанное с этим сомнение: в другой части моего кода мне нужно включить суффиксы к этим путям, что-то вроде «?err=invalid_key». Я пытаюсь использовать макрос path! в другом макросе, который вызывает его только с одной частью: concat!("/failure", $suffix), но не получается с no rules expected the token concat!("/failure", "?err=invalid_key")... Могу ли я что-то сделать, чтобы сначала разрешить этот литерал?
Может быть полезно кинуть ссылку на неофициальную ссылку на tt munchers.
Спасибо @Jmb, но несколько часов назад я бросил эту точную ссылку в комментариях к вопросу.
@rsalmei, к сожалению, именно так работают макросы в Rust, но вы можете справиться с этим вручную, как показано здесь
На самом деле вот более простая версия, которая поддерживает все виды выражений в качестве заключительной части: площадка. Он не поддерживает конечные косые черты, но я думаю, что это не так уж и важно.
Можете ли вы привести еще несколько примеров использования с вводом и ожидаемым выводом?