Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling for trailing slashes. (#2154) #2172

Merged
merged 12 commits into from
Jan 11, 2024
3 changes: 2 additions & 1 deletion examples/ssr_modes/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ use thiserror::Error;
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();

view! {
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
<Title text="Welcome to Leptos"/>

<Router>
<Router fallback>
<main>
<Routes>
// We’ll load the home page with out-of-order streaming and <Suspense/>
Expand Down
3 changes: 2 additions & 1 deletion examples/ssr_modes_axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ use thiserror::Error;
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();

view! {
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
<Title text="Welcome to Leptos"/>

<Router>
<Router fallback>
<main>
<Routes>
// We’ll load the home page with out-of-order streaming and <Suspense/>
Expand Down
148 changes: 148 additions & 0 deletions integrations/actix/tests/extract_routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use leptos::*;
use leptos_actix::generate_route_list;
use leptos_router::{Route, Router, Routes, TrailingSlash};

#[component]
fn DefaultApp() -> impl IntoView {
let view = || view! { "" };
view! {
<Router>
<Routes>
<Route path="/foo" view/>
<Route path="/bar/" view/>
<Route path="/baz/:id" view/>
<Route path="/baz/:name/" view/>
<Route path="/baz/*any" view/>
</Routes>
</Router>
}
}

#[test]
fn test_default_app() {
let routes = generate_route_list(DefaultApp);

// We still have access to the original (albeit normalized) Leptos paths:
assert_same(
&routes,
|r| r.leptos_path(),
&["/bar", "/baz/*any", "/baz/:id", "/baz/:name", "/foo"],
);

// ... But leptos-actix has also reformatted "paths" to work for Actix.
assert_same(
&routes,
|r| r.path(),
&["/bar", "/baz/{id}", "/baz/{name}", "/baz/{tail:.*}", "/foo"],
);
}

#[component]
fn ExactApp() -> impl IntoView {
let view = || view! { "" };
let trailing_slash = TrailingSlash::Exact;
view! {
<Router trailing_slash>
<Routes>
<Route path="/foo" view/>
<Route path="/bar/" view/>
<Route path="/baz/:id" view/>
<Route path="/baz/:name/" view/>
<Route path="/baz/*any" view/>
</Routes>
</Router>
}
}

#[test]
fn test_exact_app() {
let routes = generate_route_list(ExactApp);

// In Exact mode, the Leptos paths no longer have their trailing slashes stripped:
assert_same(
&routes,
|r| r.leptos_path(),
&["/bar/", "/baz/*any", "/baz/:id", "/baz/:name/", "/foo"],
);

// Actix paths also have trailing slashes as a result:
assert_same(
&routes,
|r| r.path(),
&[
"/bar/",
"/baz/{id}",
"/baz/{name}/",
"/baz/{tail:.*}",
"/foo",
],
);
}

#[component]
fn RedirectApp() -> impl IntoView {
let view = || view! { "" };
let trailing_slash = TrailingSlash::Redirect;
view! {
<Router trailing_slash>
<Routes>
<Route path="/foo" view/>
<Route path="/bar/" view/>
<Route path="/baz/:id" view/>
<Route path="/baz/:name/" view/>
<Route path="/baz/*any" view/>
</Routes>
</Router>
}
}

#[test]
fn test_redirect_app() {
let routes = generate_route_list(RedirectApp);

assert_same(
&routes,
|r| r.leptos_path(),
&[
"/bar",
"/bar/",
"/baz/*any",
"/baz/:id",
"/baz/:id/",
"/baz/:name",
"/baz/:name/",
"/foo",
"/foo/",
],
);

// ... But leptos-actix has also reformatted "paths" to work for Actix.
assert_same(
&routes,
|r| r.path(),
&[
"/bar",
"/bar/",
"/baz/{id}",
"/baz/{id}/",
"/baz/{name}",
"/baz/{name}/",
"/baz/{tail:.*}",
"/foo",
"/foo/",
],
);
}

fn assert_same<'t, T, F, U>(
input: &'t Vec<T>,
mapper: F,
expected_sorted_values: &[U],
) where
F: Fn(&'t T) -> U + 't,
U: Ord + std::fmt::Debug,
{
let mut values: Vec<U> = input.iter().map(mapper).collect();
values.sort();
assert_eq!(values, expected_sorted_values);
}
39 changes: 32 additions & 7 deletions router/src/components/route.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
ParamsMap, RouterContext, SsrMode, StaticData, StaticMode, StaticParamsMap,
TrailingSlash,
};
use leptos::{leptos_dom::Transparent, *};
use std::{
Expand All @@ -17,6 +18,16 @@ thread_local! {
static ROUTE_ID: Cell<usize> = Cell::new(0);
}

// RouteDefinition.id is `pub` and required to be unique.
// Should we make this public so users can generate unique IDs?
pub(in crate::components) fn new_route_id() -> usize {
ROUTE_ID.with(|id| {
let next = id.get() + 1;
id.set(next);
next
})
}

/// Represents an HTTP method that can be handled by this route.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum Method {
Expand Down Expand Up @@ -65,6 +76,11 @@ pub fn Route<E, F, P>(
/// accessed with [`use_route_data`](crate::use_route_data).
#[prop(optional, into)]
data: Option<Loader>,
/// How this route should handle trailing slashes in its path.
/// Overrides any setting applied to [`crate::components::Router`].
/// Serves as a default for any inner Routes.
#[prop(optional)]
trailing_slash: Option<TrailingSlash>,
/// `children` may be empty or include nested routes.
#[prop(optional)]
children: Option<Children>,
Expand All @@ -83,6 +99,7 @@ where
data,
None,
None,
trailing_slash,
)
}

Expand Down Expand Up @@ -115,6 +132,11 @@ pub fn ProtectedRoute<P, E, F, C>(
/// accessed with [`use_route_data`](crate::use_route_data).
#[prop(optional, into)]
data: Option<Loader>,
/// How this route should handle trailing slashes in its path.
/// Overrides any setting applied to [`crate::components::Router`].
/// Serves as a default for any inner Routes.
#[prop(optional)]
trailing_slash: Option<TrailingSlash>,
/// `children` may be empty or include nested routes.
#[prop(optional)]
children: Option<Children>,
Expand Down Expand Up @@ -143,6 +165,7 @@ where
data,
None,
None,
trailing_slash,
)
}

Expand Down Expand Up @@ -171,6 +194,11 @@ pub fn StaticRoute<E, F, P, S>(
/// accessed with [`use_route_data`](crate::use_route_data).
#[prop(optional, into)]
data: Option<Loader>,
/// How this route should handle trailing slashes in its path.
/// Overrides any setting applied to [`crate::components::Router`].
/// Serves as a default for any inner Routes.
#[prop(optional)]
trailing_slash: Option<TrailingSlash>,
/// `children` may be empty or include nested routes.
#[prop(optional)]
children: Option<Children>,
Expand All @@ -193,6 +221,7 @@ where
data,
Some(mode),
Some(Arc::new(static_params)),
trailing_slash,
)
}

Expand All @@ -210,6 +239,7 @@ pub(crate) fn define_route(
data: Option<Loader>,
static_mode: Option<StaticMode>,
static_params: Option<StaticData>,
trailing_slash: Option<TrailingSlash>,
) -> RouteDefinition {
let children = children
.map(|children| {
Expand All @@ -226,14 +256,8 @@ pub(crate) fn define_route(
})
.unwrap_or_default();

let id = ROUTE_ID.with(|id| {
let next = id.get() + 1;
id.set(next);
next
});

RouteDefinition {
id,
id: new_route_id(),
path,
children,
view,
Expand All @@ -242,6 +266,7 @@ pub(crate) fn define_route(
data,
static_mode,
static_params,
trailing_slash,
}
}

Expand Down
Loading