Skip to content

Commit

Permalink
feat: add support for trailing slashes (closes #2154) (#2217)
Browse files Browse the repository at this point in the history
  • Loading branch information
skirsdeda authored Feb 27, 2024
1 parent c16189f commit aa97700
Show file tree
Hide file tree
Showing 13 changed files with 799 additions and 55 deletions.
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> = const { 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

0 comments on commit aa97700

Please sign in to comment.