Router: Non-Closure Redirects
Allows support for a few non-closure redirect types:
enum Route {
...
// Directly using route variants with static fields.
#[redirect("/hi", Route::Hi {})]
#[redirect("/thecoolestperson", Route::User { name: "Doge" })]
// Paths to functions
#[redirect("/hi2", Route::default)]
#[redirect("/coolroute", Route::the_coolest_route)]
// This is unchanged
#[redirect("/hi3", || Route::Hi {})]
#[redirect("/somealias/:id", |id: u32|, Route::Blog { id })]
...
}
Closes #2591
I'm worried that allowing only redirects with no dynamic paths to be extracted into a separate function will be confusing. If this works:
#[redirect("/:id", |id: usize| Route::Home)]
And this works:
#[redirect("/static", static_redirect)]
Then it seems like this should work:
#[redirect("/:id", id_redirect)]
fn id_redirect(id: usize) -> Route {
Route::Home
}
It currently doesn't because we rely on the names matching between the dynamic segments and the arguments in the redirect function. Functions don't have named arguments so we can't allow extracting the function directly with the current syntax. However, if we just call the function with arguments parsed in order from the route, we can extract any number of dynamic segments. This would be a breaking change, but I feel like most redirect closures will end up accepting arguments in the order the dynamic segments are defined anyway, so it seems fine?
We could use a trait to extract the argument types from the anonymous closures:
use std::str::FromStr;
fn main() {
// #[redirect("/:route", |_: i32| Route::Home)] could expand to something like this:
{
let route = "/1234";
let mut segments = route.split('/');
let redirect = |_: i32| Route::Home;
if let Ok(parsed) = parse_nth_argument::<1, _, _>(redirect, segments.next().unwrap()) {
move_to_route(redirect(parsed));
}
}
// #[redirect("/:id/:name", my_redirect_function)] could expand to something like this:
{
fn my_redirect_function(_: i32, _: String) -> Route {
Route::Home
}
let route = "/1234/Bob";
let mut segments = route.split('/');
let redirect = my_redirect_function;
if let Ok(__arg_1) = parse_nth_argument::<1, _, _>(redirect, segments.next().unwrap()) {
if let Ok(__arg_2) = parse_nth_argument::<2, _, _>(redirect, segments.next().unwrap()) {
move_to_route(redirect(__arg_1, __arg_2));
}
}
}
}
trait FunctionArguments<const N: usize, Marker = ()> {
type Argument;
}
macro_rules! impl_function_arguments {
($n:literal, $marker:ident, [$($number:literal),+], [$($variable:ident),+]) => {
#[doc = concat!("A marker type for functions with ", $n, " arguments")]
pub struct $marker;
impl_function_arguments!(inner, $n, $marker, [$($number),+], [$($variable),+], [$($variable),+]);
};
(inner, $n:literal, $marker:ident, [], [], [$($variable_again:ident),*]) => {};
(inner, $n:literal, $marker:ident, [$first_number:literal $(, $number:literal)*], [$first_variable:ident $(, $variable:ident)*], [$($variable_again:ident),*]) => {
impl<Function: Fn($($variable_again,)*) -> R, R, $($variable_again,)*> FunctionArguments<$first_number, ($marker, $($variable_again,)*)> for Function {
type Argument = $first_variable;
}
impl_function_arguments!(inner, $n, $marker, [$($number),*], [$($variable),*], [$($variable_again),*]);
};
}
impl_function_arguments!(1, OneArgument, [1], [A]);
impl_function_arguments!(2, TwoArguments, [1, 2], [A, B]);
impl_function_arguments!(3, ThreeArguments, [1, 2, 3], [A, B, C]);
impl_function_arguments!(4, FourArguments, [1, 2, 3, 4], [A, B, C, D]);
impl_function_arguments!(5, FiveArguments, [1, 2, 3, 4, 5], [A, B, C, D, E]);
impl_function_arguments!(6, SixArguments, [1, 2, 3, 4, 5, 6], [A, B, C, D, E, F]);
impl_function_arguments!(
7,
SevenArguments,
[1, 2, 3, 4, 5, 6, 7],
[A, B, C, D, E, F, G]
);
// Parse an arbitrary argument of a function. Checked at compile time.
fn parse_nth_argument<const N: usize, M, F>(_: F, text: &str) -> Result<F::Argument, <F::Argument as FromStr>::Err>
where
F: FunctionArguments<N, M>,
F::Argument: FromStr,
{
text.parse()
}
enum Route {
Home,
About
}
fn move_to_route(route: Route) {}
I think the actual bug in the original issue is that we're not passing attributes to items in our macros, so the clippy lint exclusion isn't passing. Since this isn't as simple of a change as we were hoping, I'm just going to close this.