remove old router files

This commit is contained in:
Greg Johnston 2024-06-14 16:19:41 -04:00
parent a29ffc8dcb
commit 3382047857
44 changed files with 0 additions and 7958 deletions

View File

@ -1,80 +0,0 @@
[package]
name = "leptos_router"
version = "0.7.0-preview2"
edition = "2021"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Router for the Leptos web framework."
rust-version.workspace = true
[dependencies]
leptos = { workspace = true }
leptos_integration_utils = { workspace = true, optional = true }
leptos_meta = { workspace = true, optional = true }
cached = { version = "0.45.0", optional = true }
cfg-if = "1"
gloo-net = { version = "0.5", features = ["http"] }
lazy_static = "1"
linear-map = { version = "1", features = ["serde_impl"] }
once_cell = "1.18"
regex = { version = "1", optional = true }
url = { version = "2", optional = true }
percent-encoding = "2"
thiserror = "1"
serde_qs = "0.12"
serde = "1"
tracing = "0.1"
js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
lru = { version = "0.11", optional = true }
serde_json = "1.0.96"
itertools = "0.12.0"
send_wrapper = "0.6.0"
[dependencies.web-sys]
version = "0.3"
features = [
# History/Routing
"History",
"HtmlAnchorElement",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
]
[features]
default = []
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = [
"leptos/ssr",
"dep:cached",
"dep:lru",
"dep:url",
"dep:regex",
"dep:leptos_integration_utils",
"dep:leptos_meta",
]
nightly = ["leptos/nightly"]
[package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature
denylist = ["url", "regex", "nightly"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@ -1 +0,0 @@
extend = { path = "../cargo-make/main.toml" }

View File

@ -1,96 +0,0 @@
/// Configures what animation should be shown when transitioning
/// between two root routes. Defaults to `None`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Animation {
/// Class set when a route is first painted.
pub start: Option<&'static str>,
/// Class set when a route is fading out.
pub outro: Option<&'static str>,
/// Class set when a route is fading in.
pub intro: Option<&'static str>,
/// Class set when a route is fading out, if its a “back” navigation.
pub outro_back: Option<&'static str>,
/// Class set when a route is fading in, if its a “back” navigation.
pub intro_back: Option<&'static str>,
/// Class set when all animations have finished.
pub finally: Option<&'static str>,
}
impl Animation {
pub(crate) fn next_state(
&self,
current: &AnimationState,
is_back: bool,
) -> (AnimationState, bool) {
let Animation {
start,
outro,
intro,
intro_back,
..
} = self;
match current {
AnimationState::Outro => {
let next = if start.is_some() {
AnimationState::Start
} else if intro.is_some() {
AnimationState::Intro
} else {
AnimationState::Finally
};
(next, true)
}
AnimationState::OutroBack => {
let next = if start.is_some() {
AnimationState::Start
} else if intro_back.is_some() {
AnimationState::IntroBack
} else if intro.is_some() {
AnimationState::Intro
} else {
AnimationState::Finally
};
(next, true)
}
AnimationState::Start => {
let next = if intro.is_some() {
AnimationState::Intro
} else {
AnimationState::Finally
};
(next, false)
}
AnimationState::Intro => (AnimationState::Finally, false),
AnimationState::IntroBack => (AnimationState::Finally, false),
AnimationState::Finally => {
if outro.is_some() {
if is_back {
(AnimationState::OutroBack, false)
} else {
(AnimationState::Outro, false)
}
} else if start.is_some() {
(AnimationState::Start, true)
} else if intro.is_some() {
if is_back {
(AnimationState::IntroBack, false)
} else {
(AnimationState::Intro, false)
}
} else {
(AnimationState::Finally, true)
}
}
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord)]
pub(crate) enum AnimationState {
Outro,
OutroBack,
Start,
Intro,
IntroBack,
Finally,
}

View File

@ -1,838 +0,0 @@
use crate::{
hooks::has_router, resolve_redirect_url, use_navigate, use_resolved_path,
NavigateOptions, ToHref, Url,
};
use leptos::{
html::form,
logging::*,
server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn},
*,
};
use serde::de::DeserializeOwned;
use std::{error::Error, fmt::Debug, rc::Rc};
use thiserror::Error;
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
use web_sys::{
Event, FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement,
RequestRedirect, SubmitEvent,
};
type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
type OnResponse = Rc<dyn Fn(&web_sys::Response)>;
type OnError = Rc<dyn Fn(&gloo_net::Error)>;
/// An HTML [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) progressively
/// enhanced to use client-side routing.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component]
pub fn Form<A>(
/// [`method`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method)
/// is the HTTP method to submit the form with (`get` or `post`).
#[prop(optional)]
method: Option<&'static str>,
/// [`action`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action)
/// is the URL that processes the form submission. Takes a [`String`], [`&str`], or a reactive
/// function that returns a [`String`].
action: A,
/// [`enctype`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype)
/// is the MIME type of the form submission if `method` is `post`.
#[prop(optional)]
enctype: Option<String>,
/// A signal that will be incremented whenever the form is submitted with `post`. This can useful
/// for reactively updating a [Resource] or another signal whenever the form has been submitted.
#[prop(optional)]
version: Option<RwSignal<usize>>,
/// A signal that will be set if the form submission ends in an error.
#[prop(optional)]
error: Option<RwSignal<Option<Box<dyn Error>>>>,
/// A callback will be called with the [`FormData`](web_sys::FormData) when the form is submitted.
#[prop(optional)]
on_form_data: Option<OnFormData>,
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<AttributeValue>,
/// A callback will be called with the [`Response`](web_sys::Response) the server sends in response
/// to a form submission.
#[prop(optional)]
on_response: Option<OnResponse>,
/// A callback will be called if the attempt to submit the form results in an error.
#[prop(optional)]
on_error: Option<OnError>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
#[prop(optional)]
node_ref: Option<NodeRef<html::Form>>,
/// Sets whether the page should be scrolled to the top when the form is submitted.
#[prop(optional)]
noscroll: bool,
/// Sets whether the page should replace the current location in the history when the form is submitted.
#[prop(optional)]
replace: bool,
/// Arbitrary attributes to add to the `<form>`. Attributes can be added with the
/// `attr:` syntax in the `view` macro.
#[prop(attrs)]
attributes: Vec<(&'static str, Attribute)>,
/// Component children; should include the HTML of the form elements.
children: Children,
) -> impl IntoView
where
A: ToHref + 'static,
{
async fn post_form_data(
action: &str,
form_data: FormData,
) -> Result<gloo_net::http::Response, gloo_net::Error> {
gloo_net::http::Request::post(action)
.header("Accept", "application/json")
.redirect(RequestRedirect::Follow)
.body(form_data)?
.send()
.await
}
async fn post_params(
action: &str,
enctype: &str,
params: web_sys::UrlSearchParams,
) -> Result<gloo_net::http::Response, gloo_net::Error> {
gloo_net::http::Request::post(action)
.header("Accept", "application/json")
.header("Content-Type", enctype)
.redirect(RequestRedirect::Follow)
.body(params)?
.send()
.await
}
fn inner(
has_router: bool,
method: Option<&'static str>,
action: Memo<Option<String>>,
enctype: Option<String>,
version: Option<RwSignal<usize>>,
error: Option<RwSignal<Option<Box<dyn Error>>>>,
on_form_data: Option<OnFormData>,
on_response: Option<OnResponse>,
on_error: Option<OnError>,
class: Option<Attribute>,
children: Children,
node_ref: Option<NodeRef<html::Form>>,
noscroll: bool,
replace: bool,
attributes: Vec<(&'static str, Attribute)>,
) -> HtmlElement<html::Form> {
let action_version = version;
let on_submit = {
move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}
let navigate = has_router.then(use_navigate);
let navigate_options = NavigateOptions {
scroll: !noscroll,
replace,
..Default::default()
};
let (form, method, action, enctype) =
extract_form_attributes(&ev);
let form_data =
web_sys::FormData::new_with_form(&form).unwrap_throw();
if let Some(on_form_data) = on_form_data.clone() {
on_form_data(&form_data);
}
let params =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(
&form_data,
)
.unwrap_throw();
let action = if has_router {
use_resolved_path(move || action.clone())
.get_untracked()
.unwrap_or_default()
} else {
action
};
// multipart POST (setting Context-Type breaks the request)
if method == "post" && enctype == "multipart/form-data" {
ev.prevent_default();
ev.stop_propagation();
let on_response = on_response.clone();
let on_error = on_error.clone();
spawn_local(async move {
let res = post_form_data(&action, form_data).await;
match res {
Err(e) => {
error!("<Form/> error while POSTing: {e:#?}");
if let Some(on_error) = on_error {
on_error(&e);
}
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
}
}
Ok(resp) => {
let resp = web_sys::Response::from(resp);
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.try_set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(&resp);
}
// Check all the logical 3xx responses that might
// get returned from a server function
if resp.redirected() {
let resp_url = &resp.url();
match Url::try_from(resp_url.as_str()) {
Ok(url) => {
if url.origin
!= current_window_origin()
|| navigate.is_none()
{
_ = window()
.location()
.set_href(
resp_url.as_str(),
);
} else {
#[allow(
clippy::unnecessary_unwrap
)]
let navigate =
navigate.unwrap();
navigate(
&format!(
"{}{}{}",
url.pathname,
if url.search.is_empty()
{
""
} else {
"?"
},
url.search,
),
navigate_options,
)
}
}
Err(e) => warn!("{}", e),
}
}
}
}
});
}
// POST
else if method == "post" {
ev.prevent_default();
ev.stop_propagation();
let on_response = on_response.clone();
let on_error = on_error.clone();
spawn_local(async move {
let res = post_params(&action, &enctype, params).await;
match res {
Err(e) => {
error!("<Form/> error while POSTing: {e:#?}");
if let Some(on_error) = on_error {
on_error(&e);
}
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
}
}
Ok(resp) => {
let resp = web_sys::Response::from(resp);
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.try_set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(&resp);
}
// Check all the logical 3xx responses that might
// get returned from a server function
if resp.redirected() {
let resp_url = &resp.url();
match Url::try_from(resp_url.as_str()) {
Ok(url) => {
if url.origin
!= current_window_origin()
|| navigate.is_none()
{
_ = window()
.location()
.set_href(
resp_url.as_str(),
);
} else {
#[allow(
clippy::unnecessary_unwrap
)]
let navigate =
navigate.unwrap();
navigate(
&format!(
"{}{}{}",
url.pathname,
if url.search.is_empty()
{
""
} else {
"?"
},
url.search,
),
navigate_options,
)
}
}
Err(e) => warn!("{}", e),
}
}
}
}
});
}
// otherwise, GET
else {
let params =
params.to_string().as_string().unwrap_or_default();
if let Some(navigate) = navigate {
navigate(
&format!("{action}?{params}"),
navigate_options,
);
} else {
_ = window()
.location()
.set_href(&format!("{action}?{params}"));
}
ev.prevent_default();
ev.stop_propagation();
}
}
};
let method = method.unwrap_or("get");
let mut form = form()
.attr("method", method)
.attr("action", move || action.get())
.attr("enctype", enctype)
.on(ev::submit, on_submit)
.attr("class", class)
.child(children());
if let Some(node_ref) = node_ref {
form = form.node_ref(node_ref)
};
for (attr_name, attr_value) in attributes {
form = form.attr(attr_name, attr_value);
}
form
}
let has_router = has_router();
let action = if has_router {
use_resolved_path(move || action.to_href()())
} else {
create_memo(move |_| Some(action.to_href()()))
};
let class = class.map(|bx| bx.into_attribute_boxed());
inner(
has_router,
method,
action,
enctype,
version,
error,
on_form_data,
on_response,
on_error,
class,
children,
node_ref,
noscroll,
replace,
attributes,
)
}
fn current_window_origin() -> String {
let location = window().location();
let protocol = location.protocol().unwrap_or_default();
let hostname = location.hostname().unwrap_or_default();
let port = location.port().unwrap_or_default();
format!(
"{}//{}{}{}",
protocol,
hostname,
if port.is_empty() { "" } else { ":" },
port
)
}
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
///
/// ## Encoding
/// **Note:** `<ActionForm/>` only works with server functions that use the
/// default `Url` encoding. This is to ensure that `<ActionForm/>` works correctly
/// both before and after WASM has loaded.
///
/// ## Complex Inputs
/// Server function arguments that are structs with nested serializable fields
/// should make use of indexing notation of `serde_qs`.
///
/// ```rust
/// # use leptos::*;
/// # use leptos_router::*;
///
/// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
/// struct HeftyData {
/// first_name: String,
/// last_name: String,
/// }
///
/// #[component]
/// fn ComplexInput() -> impl IntoView {
/// let submit = Action::<VeryImportantFn, _>::server();
///
/// view! {
/// <ActionForm action=submit>
/// <input type="text" name="hefty_arg[first_name]" value="leptos"/>
/// <input
/// type="text"
/// name="hefty_arg[last_name]"
/// value="closures-everywhere"
/// />
/// <input type="submit"/>
/// </ActionForm>
/// }
/// }
///
/// #[server]
/// async fn very_important_fn(
/// hefty_arg: HeftyData,
/// ) -> Result<(), ServerFnError> {
/// assert_eq!(hefty_arg.first_name.as_str(), "leptos");
/// assert_eq!(hefty_arg.last_name.as_str(), "closures-everywhere");
/// Ok(())
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component]
pub fn ActionForm<ServFn>(
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [`create_server_action`](leptos_server::create_server_action) or added
/// manually using [`using_server_fn`](leptos_server::Action::using_server_fn).
action: Action<
ServFn,
Result<ServFn::Output, ServerFnError<ServFn::Error>>,
>,
/// Sets the `id` attribute on the underlying `<form>` tag
#[prop(optional, into)]
id: Option<AttributeValue>,
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<AttributeValue>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
#[prop(optional)]
node_ref: Option<NodeRef<html::Form>>,
/// Arbitrary attributes to add to the `<form>`
#[prop(attrs, optional)]
attributes: Vec<(&'static str, Attribute)>,
/// Component children; should include the HTML of the form elements.
children: Children,
) -> impl IntoView
where
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
ServFn::Error,
>>::FormData: From<FormData>,
{
let has_router = has_router();
if !has_router {
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
if let Some(url) = resolve_redirect_url(loc) {
_ = window().location().set_href(&url.href());
}
});
}
let action_url = action.url().unwrap_or_else(|| {
debug_warn!(
"<ActionForm/> action needs a URL. Either use \
create_server_action() or Action::using_server_fn()."
);
String::new()
});
let version = action.version();
let value = action.value();
let class = class.map(|bx| bx.into_attribute_boxed());
let id = id.map(|bx| bx.into_attribute_boxed());
let on_submit = {
move |ev: SubmitEvent| {
if ev.default_prevented() {
return;
}
// <button formmethod="dialog"> should *not* dispatch the action, but should be allowed to
// just bubble up and close the <dialog> naturally
let is_dialog = ev
.submitter()
.and_then(|el| el.get_attribute("formmethod"))
.as_deref()
== Some("dialog");
if is_dialog {
return;
}
ev.prevent_default();
match ServFn::from_event(&ev) {
Ok(new_input) => {
action.dispatch(new_input);
}
Err(err) => {
error!(
"Error converting form field into server function \
arguments: {err:?}"
);
batch(move || {
value.set(Some(Err(ServerFnError::Serialization(
err.to_string(),
))));
version.update(|n| *n += 1);
});
}
}
}
};
let mut action_form = form()
.attr("action", action_url)
.attr("method", "post")
.attr("id", id)
.attr("class", class)
.on(ev::submit, on_submit)
.child(children());
if let Some(node_ref) = node_ref {
action_form = action_form.node_ref(node_ref)
};
for (attr_name, attr_value) in attributes {
action_form = action_form.attr(attr_name, attr_value);
}
action_form
}
/// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component]
pub fn MultiActionForm<ServFn>(
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [create_server_action](leptos_server::create_server_action) or added
/// manually using [leptos_server::Action::using_server_fn].
action: MultiAction<ServFn, Result<ServFn::Output, ServerFnError>>,
/// Sets the `id` attribute on the underlying `<form>` tag
#[prop(optional, into)]
id: Option<AttributeValue>,
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<AttributeValue>,
/// A signal that will be set if the form submission ends in an error.
#[prop(optional)]
error: Option<RwSignal<Option<Box<dyn Error>>>>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
#[prop(optional)]
node_ref: Option<NodeRef<html::Form>>,
/// Arbitrary attributes to add to the `<form>`
#[prop(attrs, optional)]
attributes: Vec<(&'static str, Attribute)>,
/// Component children; should include the HTML of the form elements.
children: Children,
) -> impl IntoView
where
ServFn:
Clone + DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
ServFn::Error,
>>::FormData: From<FormData>,
{
let has_router = has_router();
if !has_router {
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
if let Some(url) = resolve_redirect_url(loc) {
_ = window().location().set_href(&url.href());
}
});
}
let action_url = action.url().unwrap_or_else(|| {
debug_warn!(
"<MultiActionForm/> action needs a URL. Either use \
create_server_action() or Action::using_server_fn()."
);
String::new()
});
let on_submit = move |ev: SubmitEvent| {
if ev.default_prevented() {
return;
}
ev.prevent_default();
match ServFn::from_event(&ev) {
Err(e) => {
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
}
}
Ok(input) => {
action.dispatch(input);
if let Some(error) = error {
error.try_set(None);
}
}
}
};
let class = class.map(|bx| bx.into_attribute_boxed());
let id = id.map(|bx| bx.into_attribute_boxed());
let mut action_form = form()
.attr("action", action_url)
.attr("method", "post")
.attr("id", id)
.attr("class", class)
.on(ev::submit, on_submit)
.child(children());
if let Some(node_ref) = node_ref {
action_form = action_form.node_ref(node_ref)
};
for (attr_name, attr_value) in attributes {
action_form = action_form.attr(attr_name, attr_value);
}
action_form
}
fn form_data_from_event(
ev: &SubmitEvent,
) -> Result<FormData, FromFormDataError> {
let submitter = ev.submitter();
let mut submitter_name_value = None;
let opt_form = match &submitter {
Some(el) => {
if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
Some(form.clone())
} else if let Some(input) = el.dyn_ref::<HtmlInputElement>() {
submitter_name_value = Some((input.name(), input.value()));
Some(ev.target().unwrap().unchecked_into())
} else if let Some(button) = el.dyn_ref::<HtmlButtonElement>() {
submitter_name_value = Some((button.name(), button.value()));
Some(ev.target().unwrap().unchecked_into())
} else {
None
}
}
None => ev.target().map(|form| form.unchecked_into()),
};
match opt_form.as_ref().map(FormData::new_with_form) {
None => Err(FromFormDataError::MissingForm(ev.clone().into())),
Some(Err(e)) => Err(FromFormDataError::FormData(e)),
Some(Ok(form_data)) => {
if let Some((name, value)) = submitter_name_value {
form_data
.append_with_str(&name, &value)
.map_err(FromFormDataError::FormData)?;
}
Ok(form_data)
}
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
fn extract_form_attributes(
ev: &web_sys::Event,
) -> (web_sys::HtmlFormElement, String, String, String) {
let submitter = ev.unchecked_ref::<web_sys::SubmitEvent>().submitter();
match &submitter {
Some(el) => {
if let Some(form) = el.dyn_ref::<web_sys::HtmlFormElement>() {
(
form.clone(),
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string())
.to_lowercase(),
form.get_attribute("action")
.unwrap_or_default()
.to_lowercase(),
form.get_attribute("enctype")
.unwrap_or_else(|| {
"application/x-www-form-urlencoded".to_string()
})
.to_lowercase(),
)
} else if let Some(input) =
el.dyn_ref::<web_sys::HtmlInputElement>()
{
let form = ev
.target()
.unwrap()
.unchecked_into::<web_sys::HtmlFormElement>();
(
form.clone(),
input.get_attribute("method").unwrap_or_else(|| {
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string())
.to_lowercase()
}),
input.get_attribute("action").unwrap_or_else(|| {
form.get_attribute("action")
.unwrap_or_default()
.to_lowercase()
}),
input.get_attribute("enctype").unwrap_or_else(|| {
form.get_attribute("enctype")
.unwrap_or_else(|| {
"application/x-www-form-urlencoded".to_string()
})
.to_lowercase()
}),
)
} else if let Some(button) =
el.dyn_ref::<web_sys::HtmlButtonElement>()
{
let form = ev
.target()
.unwrap()
.unchecked_into::<web_sys::HtmlFormElement>();
(
form.clone(),
button.get_attribute("method").unwrap_or_else(|| {
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string())
.to_lowercase()
}),
button.get_attribute("action").unwrap_or_else(|| {
form.get_attribute("action")
.unwrap_or_default()
.to_lowercase()
}),
button.get_attribute("enctype").unwrap_or_else(|| {
form.get_attribute("enctype")
.unwrap_or_else(|| {
"application/x-www-form-urlencoded".to_string()
})
.to_lowercase()
}),
)
} else {
leptos_dom::debug_warn!(
"<Form/> cannot be submitted from a tag other than \
<form>, <input>, or <button>"
);
panic!()
}
}
None => match ev.target() {
None => {
leptos_dom::debug_warn!(
"<Form/> SubmitEvent fired without a target."
);
panic!()
}
Some(form) => {
let form = form.unchecked_into::<web_sys::HtmlFormElement>();
(
form.clone(),
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string()),
form.get_attribute("action").unwrap_or_default(),
form.get_attribute("enctype").unwrap_or_else(|| {
"application/x-www-form-urlencoded".to_string()
}),
)
}
},
}
}
/// Tries to deserialize a type from form data. This can be used for client-side
/// validation during form submission.
pub trait FromFormData
where
Self: Sized + serde::de::DeserializeOwned,
{
/// Tries to deserialize the data, given only the `submit` event.
fn from_event(ev: &web_sys::Event) -> Result<Self, FromFormDataError>;
/// Tries to deserialize the data, given the actual form data.
fn from_form_data(
form_data: &web_sys::FormData,
) -> Result<Self, serde_qs::Error>;
}
#[derive(Error, Debug)]
pub enum FromFormDataError {
#[error("Could not find <form> connected to event.")]
MissingForm(Event),
#[error("Could not create FormData from <form>: {0:?}")]
FormData(JsValue),
#[error("Deserialization error: {0:?}")]
Deserialization(serde_qs::Error),
}
impl<T> FromFormData for T
where
T: serde::de::DeserializeOwned,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
fn from_event(ev: &Event) -> Result<Self, FromFormDataError> {
let submit_ev = ev.unchecked_ref();
let form_data = form_data_from_event(submit_ev)?;
Self::from_form_data(&form_data)
.map_err(FromFormDataError::Deserialization)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
fn from_form_data(
form_data: &web_sys::FormData,
) -> Result<Self, serde_qs::Error> {
let data =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
.unwrap_throw();
let data = data.to_string().as_string().unwrap_or_default();
serde_qs::Config::new(5, false).deserialize_str::<Self>(&data)
}
}

View File

@ -1,255 +0,0 @@
use crate::{use_location, use_resolved_path, State};
use leptos::*;
use std::borrow::Cow;
/// Describes a value that is either a static or a reactive URL, i.e.,
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
pub trait ToHref {
/// Converts the (static or reactive) URL into a function that can be called to
/// return the URL.
fn to_href(&self) -> Box<dyn Fn() -> String + '_>;
}
impl ToHref for &str {
fn to_href(&self) -> Box<dyn Fn() -> String> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for String {
fn to_href(&self) -> Box<dyn Fn() -> String> {
let s = self.clone();
Box::new(move || s.clone())
}
}
impl ToHref for Cow<'_, str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl ToHref for Oco<'_, str> {
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
let s = self.to_string();
Box::new(move || s.clone())
}
}
impl<F> ToHref for F
where
F: Fn() -> String + 'static,
{
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
Box::new(self)
}
}
/// An HTML [`a`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
/// progressively enhanced to use client-side routing.
///
/// Client-side routing also works with ordinary HTML `<a>` tags, but `<A>` does two additional things:
/// 1) Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky.
/// For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative
/// route, but `<a href="1">` likely will not (depending on where it appears in your view.)
/// 2) Sets the `aria-current` attribute if this link is the active link (i.e., its a link to the page youre on).
/// This is helpful for accessibility and for styling. For example, maybe you want to set the link a
/// different color if its a link to the page youre currently on.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component]
pub fn A<H>(
/// Used to calculate the link's `href` attribute. Will be resolved relative
/// to the current route.
href: H,
/// Where to display the linked URL, as the name for a browsing context (a tab, window, or `<iframe>`).
#[prop(optional, into)]
target: Option<Oco<'static, str>>,
/// If `true`, the link is marked active when the location matches exactly;
/// if false, link is marked active if the current route starts with it.
#[prop(optional)]
exact: bool,
/// Provides a class to be added when the link is active. If provided, it will
/// be added at the same time that the `aria-current` attribute is set.
///
/// This supports multiple space-separated class names.
///
/// **Performance**: If its possible to style the link using the CSS with the
/// `[aria-current=page]` selector, you should prefer that, as it enables significant
/// SSR optimizations.
#[prop(optional, into)]
active_class: Option<Oco<'static, str>>,
/// An object of any type that will be pushed to router state
#[prop(optional)]
state: Option<State>,
/// If `true`, the link will not add to the browser's history (so, pressing `Back`
/// will skip this page.)
#[prop(optional)]
replace: bool,
/// Sets the `class` attribute on the underlying `<a>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<AttributeValue>,
/// Sets the `id` attribute on the underlying `<a>` tag, making it easier to target.
#[prop(optional, into)]
id: Option<Oco<'static, str>>,
/// Arbitrary attributes to add to the `<a>`. Attributes can be added with the
/// `attr:` syntax in the `view` macro.
#[prop(attrs)]
attributes: Vec<(&'static str, Attribute)>,
/// The nodes or elements to be shown inside the link.
children: Children,
) -> impl IntoView
where
H: ToHref + 'static,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
fn inner(
href: Memo<Option<String>>,
target: Option<Oco<'static, str>>,
exact: bool,
#[allow(unused)] state: Option<State>,
#[allow(unused)] replace: bool,
class: Option<AttributeValue>,
#[allow(unused)] active_class: Option<Oco<'static, str>>,
id: Option<Oco<'static, str>>,
#[allow(unused)] attributes: Vec<(&'static str, Attribute)>,
children: Children,
) -> View {
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
{
_ = state;
_ = replace;
}
let location = use_location();
let is_active = create_memo(move |_| {
href.with(|href| {
href.as_deref().is_some_and(|to| {
let path = to
.split(['?', '#'])
.next()
.unwrap_or_default()
.to_lowercase();
location.pathname.with(|loc| {
let loc = loc.to_lowercase();
if exact {
loc == path
} else {
std::iter::zip(loc.split('/'), path.split('/'))
.all(|(loc_p, path_p)| loc_p == path_p)
}
})
})
})
});
#[cfg(feature = "ssr")]
{
// if we have `active_class` or arbitrary attributes,
// the SSR optimization doesn't play nicely
// so we use the builder instead
let needs_builder =
active_class.is_some() || !attributes.is_empty();
if needs_builder {
let mut a = leptos::html::a()
.attr("href", move || href.get().unwrap_or_default())
.attr("target", target)
.attr("aria-current", move || {
if is_active.get() {
Some("page")
} else {
None
}
})
.attr(
"class",
class.map(|class| class.into_attribute_boxed()),
);
if let Some(active_class) = active_class {
for class_name in active_class.split_ascii_whitespace()
{
a = a.class(class_name.to_string(), move || {
is_active.get()
})
}
}
a = a.attr("id", id).child(children());
for (attr_name, attr_value) in attributes {
a = a.attr(attr_name, attr_value);
}
a
}
// but keep the nice SSR optimization in most cases
else {
view! {
<a
href=move || href.get().unwrap_or_default()
target=target
aria-current=move || if is_active.get() { Some("page") } else { None }
class=class
id=id
>
{children()}
</a>
}
}
.into_view()
}
// the non-SSR version doesn't need the SSR optimizations
// DRY here to avoid WASM binary size bloat
#[cfg(not(feature = "ssr"))]
{
let mut a = view! {
<a
href=move || href.get().unwrap_or_default()
target=target
prop:state=state.map(|s| s.to_js_value())
prop:replace=replace
aria-current=move || if is_active.get() { Some("page") } else { None }
class=class
id=id
>
{children()}
</a>
};
if let Some(active_class) = active_class {
for class_name in active_class.split_ascii_whitespace() {
a = a.class(class_name.to_string(), move || is_active.get())
}
}
for (attr_name, attr_value) in attributes {
a = a.attr(attr_name, attr_value);
}
a.into_view()
}
}
let href = use_resolved_path(move || href.to_href()());
inner(
href,
target,
exact,
state,
replace,
class,
active_class,
id,
attributes,
children,
)
}

View File

@ -1,19 +0,0 @@
mod form;
mod link;
mod outlet;
mod progress;
mod redirect;
mod route;
mod router;
mod routes;
mod static_render;
pub use form::*;
pub use link::*;
pub use outlet::*;
pub use progress::*;
pub use redirect::*;
pub use route::*;
pub use router::*;
pub use routes::*;
pub use static_render::*;

View File

@ -1,402 +0,0 @@
use crate::{
animation::{Animation, AnimationState},
use_is_back_navigation, use_location, use_route, RouteContext,
SetIsRouting,
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{cell::Cell, rc::Rc};
use web_sys::AnimationEvent;
/// Displays the child route nested in a parent route, allowing you to control exactly where
/// that child route is displayed. Renders nothing if there is no nested child.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component]
pub fn Outlet() -> impl IntoView {
_ = HydrationCtx::next_outlet();
let id = HydrationCtx::id();
let route = use_route();
let route_states = expect_context::<Memo<crate::RouterState>>();
let child_id = create_memo({
let route = route.clone();
move |_| {
route_states.track();
route.child().map(|child| child.id())
}
});
let is_showing = Rc::new(Cell::new(None::<usize>));
let (outlet, set_outlet) = create_signal(None::<View>);
let build_outlet = as_child_of_current_owner(|child: RouteContext| {
provide_context(child.clone());
child.outlet().into_view()
});
create_isomorphic_effect(move |prev_disposer| {
child_id.track();
match (route.child(), &is_showing.get()) {
(None, _) => {
set_outlet.set(None);
// previous disposer will be dropped, and therefore disposed
None
}
(Some(child), Some(is_showing_val))
if child.id() == *is_showing_val =>
{
// do nothing: we don't need to rerender the component, because it's the same
// returning the disposer keeps it alive until the next iteration
prev_disposer.flatten()
}
(Some(child), _) => {
drop(prev_disposer);
is_showing.set(Some(child.id()));
let (outlet, disposer) = build_outlet(child);
set_outlet.set(Some(outlet));
// returning the disposer keeps it alive until the next iteration
Some(disposer)
}
}
});
let outlet: Signal<Option<View>> =
if cfg!(any(feature = "csr", feature = "hydrate"))
&& use_context::<SetIsRouting>().is_some()
{
let global_suspense = expect_context::<GlobalSuspenseContext>();
let (current_view, set_current_view) = create_signal(None);
create_render_effect({
move |prev| {
let outlet = outlet.get();
let is_fallback =
!global_suspense.with_inner(|c| c.ready().get());
if prev.is_none() {
set_current_view.set(outlet);
} else if !is_fallback {
queue_microtask({
let global_suspense = global_suspense.clone();
move || {
let is_fallback = untrack(move || {
!global_suspense
.with_inner(|c| c.ready().get())
});
if !is_fallback {
set_current_view.set(outlet);
}
}
});
}
}
});
current_view.into()
} else {
outlet.into()
};
leptos::leptos_dom::DynChild::new_with_id(id, move || outlet.get())
}
/// Displays the child route nested in a parent route, allowing you to control exactly where
/// that child route is displayed. Renders nothing if there is no nested child.
///
/// ## Animations
/// The router uses CSS classes for animations, and transitions to the next specified class in order when
/// the `animationend` event fires. Each property takes a `&'static str` that can contain a class or classes
/// to be added at certain points. These CSS classes must have associated animations.
/// - `outro`: added when route is being unmounted
/// - `start`: added when route is first created
/// - `intro`: added after `start` has completed (if defined), and the route is being mounted
/// - `finally`: added after the `intro` animation is complete
///
/// Each of these properties is optional, and the router will transition to the next correct state
/// whenever an `animationend` event fires.
#[component]
pub fn AnimatedOutlet(
/// Base classes to be applied to the `<div>` wrapping the outlet during any animation state.
#[prop(optional, into)]
class: Option<TextProp>,
/// CSS class added when route is being unmounted
#[prop(optional)]
outro: Option<&'static str>,
/// CSS class added when route is being unmounted, in a “back” navigation
#[prop(optional)]
outro_back: Option<&'static str>,
/// CSS class added when route is first created
#[prop(optional)]
start: Option<&'static str>,
/// CSS class added while the route is being mounted
#[prop(optional)]
intro: Option<&'static str>,
/// CSS class added while the route is being mounted, in a “back” navigation
#[prop(optional)]
intro_back: Option<&'static str>,
/// CSS class added after other animations have completed.
#[prop(optional)]
finally: Option<&'static str>,
) -> impl IntoView {
let pathname = use_location().pathname;
let route = use_route();
let is_showing = Rc::new(Cell::new(None::<usize>));
let (outlet, set_outlet) = create_signal(None::<View>);
let build_outlet = as_child_of_current_owner(|child: RouteContext| {
provide_context(child.clone());
child.outlet().into_view()
});
let animation = Animation {
outro,
start,
intro,
finally,
outro_back,
intro_back,
};
let (animation_state, set_animation_state) =
create_signal(AnimationState::Finally);
let trigger_animation = create_rw_signal(());
let is_back = use_is_back_navigation();
let animation_and_outlet = create_memo({
move |prev: Option<&(AnimationState, View)>| {
let animation_state = animation_state.get();
let next_outlet = outlet.get().unwrap_or_default();
trigger_animation.track();
match prev {
None => (animation_state, next_outlet),
Some((prev_state, prev_outlet)) => {
let (next_state, can_advance) = animation
.next_state(prev_state, is_back.get_untracked());
if can_advance {
(next_state, next_outlet)
} else {
(next_state, prev_outlet.to_owned())
}
}
}
}
});
let current_animation = create_memo(move |_| animation_and_outlet.get().0);
let current_outlet = create_memo(move |_| animation_and_outlet.get().1);
create_isomorphic_effect(move |prev_disposer| {
pathname.track();
match (route.child(), &is_showing.get()) {
(None, _) => {
set_outlet.set(None);
// previous disposer will be dropped, and therefore disposed
None
}
(Some(child), Some(is_showing_val))
if child.id() == *is_showing_val =>
{
trigger_animation.set(());
// do nothing: we don't need to rerender the component, because it's the same
// returning the disposer keeps it alive until the next iteration
prev_disposer.flatten()
}
(Some(child), _) => {
trigger_animation.set(());
is_showing.set(Some(child.id()));
let (outlet, disposer) = build_outlet(child);
set_outlet.set(Some(outlet));
// returning the disposer keeps it alive until the next iteration
Some(disposer)
}
}
});
let class = move || {
let animation_class = match current_animation.get() {
AnimationState::Outro => outro.unwrap_or_default(),
AnimationState::Start => start.unwrap_or_default(),
AnimationState::Intro => intro.unwrap_or_default(),
AnimationState::Finally => finally.unwrap_or_default(),
AnimationState::OutroBack => outro_back.unwrap_or_default(),
AnimationState::IntroBack => intro_back.unwrap_or_default(),
};
if let Some(class) = &class {
format!("{} {animation_class}", class.get())
} else {
animation_class.to_string()
}
};
let node_ref = create_node_ref::<html::Div>();
let animationend = move |ev: AnimationEvent| {
use wasm_bindgen::JsCast;
if let Some(target) = ev.target() {
let node_ref = node_ref.get();
if node_ref.is_none()
|| target
.unchecked_ref::<web_sys::Node>()
.is_same_node(Some(&*node_ref.unwrap()))
{
ev.stop_propagation();
let current = current_animation.get();
set_animation_state.update(|current_state| {
let (next, _) =
animation.next_state(&current, is_back.get_untracked());
*current_state = next;
});
}
}
};
view! {
<div class=class on:animationend=animationend>
{move || current_outlet.get()}
</div>
}
}
/*
/// Displays the child route nested in a parent route, allowing you to control exactly where
/// that child route is displayed. Renders nothing if there is no nested child.
///
/// ## Animations
/// The router uses CSS classes for animations, and transitions to the next specified class in order when
/// the `animationend` event fires. Each property takes a `&'static str` that can contain a class or classes
/// to be added at certain points. These CSS classes must have associated animations.
/// - `outro`: added when route is being unmounted
/// - `start`: added when route is first created
/// - `intro`: added after `start` has completed (if defined), and the route is being mounted
/// - `finally`: added after the `intro` animation is complete
///
/// Each of these properties is optional, and the router will transition to the next correct state
/// whenever an `animationend` event fires.
#[component]
pub fn AnimatedOutlet(
/// Base classes to be applied to the `<div>` wrapping the outlet during any animation state.
#[prop(optional, into)]
class: Option<TextProp>,
/// CSS class added when route is being unmounted
#[prop(optional)]
outro: Option<&'static str>,
/// CSS class added when route is being unmounted, in a “back” navigation
#[prop(optional)]
outro_back: Option<&'static str>,
/// CSS class added when route is first created
#[prop(optional)]
start: Option<&'static str>,
/// CSS class added while the route is being mounted
#[prop(optional)]
intro: Option<&'static str>,
/// CSS class added while the route is being mounted, in a “back” navigation
#[prop(optional)]
intro_back: Option<&'static str>,
/// CSS class added after other animations have completed.
#[prop(optional)]
finally: Option<&'static str>,
) -> impl IntoView {
let route = use_route();
let is_showing = Rc::new(Cell::new(None::<usize>));
let (outlet, set_outlet) = create_signal(None::<View>);
let animation = Animation {
outro,
start,
intro,
finally,
outro_back,
intro_back,
};
let (animation_state, set_animation_state) =
create_signal(AnimationState::Finally);
let trigger_animation = create_rw_signal(());
let is_back = use_is_back_navigation();
let animation_and_outlet = create_memo({
move |prev: Option<&(AnimationState, View)>| {
let animation_state = animation_state.get();
let next_outlet = outlet.get().unwrap_or_default();
trigger_animation.track();
match prev {
None => (animation_state, next_outlet),
Some((prev_state, prev_outlet)) => {
let (next_state, can_advance) = animation
.next_state(prev_state, is_back.get_untracked());
if can_advance {
(next_state, next_outlet)
} else {
(next_state, prev_outlet.to_owned())
}
}
}
}
});
let current_animation = create_memo(move |_| animation_and_outlet.get().0);
let current_outlet = create_memo(move |_| animation_and_outlet.get().1);
create_isomorphic_effect(move |_| {
match (route.child(), &is_showing.get()) {
(None, prev) => {
/* if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
prev_scope.dispose();
} */
set_outlet.set(None);
}
(Some(child), Some(is_showing_val))
if child.id() == *is_showing_val =>
{
// do nothing: we don't need to rerender the component, because it's the same
trigger_animation.set(());
}
(Some(child), prev) => {
//provide_context(child_child.clone());
set_outlet
.set(Some(child.outlet().into_view()));
is_showing.set(Some(child.id()));
}
}
});
let class = move || {
let animation_class = match current_animation.get() {
AnimationState::Outro => outro.unwrap_or_default(),
AnimationState::Start => start.unwrap_or_default(),
AnimationState::Intro => intro.unwrap_or_default(),
AnimationState::Finally => finally.unwrap_or_default(),
AnimationState::OutroBack => outro_back.unwrap_or_default(),
AnimationState::IntroBack => intro_back.unwrap_or_default(),
};
if let Some(class) = &class {
format!("{} {animation_class}", class.get())
} else {
animation_class.to_string()
}
};
let node_ref = create_node_ref::<html::Div>();
let animationend = move |ev: AnimationEvent| {
use wasm_bindgen::JsCast;
if let Some(target) = ev.target() {
let node_ref = node_ref.get();
if node_ref.is_none()
|| target
.unchecked_ref::<web_sys::Node>()
.is_same_node(Some(&*node_ref.unwrap()))
{
ev.stop_propagation();
let current = current_animation.get();
set_animation_state.update(|current_state| {
let (next, _) =
animation.next_state(&current, is_back.get_untracked());
*current_state = next;
});
}
}
};
view! {
<div class=class on:animationend=animationend>
{move || current_outlet.get()}
</div>
}
}
*/

View File

@ -1,68 +0,0 @@
use leptos::{leptos_dom::helpers::IntervalHandle, *};
/// A visible indicator that the router is in the process of navigating
/// to another route.
///
/// This is used when `<Router set_is_routing>` has been provided, to
/// provide some visual indicator that the page is currently loading
/// async data, so that it is does not appear to have frozen. It can be
/// styled independently.
#[component]
pub fn RoutingProgress(
/// Whether the router is currently loading the new page.
#[prop(into)]
is_routing: Signal<bool>,
/// The maximum expected time for loading, which is used to
/// calibrate the animation process.
#[prop(optional, into)]
max_time: std::time::Duration,
/// The time to show the full progress bar after page has loaded, before hiding it. (Defaults to 100ms.)
#[prop(default = std::time::Duration::from_millis(250))]
before_hiding: std::time::Duration,
/// CSS classes to be applied to the `<progress>`.
#[prop(optional, into)]
class: String,
) -> impl IntoView {
const INCREMENT_EVERY_MS: f32 = 5.0;
let expected_increments =
max_time.as_secs_f32() / (INCREMENT_EVERY_MS / 1000.0);
let percent_per_increment = 100.0 / expected_increments;
let (is_showing, set_is_showing) = create_signal(false);
let (progress, set_progress) = create_signal(0.0);
create_render_effect(move |prev: Option<Option<IntervalHandle>>| {
if is_routing.get() && !is_showing.get() {
set_is_showing.set(true);
set_interval_with_handle(
move || {
set_progress.update(|n| *n += percent_per_increment);
},
std::time::Duration::from_millis(INCREMENT_EVERY_MS as u64),
)
.ok()
} else if is_routing.get() && is_showing.get() {
set_progress.set(0.0);
prev?
} else {
set_progress.set(100.0);
set_timeout(
move || {
set_progress.set(0.0);
set_is_showing.set(false);
},
before_hiding,
);
if let Some(Some(interval)) = prev {
interval.clear();
}
None
}
});
view! {
<Show when=move || is_showing.get() fallback=|| ()>
<progress class=class.clone() min="0" max="100" value=move || progress.get()/>
</Show>
}
}

View File

@ -1,87 +0,0 @@
use crate::{use_navigate, use_resolved_path, NavigateOptions};
use leptos::{
component, provide_context, signal_prelude::*, use_context, IntoView,
};
use std::rc::Rc;
/// Redirects the user to a new URL, whether on the client side or on the server
/// side. If rendered on the server, this sets a `302` status code and sets a `Location`
/// header. If rendered in the browser, it uses client-side navigation to redirect.
/// In either case, it resolves the route relative to the current route. (To use
/// an absolute path, prefix it with `/`).
///
/// **Note**: Support for server-side redirects is provided by the server framework
/// integrations ([`leptos_actix`] and [`leptos_axum`]. If youre not using one of those
/// integrations, you should manually provide a way of redirecting on the server
/// using [`provide_server_redirect`].
///
/// [`leptos_actix`]: <https://docs.rs/leptos_actix/>
/// [`leptos_axum`]: <https://docs.rs/leptos_axum/>
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component]
pub fn Redirect<P>(
/// The relative path to which the user should be redirected.
path: P,
/// Navigation options to be used on the client side.
#[prop(optional)]
#[allow(unused)]
options: Option<NavigateOptions>,
) -> impl IntoView
where
P: core::fmt::Display + 'static,
{
// resolve relative path
let path = use_resolved_path(move || path.to_string());
let path = path.get_untracked().unwrap_or_else(|| "/".to_string());
// redirect on the server
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>() {
(redirect_fn.f)(&path);
}
// redirect on the client
else {
#[allow(unused)]
let navigate = use_navigate();
#[cfg(any(feature = "csr", feature = "hydrate"))]
navigate(&path, options.unwrap_or_default());
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
{
leptos::logging::debug_warn!(
"<Redirect/> is trying to redirect without \
`ServerRedirectFunction` being provided. (If youre getting \
this on initial server start-up, its okay to ignore. It \
just means that your root route is a redirect.)"
);
}
}
}
/// Wrapping type for a function provided as context to allow for
/// server-side redirects. See [`provide_server_redirect`]
/// and [`Redirect`].
#[derive(Clone)]
pub struct ServerRedirectFunction {
f: Rc<dyn Fn(&str)>,
}
impl core::fmt::Debug for ServerRedirectFunction {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ServerRedirectFunction").finish()
}
}
/// Provides a function that can be used to redirect the user to another
/// absolute path, on the server. This should set a `302` status code and an
/// appropriate `Location` header.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn provide_server_redirect(handler: impl Fn(&str) + 'static) {
provide_context(ServerRedirectFunction {
f: Rc::new(handler),
})
}

View File

@ -1,496 +0,0 @@
use crate::{
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
ParamsMap, RouterContext, SsrMode, StaticData, StaticMode, StaticParamsMap,
TrailingSlash,
};
use leptos::{leptos_dom::Transparent, *};
use std::{
any::Any,
borrow::Cow,
cell::{Cell, RefCell},
future::Future,
pin::Pin,
rc::Rc,
sync::Arc,
};
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 {
/// The [`GET`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) method
/// requests a representation of the specified resource.
#[default]
Get,
/// The [`POST`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) method
/// submits an entity to the specified resource, often causing a change in
/// state or side effects on the server.
Post,
/// The [`PUT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) method
/// replaces all current representations of the target resource with the request payload.
Put,
/// The [`DELETE`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE) method
/// deletes the specified resource.
Delete,
/// The [`PATCH`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) method
/// applies partial modifications to a resource.
Patch,
}
/// Describes a portion of the nested layout of the app, specifying the route it should match,
/// the element it should display, and data that should be loaded alongside the route.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component(transparent)]
pub fn Route<E, F, P>(
/// The path fragment that this route should match. This can be static (`users`),
/// include a parameter (`:id`) or an optional parameter (`:id?`), or match a
/// wildcard (`user/*any`).
path: P,
/// The view that should be shown when this route is matched. This can be any function
/// that returns a type that implements [`IntoView`] (like `|| view! { <p>"Show this"</p> })`
/// or `|| view! { <MyComponent/>` } or even, for a component with no props, `MyComponent`).
view: F,
/// The mode that this route prefers during server-side rendering. Defaults to out-of-order streaming.
#[prop(optional)]
ssr: SsrMode,
/// The HTTP methods that this route can handle (defaults to only `GET`).
#[prop(default = &[Method::Get])]
methods: &'static [Method],
/// A data-loading function that will be called when the route is matched. Its results can be
/// 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>,
) -> impl IntoView
where
E: IntoView,
F: Fn() -> E + 'static,
P: core::fmt::Display,
{
define_route(
children,
path.to_string(),
Rc::new(move || view().into_view()),
ssr,
methods,
data,
None,
None,
trailing_slash,
)
}
/// Describes a route that is guarded by a certain condition. This works the same way as
/// [`<Route/>`](Route), except that if the `condition` function evaluates to `false`, it
/// redirects to `redirect_path` instead of displaying its `view`.
///
/// ## Reactive or Asynchronous Conditions
///
/// Note that the condition check happens once, at the time of navigation to the page. It
/// is not reactive (i.e., it will not cause the user to navigate away from the page if the
/// condition changes to `false`), which means it does not work well with asynchronous conditions.
/// If you need to protect a route conditionally or via `Suspense`, you should used nested routing
/// and wrap the condition around the `<Outlet/>`.
///
/// ```rust
/// # use leptos::*; use leptos_router::*;
/// # if false {
/// let has_permission = move || true; // TODO!
///
/// view! {
/// <Routes>
/// // parent route
/// <Route path="/" view=move || {
/// view! {
/// // only show the outlet when `has_permission` is `true`, and hide it when it is `false`
/// <Show when=move || has_permission() fallback=|| "Access denied!">
/// <Outlet/>
/// </Show>
/// }
/// }>
/// // nested child route
/// <Route path="/" view=|| view! { <p>"Protected data" </p> }/>
/// </Route>
/// </Routes>
/// }
/// # ;}
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component(transparent)]
pub fn ProtectedRoute<P, E, F, C>(
/// The path fragment that this route should match. This can be static (`users`),
/// include a parameter (`:id`) or an optional parameter (`:id?`), or match a
/// wildcard (`user/*any`).
path: P,
/// The path that will be redirected to if the condition is `false`.
redirect_path: P,
/// Condition function that returns a boolean.
condition: C,
/// View that will be exposed if the condition is `true`.
view: F,
/// The mode that this route prefers during server-side rendering. Defaults to out-of-order streaming.
#[prop(optional)]
ssr: SsrMode,
/// The HTTP methods that this route can handle (defaults to only `GET`).
#[prop(default = &[Method::Get])]
methods: &'static [Method],
/// A data-loading function that will be called when the route is matched. Its results can be
/// 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>,
) -> impl IntoView
where
E: IntoView,
F: Fn() -> E + 'static,
P: core::fmt::Display + 'static,
C: Fn() -> bool + 'static,
{
use crate::Redirect;
let redirect_path = redirect_path.to_string();
define_route(
children,
path.to_string(),
Rc::new(move || {
if condition() {
view().into_view()
} else {
view! { <Redirect path=redirect_path.clone()/> }.into_view()
}
}),
ssr,
methods,
data,
None,
None,
trailing_slash,
)
}
/// Describes a portion of the nested layout of the app, specifying the route it should match,
/// the element it should display, and data that should be loaded alongside the route.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component(transparent)]
pub fn StaticRoute<E, F, P, S>(
/// The path fragment that this route should match. This can be static (`users`),
/// include a parameter (`:id`) or an optional parameter (`:id?`), or match a
/// wildcard (`user/*any`).
path: P,
/// The view that should be shown when this route is matched. This can be any function
/// that returns a type that implements [IntoView] (like `|| view! { <p>"Show this"</p> })`
/// or `|| view! { <MyComponent/>` } or even, for a component with no props, `MyComponent`).
view: F,
/// Creates a map of the params that should be built for a particular route.
static_params: S,
/// The static route mode
#[prop(optional)]
mode: StaticMode,
/// A data-loading function that will be called when the route is matched. Its results can be
/// 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>,
) -> impl IntoView
where
E: IntoView,
F: Fn() -> E + 'static,
P: core::fmt::Display,
S: Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap> + Send + Sync>>
+ Send
+ Sync
+ 'static,
{
define_route(
children,
path.to_string(),
Rc::new(move || view().into_view()),
SsrMode::default(),
&[Method::Get],
data,
Some(mode),
Some(Arc::new(static_params)),
trailing_slash,
)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn define_route(
children: Option<Children>,
path: String,
view: Rc<dyn Fn() -> View>,
ssr_mode: SsrMode,
methods: &'static [Method],
data: Option<Loader>,
static_mode: Option<StaticMode>,
static_params: Option<StaticData>,
trailing_slash: Option<TrailingSlash>,
) -> RouteDefinition {
let children = children
.map(|children| {
children()
.as_children()
.iter()
.filter_map(|child| {
child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>())
})
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
RouteDefinition {
id: new_route_id(),
path,
children,
view,
ssr_mode,
methods,
data,
static_mode,
static_params,
trailing_slash,
}
}
impl IntoView for RouteDefinition {
fn into_view(self) -> View {
Transparent::new(self).into_view()
}
}
/// Context type that contains information about the current, matched route.
#[derive(Debug, Clone, PartialEq)]
pub struct RouteContext {
pub(crate) inner: Rc<RouteContextInner>,
}
impl RouteContext {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub(crate) fn new(
router: &RouterContext,
child: impl Fn() -> Option<RouteContext> + 'static,
matcher: impl Fn() -> Option<RouteMatch> + 'static,
) -> Option<Self> {
let base = router.base();
let base = base.path();
let RouteMatch { path_match, route } = matcher()?;
let PathMatch { path, .. } = path_match;
let RouteDefinition {
view: element,
id,
data,
..
} = route.key;
let params = create_memo(move |_| {
matcher()
.map(|matched| matched.path_match.params)
.unwrap_or_default()
});
let inner = Rc::new(RouteContextInner {
id,
base_path: base,
child: Box::new(child),
path: create_rw_signal(path),
original_path: route.original_path.to_string(),
params,
outlet: Box::new(move || Some(element())),
data: RefCell::new(None),
});
if let Some(loader) = data {
let data = {
let inner = Rc::clone(&inner);
provide_context(RouteContext { inner });
(loader.data)()
};
*inner.data.borrow_mut() = Some(data);
}
Some(RouteContext { inner })
}
pub(crate) fn id(&self) -> usize {
self.inner.id
}
/// Returns the URL path of the current route,
/// including param values in their places.
///
/// e.g., this will return `/article/0` rather than `/article/:id`.
/// For the opposite behavior, see [`RouteContext::original_path`].
#[track_caller]
pub fn path(&self) -> String {
#[cfg(debug_assertions)]
let caller = std::panic::Location::caller();
self.inner.path.try_get_untracked().unwrap_or_else(|| {
leptos::logging::debug_warn!(
"at {caller}, you call `.path()` on a `<Route/>` that has \
already been disposed"
);
Default::default()
})
}
pub(crate) fn set_path(&self, path: String) {
self.inner.path.set(path);
}
/// Returns the original URL path of the current route,
/// with the param name rather than the matched parameter itself.
///
/// e.g., this will return `/article/:id` rather than `/article/0`
/// For the opposite behavior, see [`RouteContext::path`].
pub fn original_path(&self) -> &str {
&self.inner.original_path
}
/// A reactive wrapper for the route parameters that are currently matched.
pub fn params(&self) -> Memo<ParamsMap> {
self.inner.params
}
pub(crate) fn base(path: &str, fallback: Option<fn() -> View>) -> Self {
Self {
inner: Rc::new(RouteContextInner {
id: 0,
base_path: path.to_string(),
child: Box::new(|| None),
path: create_rw_signal(path.to_string()),
original_path: path.to_string(),
params: create_memo(|_| ParamsMap::new()),
outlet: Box::new(move || fallback.as_ref().map(move |f| f())),
data: Default::default(),
}),
}
}
/// Resolves a relative route, relative to the current route's path.
pub fn resolve_path(&self, to: &str) -> Option<String> {
resolve_path(
&self.inner.base_path,
to,
Some(&self.inner.path.get_untracked()),
)
.map(String::from)
}
pub(crate) fn resolve_path_tracked(&self, to: &str) -> Option<String> {
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.get()))
.map(Cow::into_owned)
}
/// The nested child route, if any.
pub fn child(&self) -> Option<RouteContext> {
(self.inner.child)()
}
/// The view associated with the current route.
pub fn outlet(&self) -> impl IntoView {
(self.inner.outlet)()
}
/// The http method used to navigate to this route. Defaults to [`Method::Get`] when unavailable like in client side routing
pub fn method(&self) -> Method {
use_context().unwrap_or_default()
}
}
pub(crate) struct RouteContextInner {
base_path: String,
pub(crate) id: usize,
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
pub(crate) path: RwSignal<String>,
pub(crate) original_path: String,
pub(crate) params: Memo<ParamsMap>,
pub(crate) outlet: Box<dyn Fn() -> Option<View>>,
pub(crate) data: RefCell<Option<Rc<dyn Any>>>,
}
impl PartialEq for RouteContextInner {
fn eq(&self, other: &Self) -> bool {
self.base_path == other.base_path
&& self.path == other.path
&& self.original_path == other.original_path
&& self.params == other.params
}
}
impl core::fmt::Debug for RouteContextInner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RouteContextInner")
.field("path", &self.path)
.field("ParamsMap", &self.params)
.finish()
}
}
#[derive(Clone)]
pub struct Loader {
pub(crate) data: Rc<dyn Fn() -> Rc<dyn Any>>,
}
impl<F, T> From<F> for Loader
where
F: Fn() -> T + 'static,
T: Any + Clone + 'static,
{
fn from(f: F) -> Self {
Self {
data: Rc::new(move || Rc::new(f())),
}
}
}

View File

@ -1,581 +0,0 @@
use crate::{
create_location, matching::resolve_path, resolve_redirect_url,
scroll_to_el, use_location, use_navigate, Branch, History, Location,
LocationChange, RouteContext, RouterIntegrationContext, State,
};
#[cfg(not(feature = "ssr"))]
use crate::{unescape, Url};
use cfg_if::cfg_if;
use leptos::{
server_fn::{
error::{ServerFnErrorSerde, ServerFnUrlError},
redirect::RedirectHook,
},
*,
};
use send_wrapper::SendWrapper;
use std::{cell::RefCell, rc::Rc};
use thiserror::Error;
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsCast;
use wasm_bindgen::UnwrapThrowExt;
/// Provides for client-side and server-side routing. This should usually be somewhere near
/// the root of the application.
#[component]
pub fn Router(
/// The base URL for the router. Defaults to `""`.
#[prop(optional)]
base: Option<&'static str>,
/// A fallback that should be shown if no route is matched.
#[prop(optional)]
fallback: Option<fn() -> View>,
/// A signal that will be set while the navigation process is underway.
#[prop(optional, into)]
set_is_routing: Option<SignalSetter<bool>>,
/// How trailing slashes should be handled in [`Route`] paths.
#[prop(optional)]
trailing_slash: TrailingSlash,
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [`Routes`](crate::Routes) component somewhere
/// to define and display [`Route`](crate::Route)s.
children: Children,
/// A unique identifier for this router, allowing you to mount multiple Leptos apps with
/// different routes from the same server.
#[prop(optional)]
id: usize,
) -> impl IntoView {
// create a new RouterContext and provide it to every component beneath the router
let router = RouterContext::new(id, base, fallback, trailing_slash);
provide_context(router);
provide_context(GlobalSuspenseContext::new());
if let Some(set_is_routing) = set_is_routing {
provide_context(SetIsRouting(set_is_routing));
}
// set server function redirect hook
let navigate = use_navigate();
let navigate = SendWrapper::new(navigate);
let router_hook = Box::new(move |loc: &str| {
let Some(url) = resolve_redirect_url(loc) else {
return; // resolve_redirect_url() already logs an error
};
let current_origin =
leptos_dom::helpers::location().origin().unwrap_throw();
if url.origin() == current_origin {
let navigate = navigate.clone();
// delay by a tick here, so that the Action updates *before* the redirect
request_animation_frame(move || {
navigate(&url.href(), Default::default());
});
// Use set_href() if the conditions for client-side navigation were not satisfied
} else if let Err(e) =
leptos_dom::helpers::location().set_href(&url.href())
{
leptos::logging::error!("Failed to redirect: {e:#?}");
}
}) as RedirectHook;
_ = server_fn::redirect::set_redirect_hook(router_hook);
// provide ServerFnUrlError if it exists
let location = use_location();
if let (Some(path), Some(err)) = location
.query
.with_untracked(|q| (q.get("__path").cloned(), q.get("__err").cloned()))
{
let err: ServerFnError = ServerFnErrorSerde::de(&err);
provide_context(Rc::new(ServerFnUrlError::new(path, err)))
}
children()
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct SetIsRouting(pub SignalSetter<bool>);
/// Context type that contains information about the current router state.
#[derive(Debug, Clone)]
pub struct RouterContext {
pub(crate) inner: Rc<RouterContextInner>,
}
pub(crate) struct RouterContextInner {
id: usize,
pub location: Location,
pub base: RouteContext,
trailing_slash: TrailingSlash,
pub possible_routes: RefCell<Option<Vec<Branch>>>,
#[allow(unused)] // used in CSR/hydrate
base_path: String,
history: Box<dyn History>,
reference: ReadSignal<String>,
set_reference: WriteSignal<String>,
referrers: Rc<RefCell<Vec<LocationChange>>>,
state: ReadSignal<State>,
set_state: WriteSignal<State>,
pub(crate) is_back: RwSignal<bool>,
pub(crate) path_stack: StoredValue<Vec<String>>,
}
impl core::fmt::Debug for RouterContextInner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RouterContextInner")
.field("location", &self.location)
.field("base", &self.base)
.field("reference", &self.reference)
.field("set_reference", &self.set_reference)
.field("referrers", &self.referrers)
.field("state", &self.state)
.field("set_state", &self.set_state)
.field("path_stack", &self.path_stack)
.finish()
}
}
impl RouterContext {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub(crate) fn new(
id: usize,
base: Option<&'static str>,
fallback: Option<fn() -> View>,
trailing_slash: TrailingSlash,
) -> Self {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
let history = use_context::<RouterIntegrationContext>()
.unwrap_or_else(|| RouterIntegrationContext(Rc::new(crate::BrowserIntegration {})));
} else {
let history = use_context::<RouterIntegrationContext>().unwrap_or_else(|| {
let msg = "No router integration found.\n\nIf you are using this in the browser, \
you should enable `features = [\"csr\"]` or `features = [\"hydrate\"] in your \
`leptos_router` import.\n\nIf you are using this on the server without a \
Leptos server integration, you must call provide_context::<RouterIntegrationContext>(...) \
somewhere above the <Router/>.";
leptos::logging::debug_warn!("{}", msg);
panic!("{}", msg);
});
}
};
// Any `History` type gives a way to get a reactive signal of the current location
// in the browser context, this is drawn from the `popstate` event
// different server adapters can provide different `History` implementations to allow server routing
let source = history.location();
// if initial route is empty, redirect to base path, if it exists
let base = base.unwrap_or_default();
let base_path = resolve_path("", base, None);
if let Some(base_path) = &base_path {
if source.with_untracked(|s| s.value.is_empty()) {
history.navigate(&LocationChange {
value: base_path.to_string(),
replace: true,
scroll: false,
state: State(None),
});
}
}
// the current URL
let (reference, set_reference) =
create_signal(source.with_untracked(|s| s.value.clone()));
// the current History.state
let (state, set_state) =
create_signal(source.with_untracked(|s| s.state.clone()));
// Each field of `location` reactively represents a different part of the current location
let location = create_location(reference, state);
let referrers: Rc<RefCell<Vec<LocationChange>>> =
Rc::new(RefCell::new(Vec::new()));
// Create base route with fallback element
let base_path = base_path.unwrap_or_default();
let base = RouteContext::base(&base_path, fallback);
// Every time the History gives us a new location,
// 1) start a transition
// 2) update the reference (URL)
// 3) update the state
// this will trigger the new route match below
create_render_effect(move |_| {
let LocationChange { value, state, .. } = source.get();
untrack(move || {
if value != reference.get() {
set_reference.update(move |r| *r = value);
set_state.update(move |s| *s = state);
}
});
});
let inner = Rc::new(RouterContextInner {
id,
base_path: base_path.into_owned(),
path_stack: store_value(vec![location.pathname.get_untracked()]),
location,
base,
trailing_slash,
history: Box::new(history),
reference,
set_reference,
referrers,
state,
set_state,
possible_routes: Default::default(),
is_back: create_rw_signal(false),
});
// handle all click events on anchor tags
#[cfg(not(feature = "ssr"))]
{
let click_event = leptos::window_event_listener_untyped("click", {
let inner = Rc::clone(&inner);
move |ev| inner.clone().handle_anchor_click(ev)
});
on_cleanup(move || click_event.remove());
}
Self { inner }
}
/// The current [`pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname).
pub fn pathname(&self) -> Memo<String> {
self.inner.location.pathname
}
/// The [`RouteContext`] of the base route.
pub fn base(&self) -> RouteContext {
self.inner.base.clone()
}
pub(crate) fn id(&self) -> usize {
self.inner.id
}
pub(crate) fn trailing_slash(&self) -> TrailingSlash {
self.inner.trailing_slash.clone()
}
/// A list of all possible routes this router can match.
pub fn possible_branches(&self) -> Vec<Branch> {
self.inner
.possible_routes
.borrow()
.clone()
.unwrap_or_default()
}
}
impl RouterContextInner {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub(crate) fn navigate_from_route(
self: Rc<Self>,
to: &str,
options: &NavigateOptions,
) -> Result<(), NavigationError> {
let this = Rc::clone(&self);
untrack(move || {
let resolved_to = if options.resolve {
this.base.resolve_path(to)
} else {
resolve_path("", to, None).map(String::from)
};
// reset count of pending resources at global level
if let Some(global) = use_context::<GlobalSuspenseContext>() {
global.reset();
}
match resolved_to {
None => Err(NavigationError::NotRoutable(to.to_string())),
Some(resolved_to) => {
if self.referrers.borrow().len() > 32 {
return Err(NavigationError::MaxRedirects);
}
if resolved_to != this.reference.get()
|| options.state != (this.state).get()
{
{
self.referrers.borrow_mut().push(LocationChange {
value: self.reference.get(),
replace: options.replace,
scroll: options.scroll,
state: self.state.get(),
});
}
let len = self.referrers.borrow().len();
let set_reference = self.set_reference;
let set_state = self.set_state;
let referrers = self.referrers.clone();
let this = Rc::clone(&self);
let resolved = resolved_to.to_string();
let state = options.state.clone();
set_reference.update(move |r| *r = resolved);
set_state.update({
let next_state = state.clone();
move |state| *state = next_state
});
let global_suspense =
use_context::<GlobalSuspenseContext>();
let path_stack = self.path_stack;
let is_navigating_back = self.is_back.get_untracked();
if !is_navigating_back {
path_stack.update_value(|stack| {
stack.push(resolved_to.clone())
});
}
let set_is_routing = use_context::<SetIsRouting>();
if let Some(set_is_routing) = set_is_routing {
set_is_routing.0.set(true);
}
spawn_local(async move {
if let Some(set_is_routing) = set_is_routing {
if let Some(global) = global_suspense {
global.with_inner(|s| s.to_future()).await;
}
set_is_routing.0.set(false);
}
if referrers.borrow().len() == len {
this.navigate_end(LocationChange {
value: resolved_to,
replace: false,
scroll: true,
state,
});
}
});
} else {
scroll_to_el(false);
}
Ok(())
}
}
})
}
pub(crate) fn navigate_end(self: Rc<Self>, mut next: LocationChange) {
let first = self.referrers.borrow().first().cloned();
if let Some(first) = first {
if next.value != first.value || next.state != first.state {
next.replace = first.replace;
next.scroll = first.scroll;
self.history.navigate(&next);
}
self.referrers.borrow_mut().clear();
}
}
#[cfg(not(feature = "ssr"))]
pub(crate) fn handle_anchor_click(self: Rc<Self>, ev: web_sys::Event) {
use wasm_bindgen::JsValue;
let ev = ev.unchecked_into::<web_sys::MouseEvent>();
if ev.default_prevented()
|| ev.button() != 0
|| ev.meta_key()
|| ev.alt_key()
|| ev.ctrl_key()
|| ev.shift_key()
{
return;
}
let composed_path = ev.composed_path();
let mut a: Option<web_sys::HtmlAnchorElement> = None;
for i in 0..composed_path.length() {
if let Ok(el) = composed_path
.get(i)
.dyn_into::<web_sys::HtmlAnchorElement>()
{
a = Some(el);
}
}
if let Some(a) = a {
let href = a.href();
let target = a.target();
// let browser handle this event if link has target,
// or if it doesn't have href or state
// TODO "state" is set as a prop, not an attribute
if !target.is_empty()
|| (href.is_empty() && !a.has_attribute("state"))
{
return;
}
let rel = a.get_attribute("rel").unwrap_or_default();
let mut rel = rel.split([' ', '\t']);
// let browser handle event if it has rel=external or download
if a.has_attribute("download") || rel.any(|p| p == "external") {
return;
}
let url = Url::try_from(href.as_str()).unwrap();
let path_name = crate::history::unescape_minimal(&url.pathname);
// let browser handle this event if it leaves our domain
// or our base path
if url.origin
!= leptos_dom::helpers::location().origin().unwrap_or_default()
|| (!self.base_path.is_empty()
&& !path_name.is_empty()
&& !path_name
.to_lowercase()
.starts_with(&self.base_path.to_lowercase()))
{
return;
}
let to = path_name
+ if url.search.is_empty() { "" } else { "?" }
+ &unescape(&url.search)
+ &unescape(&url.hash);
let state =
leptos_dom::helpers::get_property(a.unchecked_ref(), "state")
.ok()
.and_then(|value| {
if value == JsValue::UNDEFINED {
None
} else {
Some(value)
}
});
ev.prevent_default();
let replace =
leptos_dom::helpers::get_property(a.unchecked_ref(), "replace")
.ok()
.and_then(|value| value.as_bool())
.unwrap_or(false);
if let Err(e) = self.navigate_from_route(
&to,
&NavigateOptions {
resolve: false,
replace,
scroll: !a.has_attribute("noscroll"),
state: State(state),
},
) {
leptos::logging::error!("{e:#?}");
}
}
}
}
/// An error that occurs during navigation.
#[derive(Debug, Error)]
pub enum NavigationError {
/// The given path is not routable.
#[error("Path {0:?} is not routable")]
NotRoutable(String),
/// Too many redirects occurred during routing (prevents and infinite loop.)
#[error("Too many redirects")]
MaxRedirects,
}
/// Options that can be used to configure a navigation. Used with [use_navigate](crate::use_navigate).
#[derive(Clone, Debug)]
pub struct NavigateOptions {
/// Whether the URL being navigated to should be resolved relative to the current route.
pub resolve: bool,
/// If `true` the new location will replace the current route in the history stack, meaning
/// the "back" button will skip over the current route. (Defaults to `false`).
pub replace: bool,
/// If `true`, the router will scroll to the top of the window at the end of navigation.
/// Defaults to `true`.
pub scroll: bool,
/// [State](https://developer.mozilla.org/en-US/docs/Web/API/History/state) that should be pushed
/// onto the history stack during navigation.
pub state: State,
}
impl Default for NavigateOptions {
fn default() -> Self {
Self {
resolve: true,
replace: false,
scroll: true,
state: State(None),
}
}
}
/// Declares how you would like to handle trailing slashes in Route paths. This
/// can be set on [`Router`] and overridden in [`crate::components::Route`]
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub enum TrailingSlash {
/// This is the default behavior as of Leptos 0.5. Trailing slashes in your
/// `Route` path are stripped. i.e.: the following two route declarations
/// are equivalent:
/// * `<Route path="/foo">`
/// * `<Route path="/foo/">`
#[default]
Drop,
/// This mode will respect your path as it is written. Ex:
/// * If you specify `<Route path="/foo">`, then `/foo` matches, but
/// `/foo/` does not.
/// * If you specify `<Route path="/foo/">`, then `/foo/` matches, but
/// `/foo` does not.
Exact,
/// Like `Exact`, this mode respects your path as-written. But it will also
/// add redirects to the specified path if a user nagivates to a URL that is
/// off by only the trailing slash.
///
/// Given `<Route path="/foo">`
/// * Visiting `/foo` is valid.
/// * Visiting `/foo/` serves a redirect to `/foo`
///
/// Given `<Route path="/foo/">`
/// * Visiting `/foo` serves a redirect to `/foo/`
/// * Visiting `/foo/` is valid.
Redirect,
}
impl TrailingSlash {
/// Should we redirect requests that come in with the wrong (extra/missing) trailng slash?
pub(crate) fn should_redirect(&self) -> bool {
use TrailingSlash::*;
match self {
Redirect => true,
Drop | Exact => false,
}
}
pub(crate) fn normalize_route_path(&self, path: &mut String) {
if !self.should_drop() {
return;
}
while path.ends_with('/') {
path.pop();
}
}
fn should_drop(&self) -> bool {
use TrailingSlash::*;
match self {
Redirect | Exact => false,
Drop => true,
}
}
}

View File

@ -1,790 +0,0 @@
use crate::{
animation::*,
components::route::new_route_id,
matching::{
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
RouteDefinition, RouteMatch,
},
use_is_back_navigation, use_route, NavigateOptions, Redirect, RouteContext,
RouterContext, SetIsRouting, TrailingSlash,
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{
borrow::Cow,
cell::{Cell, RefCell},
cmp::Reverse,
collections::HashMap,
ops::IndexMut,
rc::Rc,
};
/// Contains route definitions and manages the actual routing process.
///
/// You should locate the `<Routes/>` component wherever on the page you want the routes to appear.
///
/// **Note:** Your application should only include one `<Routes/>` or `<AnimatedRoutes/>` component.
///
/// You should not conditionally render `<Routes/>` using another component like `<Show/>` or `<Suspense/>`.
///
/// ```rust
/// # use leptos::*;
/// # use leptos_router::*;
/// # if false {
/// // ❌ don't do this!
/// view! {
/// <Show when=|| 1 == 2 fallback=|| view! { <p>"Loading"</p> }>
/// <Routes>
/// <Route path="/" view=|| "Home"/>
/// </Routes>
/// </Show>
/// }
/// # ;}
/// ```
///
/// Instead, you can use nested routing to render your `<Routes/>` once, and conditionally render the router outlet:
///
/// ```rust
/// # use leptos::*;
/// # use leptos_router::*;
/// # if false {
/// // ✅ do this instead!
/// view! {
/// <Routes>
/// // parent route
/// <Route path="/" view=move || {
/// view! {
/// // only show the outlet if data have loaded
/// <Show when=|| 1 == 2 fallback=|| view! { <p>"Loading"</p> }>
/// <Outlet/>
/// </Show>
/// }
/// }>
/// // nested child route
/// <Route path="/" view=|| "Home"/>
/// </Route>
/// </Routes>
/// }
/// # ;}
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[component]
pub fn Routes(
/// Base path relative at which the routes are mounted.
#[prop(optional)]
base: Option<String>,
children: Children,
) -> impl IntoView {
let router = use_context::<RouterContext>()
.expect("<Routes/> component should be nested within a <Router/>.");
let router_id = router.id();
let base_route = router.base();
let base = base.unwrap_or_default();
Branches::initialize(&router, &base, children());
#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
Branches::with(router_id, &base, |branches| {
*context.0.borrow_mut() = branches.to_vec()
});
}
let next_route = router.pathname();
let current_route = next_route;
let root_equal = Rc::new(Cell::new(true));
let route_states =
route_states(router_id, base, &router, current_route, &root_equal);
provide_context(route_states);
let id = HydrationCtx::id();
let root_route =
as_child_of_current_owner(move |(base_route, root_equal)| {
root_route(base_route, route_states, root_equal)
});
let (root, dis) = root_route((base_route, root_equal));
on_cleanup(move || drop(dis));
leptos::leptos_dom::DynChild::new_with_id(id, move || root.get())
.into_view()
}
/// Contains route definitions and manages the actual routing process, with animated transitions
/// between routes.
///
/// You should locate the `<AnimatedRoutes/>` component wherever on the page you want the routes to appear.
///
/// ## Animations
/// The router uses CSS classes for animations, and transitions to the next specified class in order when
/// the `animationend` event fires. Each property takes a `&'static str` that can contain a class or classes
/// to be added at certain points. These CSS classes must have associated animations.
/// - `outro`: added when route is being unmounted
/// - `start`: added when route is first created
/// - `intro`: added after `start` has completed (if defined), and the route is being mounted
/// - `finally`: added after the `intro` animation is complete
///
/// Each of these properties is optional, and the router will transition to the next correct state
/// whenever an `animationend` event fires.
///
/// **Note:** Your application should only include one `<AnimatedRoutes/>` or `<Routes/>` component.
#[component]
pub fn AnimatedRoutes(
/// Base classes to be applied to the `<div>` wrapping the routes during any animation state.
#[prop(optional, into)]
class: Option<TextProp>,
/// Base path relative at which the routes are mounted.
#[prop(optional)]
base: Option<String>,
/// CSS class added when route is being unmounted
#[prop(optional)]
outro: Option<&'static str>,
/// CSS class added when route is being unmounted, in a “back” navigation
#[prop(optional)]
outro_back: Option<&'static str>,
/// CSS class added when route is first created
#[prop(optional)]
start: Option<&'static str>,
/// CSS class added while the route is being mounted
#[prop(optional)]
intro: Option<&'static str>,
/// CSS class added while the route is being mounted, in a “back” navigation
#[prop(optional)]
intro_back: Option<&'static str>,
/// CSS class added after other animations have completed.
#[prop(optional)]
finally: Option<&'static str>,
children: Children,
) -> impl IntoView {
let router = use_context::<RouterContext>()
.expect("<Routes/> component should be nested within a <Router/>.");
let router_id = router.id();
let base_route = router.base();
let base = base.unwrap_or_default();
Branches::initialize(&router, &base, children());
#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
Branches::with(router_id, &base, |branches| {
*context.0.borrow_mut() = branches.to_vec()
});
}
let animation = Animation {
outro,
start,
intro,
finally,
outro_back,
intro_back,
};
let is_back = use_is_back_navigation();
let (animation_state, set_animation_state) =
create_signal(AnimationState::Finally);
let next_route = router.pathname();
let is_complete = Rc::new(Cell::new(true));
let animation_and_route = create_memo({
let is_complete = Rc::clone(&is_complete);
let base = base.clone();
move |prev: Option<&(AnimationState, String)>| {
let animation_state = animation_state.get();
let next_route = next_route.get();
let prev_matches = prev
.map(|(_, r)| r)
.cloned()
.map(|location| get_route_matches(router_id, &base, location));
let matches =
get_route_matches(router_id, &base, next_route.clone());
let same_route = prev_matches
.and_then(|p| p.first().map(|r| r.route.key.clone()))
== matches.first().map(|r| r.route.key.clone());
if same_route {
(animation_state, next_route)
} else {
match prev {
None => (animation_state, next_route),
Some((prev_state, prev_route)) => {
let (next_state, can_advance) = animation
.next_state(prev_state, is_back.get_untracked());
if can_advance || !is_complete.get() {
(next_state, next_route)
} else {
(next_state, prev_route.to_owned())
}
}
}
}
}
});
let current_animation = create_memo(move |_| animation_and_route.get().0);
let current_route = create_memo(move |_| animation_and_route.get().1);
let root_equal = Rc::new(Cell::new(true));
let route_states =
route_states(router_id, base, &router, current_route, &root_equal);
let root = root_route(base_route, route_states, root_equal);
let node_ref = create_node_ref::<html::Div>();
html::div()
.node_ref(node_ref)
.attr("class", move || {
let animation_class = match current_animation.get() {
AnimationState::Outro => outro.unwrap_or_default(),
AnimationState::Start => start.unwrap_or_default(),
AnimationState::Intro => intro.unwrap_or_default(),
AnimationState::Finally => finally.unwrap_or_default(),
AnimationState::OutroBack => outro_back.unwrap_or_default(),
AnimationState::IntroBack => intro_back.unwrap_or_default(),
};
is_complete.set(animation_class == finally.unwrap_or_default());
if let Some(class) = &class {
format!("{} {animation_class}", class.get())
} else {
animation_class.to_string()
}
})
.on(leptos::ev::animationend, move |ev| {
use wasm_bindgen::JsCast;
if let Some(target) = ev.target() {
if target
.unchecked_ref::<web_sys::Node>()
.is_same_node(Some(&*node_ref.get().unwrap()))
{
let current = current_animation.get();
set_animation_state.update(|current_state| {
let (next, _) = animation
.next_state(&current, is_back.get_untracked());
*current_state = next;
})
}
}
})
.child(move || root.get())
.into_view()
}
pub(crate) struct Branches;
type BranchesCacheKey = (usize, Cow<'static, str>);
thread_local! {
static BRANCHES: RefCell<HashMap<BranchesCacheKey, Vec<Branch>>> = RefCell::new(HashMap::new());
}
impl Branches {
pub fn initialize(router: &RouterContext, base: &str, children: Fragment) {
BRANCHES.with(|branches| {
#[cfg(debug_assertions)]
{
if cfg!(any(feature = "csr", feature = "hydrate"))
&& !branches.borrow().is_empty()
{
leptos::logging::warn!(
"You should only render the <Routes/> component once \
in your app. Please see the docs at https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html."
);
}
}
let mut current = branches.borrow_mut();
if !current.contains_key(&(router.id(), Cow::from(base))) {
let mut branches = Vec::new();
let mut children = children
.as_children()
.iter()
.filter_map(|child| {
let def = child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>());
if def.is_none() {
leptos::logging::warn!(
"[NOTE] The <Routes/> component should \
include *only* <Route/>or <ProtectedRoute/> \
components, or some \
#[component(transparent)] that returns a \
RouteDefinition."
);
}
def
})
.cloned()
.collect::<Vec<_>>();
inherit_settings(&mut children, router);
create_branches(
&children,
base,
&mut Vec::new(),
&mut branches,
true,
base,
);
current.insert((router.id(), Cow::Owned(base.into())), branches);
}
})
}
pub fn with<T>(
router_id: usize,
base: &str,
cb: impl FnOnce(&[Branch]) -> T,
) -> T {
BRANCHES.with(|branches| {
let branches = branches.borrow();
let branches = branches.get(&(router_id, Cow::from(base))).expect(
"Branches::initialize() should be called before \
Branches::with()",
);
cb(branches)
})
}
}
// <Route>s may inherit settings from each other or <Router>.
// This mutates RouteDefinitions to propagate those settings.
fn inherit_settings(children: &mut [RouteDefinition], router: &RouterContext) {
struct InheritProps {
trailing_slash: Option<TrailingSlash>,
}
fn route_def_inherit(
children: &mut [RouteDefinition],
inherited: InheritProps,
) {
for child in children {
if child.trailing_slash.is_none() {
child.trailing_slash.clone_from(&inherited.trailing_slash);
}
route_def_inherit(
&mut child.children,
InheritProps {
trailing_slash: child.trailing_slash.clone(),
},
);
}
}
route_def_inherit(
children,
InheritProps {
trailing_slash: Some(router.trailing_slash()),
},
);
}
fn route_states(
router_id: usize,
base: String,
router: &RouterContext,
current_route: Memo<String>,
root_equal: &Rc<Cell<bool>>,
) -> Memo<RouterState> {
// whenever path changes, update matches
let matches = create_memo(move |_| {
get_route_matches(router_id, &base, current_route.get())
});
// iterate over the new matches, reusing old routes when they are the same
// and replacing them with new routes when they differ
let next: Rc<RefCell<Vec<RouteContext>>> = Default::default();
let router = Rc::clone(&router.inner);
let owner =
Owner::current().expect("<Routes/> created outside reactive system.");
create_memo({
let root_equal = Rc::clone(root_equal);
move |prev: Option<&RouterState>| {
root_equal.set(true);
next.borrow_mut().clear();
let next_matches = matches.get();
let prev_matches = prev.as_ref().map(|p| &p.matches);
let prev_routes = prev.as_ref().map(|p| &p.routes);
// are the new route matches the same as the previous route matches so far?
let mut equal = prev_matches
.map(|prev_matches| next_matches.len() == prev_matches.len())
.unwrap_or(false);
for i in 0..next_matches.len() {
let next = next.clone();
let prev_match = prev_matches.and_then(|p| p.get(i));
let next_match = next_matches.get(i).unwrap();
match (prev_routes, prev_match) {
(Some(prev), Some(prev_match))
if next_match.route.key == prev_match.route.key
&& next_match.route.id == prev_match.route.id =>
{
let prev_one = { prev.borrow()[i].clone() };
if next_match.path_match.path != prev_one.path() {
prev_one
.set_path(next_match.path_match.path.clone());
}
if i >= next.borrow().len() {
next.borrow_mut().push(prev_one);
} else {
*(next.borrow_mut().index_mut(i)) = prev_one;
}
}
_ => {
equal = false;
if i == 0 {
root_equal.set(false);
}
let next_ctx = with_owner(owner, {
let next = Rc::clone(&next);
let router = Rc::clone(&router);
move || {
RouteContext::new(
&RouterContext { inner: router },
move || {
if let Some(route_states) =
use_context::<Memo<RouterState>>()
{
route_states.with(|route_states| {
let routes = route_states
.routes
.borrow();
routes.get(i + 1).cloned()
})
} else {
next.borrow().get(i + 1).cloned()
}
},
move || matches.with(|m| m.get(i).cloned()),
)
}
});
if let Some(next_ctx) = next_ctx {
if next.borrow().len() > i + 1 {
next.borrow_mut()[i] = next_ctx;
} else {
next.borrow_mut().push(next_ctx);
}
}
}
}
}
if let Some(prev) = &prev {
if equal {
RouterState {
matches: next_matches.to_vec(),
routes: prev_routes.cloned().unwrap_or_default(),
root: prev.root.clone(),
}
} else {
let root = next.borrow().first().cloned();
RouterState {
matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root,
}
}
} else {
let root = next.borrow().first().cloned();
RouterState {
matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root,
}
}
}
})
}
fn root_route(
base_route: RouteContext,
route_states: Memo<RouterState>,
root_equal: Rc<Cell<bool>>,
) -> Signal<Option<View>> {
let root_disposer = RefCell::new(None);
let outlet = as_child_of_current_owner(|route: RouteContext| {
provide_context(route.clone());
route.outlet().into_view()
});
let root_view = create_memo({
let root_equal = Rc::clone(&root_equal);
move |prev| {
provide_context(route_states);
route_states.with(|state| {
if state.routes.borrow().is_empty() {
let (outlet, disposer) = outlet(base_route.clone());
drop(std::mem::replace(
&mut *root_disposer.borrow_mut(),
Some(disposer),
));
Some(outlet)
} else {
let root = state.routes.borrow();
let root = root.first();
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| {
drop(std::mem::take(
&mut *root_disposer.borrow_mut(),
));
let (outlet, disposer) = outlet((*route).clone());
*root_disposer.borrow_mut() = Some(disposer);
outlet
})
} else {
prev.cloned().unwrap()
}
}
})
}
});
if cfg!(any(feature = "csr", feature = "hydrate"))
&& use_context::<SetIsRouting>().is_some()
{
let global_suspense = expect_context::<GlobalSuspenseContext>();
let (current_view, set_current_view) = create_signal(None);
create_render_effect(move |prev| {
let root = root_view.get();
let is_fallback = !global_suspense.with_inner(|c| c.ready().get());
if prev.is_none() {
set_current_view.set(root);
} else if !is_fallback {
queue_microtask({
let global_suspense = global_suspense.clone();
move || {
let is_fallback = untrack(move || {
!global_suspense.with_inner(|c| c.ready().get())
});
if !is_fallback {
set_current_view.set(root);
}
}
});
}
});
current_view.into()
} else {
root_view.into()
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct RouterState {
matches: Vec<RouteMatch>,
routes: Rc<RefCell<Vec<RouteContext>>>,
root: Option<RouteContext>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RouteData {
// This ID is always the same as key.id. Deprecate?
pub id: usize,
pub key: RouteDefinition,
pub pattern: String,
pub original_path: String,
pub matcher: Matcher,
}
impl RouteData {
fn score(&self) -> i32 {
let (pattern, splat) = match self.pattern.split_once("/*") {
Some((p, s)) => (p, Some(s)),
None => (self.pattern.as_str(), None),
};
let segments = pattern
.split('/')
.filter(|n| !n.is_empty())
.collect::<Vec<_>>();
#[allow(clippy::bool_to_int_with_if)] // on the splat.is_none()
segments.iter().fold(
(segments.len() as i32) - if splat.is_none() { 0 } else { 1 },
|score, segment| {
score + if segment.starts_with(':') { 2 } else { 3 }
},
)
}
}
fn create_branches(
route_defs: &[RouteDefinition],
base: &str,
stack: &mut Vec<RouteData>,
branches: &mut Vec<Branch>,
static_valid: bool,
parents_path: &str,
) {
for def in route_defs {
let routes = create_routes(
def,
base,
static_valid && def.static_mode.is_some(),
parents_path,
);
for route in routes {
stack.push(route.clone());
if def.children.is_empty() {
let branch = create_branch(stack, branches.len());
branches.push(branch);
} else {
create_branches(
&def.children,
&route.pattern,
stack,
branches,
static_valid && route.key.static_mode.is_some(),
&format!("{}{}", parents_path, def.path),
);
}
stack.pop();
}
}
if stack.is_empty() {
branches.sort_by_key(|branch| Reverse(branch.score));
}
}
pub(crate) fn create_branch(routes: &[RouteData], index: usize) -> Branch {
Branch {
routes: routes.to_vec(),
score: routes.last().unwrap().score() * 10000 - (index as i32),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
fn create_routes(
route_def: &RouteDefinition,
base: &str,
static_valid: bool,
parents_path: &str,
) -> Vec<RouteData> {
let RouteDefinition { children, .. } = route_def;
let is_leaf = children.is_empty();
if is_leaf && route_def.static_mode.is_some() && !static_valid {
panic!(
"Static rendering is not valid for route '{}{}', all parent \
routes must also be statically renderable.",
parents_path, route_def.path
);
}
let trailing_slash = route_def
.trailing_slash
.clone()
.expect("trailng_slash should be set by this point");
let mut acc = Vec::new();
for original_path in expand_optionals(&route_def.path) {
let mut path = join_paths(base, &original_path).to_string();
trailing_slash.normalize_route_path(&mut path);
let pattern = if is_leaf {
path
} else if let Some((path, _splat)) = path.split_once("/*") {
path.to_string()
} else {
path
};
let route_data = RouteData {
key: route_def.clone(),
id: route_def.id,
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
pattern,
original_path: original_path.into_owned(),
};
if route_data.matcher.is_wildcard() {
// already handles trailing_slash
} else if let Some(redirect_route) = redirect_route_for(route_def) {
let pattern = &redirect_route.path;
let redirect_route_data = RouteData {
id: redirect_route.id,
matcher: Matcher::new_with_partial(pattern, !is_leaf),
pattern: pattern.to_owned(),
original_path: pattern.to_owned(),
key: redirect_route,
};
acc.push(redirect_route_data);
}
acc.push(route_data);
}
acc
}
/// A new route that redirects to `route` with the correct trailng slash.
fn redirect_route_for(route: &RouteDefinition) -> Option<RouteDefinition> {
if matches!(route.path.as_str(), "" | "/") {
// Root paths are an exception to the rule and are always equivalent:
return None;
}
let trailing_slash = route
.trailing_slash
.clone()
.expect("trailing_slash should be defined by now");
if !trailing_slash.should_redirect() {
return None;
}
// Are we creating a new route that adds or removes a slash?
let add_slash = route.path.ends_with('/');
let view = Rc::new(move || {
view! {
<FixTrailingSlash add_slash />
}
.into_view()
});
let new_pattern = if add_slash {
// If we need to add a slash, we need to match on the path w/o it:
route.path.trim_end_matches('/').to_string()
} else {
format!("{}/", route.path)
};
let new_route = RouteDefinition {
path: new_pattern,
children: vec![],
data: None,
methods: route.methods,
id: new_route_id(),
view,
ssr_mode: route.ssr_mode,
static_mode: route.static_mode,
static_params: None,
trailing_slash: None, // Shouldn't be needed/used from here on out
};
Some(new_route)
}
#[component]
fn FixTrailingSlash(add_slash: bool) -> impl IntoView {
let route = use_route();
let path = if add_slash {
format!("{}/", route.path())
} else {
route.path().trim_end_matches('/').to_string()
};
let options = NavigateOptions {
replace: true,
..Default::default()
};
view! {
<Redirect path options/>
}
}

View File

@ -1,426 +0,0 @@
#[cfg(feature = "ssr")]
use crate::{RouteListing, RouterIntegrationContext, ServerIntegration};
#[cfg(feature = "ssr")]
use leptos::{create_runtime, provide_context, IntoView, LeptosOptions};
#[cfg(feature = "ssr")]
use leptos_meta::MetaContext;
use linear_map::LinearMap;
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use std::path::Path;
use std::{
collections::HashMap,
fmt::Display,
future::Future,
hash::{Hash, Hasher},
path::PathBuf,
pin::Pin,
sync::Arc,
};
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct StaticParamsMap(pub LinearMap<String, Vec<String>>);
impl StaticParamsMap {
/// Create a new empty `StaticParamsMap`.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Insert a value into the map.
#[inline]
pub fn insert(&mut self, key: impl ToString, value: Vec<String>) {
self.0.insert(key.to_string(), value);
}
/// Get a value from the map.
#[inline]
pub fn get(&self, key: &str) -> Option<&Vec<String>> {
self.0.get(key)
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct StaticPath<'b, 'a: 'b> {
path: &'a str,
segments: Vec<StaticPathSegment<'a>>,
params: LinearMap<&'a str, &'b Vec<String>>,
}
#[doc(hidden)]
#[derive(Debug)]
enum StaticPathSegment<'a> {
Static(&'a str),
Param(&'a str),
Wildcard(&'a str),
}
impl<'b, 'a: 'b> StaticPath<'b, 'a> {
pub fn new(path: &'a str) -> StaticPath<'b, 'a> {
use StaticPathSegment::*;
Self {
path,
segments: path
.split('/')
.filter(|s| !s.is_empty())
.map(|s| match s.chars().next() {
Some(':') => Param(&s[1..]),
Some('*') => Wildcard(&s[1..]),
_ => Static(s),
})
.collect::<Vec<_>>(),
params: LinearMap::new(),
}
}
pub fn add_params(&mut self, params: &'b StaticParamsMap) {
use StaticPathSegment::*;
for segment in self.segments.iter() {
match segment {
Param(name) | Wildcard(name) => {
if let Some(value) = params.get(name) {
self.params.insert(name, value);
}
}
_ => {}
}
}
}
pub fn into_paths(self) -> Vec<ResolvedStaticPath> {
use StaticPathSegment::*;
let mut paths = vec![ResolvedStaticPath(String::new())];
for segment in self.segments {
match segment {
Static(s) => {
paths = paths
.into_iter()
.map(|p| ResolvedStaticPath(format!("{p}/{s}")))
.collect::<Vec<_>>();
}
Param(name) | Wildcard(name) => {
let mut new_paths = vec![];
for path in paths {
let Some(params) = self.params.get(name) else {
panic!(
"missing param {} for path: {}",
name, self.path
);
};
for val in params.iter() {
new_paths.push(ResolvedStaticPath(format!(
"{path}/{val}"
)));
}
}
paths = new_paths;
}
}
}
paths
}
pub fn parent(&self) -> Option<StaticPath<'b, 'a>> {
if self.path == "/" || self.path.is_empty() {
return None;
}
self.path
.rfind('/')
.map(|i| StaticPath::new(&self.path[..i]))
}
pub fn parents(&self) -> Vec<StaticPath<'b, 'a>> {
let mut parents = vec![];
let mut parent = self.parent();
while let Some(p) = parent {
parent = p.parent();
parents.push(p);
}
parents
}
pub fn path(&self) -> &'a str {
self.path
}
}
impl Hash for StaticPath<'_, '_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}
impl StaticPath<'_, '_> {}
#[doc(hidden)]
#[repr(transparent)]
pub struct ResolvedStaticPath(pub String);
impl Display for ResolvedStaticPath {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.0.fmt(f)
}
}
impl ResolvedStaticPath {
#[cfg(feature = "ssr")]
pub async fn build<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Clone,
additional_context: impl Fn() + 'static + Clone,
) -> String
where
IV: IntoView + 'static,
{
let url = format!("http://leptos{self}");
let app = {
let app_fn = app_fn.clone();
move || {
provide_context(RouterIntegrationContext::new(
ServerIntegration { path: url },
));
provide_context(MetaContext::new());
(app_fn)().into_view()
}
};
let (stream, runtime) = leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(app, move || "".into(), additional_context.clone());
leptos_integration_utils::build_async_response(stream, options, runtime)
.await
}
#[cfg(feature = "ssr")]
pub async fn write<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Clone,
additional_context: impl Fn() + 'static + Clone,
) -> Result<String, std::io::Error>
where
IV: IntoView + 'static,
{
let html = self.build(options, app_fn, additional_context).await;
let file_path = static_file_path(options, &self.0);
let path = Path::new(&file_path);
if let Some(path) = path.parent() {
std::fs::create_dir_all(path)?
}
std::fs::write(path, &html)?;
Ok(html)
}
}
#[cfg(feature = "ssr")]
pub async fn build_static_routes<IV>(
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Clone,
routes: &[RouteListing],
static_data_map: &StaticDataMap,
) -> Result<(), std::io::Error>
where
IV: IntoView + 'static,
{
build_static_routes_with_additional_context(
options,
app_fn,
|| {},
routes,
static_data_map,
)
.await
}
#[cfg(feature = "ssr")]
pub async fn build_static_routes_with_additional_context<IV>(
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Clone,
additional_context: impl Fn() + 'static + Clone,
routes: &[RouteListing],
static_data_map: &StaticDataMap,
) -> Result<(), std::io::Error>
where
IV: IntoView + 'static,
{
let mut static_data: HashMap<&str, StaticParamsMap> = HashMap::new();
let runtime = create_runtime();
additional_context();
for (key, value) in static_data_map {
match value {
Some(value) => static_data.insert(key, value.as_ref()().await),
None => static_data.insert(key, StaticParamsMap::default()),
};
}
runtime.dispose();
let static_routes = routes
.iter()
.filter(|route| route.static_mode().is_some())
.collect::<Vec<_>>();
// TODO: maybe make this concurrent in some capacity
for route in static_routes {
let mut path = StaticPath::new(route.leptos_path());
for p in path.parents().into_iter().rev() {
if let Some(data) = static_data.get(p.path()) {
path.add_params(data);
}
}
if let Some(data) = static_data.get(path.path()) {
path.add_params(data);
}
#[allow(clippy::print_stdout)]
for path in path.into_paths() {
println!("building static route: {path}");
path.write(options, app_fn.clone(), additional_context.clone())
.await?;
}
}
Ok(())
}
pub type StaticData = Arc<StaticDataFn>;
pub type StaticDataFn = dyn Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap> + Send + Sync>>
+ Send
+ Sync
+ 'static;
pub type StaticDataMap = HashMap<String, Option<StaticData>>;
/// The mode to use when rendering the route statically.
/// On mode `Upfront`, the route will be built with the server is started using the provided static
/// data. On mode `Incremental`, the route will be built on the first request to it and then cached
/// and returned statically for subsequent requests.
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum StaticMode {
#[default]
Upfront,
Incremental,
}
#[doc(hidden)]
pub enum StaticStatusCode {
Ok,
NotFound,
InternalServerError,
}
#[doc(hidden)]
pub enum StaticResponse {
ReturnResponse {
body: String,
status: StaticStatusCode,
content_type: Option<&'static str>,
},
RenderDynamic,
RenderNotFound,
WriteFile {
body: String,
path: PathBuf,
},
}
#[doc(hidden)]
#[inline(always)]
#[cfg(feature = "ssr")]
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
let trimmed_path = path.trim_start_matches('/');
let path = if trimmed_path.is_empty() {
"index"
} else {
trimmed_path
};
format!("{}/{}.html", options.site_root, path)
}
#[doc(hidden)]
#[inline(always)]
#[cfg(feature = "ssr")]
pub fn not_found_path(options: &LeptosOptions) -> String {
format!("{}{}.html", options.site_root, options.not_found_path)
}
#[doc(hidden)]
#[inline(always)]
pub fn upfront_static_route(
res: Result<String, std::io::Error>,
) -> StaticResponse {
match res {
Ok(body) => StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => StaticResponse::RenderNotFound,
_ => {
tracing::error!("error reading file: {}", e);
StaticResponse::ReturnResponse {
body: "Internal Server Error".into(),
status: StaticStatusCode::InternalServerError,
content_type: None,
}
}
},
}
}
#[doc(hidden)]
#[inline(always)]
pub fn not_found_page(res: Result<String, std::io::Error>) -> StaticResponse {
match res {
Ok(body) => StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::NotFound,
content_type: Some("text/html"),
},
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => StaticResponse::ReturnResponse {
body: "Not Found".into(),
status: StaticStatusCode::Ok,
content_type: None,
},
_ => {
tracing::error!("error reading not found file: {}", e);
StaticResponse::ReturnResponse {
body: "Internal Server Error".into(),
status: StaticStatusCode::InternalServerError,
content_type: None,
}
}
},
}
}
#[doc(hidden)]
pub fn incremental_static_route(
res: Result<String, std::io::Error>,
) -> StaticResponse {
match res {
Ok(body) => StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
Err(_) => StaticResponse::RenderDynamic,
}
}
#[doc(hidden)]
#[cfg(feature = "ssr")]
pub async fn render_dynamic<IV>(
path: &str,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
) -> StaticResponse
where
IV: IntoView + 'static,
{
let body = ResolvedStaticPath(path.into())
.build(options, app_fn, additional_context)
.await;
let path = Path::new(&static_file_path(options, path)).into();
StaticResponse::WriteFile { body, path }
}

View File

@ -1,204 +0,0 @@
mod test_extract_routes;
use crate::{
provide_server_redirect, Branch, Method, RouterIntegrationContext,
ServerIntegration, SsrMode, StaticDataMap, StaticMode, StaticParamsMap,
StaticPath,
};
use leptos::*;
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
rc::Rc,
};
/// Context to contain all possible routes.
#[derive(Clone, Default, Debug)]
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
/// A route that this application can serve.
pub struct RouteListing {
path: String,
leptos_path: String,
mode: SsrMode,
methods: HashSet<Method>,
static_mode: Option<StaticMode>,
}
impl RouteListing {
/// Create a route listing from its parts.
pub fn new(
path: impl ToString,
leptos_path: impl ToString,
mode: SsrMode,
methods: impl IntoIterator<Item = Method>,
static_mode: Option<StaticMode>,
) -> Self {
Self {
path: path.to_string(),
leptos_path: leptos_path.to_string(),
mode,
methods: methods.into_iter().collect(),
static_mode,
}
}
/// The path this route handles.
///
/// This should be formatted for whichever web server integegration is being used. (ex: leptos-actix.)
/// When returned from leptos-router, it matches `self.leptos_path()`.
pub fn path(&self) -> &str {
&self.path
}
/// The leptos-formatted path this route handles.
pub fn leptos_path(&self) -> &str {
&self.leptos_path
}
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode
}
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
/// is not marked as statically rendered. All route parameters to use when resolving all paths
/// to render should be passed in the `params` argument.
pub async fn build_static<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + Send + 'static + Clone,
additional_context: impl Fn() + Send + 'static + Clone,
params: &StaticParamsMap,
) -> Result<bool, std::io::Error>
where
IV: IntoView + 'static,
{
match self.static_mode {
None => Ok(false),
Some(_) => {
let mut path = StaticPath::new(&self.leptos_path);
path.add_params(params);
for path in path.into_paths() {
path.write(
options,
app_fn.clone(),
additional_context.clone(),
)
.await?;
}
Ok(true)
}
}
}
}
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
/// format. Odds are you want `generate_route_list()` from either the [`actix`] or [`axum`] integrations if you want
/// to work with their router.
///
/// [`actix`]: <https://docs.rs/actix/>
/// [`axum`]: <https://docs.rs/axum/>
pub fn generate_route_list_inner<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_inner_with_context(app_fn, || {})
}
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
/// format. Odds are you want `generate_route_list()` from either the [`actix`] or [`axum`] integrations if you want
/// to work with their router.
///
/// [`actix`]: <https://docs.rs/actix/>
/// [`axum`]: <https://docs.rs/axum/>
pub fn generate_route_list_inner_with_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
additional_context: impl Fn() + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let runtime = create_runtime();
let branches = get_branches(app_fn, additional_context);
let branches = branches.0.borrow();
let mut static_data_map: StaticDataMap = HashMap::new();
let routes = branches
.iter()
.flat_map(|branch| {
let mode = branch
.routes
.iter()
.map(|route| route.key.ssr_mode)
.max()
.unwrap_or_default();
let methods = branch
.routes
.iter()
.flat_map(|route| route.key.methods)
.copied()
.collect::<HashSet<_>>();
let route = branch
.routes
.last()
.map(|route| (route.key.static_mode, route.pattern.clone()));
for route in branch.routes.iter() {
static_data_map.insert(
route.pattern.to_string(),
route.key.static_params.clone(),
);
}
route.map(|(static_mode, path)| RouteListing {
leptos_path: path.clone(),
path,
mode,
methods: methods.clone(),
static_mode,
})
})
.collect::<Vec<_>>();
runtime.dispose();
(routes, static_data_map)
}
fn get_branches<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
additional_context: impl Fn() + 'static + Clone,
) -> PossibleBranchContext
where
IV: IntoView + 'static,
{
let integration = ServerIntegration {
path: "http://leptos.rs/".to_string(),
};
provide_context(RouterIntegrationContext::new(integration));
let branches = PossibleBranchContext::default();
provide_context(branches.clone());
// Suppress startup warning about using <Redirect/> without ServerRedirectFunction:
provide_server_redirect(|_str| ());
additional_context();
leptos::suppress_resource_load(true);
_ = app_fn().into_view();
leptos::suppress_resource_load(false);
branches
}

View File

@ -1,258 +0,0 @@
// This is here, vs /router/tests/, because it accesses some `pub(crate)`
// features to test crate internals that wouldn't be available there.
#![cfg(all(test, feature = "ssr"))]
use crate::*;
use itertools::Itertools;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
#[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="/name/:name/" view/>
<Route path="/any/*any" view/>
</Routes>
</Router>
}
}
#[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="/name/:name/" view/>
<Route path="/any/*any" view/>
</Routes>
</Router>
}
}
#[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="/name/:name/" view/>
<Route path="/any/*any" view/>
</Routes>
</Router>
}
}
#[test]
fn test_generated_routes_default() {
// By default, we use the behavior as of Leptos 0.5, which is equivalent to TrailingSlash::Drop.
assert_generated_paths(
DefaultApp,
&["/any/*any", "/bar", "/baz/:id", "/foo", "/name/:name"],
);
}
#[test]
fn test_generated_routes_exact() {
// Allow users to precisely define whether slashes are present:
assert_generated_paths(
ExactApp,
&["/any/*any", "/bar/", "/baz/:id", "/foo", "/name/:name/"],
);
}
#[test]
fn test_generated_routes_redirect() {
// TralingSlashes::Redirect generates paths to redirect to the path with the "correct" trailing slash ending (or lack thereof).
assert_generated_paths(
RedirectApp,
&[
"/any/*any",
"/bar",
"/bar/",
"/baz/:id",
"/baz/:id/",
"/foo",
"/foo/",
"/name/:name",
"/name/:name/",
],
)
}
#[test]
fn test_rendered_redirect() {
// Given an app that uses TrailngSlsahes::Redirect, rendering the redirected path
// should render the redirect. Other paths should not.
let expected_redirects = &[
("/bar", "/bar/"),
("/baz/some_id/", "/baz/some_id"),
("/name/some_name", "/name/some_name/"),
("/foo/", "/foo"),
];
let redirect_result = Rc::new(RefCell::new(Option::None));
let rc = redirect_result.clone();
let server_redirect = move |new_value: &str| {
rc.replace(Some(new_value.to_string()));
};
let _runtime = Disposable(create_runtime());
let history = TestHistory::new("/");
provide_context(RouterIntegrationContext::new(history.clone()));
provide_server_redirect(server_redirect);
// We expect these redirects to exist:
for (src, dest) in expected_redirects {
let loc = format!("https://example.com{src}");
history.goto(&loc);
redirect_result.replace(None);
RedirectApp().into_view().render_to_string();
let redirected_to = redirect_result.borrow().clone();
assert!(
redirected_to.is_some(),
"Should redirect from {src} to {dest}"
);
assert_eq!(redirected_to.unwrap(), *dest);
}
// But the destination paths shouldn't themselves redirect:
redirect_result.replace(None);
for (_src, dest) in expected_redirects {
let loc = format!("https://example.com{dest}");
history.goto(&loc);
RedirectApp().into_view().render_to_string();
let redirected_to = redirect_result.borrow().clone();
assert!(
redirected_to.is_none(),
"Destination of redirect shouldn't also redirect: {dest}"
);
}
}
struct Disposable(RuntimeId);
// If the test fails, and we don't dispose, we get irrelevant panics.
impl Drop for Disposable {
fn drop(&mut self) {
self.0.dispose()
}
}
#[derive(Clone)]
struct TestHistory {
loc: RwSignal<LocationChange>,
}
impl TestHistory {
fn new(initial: &str) -> Self {
let lc = LocationChange {
value: initial.to_owned(),
..Default::default()
};
Self {
loc: create_rw_signal(lc),
}
}
fn goto(&self, loc: &str) {
let change = LocationChange {
value: loc.to_string(),
..Default::default()
};
self.navigate(&change);
}
}
impl History for TestHistory {
fn location(&self) -> ReadSignal<LocationChange> {
self.loc.read_only()
}
fn navigate(&self, new_loc: &LocationChange) {
self.loc.update(|loc| loc.value.clone_from(&new_loc.value))
}
}
// WARNING!
//
// Despite generate_route_list_inner() using a new leptos_reactive::RuntimeID
// each time we call this function, somehow Routes are leaked between different
// apps. To avoid that, make sure to put each call in a separate #[test] method.
//
// TODO: Better isolation for different apps to avoid this issue?
fn assert_generated_paths<F, IV>(app: F, expected_sorted_paths: &[&str])
where
F: Clone + Fn() -> IV + 'static,
IV: IntoView + 'static,
{
let (routes, static_data) = generate_route_list_inner(app);
let mut paths = routes.iter().map(|route| route.path()).collect_vec();
paths.sort();
assert_eq!(paths, expected_sorted_paths);
let mut keys = static_data.keys().collect_vec();
keys.sort();
assert_eq!(paths, keys);
// integrations can update "path" to be valid for themselves, but
// when routes are returned by leptos_router, these are equal:
assert!(routes
.iter()
.all(|route| route.path() == route.leptos_path()));
}
#[test]
fn test_unique_route_ids() {
let branches = get_branches(RedirectApp);
assert!(!branches.is_empty());
assert!(branches
.iter()
.flat_map(|branch| &branch.routes)
.map(|route| route.id)
.all_unique());
}
#[test]
fn test_unique_route_patterns() {
let branches = get_branches(RedirectApp);
assert!(!branches.is_empty());
assert!(branches
.iter()
.flat_map(|branch| &branch.routes)
.map(|route| route.pattern.as_str())
.all_unique());
}
fn get_branches<F, IV>(app_fn: F) -> Vec<Branch>
where
F: Fn() -> IV + Clone + 'static,
IV: IntoView + 'static,
{
let runtime = create_runtime();
let additional_context = || ();
let branches = super::get_branches(app_fn, additional_context);
let branches = branches.0.borrow().clone();
runtime.dispose();
branches
}

View File

@ -1,75 +0,0 @@
use super::params::ParamsMap;
use crate::{State, Url};
use leptos::*;
/// Creates a reactive location from the given path and state.
pub fn create_location(
path: ReadSignal<String>,
state: ReadSignal<State>,
) -> Location {
let url = create_memo(move |prev: Option<&Url>| {
path.with(|path| match Url::try_from(path.as_str()) {
Ok(url) => url,
Err(e) => {
leptos::logging::error!(
"[Leptos Router] Invalid path {path}\n\n{e:?}"
);
prev.cloned().unwrap()
}
})
});
let pathname = create_memo(move |_| url.with(|url| url.pathname.clone()));
let search = create_memo(move |_| url.with(|url| url.search.clone()));
let hash = create_memo(move |_| url.with(|url| url.hash.clone()));
let query = create_memo(move |_| url.with(|url| url.search_params.clone()));
Location {
pathname,
search,
hash,
query,
state,
}
}
/// A reactive description of the current URL, containing equivalents to the local parts of
/// the browser's [`Location`](https://developer.mozilla.org/en-US/docs/Web/API/Location).
#[derive(Debug, Clone, PartialEq)]
pub struct Location {
/// The path of the URL, not containing the query string or hash fragment.
pub pathname: Memo<String>,
/// The raw query string.
pub search: Memo<String>,
/// The query string parsed into its key-value pairs.
pub query: Memo<ParamsMap>,
/// The hash fragment.
pub hash: Memo<String>,
/// The [`state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) at the top of the history stack.
pub state: ReadSignal<State>,
}
/// A description of a navigation.
#[derive(Debug, Clone, PartialEq)]
pub struct LocationChange {
/// The new URL.
pub value: String,
/// If true, the new location will replace the current one in the history stack, i.e.,
/// clicking the "back" button will not return to the current location.
pub replace: bool,
/// If true, the router will scroll to the top of the page at the end of the navigation.
pub scroll: bool,
/// The [`state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) that will be added during navigation.
pub state: State,
}
impl Default for LocationChange {
fn default() -> Self {
Self {
value: Default::default(),
replace: true,
scroll: true,
state: Default::default(),
}
}
}

View File

@ -1,221 +0,0 @@
use leptos::*;
use std::rc::Rc;
use wasm_bindgen::UnwrapThrowExt;
mod location;
mod params;
mod state;
mod url;
pub use self::url::*;
pub use location::*;
pub use params::*;
pub use state::*;
impl core::fmt::Debug for RouterIntegrationContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RouterIntegrationContext").finish()
}
}
/// The [`Router`](crate::Router) relies on a [`RouterIntegrationContext`], which tells the router
/// how to find things like the current URL, and how to navigate to a new page. The [`History`] trait
/// can be implemented on any type to provide this information.
pub trait History {
/// A signal that updates whenever the current location changes.
fn location(&self) -> ReadSignal<LocationChange>;
/// Called to navigate to a new location.
fn navigate(&self, loc: &LocationChange);
}
/// The default integration when you are running in the browser, which uses
/// the [`History API`](https://developer.mozilla.org/en-US/docs/Web/API/History).
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct BrowserIntegration {}
impl BrowserIntegration {
fn current() -> LocationChange {
let loc = leptos_dom::helpers::location();
let state = window()
.history()
.and_then(|h| h.state())
.ok()
.and_then(|s| (!s.is_null()).then_some(s));
LocationChange {
value: loc.pathname().unwrap_or_default()
+ loc.search().unwrap_or_default().as_str()
+ loc.hash().unwrap_or_default().as_str(),
replace: true,
scroll: true,
state: State(state),
}
}
}
impl History for BrowserIntegration {
fn location(&self) -> ReadSignal<LocationChange> {
use crate::{NavigateOptions, RouterContext};
let (location, set_location) = create_signal(Self::current());
leptos::window_event_listener_untyped("popstate", move |_| {
let router = use_context::<RouterContext>();
if let Some(router) = router {
let path_stack = router.inner.path_stack;
let is_back = router.inner.is_back;
let change = Self::current();
let is_navigating_back = path_stack.with_value(|stack| {
stack.len() == 1
|| (stack.len() >= 2
&& stack.get(stack.len() - 2)
== Some(&change.value))
});
if is_navigating_back {
path_stack.update_value(|stack| {
stack.pop();
});
}
is_back.set(is_navigating_back);
request_animation_frame(move || {
is_back.set(false);
});
if let Err(e) = router.inner.navigate_from_route(
&change.value,
&NavigateOptions {
resolve: false,
replace: change.replace,
scroll: change.scroll,
state: change.state,
},
) {
leptos::logging::error!("{e:#?}");
}
set_location.set(Self::current());
} else {
leptos::logging::warn!("RouterContext not found");
}
});
location
}
fn navigate(&self, loc: &LocationChange) {
let history = leptos_dom::window().history().unwrap_throw();
if loc.replace {
history
.replace_state_with_url(
&loc.state.to_js_value(),
"",
Some(&loc.value),
)
.unwrap_throw();
} else {
// push the "forward direction" marker
let state = &loc.state.to_js_value();
history
.push_state_with_url(state, "", Some(&loc.value))
.unwrap_throw();
}
// scroll to el
scroll_to_el(loc.scroll);
}
}
pub(crate) fn scroll_to_el(loc_scroll: bool) {
if let Ok(hash) = leptos_dom::helpers::location().hash() {
if !hash.is_empty() {
let hash = js_sys::decode_uri(&hash[1..])
.ok()
.and_then(|decoded| decoded.as_string())
.unwrap_or(hash);
let el = leptos_dom::document().get_element_by_id(&hash);
if let Some(el) = el {
el.scroll_into_view();
return;
}
}
}
// scroll to top
if loc_scroll {
leptos_dom::window().scroll_to_with_x_and_y(0.0, 0.0);
}
}
/// The wrapper type that the [`Router`](crate::Router) uses to interact with a [`History`].
/// This is automatically provided in the browser. For the server, it should be provided
/// as a context. Be sure that it can survive conversion to a URL in the browser.
///
/// ```
/// # use leptos_router::*;
/// # use leptos::*;
/// # let rt = create_runtime();
/// let integration = ServerIntegration {
/// path: "http://leptos.rs/".to_string(),
/// };
/// provide_context(RouterIntegrationContext::new(integration));
/// # rt.dispose();
/// ```
#[derive(Clone)]
pub struct RouterIntegrationContext(pub Rc<dyn History>);
impl RouterIntegrationContext {
/// Creates a new router integration.
pub fn new(history: impl History + 'static) -> Self {
Self(Rc::new(history))
}
}
impl History for RouterIntegrationContext {
fn location(&self) -> ReadSignal<LocationChange> {
self.0.location()
}
fn navigate(&self, loc: &LocationChange) {
self.0.navigate(loc)
}
}
/// A generic router integration for the server side.
///
/// This should match what the browser history will show.
///
/// Generally, this will already be provided if you are using the leptos
/// server integrations.
///
/// ```
/// # use leptos_router::*;
/// # use leptos::*;
/// # let rt = create_runtime();
/// let integration = ServerIntegration {
/// // Swap out with your URL if integrating manually.
/// path: "http://leptos.rs/".to_string(),
/// };
/// provide_context(RouterIntegrationContext::new(integration));
/// # rt.dispose();
/// ```
#[derive(Clone, Debug)]
pub struct ServerIntegration {
pub path: String,
}
impl History for ServerIntegration {
fn location(&self) -> ReadSignal<LocationChange> {
create_signal(LocationChange {
value: self.path.clone(),
replace: false,
scroll: true,
state: State(None),
})
.0
}
fn navigate(&self, _loc: &LocationChange) {}
}

View File

@ -1,196 +0,0 @@
use linear_map::LinearMap;
use serde::{Deserialize, Serialize};
use std::{str::FromStr, sync::Arc};
use thiserror::Error;
/// A key-value map of the current named route params and their values.
///
/// For now, implemented with a [`LinearMap`], as `n` is small enough
/// that O(n) iteration over a vectorized map is (*probably*) more space-
/// and time-efficient than hashing and using an actual `HashMap`
///
/// [`LinearMap`]: linear_map::LinearMap
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[repr(transparent)]
pub struct ParamsMap(pub LinearMap<String, String>);
impl ParamsMap {
/// Creates an empty map.
#[inline(always)]
pub fn new() -> Self {
Self(LinearMap::new())
}
/// Creates an empty map with the given capacity.
#[inline(always)]
pub fn with_capacity(capacity: usize) -> Self {
Self(LinearMap::with_capacity(capacity))
}
/// Inserts a value into the map.
#[inline(always)]
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
self.0.insert(key, value)
}
/// Gets a value from the map.
#[inline(always)]
pub fn get(&self, key: &str) -> Option<&String> {
self.0.get(key)
}
/// Removes a value from the map.
#[inline(always)]
pub fn remove(&mut self, key: &str) -> Option<String> {
self.0.remove(key)
}
/// Converts the map to a query string.
pub fn to_query_string(&self) -> String {
use crate::history::url::escape;
let mut buf = String::new();
if !self.0.is_empty() {
buf.push('?');
for (k, v) in &self.0 {
buf.push_str(&escape(k));
buf.push('=');
buf.push_str(&escape(v));
buf.push('&');
}
if buf.len() > 1 {
buf.pop();
}
}
buf
}
}
impl Default for ParamsMap {
#[inline(always)]
fn default() -> Self {
Self::new()
}
}
/// Create a [`ParamsMap`] in a declarative style.
///
/// ```
/// # use leptos_router::params_map;
/// # #[cfg(feature = "ssr")] {
/// let map = params_map! {
/// "crate" => "leptos",
/// 42 => true, // where key & val: core::fmt::Display
/// };
/// assert_eq!(map.get("crate"), Some(&"leptos".to_string()));
/// assert_eq!(map.get("42"), Some(&true.to_string()))
/// # }
/// ```
// Original implementation included the below credits.
//
// Adapted from hash_map! in common_macros crate
// Copyright (c) 2019 Philipp Korber
// https://github.com/rustonaut/common_macros/blob/master/src/lib.rs
#[macro_export]
macro_rules! params_map {
// Fast path avoids allocation.
() => { $crate::ParamsMap::with_capacity(0) };
// Counting repitions by n = 0 ( + 1 )*
//
// https://github.com/rust-lang/rust/issues/83527
// When stabilized you can use "metavaribale exprs" instead
//
// `$key | $val` must be included in the repetition to be valid, it is
// stringified to null out any possible side-effects.
($($key:expr => $val:expr),* $(,)?) => {{
let n = 0 $(+ { _ = stringify!($key); 1 })*;
#[allow(unused_mut)]
let mut map = $crate::ParamsMap::with_capacity(n);
$( map.insert($key.to_string(), $val.to_string()); )*
map
}};
}
/// A simple method of deserializing key-value data (like route params or URL search)
/// into a concrete data type. `Self` should typically be a struct in which
/// each field's type implements [`FromStr`].
pub trait Params
where
Self: Sized,
{
/// Attempts to deserialize the map into the given type.
fn from_map(map: &ParamsMap) -> Result<Self, ParamsError>;
}
impl Params for () {
#[inline(always)]
fn from_map(_map: &ParamsMap) -> Result<Self, ParamsError> {
Ok(())
}
}
pub trait IntoParam
where
Self: Sized,
{
fn into_param(value: Option<&str>, name: &str)
-> Result<Self, ParamsError>;
}
impl<T> IntoParam for Option<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(
value: Option<&str>,
_name: &str,
) -> Result<Self, ParamsError> {
match value {
None => Ok(None),
Some(value) => match T::from_str(value) {
Ok(value) => Ok(Some(value)),
Err(e) => Err(ParamsError::Params(Arc::new(e))),
},
}
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}
impl<T> IntoParam for T
where
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
}
}
}
}
/// Errors that can occur while parsing params using [`Params`].
#[derive(Error, Debug, Clone)]
pub enum ParamsError {
/// A field was missing from the route params.
#[error("could not find parameter {0}")]
MissingParam(String),
/// Something went wrong while deserializing a field.
#[error("failed to deserialize parameters")]
Params(Arc<dyn std::error::Error + Send + Sync>),
}
impl PartialEq for ParamsError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::MissingParam(l0), Self::MissingParam(r0)) => l0 == r0,
(Self::Params(_), Self::Params(_)) => false,
_ => false,
}
}
}

View File

@ -1,22 +0,0 @@
use wasm_bindgen::JsValue;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct State(pub Option<JsValue>);
impl State {
pub fn to_js_value(&self) -> JsValue {
match &self.0 {
Some(v) => v.clone(),
None => JsValue::NULL,
}
}
}
impl<T> From<T> for State
where
T: Into<JsValue>,
{
fn from(value: T) -> Self {
State(Some(value.into()))
}
}

View File

@ -1,134 +0,0 @@
use crate::ParamsMap;
#[cfg(not(feature = "ssr"))]
use js_sys::{try_iter, Array, JsString};
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsCast;
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsValue;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Url {
pub origin: String,
pub pathname: String,
pub search: String,
pub search_params: ParamsMap,
pub hash: String,
}
#[cfg(feature = "ssr")]
pub fn unescape(s: &str) -> String {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.unwrap()
.to_string()
}
#[cfg(not(feature = "ssr"))]
pub fn unescape(s: &str) -> String {
js_sys::decode_uri_component(s).unwrap().into()
}
#[cfg(not(feature = "ssr"))]
pub fn unescape_minimal(s: &str) -> String {
js_sys::decode_uri(s).unwrap().into()
}
#[cfg(feature = "ssr")]
pub fn escape(s: &str) -> String {
percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC)
.to_string()
}
#[cfg(not(feature = "ssr"))]
pub fn escape(s: &str) -> String {
js_sys::encode_uri_component(s).as_string().unwrap()
}
#[cfg(not(feature = "ssr"))]
impl TryFrom<&str> for Url {
type Error = String;
fn try_from(url: &str) -> Result<Self, Self::Error> {
let url = web_sys::Url::new_with_base(
&if url.starts_with("//") {
let origin =
leptos::window().location().origin().unwrap_or_default();
format!("{origin}{url}")
} else {
url.to_string()
},
"http://leptos",
)
.map_js_error()?;
Ok(Self {
origin: url.origin(),
pathname: url.pathname(),
search: url
.search()
.strip_prefix('?')
.map(String::from)
.unwrap_or_default(),
search_params: ParamsMap(
try_iter(&url.search_params())
.map_js_error()?
.ok_or(
"Failed to use URLSearchParams as an iterator"
.to_string(),
)?
.map(|value| {
let array: Array =
value.map_js_error()?.dyn_into().map_js_error()?;
Ok((
array
.get(0)
.dyn_into::<JsString>()
.map_js_error()?
.into(),
array
.get(1)
.dyn_into::<JsString>()
.map_js_error()?
.into(),
))
})
.collect::<Result<
linear_map::LinearMap<String, String>,
Self::Error,
>>()?,
),
hash: url.hash(),
})
}
}
#[cfg(feature = "ssr")]
impl TryFrom<&str> for Url {
type Error = String;
fn try_from(url: &str) -> Result<Self, Self::Error> {
let url = url::Url::parse(url).map_err(|e| e.to_string())?;
Ok(Self {
origin: url.origin().unicode_serialization(),
pathname: url.path().to_string(),
search: url.query().unwrap_or_default().to_string(),
search_params: ParamsMap(
url.query_pairs()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect::<linear_map::LinearMap<String, String>>(),
),
hash: Default::default(),
})
}
}
#[cfg(not(feature = "ssr"))]
trait MapJsError<T> {
fn map_js_error(self) -> Result<T, String>;
}
#[cfg(not(feature = "ssr"))]
impl<T> MapJsError<T> for Result<T, JsValue> {
fn map_js_error(self) -> Result<T, String> {
self.map_err(|e| e.as_string().unwrap_or_default())
}
}

View File

@ -1,242 +0,0 @@
use crate::{
Location, NavigateOptions, Params, ParamsError, ParamsMap, RouteContext,
RouterContext,
};
use leptos::{
request_animation_frame, signal_prelude::*, use_context, window, Oco,
};
use std::{rc::Rc, str::FromStr};
/// Constructs a signal synchronized with a specific URL query parameter.
///
/// The function creates a bidirectional sync mechanism between the state encapsulated in a signal and a URL query parameter.
/// This means that any change to the state will update the URL, and vice versa, making the function especially useful
/// for maintaining state consistency across page reloads.
///
/// The `key` argument is the unique identifier for the query parameter to be synced with the state.
/// It is important to note that only one state can be tied to a specific key at any given time.
///
/// The function operates with types that can be parsed from and formatted into strings, denoted by `T`.
/// If the parsing fails for any reason, the function treats the value as `None`.
/// The URL parameter can be cleared by setting the signal to `None`.
///
/// ```rust
/// use leptos::*;
/// use leptos_router::*;
///
/// #[component]
/// pub fn SimpleQueryCounter() -> impl IntoView {
/// let (count, set_count) = create_query_signal::<i32>("count");
/// let clear = move |_| set_count.set(None);
/// let decrement =
/// move |_| set_count.set(Some(count.get().unwrap_or(0) - 1));
/// let increment =
/// move |_| set_count.set(Some(count.get().unwrap_or(0) + 1));
///
/// view! {
/// <div>
/// <button on:click=clear>"Clear"</button>
/// <button on:click=decrement>"-1"</button>
/// <span>"Value: " {move || count.get().unwrap_or(0)} "!"</span>
/// <button on:click=increment>"+1"</button>
/// </div>
/// }
/// }
/// ```
#[track_caller]
pub fn create_query_signal<T>(
key: impl Into<Oco<'static, str>>,
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
where
T: FromStr + ToString + PartialEq,
{
let mut key: Oco<'static, str> = key.into();
let query_map = use_query_map();
let navigate = use_navigate();
let location = use_location();
let get = create_memo({
let key = key.clone_inplace();
move |_| {
query_map
.with(|map| map.get(&key).and_then(|value| value.parse().ok()))
}
});
let set = SignalSetter::map(move |value: Option<T>| {
let mut new_query_map = query_map.get();
match value {
Some(value) => {
new_query_map.insert(key.to_string(), value.to_string());
}
None => {
new_query_map.remove(&key);
}
}
let qs = new_query_map.to_query_string();
let path = location.pathname.get_untracked();
let hash = location.hash.get_untracked();
let new_url = format!("{path}{qs}{hash}");
navigate(&new_url, NavigateOptions::default());
});
(get, set)
}
#[track_caller]
pub(crate) fn has_router() -> bool {
use_context::<RouterContext>().is_some()
}
/// Returns the current [`RouterContext`], containing information about the router's state.
#[track_caller]
pub fn use_router() -> RouterContext {
if let Some(router) = use_context::<RouterContext>() {
router
} else {
leptos::leptos_dom::debug_warn!(
"You must call use_router() within a <Router/> component {:?}",
std::panic::Location::caller()
);
panic!("You must call use_router() within a <Router/> component");
}
}
/// Returns the current [`RouteContext`], containing information about the matched route.
#[track_caller]
pub fn use_route() -> RouteContext {
use_context::<RouteContext>().unwrap_or_else(|| use_router().base())
}
/// Returns the data for the current route, which is provided by the `data` prop on `<Route/>`.
#[track_caller]
pub fn use_route_data<T: Clone + 'static>() -> Option<T> {
let route = use_context::<RouteContext>()?;
let data = route.inner.data.borrow();
let data = data.clone()?;
let downcast = data.downcast_ref::<T>().cloned();
downcast
}
/// Returns the current [`Location`], which contains reactive variables
#[track_caller]
pub fn use_location() -> Location {
use_router().inner.location.clone()
}
/// Returns a raw key-value map of route params.
#[track_caller]
pub fn use_params_map() -> Memo<ParamsMap> {
let route = use_route();
route.params()
}
/// Returns the current route params, parsed into the given type, or an error.
#[track_caller]
pub fn use_params<T>() -> Memo<Result<T, ParamsError>>
where
T: Params + PartialEq,
{
let route = use_route();
create_memo(move |_| route.params().with(T::from_map))
}
/// Returns a raw key-value map of the URL search query.
#[track_caller]
pub fn use_query_map() -> Memo<ParamsMap> {
use_router().inner.location.query
}
/// Returns the current URL search query, parsed into the given type, or an error.
#[track_caller]
pub fn use_query<T>() -> Memo<Result<T, ParamsError>>
where
T: Params + PartialEq,
{
let router = use_router();
create_memo(move |_| router.inner.location.query.with(|m| T::from_map(m)))
}
/// Resolves the given path relative to the current route.
#[track_caller]
pub fn use_resolved_path(
path: impl Fn() -> String + 'static,
) -> Memo<Option<String>> {
let route = use_route();
create_memo(move |_| {
let path = path();
if path.starts_with('/') {
Some(path)
} else {
route.resolve_path_tracked(&path)
}
})
}
/// Returns a function that can be used to navigate to a new route.
///
/// This should only be called on the client; it does nothing during
/// server rendering.
///
/// ```rust
/// # use leptos::{request_animation_frame, create_runtime};
/// # let runtime = create_runtime();
/// # if false { // can't actually navigate, no <Router/>
/// let navigate = leptos_router::use_navigate();
/// navigate("/", Default::default());
/// # }
/// # runtime.dispose();
/// ```
#[track_caller]
pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone {
let router = use_router();
move |to, options| {
let router = Rc::clone(&router.inner);
let to = to.to_string();
if cfg!(any(feature = "csr", feature = "hydrate")) {
request_animation_frame(move || {
#[allow(unused_variables)]
if let Err(e) = router.navigate_from_route(&to, &options) {
leptos::logging::debug_warn!("use_navigate error: {e:?}");
}
});
} else {
leptos::logging::warn!(
"The navigation function returned by `use_navigate` should \
not be called during server rendering."
);
}
}
}
/// Returns a signal that tells you whether you are currently navigating backwards.
pub(crate) fn use_is_back_navigation() -> ReadSignal<bool> {
let router = use_router();
router.inner.is_back.read_only()
}
/// Resolves a redirect location to an (absolute) URL.
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
let origin = match window().location().origin() {
Ok(origin) => origin,
Err(e) => {
leptos::logging::error!("Failed to get origin: {:#?}", e);
return None;
}
};
// TODO: Use server function's URL as base instead.
let base = origin;
match web_sys::Url::new_with_base(loc, &base) {
Ok(url) => Some(url),
Err(e) => {
leptos::logging::error!(
"Invalid redirect location: {}",
e.as_string().unwrap_or_default(),
);
None
}
}
}

View File

@ -1,208 +0,0 @@
#![forbid(unsafe_code)]
//! # Leptos Router
//!
//! Leptos Router is a router and state management tool for web applications
//! written in Rust using the [`Leptos`] web framework.
//! It is ”isomorphic”, i.e., it can be used for client-side applications/single-page
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
//! state between the two.
//!
//! ## Philosophy
//!
//! Leptos Router is built on a few simple principles:
//! 1. **URL drives state.** For web applications, the URL should be the ultimate
//! source of truth for most of your apps state. (Its called a **Universal
//! Resource Locator** for a reason!)
//!
//! 2. **Nested routing.** A URL can match multiple routes that exist in a nested tree
//! and are rendered by different components. This means you can navigate between siblings
//! in this tree without re-rendering or triggering any change in the parent routes.
//!
//! 3. **Progressive enhancement.** The [`A`] and [`Form`] components resolve any relative
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
//! upgrading them to handle those navigations with client-side routing. If youre using
//! them with server-side rendering (with or without hydration), they just work,
//! whether JS/WASM have loaded or not.
//!
//! ## Example
//!
//! ```rust
//! use leptos::*;
//! use leptos_router::*;
//!
//! #[component]
//! pub fn RouterExample() -> impl IntoView {
//! view! {
//!
//! <div id="root">
//! // we wrap the whole app in a <Router/> to allow client-side navigation
//! // from our nav links below
//! <Router>
//! // <nav> and <main> will show on every route
//! <nav>
//! // LR will enhance the active <a> link with the [aria-current] attribute
//! // we can use this for styling them with CSS like `[aria-current] { font-weight: bold; }`
//! <A href="contacts">"Contacts"</A>
//! // But we can also use a normal class attribute like it is a normal component
//! <A href="settings" class="my-class">"Settings"</A>
//! // It also supports signals!
//! <A href="about" class=move || "my-class">"About"</A>
//! </nav>
//! <main>
//! // <Routes/> both defines our routes and shows them on the page
//! <Routes>
//! // our root route: the contact list is always shown
//! <Route
//! path=""
//! view=ContactList
//! >
//! // users like /gbj or /bob
//! <Route
//! path=":id"
//! view=Contact
//! />
//! // a fallback if the /:id segment is missing from the URL
//! <Route
//! path=""
//! view=move || view! { <p class="contact">"Select a contact."</p> }
//! />
//! </Route>
//! // LR will automatically use this for /about, not the /:id match above
//! <Route
//! path="about"
//! view=About
//! />
//! </Routes>
//! </main>
//! </Router>
//! </div>
//! }
//! }
//!
//! type ContactSummary = (); // TODO!
//! type Contact = (); // TODO!()
//!
//! // contact_data reruns whenever the :id param changes
//! async fn contact_data(id: String) -> Contact {
//! todo!()
//! }
//!
//! // contact_list_data *doesn't* rerun when the :id changes,
//! // because that param is nested lower than the <ContactList/> route
//! async fn contact_list_data() -> Vec<ContactSummary> {
//! todo!()
//! }
//!
//! #[component]
//! fn ContactList() -> impl IntoView {
//! // loads the contact list data once; doesn't reload when nested routes change
//! let contacts = create_resource(|| (), |_| contact_list_data());
//! view! {
//!
//! <div>
//! // show the contacts
//! <ul>
//! {move || contacts.read().map(|contacts| view! { <li>"todo contact info"</li> } )}
//! </ul>
//!
//! // insert the nested child route here
//! <Outlet/>
//! </div>
//! }
//! }
//!
//! #[component]
//! fn Contact() -> impl IntoView {
//! let params = use_params_map();
//! let data = create_resource(
//!
//! move || params.with(|p| p.get("id").cloned().unwrap_or_default()),
//! move |id| contact_data(id)
//! );
//! todo!()
//! }
//!
//! #[component]
//! fn About() -> impl IntoView {
//! todo!()
//! }
//! ```
//!
//! ## Module Route Definitions
//! Routes can also be modularized and nested by defining them in separate components, which can be
//! located in and imported from other modules. Components that return `<Route/>` should be marked
//! `#[component(transparent)]`, as in this example:
//! ```rust
//! use leptos::*;
//! use leptos_router::*;
//!
//! #[component]
//! pub fn App() -> impl IntoView {
//! view! {
//! <Router>
//! <Routes>
//! <Route path="/" view=move || {
//! view! { "-> /" }
//! }/>
//! <ExternallyDefinedRoute/>
//! </Routes>
//! </Router>
//! }
//! }
//!
//! // `transparent` here marks the component as returning data (a RouteDefinition), not a view
//! #[component(transparent)]
//! pub fn ExternallyDefinedRoute() -> impl IntoView {
//! view! {
//! <Route path="/some-area" view=move || {
//! view! { <div>
//! <h2>"Some Area"</h2>
//! <Outlet/>
//! </div> }
//! }>
//! <Route path="/path-a/:id" view=move || {
//! view! { <p>"Path A"</p> }
//! }/>
//! <Route path="/path-b/:id" view=move || {
//! view! { <p>"Path B"</p> }
//! }/>
//! </Route>
//! }
//! }
//! ```
//!
//! # Feature Flags
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
//!
//! [`Leptos`]: <https://github.com/leptos-rs/leptos>
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
// to prevent warnings from popping up when a nightly feature is stabilized
#![allow(stable_features)]
mod animation;
mod components;
#[cfg(any(feature = "ssr", doc))]
mod extract_routes;
mod history;
mod hooks;
#[doc(hidden)]
pub mod matching;
mod render_mode;
pub use components::*;
#[cfg(any(feature = "ssr", doc))]
pub use extract_routes::*;
pub use history::*;
pub use hooks::*;
pub use matching::{RouteDefinition, *};
pub use render_mode::*;
extern crate tracing;

View File

@ -1,105 +0,0 @@
use std::borrow::Cow;
#[doc(hidden)]
#[cfg(not(feature = "ssr"))]
pub fn expand_optionals(pattern: &str) -> Vec<Cow<'_, str>> {
use js_sys::RegExp;
use once_cell::unsync::Lazy;
use wasm_bindgen::JsValue;
thread_local! {
static OPTIONAL_RE: Lazy<RegExp> = Lazy::new(|| {
RegExp::new(OPTIONAL, "")
});
static OPTIONAL_RE_2: Lazy<RegExp> = Lazy::new(|| {
RegExp::new(OPTIONAL_2, "")
});
}
let captures = OPTIONAL_RE.with(|re| re.exec(pattern));
match captures {
None => vec![pattern.into()],
Some(matched) => {
let start: usize =
js_sys::Reflect::get(&matched, &JsValue::from_str("index"))
.unwrap()
.as_f64()
.unwrap() as usize;
let mut prefix = pattern[0..start].to_string();
let mut suffix =
&pattern[start + matched.get(1).as_string().unwrap().len()..];
let mut prefixes = vec![prefix.clone()];
prefix += &matched.get(1).as_string().unwrap();
prefixes.push(prefix.clone());
while let Some(matched) =
OPTIONAL_RE_2.with(|re| re.exec(suffix.trim_start_matches('?')))
{
prefix += &matched.get(1).as_string().unwrap();
prefixes.push(prefix.clone());
suffix = &suffix[matched.get(0).as_string().unwrap().len()..];
}
expand_optionals(suffix).iter().fold(
Vec::new(),
|mut results, expansion| {
results.extend(prefixes.iter().map(|prefix| {
Cow::Owned(
prefix.clone() + expansion.trim_start_matches('?'),
)
}));
results
},
)
}
}
}
#[doc(hidden)]
#[cfg(feature = "ssr")]
pub fn expand_optionals(pattern: &str) -> Vec<Cow<'_, str>> {
use regex::Regex;
lazy_static::lazy_static! {
pub static ref OPTIONAL_RE: Regex = Regex::new(OPTIONAL).expect("could not compile OPTIONAL_RE");
pub static ref OPTIONAL_RE_2: Regex = Regex::new(OPTIONAL_2).expect("could not compile OPTIONAL_RE_2");
}
let captures = OPTIONAL_RE.find(pattern);
match captures {
None => vec![pattern.into()],
Some(matched) => {
let mut prefix = pattern[0..matched.start()].to_string();
let captures = OPTIONAL_RE.captures(pattern).unwrap();
let mut suffix = &pattern[matched.start() + captures[1].len()..];
let mut prefixes = vec![prefix.clone()];
prefix += &captures[1];
prefixes.push(prefix.clone());
while let Some(captures) =
OPTIONAL_RE_2.captures(suffix.trim_start_matches('?'))
{
prefix += &captures[1];
prefixes.push(prefix.clone());
suffix = &suffix[captures[0].len()..];
}
expand_optionals(suffix).iter().fold(
Vec::new(),
|mut results, expansion| {
results.extend(prefixes.iter().map(|prefix| {
Cow::Owned(
prefix.clone() + expansion.trim_start_matches('?'),
)
}));
results
},
)
}
}
}
const OPTIONAL: &str = r"(/?:[^/]+)\?";
const OPTIONAL_2: &str = r"^(/:[^/]+)\?";

View File

@ -1,121 +0,0 @@
// Implementation based on Solid Router
// see <https://github.com/solidjs/solid-router/blob/main/src/utils.ts>
use crate::{unescape, ParamsMap};
#[derive(Debug, Clone, PartialEq, Eq)]
#[doc(hidden)]
pub struct PathMatch {
pub path: String,
pub params: ParamsMap,
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Matcher {
splat: Option<String>,
segments: Vec<String>,
len: usize,
partial: bool,
}
impl Matcher {
#[doc(hidden)]
pub fn new(path: &str) -> Self {
Self::new_with_partial(path, false)
}
#[doc(hidden)]
pub fn new_with_partial(path: &str, partial: bool) -> Self {
let (pattern, splat) = match path.split_once("/*") {
Some((p, s)) => (p, Some(s.to_string())),
None => (path, None),
};
let segments: Vec<String> = get_segments(pattern);
let len = segments.len();
Self {
splat,
segments,
len,
partial,
}
}
#[doc(hidden)]
pub fn test(&self, location: &str) -> Option<PathMatch> {
let loc_segments: Vec<&str> = get_segments(location);
let loc_len = loc_segments.len();
let len_diff: i32 = loc_len as i32 - self.len as i32;
let trailing_iter = location.chars().rev().take_while(|n| *n == '/');
// quick path: not a match if
// 1) matcher has add'l segments not found in location
// 2) location has add'l segments, there's no splat, and partial matches not allowed
if loc_len < self.len
|| (len_diff > 0 && self.splat.is_none() && !self.partial)
|| (self.splat.is_none() && trailing_iter.clone().count() > 1)
{
None
}
// otherwise, start building a match
else {
let mut path = String::new();
let mut params = ParamsMap::new();
for (segment, loc_segment) in
self.segments.iter().zip(loc_segments.iter())
{
if let Some(param_name) = segment.strip_prefix(':') {
params.insert(param_name.into(), unescape(loc_segment));
} else if segment != loc_segment {
// if any segment doesn't match and isn't a param, there's no path match
return None;
}
path.push('/');
path.push_str(loc_segment);
}
if let Some(splat) = &self.splat {
if !splat.is_empty() {
let mut value = if len_diff > 0 {
loc_segments[self.len..].join("/")
} else {
"".into()
};
// add trailing slashes to splat
let trailing_slashes =
trailing_iter.skip(1).collect::<String>();
value.push_str(&trailing_slashes);
params.insert(splat.into(), value);
}
}
Some(PathMatch { path, params })
}
}
#[doc(hidden)]
pub(crate) fn is_wildcard(&self) -> bool {
self.splat.is_some()
}
}
fn get_segments<'a, S: From<&'a str>>(pattern: &'a str) -> Vec<S> {
// URL root paths ("/" and "") are equivalent and treated as 0-segment paths.
// non-root paths with trailing slashes get extra empty segment at the end.
// This makes sure that segment matching is trailing-slash sensitive.
let mut segments: Vec<S> = pattern
.split('/')
.filter(|p| !p.is_empty())
.map(Into::into)
.collect();
if !segments.is_empty() && pattern.ends_with('/') {
segments.push("".into());
}
segments
}

View File

@ -1,86 +0,0 @@
mod expand_optionals;
mod matcher;
mod resolve_path;
mod route;
use crate::{Branches, RouteData};
pub use expand_optionals::*;
pub use matcher::*;
pub use resolve_path::*;
pub use route::*;
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RouteMatch {
pub path_match: PathMatch,
pub route: RouteData,
}
pub(crate) fn get_route_matches(
router_id: usize,
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
#[cfg(feature = "ssr")]
{
use lru::LruCache;
use std::{cell::RefCell, num::NonZeroUsize};
type RouteMatchCache = LruCache<(usize, String), Rc<Vec<RouteMatch>>>;
thread_local! {
static ROUTE_MATCH_CACHE: RefCell<RouteMatchCache> = RefCell::new(LruCache::new(NonZeroUsize::new(32).unwrap()));
}
ROUTE_MATCH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
Rc::clone(
cache.get_or_insert((router_id, location.clone()), || {
build_route_matches(router_id, base, location)
}),
)
})
}
#[cfg(not(feature = "ssr"))]
build_route_matches(router_id, base, location)
}
fn build_route_matches(
router_id: usize,
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
Rc::new(Branches::with(router_id, base, |branches| {
for branch in branches {
if let Some(matches) = branch.matcher(&location) {
return matches;
}
}
vec![]
}))
}
/// Describes a branch of the route tree.
#[derive(Debug, Clone, PartialEq)]
pub struct Branch {
/// All the routes contained in the branch.
pub routes: Vec<RouteData>,
/// How closely this branch matches the current URL.
pub score: i32,
}
impl Branch {
fn matcher<'a>(&'a self, location: &'a str) -> Option<Vec<RouteMatch>> {
let mut matches = Vec::new();
for route in self.routes.iter().rev() {
match route.matcher.test(location) {
None => return None,
Some(m) => matches.push(RouteMatch {
path_match: m,
route: route.clone(),
}),
}
}
matches.reverse();
Some(matches)
}
}

View File

@ -1,104 +0,0 @@
// Implementation based on Solid Router
// see <https://github.com/solidjs/solid-router/blob/main/src/utils.ts>
use std::borrow::Cow;
#[doc(hidden)]
pub fn resolve_path<'a>(
base: &'a str,
path: &'a str,
from: Option<&'a str>,
) -> Option<Cow<'a, str>> {
if has_scheme(path) {
Some(path.into())
} else {
let base_path = normalize(base, false);
let from_path = from.map(|from| normalize(from, false));
let result = if let Some(from_path) = from_path {
if path.starts_with('/') {
base_path
} else if from_path.to_lowercase().find(&base_path.to_lowercase())
!= Some(0)
{
base_path + from_path
} else {
from_path
}
} else {
base_path
};
let result_empty = result.is_empty();
let prefix = if result_empty { "/".into() } else { result };
Some(prefix + normalize(path, result_empty))
}
}
fn has_scheme(path: &str) -> bool {
path.starts_with("//")
|| path.starts_with("tel:")
|| path.starts_with("mailto:")
|| path
.split_once("://")
.map(|(prefix, _)| {
prefix.chars().all(
|c: char| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9'),
)
})
.unwrap_or(false)
}
#[doc(hidden)]
fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
let s = path.trim_start_matches('/');
let trim_end = s
.chars()
.rev()
.take_while(|c| *c == '/')
.count()
.saturating_sub(1);
let s = &s[0..s.len() - trim_end];
if s.is_empty() || omit_slash || begins_with_query_or_hash(s) {
s.into()
} else {
format!("/{s}").into()
}
}
#[doc(hidden)]
pub fn join_paths<'a>(from: &'a str, to: &'a str) -> String {
let from = remove_wildcard(&normalize(from, false));
from + normalize(to, false).as_ref()
}
fn begins_with_query_or_hash(text: &str) -> bool {
matches!(text.chars().next(), Some('#') | Some('?'))
}
fn remove_wildcard(text: &str) -> String {
text.rsplit_once('*')
.map(|(prefix, _)| prefix)
.unwrap_or(text)
.trim_end_matches('/')
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_query_string_with_opening_slash() {
assert_eq!(normalize("/?foo=bar", false), "?foo=bar");
}
#[test]
fn normalize_retain_trailing_slash() {
assert_eq!(normalize("foo/bar/", false), "/foo/bar/");
}
#[test]
fn normalize_dedup_trailing_slashes() {
assert_eq!(normalize("foo/bar/////", false), "/foo/bar/");
}
}

View File

@ -1,48 +0,0 @@
use crate::{Loader, Method, SsrMode, StaticData, StaticMode, TrailingSlash};
use leptos::leptos_dom::View;
use std::rc::Rc;
/// Defines a single route in a nested route tree. This is the return
/// type of the [`<Route/>`](crate::Route) component, but can also be
/// used to build your own configuration-based or filesystem-based routing.
#[derive(Clone)]
pub struct RouteDefinition {
/// A unique ID for each route.
pub id: usize,
/// The path. This can include params like `:id` or wildcards like `*all`.
pub path: String,
/// Other route definitions nested within this one.
pub children: Vec<RouteDefinition>,
/// The view that should be displayed when this route is matched.
pub view: Rc<dyn Fn() -> View>,
/// The mode this route prefers during server-side rendering.
pub ssr_mode: SsrMode,
/// The HTTP request methods this route is able to handle.
pub methods: &'static [Method],
/// A data loader function that will be called when this route is matched.
pub data: Option<Loader>,
/// The route's preferred mode of static generation, if any
pub static_mode: Option<StaticMode>,
/// The data required to fill any dynamic segments in the path during static rendering.
pub static_params: Option<StaticData>,
/// How a trailng slash in `path` should be handled.
pub trailing_slash: Option<TrailingSlash>,
}
impl core::fmt::Debug for RouteDefinition {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RouteDefinition")
.field("path", &self.path)
.field("children", &self.children)
.field("ssr_mode", &self.ssr_mode)
.field("static_render", &self.static_mode)
.field("trailing_slash", &self.trailing_slash)
.finish()
}
}
impl PartialEq for RouteDefinition {
fn eq(&self, other: &Self) -> bool {
self.path == other.path && self.children == other.children
}
}

View File

@ -1,32 +0,0 @@
/// Indicates which rendering mode should be used for this route during server-side rendering.
///
/// Leptos supports the following ways of rendering HTML that contains `async` data loaded
/// under `<Suspense/>`.
/// 1. **Synchronous** (use any mode except `Async`, don't depend on any resource): Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the client (using `create_local_resource`), replacing `fallback` once they're loaded.
/// - *Pros*: App shell appears very quickly: great TTFB (time to first byte).
/// - *Cons*: Resources load relatively slowly; you need to wait for JS + Wasm to load before even making a request.
/// 2. **Out-of-order streaming** (`OutOfOrder`, the default): Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
/// - *Pros*: Combines the best of **synchronous** and `Async`, with a very fast shell and resources that begin loading on the server.
/// - *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
/// 3. **Partially-blocked out-of-order streaming** (`PartiallyBlocked`): Using `create_blocking_resource` with out-of-order streaming still sends fallbacks and relies on JavaScript to fill them in with the fragments. Partially-blocked streaming does this replacement on the server, making for a slower response but requiring no JavaScript to show blocking resources.
/// - *Pros*: Works better if JS is disabled.
/// - *Cons*: Slower initial response because of additional string manipulation on server.
/// 4. **In-order streaming** (`InOrder`): Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
/// - *Pros*: Does not require JS for HTML to appear in correct order.
/// - *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
/// of the page will not be interactive until the suspended chunks have loaded.
/// 5. **`Async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
/// - *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
/// - *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
///
/// The mode defaults to out-of-order streaming. For a path that includes multiple nested routes, the most
/// restrictive mode will be used: i.e., if even a single nested route asks for `Async` rendering, the whole initial
/// request will be rendered `Async`. (`Async` is the most restricted requirement, followed by `InOrder`, `PartiallyBlocked`, and `OutOfOrder`.)
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SsrMode {
#[default]
OutOfOrder,
PartiallyBlocked,
InOrder,
Async,
}

View File

@ -1,35 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos_router::expand_optionals;
#[test]
fn expand_optionals_should_expand() {
assert_eq!(expand_optionals("/foo/:x"), vec!["/foo/:x"]);
assert_eq!(expand_optionals("/foo/:x?"), vec!["/foo", "/foo/:x"]);
assert_eq!(expand_optionals("/bar/:x?/"), vec!["/bar/", "/bar/:x/"]);
assert_eq!(
expand_optionals("/foo/:x?/:y?/:z"),
vec!["/foo/:z", "/foo/:x/:z", "/foo/:x/:y/:z"]
);
assert_eq!(
expand_optionals("/foo/:x?/:y/:z?"),
vec!["/foo/:y", "/foo/:x/:y", "/foo/:y/:z", "/foo/:x/:y/:z"]
);
assert_eq!(
expand_optionals("/foo/:x?/bar/:y?/baz/:z?"),
vec![
"/foo/bar/baz",
"/foo/:x/bar/baz",
"/foo/bar/:y/baz",
"/foo/:x/bar/:y/baz",
"/foo/bar/baz/:z",
"/foo/:x/bar/baz/:z",
"/foo/bar/:y/baz/:z",
"/foo/:x/bar/:y/baz/:z"
]
)
}
}
}

View File

@ -1,57 +0,0 @@
use cfg_if::cfg_if;
// Test cases drawn from Solid Router
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos_router::join_paths;
#[test]
fn join_paths_should_join_with_a_single_slash() {
assert_eq!(join_paths("/foo", "bar"), "/foo/bar");
assert_eq!(join_paths("/foo/", "bar"), "/foo/bar");
assert_eq!(join_paths("/foo", "/bar"), "/foo/bar");
assert_eq!(join_paths("/foo/", "/bar"), "/foo/bar");
}
#[test]
fn join_paths_should_ensure_leading_slash() {
assert_eq!(join_paths("/foo", ""), "/foo");
assert_eq!(join_paths("foo", ""), "/foo");
assert_eq!(join_paths("", "foo"), "/foo");
assert_eq!(join_paths("", "/foo"), "/foo");
assert_eq!(join_paths("/", "foo"), "/foo");
assert_eq!(join_paths("/", "/foo"), "/foo");
}
#[test]
fn join_paths_should_strip_tailing_slash_asterisk() {
assert_eq!(join_paths("foo/*", ""), "/foo");
assert_eq!(join_paths("foo/*", "/"), "/foo");
assert_eq!(join_paths("/foo/*all", ""), "/foo");
assert_eq!(join_paths("/foo/*", "bar"), "/foo/bar");
assert_eq!(join_paths("/foo/*all", "bar"), "/foo/bar");
assert_eq!(join_paths("/*", "foo"), "/foo");
assert_eq!(join_paths("/*all", "foo"), "/foo");
assert_eq!(join_paths("*", "foo"), "/foo");
}
#[test]
fn join_paths_should_preserve_parameters() {
assert_eq!(join_paths("/foo/:bar", ""), "/foo/:bar");
assert_eq!(join_paths("/foo/:bar", "baz"), "/foo/:bar/baz");
assert_eq!(join_paths("/foo", ":bar/baz"), "/foo/:bar/baz");
assert_eq!(join_paths("", ":bar/baz"), "/:bar/baz");
}
// Additional tests NOT from Solid Router:
#[test]
fn join_paths_for_root() {
assert_eq!(join_paths("", ""), "");
assert_eq!(join_paths("", "/"), "");
assert_eq!(join_paths("/", ""), "");
assert_eq!(join_paths("/", "/"), "");
}
}
}

View File

@ -1,157 +0,0 @@
use cfg_if::cfg_if;
// Test cases drawn from Solid Router
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos_router::{params_map, Matcher, PathMatch};
#[test]
fn create_matcher_should_return_no_params_when_location_matches_exactly() {
let matcher = Matcher::new("/foo/bar");
let matched = matcher.test("/foo/bar");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo/bar".into(),
params: params_map!()
})
);
}
#[test]
fn create_matcher_should_return_none_when_location_doesnt_match() {
let matcher = Matcher::new("/foo/bar");
let matched = matcher.test("/foo/baz");
assert_eq!(matched, None);
}
#[test]
fn create_matcher_should_build_params_collection() {
let matcher = Matcher::new("/foo/:id");
let matched = matcher.test("/foo/abc-123");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo/abc-123".into(),
params: params_map!(
"id" => "abc-123"
)
})
);
}
#[test]
fn create_matcher_should_build_params_collection_and_decode() {
let matcher = Matcher::new("/foo/:id");
let matched = matcher.test("/foo/%E2%89%A1abc%20123");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo/%E2%89%A1abc%20123".into(),
params: params_map!(
"id" => "≡abc 123"
)
})
);
}
#[test]
fn create_matcher_should_match_past_end_when_ending_in_asterisk() {
let matcher = Matcher::new("/foo/bar/*");
let matched = matcher.test("/foo/bar/baz");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo/bar".into(),
params: params_map!()
})
);
}
#[test]
fn create_matcher_should_not_match_past_end_when_not_ending_in_asterisk() {
let matcher = Matcher::new("/foo/bar");
let matched = matcher.test("/foo/bar/baz");
assert_eq!(matched, None);
}
#[test]
fn create_matcher_should_include_remaining_unmatched_location_as_param_when_ending_in_asterisk_and_name(
) {
let matcher = Matcher::new("/foo/bar/*something");
let matched = matcher.test("/foo/bar/baz/qux");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo/bar".into(),
params: params_map!(
"something" => "baz/qux"
)
})
);
}
#[test]
fn create_matcher_should_include_remaining_unmatched_location_as_param_when_ending_in_asterisk_and_name_2(
) {
let matcher = Matcher::new("/foo/*something");
let matched = matcher.test("/foo/baz/qux");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo".into(),
params: params_map!(
"something" => "baz/qux"
)
})
);
}
#[test]
fn create_matcher_should_include_empty_param_when_perfect_match_ends_in_asterisk_and_name() {
let matcher = Matcher::new("/foo/bar/*something");
let matched = matcher.test("/foo/bar");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo/bar".into(),
params: params_map!(
"something" => ""
)
})
);
}
#[test]
fn matcher_should_include_multiple_slashes_in_a_splat_route() {
let matcher = Matcher::new("/*any");
let matched = matcher.test("////");
assert_eq!(
matched,
Some(PathMatch {
path: "".into(),
params: params_map!(
"any" => "///"
)
})
);
}
#[test]
fn matcher_should_include_multiple_slashes_in_a_splat_route_after_others() {
let matcher = Matcher::new("/foo/bar/*any");
let matched = matcher.test("/foo/bar////");
assert_eq!(
matched,
Some(PathMatch {
path: "/foo/bar".into(),
params: params_map!(
"any" => "///"
)
})
);
}
}
}

View File

@ -1,52 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos_router::{Url, params_map};
macro_rules! assert_params_map {
([$($key:expr => $val:expr),*] , $actual:expr) => (
assert_eq!(params_map!($($key => $val),*), $actual)
);
}
#[test]
fn test_param_with_plus_sign() {
let url = Url::try_from("http://leptos.com?data=1%2B2%3D3").unwrap();
assert_params_map!{
["data" => "1+2=3"],
url.search_params
};
}
#[test]
fn test_param_with_ampersand() {
let url = Url::try_from("http://leptos.com?data=true+%26+false+%3D+false").unwrap();
assert_params_map!{
["data" => "true & false = false"],
url.search_params
};
}
#[test]
fn test_complex_query_string() {
let url = Url::try_from("http://leptos.com?data=Data%3A+%24+%26+%2B%2B+7").unwrap();
assert_params_map!{
["data" => "Data: $ & ++ 7"],
url.search_params
};
}
#[test]
fn test_multiple_query_params() {
let url = Url::try_from("http://leptos.com?param1=value1&param2=value2").unwrap();
assert_params_map!{
[
"param1" => "value1",
"param2" => "value2"
],
url.search_params
};
}
}
}

View File

@ -1,124 +0,0 @@
// Test cases drawn from Solid Router
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
use leptos_router::resolve_path;
#[test]
fn resolve_path_should_normalize_base_arg() {
assert_eq!(resolve_path("base", "", None), Some("/base".into()));
}
#[test]
fn resolve_path_should_normalize_path_arg() {
assert_eq!(resolve_path("", "path", None), Some("/path".into()));
}
#[test]
fn resolve_path_should_normalize_from_arg() {
assert_eq!(resolve_path("", "", Some("from")), Some("/from".into()));
}
#[test]
fn resolve_path_should_return_default_when_all_empty() {
assert_eq!(resolve_path("", "", None), Some("/".into()));
}
#[test]
fn resolve_path_should_resolve_root_against_base_and_ignore_from() {
assert_eq!(
resolve_path("/base", "/", Some("/base/foo")),
Some("/base".into())
);
}
#[test]
fn resolve_path_should_resolve_rooted_paths_against_base_and_ignore_from() {
assert_eq!(
resolve_path("/base", "/bar", Some("/base/foo")),
Some("/base/bar".into())
);
}
#[test]
fn resolve_path_should_resolve_empty_path_against_from() {
assert_eq!(
resolve_path("/base", "", Some("/base/foo")),
Some("/base/foo".into())
);
}
#[test]
fn resolve_path_should_resolve_relative_paths_against_from() {
assert_eq!(
resolve_path("/base", "bar", Some("/base/foo")),
Some("/base/foo/bar".into())
);
}
#[test]
fn resolve_path_should_prepend_base_if_from_doesnt_start_with_it() {
assert_eq!(
resolve_path("/base", "bar", Some("/foo")),
Some("/base/foo/bar".into())
);
}
#[test]
fn resolve_path_should_test_start_of_from_against_base_case_insensitive() {
assert_eq!(
resolve_path("/base", "bar", Some("BASE/foo")),
Some("/BASE/foo/bar".into())
);
}
#[test]
fn resolve_path_should_work_with_rooted_search_and_base() {
assert_eq!(
resolve_path("/base", "/?foo=bar", Some("/base/page")),
Some("/base?foo=bar".into())
);
}
#[test]
fn resolve_path_should_work_with_rooted_search() {
assert_eq!(
resolve_path("", "/?foo=bar", None),
Some("/?foo=bar".into())
);
}
#[test]
fn preserve_spaces() {
assert_eq!(
resolve_path(" foo ", " bar baz ", None),
Some("/ foo / bar baz ".into())
);
}
#[test]
fn will_resolve_if_path_has_scheme() {
assert_eq!(
resolve_path("", "http://example.com", None).as_deref(),
Some("http://example.com")
);
assert_eq!(
resolve_path("", "https://example.com", None).as_deref(),
Some("https://example.com")
);
assert_eq!(
resolve_path("", "example://google.com", None).as_deref(),
Some("example://google.com")
);
assert_eq!(
resolve_path("", "tel:+15555555555", None).as_deref(),
Some("tel:+15555555555")
);
assert_eq!(
resolve_path("", "mailto:name@example.com", None).as_deref(),
Some("mailto:name@example.com")
);
assert_eq!(
resolve_path("", "//relative-protocol", None).as_deref(),
Some("//relative-protocol")
);
}

View File

@ -1,59 +0,0 @@
//! Some extra tests for Matcher NOT based on SolidJS's tests cases (as in matcher.rs)
use leptos_router::*;
#[test]
fn trailing_slashes_match_exactly() {
let matcher = Matcher::new("/foo/");
assert_matches(&matcher, "/foo/");
assert_no_match(&matcher, "/foo");
let matcher = Matcher::new("/foo/bar/");
assert_matches(&matcher, "/foo/bar/");
assert_no_match(&matcher, "/foo/bar");
let matcher = Matcher::new("/");
assert_matches(&matcher, "/");
assert_matches(&matcher, "");
let matcher = Matcher::new("");
assert_matches(&matcher, "");
// Despite returning a pattern of "", web servers (known: Actix-Web and Axum)
// may send us a path of "/". We should match those at the root:
assert_matches(&matcher, "/");
}
#[cfg(feature = "ssr")]
#[test]
fn trailing_slashes_params_match_exactly() {
let matcher = Matcher::new("/foo/:bar/");
assert_matches(&matcher, "/foo/bar/");
assert_matches(&matcher, "/foo/42/");
assert_matches(&matcher, "/foo/%20/");
assert_no_match(&matcher, "/foo/bar");
assert_no_match(&matcher, "/foo/42");
assert_no_match(&matcher, "/foo/%20");
let m = matcher.test("/foo/asdf/").unwrap();
assert_eq!(m.params, params_map! { "bar" => "asdf" });
}
fn assert_matches(matcher: &Matcher, path: &str) {
assert!(
matches(matcher, path),
"{matcher:?} should match path {path:?}"
);
}
fn assert_no_match(matcher: &Matcher, path: &str) {
assert!(
!matches(matcher, path),
"{matcher:?} should NOT match path {path:?}"
);
}
fn matches(m: &Matcher, loc: &str) -> bool {
m.test(loc).is_some()
}

View File

@ -1,8 +0,0 @@
[package]
name = "routing_utils"
edition = "2021"
version.workspace = true
[dependencies]
either_of = { workspace = true }
tracing = { version = "0.1", optional = true }

View File

@ -1,26 +0,0 @@
use crate::PathSegment;
use alloc::vec::Vec;
mod param_segments;
mod static_segment;
mod tuples;
use super::PartialPathMatch;
pub use param_segments::*;
pub use static_segment::*;
/// Defines a route which may or may not be matched by any given URL,
/// or URL segment.
///
/// This is a "horizontal" matching: i.e., it treats a tuple of route segments
/// as subsequent segments of the URL and tries to match them all. For a "vertical"
/// matching that sees a tuple as alternatives to one another, see [`RouteChild`](super::RouteChild).
pub trait PossibleRouteMatch {
type ParamsIter<'a>: IntoIterator<Item = (&'a str, &'a str)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>>;
fn generate_path(&self, path: &mut Vec<PathSegment>);
}

View File

@ -1,139 +0,0 @@
use super::{PartialPathMatch, PossibleRouteMatch};
use crate::PathSegment;
use alloc::vec::Vec;
use core::iter;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ParamSegment(pub &'static str);
impl PossibleRouteMatch for ParamSegment {
type ParamsIter<'a> = iter::Once<(&'a str, &'a str)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
let mut matched_len = 0;
let mut param_offset = 0;
let mut param_len = 0;
let mut test = path.chars();
// match an initial /
if let Some('/') = test.next() {
matched_len += 1;
param_offset = 1;
}
for char in test {
// when we get a closing /, stop matching
if char == '/' {
break;
}
// otherwise, push into the matched param
else {
matched_len += char.len_utf8();
param_len += char.len_utf8();
}
}
let (matched, remaining) = path.split_at(matched_len);
let param_value =
iter::once((self.0, &path[param_offset..param_len + param_offset]));
Some(PartialPathMatch::new(remaining, param_value, matched))
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
path.push(PathSegment::Param(self.0.into()));
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct WildcardSegment(pub &'static str);
impl PossibleRouteMatch for WildcardSegment {
type ParamsIter<'a> = iter::Once<(&'a str, &'a str)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
let mut matched_len = 0;
let mut param_offset = 0;
let mut param_len = 0;
let mut test = path.chars();
// match an initial /
if let Some('/') = test.next() {
matched_len += 1;
param_offset += 1;
}
for char in test {
matched_len += char.len_utf8();
param_len += char.len_utf8();
}
let (matched, remaining) = path.split_at(matched_len);
let param_value =
iter::once((self.0, &path[param_offset..param_len + param_offset]));
Some(PartialPathMatch::new(remaining, param_value, matched))
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
path.push(PathSegment::Splat(self.0.into()));
}
}
#[cfg(test)]
mod tests {
use super::PossibleRouteMatch;
use crate::{ParamSegment, StaticSegment, WildcardSegment};
use alloc::vec::Vec;
#[test]
fn single_param_match() {
let path = "/foo";
let def = ParamSegment("a");
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
assert_eq!(params[0], ("a", "foo"));
}
#[test]
fn single_param_match_with_trailing_slash() {
let path = "/foo/";
let def = ParamSegment("a");
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "/");
let params = matched.params().collect::<Vec<_>>();
assert_eq!(params[0], ("a", "foo"));
}
#[test]
fn tuple_of_param_matches() {
let path = "/foo/bar";
let def = (ParamSegment("a"), ParamSegment("b"));
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
assert_eq!(params[0], ("a", "foo"));
assert_eq!(params[1], ("b", "bar"));
}
#[test]
fn splat_should_match_all() {
let path = "/foo/bar/////";
let def = (
StaticSegment("foo"),
StaticSegment("bar"),
WildcardSegment("rest"),
);
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar/////");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
assert_eq!(params[0], ("rest", "////"));
}
}

View File

@ -1,147 +0,0 @@
use super::{PartialPathMatch, PossibleRouteMatch};
use crate::PathSegment;
use alloc::vec::Vec;
use core::iter;
impl PossibleRouteMatch for () {
type ParamsIter<'a> = iter::Empty<(&'a str, &'a str)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
Some(PartialPathMatch::new(path, iter::empty(), ""))
}
fn generate_path(&self, _path: &mut Vec<PathSegment>) {}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct StaticSegment(pub &'static str);
impl PossibleRouteMatch for StaticSegment {
type ParamsIter<'a> = iter::Empty<(&'a str, &'a str)>;
fn test<'a>(
&self,
path: &'a str,
) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
let mut matched_len = 0;
let mut test = path.chars().peekable();
let mut this = self.0.chars();
let mut has_matched = self.0.is_empty() || self.0 == "/";
// match an initial /
if let Some('/') = test.peek() {
test.next();
if !self.0.is_empty() {
matched_len += 1;
}
if self.0.starts_with('/') || self.0.is_empty() {
this.next();
}
}
for char in test {
let n = this.next();
// when we get a closing /, stop matching
if char == '/' || n.is_none() {
break;
}
// if the next character in the path matches the
// next character in the segment, add it to the match
else if Some(char) == n {
has_matched = true;
matched_len += char.len_utf8();
}
// otherwise, this route doesn't match and we should
// return None
else {
return None;
}
}
// build the match object
// the remaining is built from the path in, with the slice moved
// by the length of this match
let (matched, remaining) = path.split_at(matched_len);
has_matched
.then(|| PartialPathMatch::new(remaining, iter::empty(), matched))
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
path.push(PathSegment::Static(self.0.into()))
}
}
#[cfg(test)]
mod tests {
use super::{PossibleRouteMatch, StaticSegment};
use alloc::vec::Vec;
#[test]
fn single_static_match() {
let path = "/foo";
let def = StaticSegment("foo");
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
assert!(params.is_empty());
}
#[test]
fn single_static_mismatch() {
let path = "/foo";
let def = StaticSegment("bar");
assert!(def.test(path).is_none());
}
#[test]
fn single_static_match_with_trailing_slash() {
let path = "/foo/";
let def = StaticSegment("foo");
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo");
assert_eq!(matched.remaining(), "/");
let params = matched.params().collect::<Vec<_>>();
assert!(params.is_empty());
}
#[test]
fn tuple_of_static_matches() {
let path = "/foo/bar";
let def = (StaticSegment("foo"), StaticSegment("bar"));
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
assert!(params.is_empty());
}
#[test]
fn tuple_static_mismatch() {
let path = "/foo/baz";
let def = (StaticSegment("foo"), StaticSegment("bar"));
assert!(def.test(path).is_none());
}
#[test]
fn arbitrary_nesting_of_tuples_has_no_effect_on_matching() {
let path = "/foo/bar";
let def = (
(),
(StaticSegment("foo")),
(),
((), ()),
StaticSegment("bar"),
(),
);
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params().collect::<Vec<_>>();
assert!(params.is_empty());
}
}

View File

@ -1,102 +0,0 @@
use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
use alloc::vec::Vec;
use core::iter::Chain;
macro_rules! chain_types {
($first:ty, $second:ty, ) => {
Chain<
$first,
<<$second as PossibleRouteMatch>::ParamsIter<'a> as IntoIterator>::IntoIter
>
};
($first:ty, $second:ty, $($rest:ty,)+) => {
chain_types!(
Chain<
$first,
<<$second as PossibleRouteMatch>::ParamsIter<'a> as IntoIterator>::IntoIter,
>,
$($rest,)+
)
}
}
macro_rules! tuples {
($first:ident => $($ty:ident),*) => {
impl<$first, $($ty),*> PossibleRouteMatch for ($first, $($ty,)*)
where
Self: core::fmt::Debug,
$first: PossibleRouteMatch,
$($ty: PossibleRouteMatch),*,
{
type ParamsIter<'a> = chain_types!(<<$first>::ParamsIter<'a> as IntoIterator>::IntoIter, $($ty,)*);
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a, Self::ParamsIter<'a>>> {
let mut matched_len = 0;
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
let remaining = path;
let PartialPathMatch {
remaining,
matched,
params
} = $first.test(remaining)?;
matched_len += matched.len();
let params_iter = params.into_iter();
$(
let PartialPathMatch {
remaining,
matched,
params
} = $ty.test(remaining)?;
matched_len += matched.len();
let params_iter = params_iter.chain(params);
)*
Some(PartialPathMatch {
remaining,
matched: &path[0..matched_len],
params: params_iter
})
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
$first.generate_path(path);
$(
$ty.generate_path(path);
)*
}
}
};
}
tuples!(A => B);
tuples!(A => B, C);
tuples!(A => B, C, D);
tuples!(A => B, C, D, E);
tuples!(A => B, C, D, E, F);
tuples!(A => B, C, D, E, F, G);
tuples!(A => B, C, D, E, F, G, H);
tuples!(A => B, C, D, E, F, G, H, I);
tuples!(A => B, C, D, E, F, G, H, I, J);
tuples!(A => B, C, D, E, F, G, H, I, J, K);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
tuples!(A => B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
/*tuples!(
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y
);
tuples!(
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y,
Z
);*/

View File

@ -1,398 +0,0 @@
#![no_std]
#[macro_use]
extern crate alloc;
mod path_segment;
use alloc::vec::Vec;
pub use path_segment::*;
mod horizontal;
mod nested;
mod vertical;
use alloc::borrow::Cow;
pub use horizontal::*;
pub use nested::*;
pub use vertical::*;
#[derive(Debug)]
pub struct Routes<Children> {
base: Option<Cow<'static, str>>,
children: Children,
}
impl<Children> Routes<Children> {
pub fn new(children: Children) -> Self {
Self {
base: None,
children,
}
}
pub fn new_with_base(
children: Children,
base: impl Into<Cow<'static, str>>,
) -> Self {
Self {
base: Some(base.into()),
children,
}
}
}
impl<'a, Children> Routes<Children>
where
Children: MatchNestedRoutes<'a>,
{
pub fn match_route(&'a self, path: &'a str) -> Option<Children::Match> {
let path = match &self.base {
None => path,
Some(base) => {
let (base, path) = if base.starts_with('/') {
(base.trim_start_matches('/'), path.trim_start_matches('/'))
} else {
(base.as_ref(), path)
};
match path.strip_prefix(base) {
Some(path) => path,
None => return None,
}
}
};
let (matched, remaining) = self.children.match_nested(path);
let matched = matched?;
if !(remaining.is_empty() || remaining == "/") {
None
} else {
Some(matched.1)
}
}
pub fn generate_routes(
&'a self,
) -> (
Option<&str>,
impl IntoIterator<Item = Vec<PathSegment>> + 'a,
) {
(self.base.as_deref(), self.children.generate_routes())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct RouteMatchId(pub(crate) u8);
pub trait MatchInterface<'a> {
type Params: IntoIterator<Item = (&'a str, &'a str)>;
type Child: MatchInterface<'a>;
type View;
fn as_id(&self) -> RouteMatchId;
fn as_matched(&self) -> &str;
fn to_params(&self) -> Self::Params;
fn into_child(self) -> Option<Self::Child>;
fn to_view(&self) -> Self::View;
}
pub trait MatchNestedRoutes<'a> {
type Data;
type Match: MatchInterface<'a>;
fn match_nested(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &'a str);
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_;
}
#[cfg(test)]
mod tests {
use super::{NestedRoute, ParamSegment, Routes};
use crate::{MatchInterface, PathSegment, StaticSegment, WildcardSegment};
use alloc::vec::Vec;
#[test]
pub fn matches_single_root_route() {
let routes = Routes::new(NestedRoute {
segments: StaticSegment("/"),
children: (),
data: (),
view: || (),
});
let matched = routes.match_route("/");
assert!(matched.is_some());
let matched = routes.match_route("");
assert!(matched.is_some());
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(paths, vec![vec![PathSegment::Static("/".into())]]);
}
#[test]
pub fn matches_nested_route() {
let routes = Routes::new(NestedRoute {
segments: StaticSegment(""),
children: NestedRoute {
segments: (StaticSegment("author"), StaticSegment("contact")),
children: (),
data: (),
view: "Contact Me",
},
data: (),
view: "Home",
});
// route generation
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(
paths,
vec![vec![
PathSegment::Static("".into()),
PathSegment::Static("author".into()),
PathSegment::Static("contact".into())
]]
);
let matched = routes.match_route("/author/contact").unwrap();
assert_eq!(matched.matched(), "");
assert_eq!(matched.into_child().unwrap().matched(), "/author/contact");
let matched = routes.match_route("/author/contact").unwrap();
let view = matched.to_view();
assert_eq!(*view, "Home");
assert_eq!(*matched.into_child().unwrap().to_view(), "Contact Me");
}
#[test]
pub fn does_not_match_incomplete_route() {
let routes = Routes::new(NestedRoute {
segments: StaticSegment(""),
children: NestedRoute {
segments: (StaticSegment("author"), StaticSegment("contact")),
children: (),
data: (),
view: "Contact Me",
},
data: (),
view: "Home",
});
let matched = routes.match_route("/");
assert!(matched.is_none());
}
#[test]
pub fn chooses_between_nested_routes() {
let routes = Routes::new((
NestedRoute {
segments: StaticSegment("/"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
},
NestedRoute {
segments: StaticSegment("about"),
children: (),
data: (),
view: || (),
},
),
data: (),
view: || (),
},
NestedRoute {
segments: StaticSegment("/blog"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
},
NestedRoute {
segments: (StaticSegment("post"), ParamSegment("id")),
children: (),
data: (),
view: || (),
},
),
data: (),
view: || (),
},
));
// generates routes correctly
let (base, paths) = routes.generate_routes();
assert_eq!(base, None);
let paths = paths.into_iter().collect::<Vec<_>>();
assert_eq!(
paths,
vec![
vec![
PathSegment::Static("/".into()),
PathSegment::Static("".into()),
],
vec![
PathSegment::Static("/".into()),
PathSegment::Static("about".into())
],
vec![
PathSegment::Static("/blog".into()),
PathSegment::Static("".into()),
],
vec![
PathSegment::Static("/blog".into()),
PathSegment::Static("post".into()),
PathSegment::Param("id".into())
]
]
);
let matched = routes.match_route("/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/blog").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("id", "42")]);
}
#[test]
pub fn arbitrary_nested_routes() {
let routes = Routes::new_with_base(
(
NestedRoute {
segments: StaticSegment("/"),
children: (
NestedRoute {
segments: StaticSegment("/"),
children: (),
data: (),
view: || (),
},
NestedRoute {
segments: StaticSegment("about"),
children: (),
data: (),
view: || (),
},
),
data: (),
view: || (),
},
NestedRoute {
segments: StaticSegment("/blog"),
children: (
NestedRoute {
segments: StaticSegment(""),
children: (),
data: (),
view: || (),
},
NestedRoute {
segments: StaticSegment("category"),
children: (),
data: (),
view: || (),
},
NestedRoute {
segments: (
StaticSegment("post"),
ParamSegment("id"),
),
children: (),
data: (),
view: || (),
},
),
data: (),
view: || (),
},
NestedRoute {
segments: (
StaticSegment("/contact"),
WildcardSegment("any"),
),
children: (),
data: (),
view: || (),
},
),
"/portfolio",
);
// generates routes correctly
let (base, _paths) = routes.generate_routes();
assert_eq!(base, Some("/portfolio"));
let matched = routes.match_route("/about");
assert!(matched.is_none());
let matched = routes.match_route("/portfolio/about").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert!(params.is_empty());
let matched = routes.match_route("/portfolio/blog/post/42").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("id", "42")]);
let matched = routes.match_route("/portfolio/contact").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("any", "")]);
let matched = routes.match_route("/portfolio/contact/foobar").unwrap();
let params = matched.to_params().collect::<Vec<_>>();
assert_eq!(params, vec![("any", "foobar")]);
}
}
#[derive(Debug)]
pub struct PartialPathMatch<'a, ParamsIter> {
pub(crate) remaining: &'a str,
pub(crate) params: ParamsIter,
pub(crate) matched: &'a str,
}
impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
pub fn new(
remaining: &'a str,
params: ParamsIter,
matched: &'a str,
) -> Self {
Self {
remaining,
params,
matched,
}
}
pub fn is_complete(&self) -> bool {
self.remaining.is_empty() || self.remaining == "/"
}
pub fn remaining(&self) -> &str {
self.remaining
}
pub fn params(self) -> ParamsIter {
self.params
}
pub fn matched(&self) -> &str {
self.matched
}
}

View File

@ -1,153 +0,0 @@
use super::{
MatchInterface, MatchNestedRoutes, PartialPathMatch, PossibleRouteMatch,
};
use crate::{PathSegment, RouteMatchId};
use alloc::vec::Vec;
use core::{fmt, iter};
mod tuples;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NestedRoute<Segments, Children, Data, View> {
pub segments: Segments,
pub children: Children,
pub data: Data,
pub view: View,
}
#[derive(PartialEq, Eq)]
pub struct NestedMatch<'a, ParamsIter, Child, View> {
id: RouteMatchId,
/// The portion of the full path matched only by this nested route.
matched: &'a str,
/// The map of params matched only by this nested route.
params: ParamsIter,
/// The nested route.
child: Child,
view: &'a View,
}
impl<'a, ParamsIter, Child, View> fmt::Debug
for NestedMatch<'a, ParamsIter, Child, View>
where
ParamsIter: fmt::Debug,
Child: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NestedMatch")
.field("matched", &self.matched)
.field("params", &self.params)
.field("child", &self.child)
.finish()
}
}
impl<'a, ParamsIter, Child, View> MatchInterface<'a>
for NestedMatch<'a, ParamsIter, Child, View>
where
ParamsIter: IntoIterator<Item = (&'a str, &'a str)> + Clone,
Child: MatchInterface<'a>,
{
type Params = ParamsIter;
type Child = Child;
type View = &'a View;
fn as_id(&self) -> RouteMatchId {
self.id
}
fn as_matched(&self) -> &str {
self.matched
}
fn to_params(&self) -> Self::Params {
self.params.clone()
}
fn into_child(self) -> Option<Self::Child> {
Some(self.child)
}
fn to_view(&self) -> Self::View {
self.view
}
}
impl<'a, ParamsIter, Child, View> NestedMatch<'a, ParamsIter, Child, View> {
pub fn matched(&self) -> &'a str {
self.matched
}
}
impl<'a, Segments, Children, Data, View> MatchNestedRoutes<'a>
for NestedRoute<Segments, Children, Data, View>
where
Segments: PossibleRouteMatch,
Children: MatchNestedRoutes<'a>,
<Segments::ParamsIter<'a> as IntoIterator>::IntoIter: Clone,
<<Children::Match as MatchInterface<'a>>::Params as IntoIterator>::IntoIter:
Clone,
Children: 'a,
View: 'a,
{
type Data = Data;
type Match = NestedMatch<'a, iter::Chain<
<Segments::ParamsIter<'a> as IntoIterator>::IntoIter,
<<Children::Match as MatchInterface<'a>>::Params as IntoIterator>::IntoIter,
>, Children::Match, View>;
fn match_nested(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
self.segments
.test(path)
.and_then(
|PartialPathMatch {
remaining,
params,
matched,
}| {
let (inner, remaining) =
self.children.match_nested(remaining);
let (id, inner) = inner?;
let params = params.into_iter();
if remaining.is_empty() || remaining == "/" {
Some((
Some((
id,
NestedMatch {
id,
matched,
params: params.chain(inner.to_params()),
child: inner,
view: &self.view,
},
)),
remaining,
))
} else {
None
}
},
)
.unwrap_or((None, path))
}
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
let mut segment_routes = Vec::new();
self.segments.generate_path(&mut segment_routes);
let segment_routes = segment_routes.into_iter();
let children_routes = self.children.generate_routes().into_iter();
children_routes.map(move |child_routes| {
segment_routes
.clone()
.chain(child_routes)
.filter(|seg| seg != &PathSegment::Unit)
.collect()
})
}
}

View File

@ -1,287 +0,0 @@
use crate::{MatchInterface, MatchNestedRoutes, PathSegment, RouteMatchId};
use alloc::vec::Vec;
use core::iter;
use either_of::*;
impl<'a> MatchInterface<'a> for () {
type Params = iter::Empty<(&'a str, &'a str)>;
type Child = ();
type View = ();
fn as_id(&self) -> RouteMatchId {
RouteMatchId(0)
}
fn as_matched(&self) -> &str {
""
}
fn to_params(&self) -> Self::Params {
iter::empty()
}
fn into_child(self) -> Option<Self::Child> {
None
}
fn to_view(&self) -> Self::View {}
}
impl<'a> MatchNestedRoutes<'a> for () {
type Data = ();
type Match = ();
fn match_nested(
&self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
(Some((RouteMatchId(0), ())), path)
}
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
iter::once(vec![PathSegment::Unit])
}
}
impl<'a, A> MatchInterface<'a> for (A,)
where
A: MatchInterface<'a>,
{
type Params = A::Params;
type Child = A::Child;
type View = A::View;
fn as_id(&self) -> RouteMatchId {
RouteMatchId(0)
}
fn as_matched(&self) -> &str {
self.0.as_matched()
}
fn to_params(&self) -> Self::Params {
self.0.to_params()
}
fn into_child(self) -> Option<Self::Child> {
self.0.into_child()
}
fn to_view(&self) -> Self::View {
self.0.to_view()
}
}
impl<'a, A> MatchNestedRoutes<'a> for (A,)
where
A: MatchNestedRoutes<'a>,
{
type Data = A::Data;
type Match = A::Match;
fn match_nested(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
self.0.match_nested(path)
}
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
self.0.generate_routes()
}
}
impl<'a, A, B> MatchInterface<'a> for Either<A, B>
where
A: MatchInterface<'a>,
B: MatchInterface<'a>,
{
type Params = Either<
<A::Params as IntoIterator>::IntoIter,
<B::Params as IntoIterator>::IntoIter,
>;
type Child = Either<A::Child, B::Child>;
type View = Either<A::View, B::View>;
fn as_id(&self) -> RouteMatchId {
match self {
Either::Left(_) => RouteMatchId(0),
Either::Right(_) => RouteMatchId(1),
}
}
fn as_matched(&self) -> &str {
match self {
Either::Left(i) => i.as_matched(),
Either::Right(i) => i.as_matched(),
}
}
fn to_params(&self) -> Self::Params {
match self {
Either::Left(i) => Either::Left(i.to_params().into_iter()),
Either::Right(i) => Either::Right(i.to_params().into_iter()),
}
}
fn into_child(self) -> Option<Self::Child> {
Some(match self {
Either::Left(i) => Either::Left(i.into_child()?),
Either::Right(i) => Either::Right(i.into_child()?),
})
}
fn to_view(&self) -> Self::View {
match self {
Either::Left(i) => Either::Left(i.to_view()),
Either::Right(i) => Either::Right(i.to_view()),
}
}
}
impl<'a, A, B> MatchNestedRoutes<'a> for (A, B)
where
A: MatchNestedRoutes<'a>,
B: MatchNestedRoutes<'a>,
{
type Data = (A::Data, B::Data);
type Match = Either<A::Match, B::Match>;
fn match_nested(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
#[allow(non_snake_case)]
let (A, B) = &self;
if let (Some((id, matched)), remaining) = A.match_nested(path) {
return (Some((id, Either::Left(matched))), remaining);
}
if let (Some((id, matched)), remaining) = B.match_nested(path) {
return (Some((id, Either::Right(matched))), remaining);
}
(None, path)
}
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
#![allow(non_snake_case)]
let (A, B) = &self;
let A = A.generate_routes().into_iter();
let B = B.generate_routes().into_iter();
A.chain(B)
}
}
macro_rules! chain_generated {
($first:expr, $second:expr, ) => {
$first.chain($second)
};
($first:expr, $second:ident, $($rest:ident,)+) => {
chain_generated!(
$first.chain($second),
$($rest,)+
)
}
}
macro_rules! tuples {
($either:ident => $($ty:ident = $count:expr),*) => {
impl<'a, $($ty,)*> MatchInterface<'a> for $either <$($ty,)*>
where
$($ty: MatchInterface<'a>),*,
$($ty::Child: 'a),*,
$($ty::View: 'a),*,
{
type Params = $either<$(
<$ty::Params as IntoIterator>::IntoIter,
)*>;
type Child = $either<$($ty::Child,)*>;
type View = $either<$($ty::View,)*>;
fn as_id(&self) -> RouteMatchId {
match self {
$($either::$ty(_) => RouteMatchId($count),)*
}
}
fn as_matched(&self) -> &str {
match self {
$($either::$ty(i) => i.as_matched(),)*
}
}
fn to_params(&self) -> Self::Params {
match self {
$($either::$ty(i) => $either::$ty(i.to_params().into_iter()),)*
}
}
fn into_child(self) -> Option<Self::Child> {
Some(match self {
$($either::$ty(i) => $either::$ty(i.into_child()?),)*
})
}
fn to_view(&self) -> Self::View {
match self {
$($either::$ty(i) => $either::$ty(i.to_view()),)*
}
}
}
impl<'a, $($ty),*> MatchNestedRoutes<'a> for ($($ty,)*)
where
$($ty: MatchNestedRoutes<'a>),*,
$($ty::Match: 'a),*,
{
type Data = ($($ty::Data,)*);
type Match = $either<$($ty::Match,)*>;
fn match_nested(&'a self, path: &'a str) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
#[allow(non_snake_case)]
let ($($ty,)*) = &self;
let mut id = 0;
$(if let (Some((_, matched)), remaining) = $ty.match_nested(path) {
return (Some((RouteMatchId(id), $either::$ty(matched))), remaining);
} else {
id += 1;
})*
(None, path)
}
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
#![allow(non_snake_case)]
let ($($ty,)*) = &self;
$(let $ty = $ty.generate_routes().into_iter();)*
chain_generated!($($ty,)*)
}
}
}
}
tuples!(EitherOf3 => A = 0, B = 1, C = 2);
tuples!(EitherOf4 => A = 0, B = 1, C = 2, D = 3);
tuples!(EitherOf5 => A = 0, B = 1, C = 2, D = 3, E = 4);
tuples!(EitherOf6 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5);
tuples!(EitherOf7 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6);
tuples!(EitherOf8 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7);
tuples!(EitherOf9 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8);
tuples!(EitherOf10 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9);
tuples!(EitherOf11 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10);
tuples!(EitherOf12 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11);
tuples!(EitherOf13 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12);
tuples!(EitherOf14 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13);
tuples!(EitherOf15 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14);
tuples!(EitherOf16 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15);

View File

@ -1,9 +0,0 @@
use alloc::borrow::Cow;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
Unit,
Static(Cow<'static, str>),
Param(Cow<'static, str>),
Splat(Cow<'static, str>),
}

View File

@ -1,10 +0,0 @@
use super::PartialPathMatch;
pub trait ChooseRoute {
fn choose_route<'a>(
&self,
path: &'a str,
) -> Option<
PartialPathMatch<'a, impl IntoIterator<Item = (&'a str, &'a str)>>,
>;
}