mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
work on routing
This commit is contained in:
parent
c1f4616a31
commit
1454c5d272
16 changed files with 939 additions and 44 deletions
1
TODO.md
1
TODO.md
|
@ -27,6 +27,7 @@
|
||||||
- escaping HTML correctly (attributes + text nodes)
|
- escaping HTML correctly (attributes + text nodes)
|
||||||
- router
|
- router
|
||||||
- nested routes
|
- nested routes
|
||||||
|
- trailing slashes
|
||||||
- \_meta package (and use in hackernews)
|
- \_meta package (and use in hackernews)
|
||||||
- integrations
|
- integrations
|
||||||
- update tests
|
- update tests
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||||
|
<meta name="color-scheme" content="light dark"/>
|
||||||
</head>
|
</head>
|
||||||
<style>
|
<style>
|
||||||
img {
|
img {
|
||||||
|
@ -17,4 +18,4 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
use futures::StreamExt;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use gloo_timers::{
|
|
||||||
callback::Interval,
|
|
||||||
future::{IntervalStream, TimeoutFuture},
|
|
||||||
};
|
|
||||||
use leptos::{
|
use leptos::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
reactive_graph::{computed::AsyncDerived, owner::Stored, signal::RwSignal},
|
reactive_graph::{computed::AsyncDerived, signal::RwSignal},
|
||||||
tachys::log,
|
tachys::log,
|
||||||
view, Executor, IntoView,
|
view, IntoView,
|
||||||
};
|
};
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use std::future::Future;
|
use std::future::{Future, IntoFuture};
|
||||||
|
|
||||||
fn wait(
|
fn wait(
|
||||||
id: char,
|
id: char,
|
||||||
|
@ -24,21 +20,90 @@ fn wait(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sleep(seconds: u32) -> impl Future<Output = ()> + Send + Sync {
|
||||||
|
SendWrapper::new(async move {
|
||||||
|
TimeoutFuture::new(seconds * 1000).await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn async_example() -> impl IntoView {
|
pub fn async_example() -> impl IntoView {
|
||||||
let a = RwSignal::new(0);
|
let a = RwSignal::new(0);
|
||||||
let b = RwSignal::new(1);
|
let b = RwSignal::new(1);
|
||||||
|
|
||||||
|
let a2 = create_resource(a, |a| wait('A', 1, a));
|
||||||
|
let b2 = create_resource(b, |a| wait('A', 1, b));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<button on:click=move |_| {
|
||||||
|
a.update(|n| *n += 1);
|
||||||
|
}>
|
||||||
|
{a}
|
||||||
|
</button>
|
||||||
|
<button on:click=move |_| {
|
||||||
|
b.update(|n| *n += 1);
|
||||||
|
}>
|
||||||
|
{b}
|
||||||
|
</button>
|
||||||
|
<Suspense>
|
||||||
|
{move || a2().map(move |a2| {
|
||||||
|
b2().map(move |b2| {
|
||||||
|
view! {
|
||||||
|
<p>{a2} + {b2}</p>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
</Suspense>
|
||||||
|
<p>
|
||||||
|
//{times}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
pub fn notes() -> impl IntoView {
|
||||||
|
let a = RwSignal::new(0);
|
||||||
|
let b = RwSignal::new(1);
|
||||||
|
|
||||||
let a2 = AsyncDerived::new(move || wait('A', 1, a.get()));
|
let a2 = AsyncDerived::new(move || wait('A', 1, a.get()));
|
||||||
let b2 = AsyncDerived::new(move || wait('B', 3, b.get()));
|
let b2 = AsyncDerived::new(move || wait('B', 3, b.get()));
|
||||||
let c = AsyncDerived::new(move || async move { a2.await + b2.await });
|
let c = AsyncDerived::new(move || async move {
|
||||||
|
sleep(1).await;
|
||||||
|
a2.await + b2.await
|
||||||
|
});
|
||||||
|
|
||||||
let times = move || {
|
let a_and_b = move || {
|
||||||
//let a2 = wait('A', 1, a.get());
|
|
||||||
//let b2 = wait('B', 3, b.get());
|
|
||||||
async move { (a2.await, " + ", b2.await) }
|
async move { (a2.await, " + ", b2.await) }
|
||||||
//async move { (a2.await, " + ", b2.await, " = ", c.await) }
|
|
||||||
.suspend()
|
.suspend()
|
||||||
.with_fallback("Loading...")
|
.with_fallback("Loading A and B...")
|
||||||
|
.track()
|
||||||
|
};
|
||||||
|
|
||||||
|
let c = move || {
|
||||||
|
c.into_future()
|
||||||
|
.suspend()
|
||||||
|
.with_fallback("Loading C...")
|
||||||
.track()
|
.track()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -53,8 +118,7 @@ pub fn async_example() -> impl IntoView {
|
||||||
}>
|
}>
|
||||||
{b}
|
{b}
|
||||||
</button>
|
</button>
|
||||||
<p>
|
<p> {a_and_b} </p>
|
||||||
{times}
|
<p> {c} </p>
|
||||||
</p>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@ tracing = { version = "0.1", optional = true }
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
features = [
|
features = [
|
||||||
|
"Document",
|
||||||
|
"Window",
|
||||||
|
"console",
|
||||||
# History/Routing
|
# History/Routing
|
||||||
"History",
|
"History",
|
||||||
"HtmlAnchorElement",
|
"HtmlAnchorElement",
|
||||||
|
@ -38,7 +41,6 @@ features = [
|
||||||
"RequestInit",
|
"RequestInit",
|
||||||
"RequestMode",
|
"RequestMode",
|
||||||
"Response",
|
"Response",
|
||||||
"Window",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
//mod generate_route_list;
|
//mod generate_route_list;
|
||||||
pub mod location;
|
pub mod location;
|
||||||
|
pub mod matching;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
|
mod path_segment;
|
||||||
|
pub use path_segment::*;
|
||||||
//pub mod matching;
|
//pub mod matching;
|
||||||
//cfg(feature = "reaccy")]
|
//cfg(feature = "reaccy")]
|
||||||
//pub mod reactive;
|
//pub mod reactive;
|
||||||
|
|
279
routing/src/location/browser.rs
Normal file
279
routing/src/location/browser.rs
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
use super::{Location, LocationChange, State, Url, BASE};
|
||||||
|
use crate::params::Params;
|
||||||
|
use alloc::{borrow::Cow, boxed::Box, rc::Rc, string::String};
|
||||||
|
use core::fmt;
|
||||||
|
use js_sys::{try_iter, Array, JsString, Reflect};
|
||||||
|
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
|
||||||
|
use web_sys::{
|
||||||
|
Document, Event, HtmlAnchorElement, MouseEvent, UrlSearchParams, Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn document() -> Document {
|
||||||
|
window().document().expect(
|
||||||
|
"router cannot be used in a JS environment without a `document`",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window() -> Window {
|
||||||
|
web_sys::window()
|
||||||
|
.expect("router cannot be used in a JS environment without a `window`")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct BrowserUrl {
|
||||||
|
navigation_hook: Option<Rc<dyn Fn(Url)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for BrowserUrl {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("BrowserUrl").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowserUrl {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_current() -> Result<Url, JsValue> {
|
||||||
|
let location = window().location();
|
||||||
|
Ok(Url {
|
||||||
|
origin: location.origin()?,
|
||||||
|
path: location.pathname()?,
|
||||||
|
search: location
|
||||||
|
.search()?
|
||||||
|
.strip_prefix('?')
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
search_params: search_params_from_web_url(
|
||||||
|
&UrlSearchParams::new_with_str(&location.search()?)?,
|
||||||
|
)?,
|
||||||
|
hash: location.hash()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unescape(s: &str) -> String {
|
||||||
|
js_sys::decode_uri(s).unwrap().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_to_el(loc_scroll: bool) {
|
||||||
|
if let Ok(hash) = window().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 = document().get_element_by_id(&hash);
|
||||||
|
if let Some(el) = el {
|
||||||
|
el.scroll_into_view();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll to top
|
||||||
|
if loc_scroll {
|
||||||
|
window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Location for BrowserUrl {
|
||||||
|
type Error = JsValue;
|
||||||
|
|
||||||
|
fn init(&self) {
|
||||||
|
let this = self.clone();
|
||||||
|
let handle_anchor_click = move |ev: Event| {
|
||||||
|
let ev = ev.unchecked_into::<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<HtmlAnchorElement> = None;
|
||||||
|
for i in 0..composed_path.length() {
|
||||||
|
if let Ok(el) =
|
||||||
|
composed_path.get(i).dyn_into::<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 base = window()
|
||||||
|
.location()
|
||||||
|
.origin()
|
||||||
|
.map(Cow::Owned)
|
||||||
|
.unwrap_or(Cow::Borrowed(BASE));
|
||||||
|
let url = Self::parse_with_base(href.as_str(), &base).unwrap();
|
||||||
|
let path_name = Self::unescape(&url.path);
|
||||||
|
|
||||||
|
// let browser handle this event if it leaves our domain
|
||||||
|
// or our base path
|
||||||
|
if url.origin
|
||||||
|
!= window().location().origin().unwrap_or_default()
|
||||||
|
// TODO base path for router
|
||||||
|
/* || (true // TODO base_path //!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 { "?" }
|
||||||
|
+ &Self::unescape(&url.search)
|
||||||
|
+ &Self::unescape(&url.hash);
|
||||||
|
let state = Reflect::get(&a, &JsValue::from_str("state"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| {
|
||||||
|
if value == JsValue::UNDEFINED {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ev.prevent_default();
|
||||||
|
|
||||||
|
let replace = Reflect::get(&a, &JsValue::from_str("replace"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let change = LocationChange {
|
||||||
|
value: to,
|
||||||
|
replace,
|
||||||
|
scroll: true,
|
||||||
|
state: State(state),
|
||||||
|
};
|
||||||
|
|
||||||
|
// run any router-specific hook
|
||||||
|
if let Some(navigate_hook) = &this.navigation_hook {
|
||||||
|
navigate_hook(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete navigation
|
||||||
|
this.navigate(&change);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let closure = Closure::wrap(
|
||||||
|
Box::new(handle_anchor_click) as Box<dyn FnMut(Event)>
|
||||||
|
)
|
||||||
|
.into_js_value();
|
||||||
|
window()
|
||||||
|
.add_event_listener_with_callback(
|
||||||
|
"click",
|
||||||
|
closure.as_ref().unchecked_ref(),
|
||||||
|
)
|
||||||
|
.expect(
|
||||||
|
"couldn't add `click` listener to `window` to handle `<a>` \
|
||||||
|
clicks",
|
||||||
|
);
|
||||||
|
|
||||||
|
// handle popstate event (forward/back navigation)
|
||||||
|
if let Some(navigation_hook) = self.navigation_hook.clone() {
|
||||||
|
let cb = move || match Self::try_current() {
|
||||||
|
Ok(url) => navigation_hook(url),
|
||||||
|
Err(e) => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
web_sys::console::error_1(&e);
|
||||||
|
_ = e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let closure =
|
||||||
|
Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||||
|
window()
|
||||||
|
.add_event_listener_with_callback(
|
||||||
|
"popstate",
|
||||||
|
closure.as_ref().unchecked_ref(),
|
||||||
|
)
|
||||||
|
.expect("couldn't add `popstate` listener to `window`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_navigation_hook(&mut self, cb: impl Fn(Url) + 'static) {
|
||||||
|
self.navigation_hook = Some(Rc::new(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(&self, loc: &LocationChange) {
|
||||||
|
let history = window().history().unwrap();
|
||||||
|
|
||||||
|
if loc.replace {
|
||||||
|
history
|
||||||
|
.replace_state_with_url(
|
||||||
|
&loc.state.to_js_value(),
|
||||||
|
"",
|
||||||
|
Some(&loc.value),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
// push the "forward direction" marker
|
||||||
|
let state = &loc.state.to_js_value();
|
||||||
|
history
|
||||||
|
.push_state_with_url(state, "", Some(&loc.value))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
// scroll to el
|
||||||
|
Self::scroll_to_el(loc.scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error> {
|
||||||
|
let location = web_sys::Url::new_with_base(url, base)?;
|
||||||
|
Ok(Url {
|
||||||
|
origin: location.origin(),
|
||||||
|
path: location.pathname(),
|
||||||
|
search: location
|
||||||
|
.search()
|
||||||
|
.strip_prefix('?')
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
search_params: search_params_from_web_url(
|
||||||
|
&location.search_params(),
|
||||||
|
)?,
|
||||||
|
hash: location.hash(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_params_from_web_url(
|
||||||
|
params: &web_sys::UrlSearchParams,
|
||||||
|
) -> Result<Params<String>, JsValue> {
|
||||||
|
let mut search_params = Params::new();
|
||||||
|
for pair in try_iter(params)?.into_iter().flatten() {
|
||||||
|
let row = pair?.unchecked_into::<Array>();
|
||||||
|
search_params.push((
|
||||||
|
row.get(0).unchecked_into::<JsString>().into(),
|
||||||
|
row.get(1).unchecked_into::<JsString>().into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(search_params)
|
||||||
|
}
|
|
@ -1,19 +1,44 @@
|
||||||
use crate::params::Params;
|
use crate::params::Params;
|
||||||
use std::fmt::Debug;
|
use alloc::string::String;
|
||||||
|
use core::fmt::Debug;
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
|
mod browser;
|
||||||
mod server;
|
mod server;
|
||||||
|
pub use browser::*;
|
||||||
pub use server::*;
|
pub use server::*;
|
||||||
|
|
||||||
pub(crate) const BASE: &str = "https://leptos.dev";
|
pub(crate) const BASE: &str = "https://leptos.dev";
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
pub struct Url {
|
pub struct Url {
|
||||||
pub origin: String,
|
origin: String,
|
||||||
pub pathname: String,
|
path: String,
|
||||||
pub search: String,
|
search: String,
|
||||||
pub search_params: Params<String>,
|
search_params: Params<String>,
|
||||||
pub hash: String,
|
hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Url {
|
||||||
|
pub fn origin(&self) -> &str {
|
||||||
|
&self.origin
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search(&self) -> &str {
|
||||||
|
&self.search
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_params(&self) -> &Params<String> {
|
||||||
|
&self.search_params
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(&self) -> &str {
|
||||||
|
&self.hash
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A description of a navigation.
|
/// A description of a navigation.
|
||||||
|
@ -47,13 +72,16 @@ pub trait Location {
|
||||||
/// Sets up any global event listeners or other initialization needed.
|
/// Sets up any global event listeners or other initialization needed.
|
||||||
fn init(&self);
|
fn init(&self);
|
||||||
|
|
||||||
/// Returns the current URL.
|
|
||||||
fn try_to_url(&self) -> Result<Url, Self::Error>;
|
|
||||||
|
|
||||||
fn set_navigation_hook(&mut self, cb: impl Fn(Url) + 'static);
|
fn set_navigation_hook(&mut self, cb: impl Fn(Url) + 'static);
|
||||||
|
|
||||||
/// Navigate to a new location.
|
/// Navigate to a new location.
|
||||||
fn navigate(&self, loc: &LocationChange);
|
fn navigate(&self, loc: &LocationChange);
|
||||||
|
|
||||||
|
fn parse(url: &str) -> Result<Url, Self::Error> {
|
||||||
|
Self::parse_with_base(url, BASE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
use super::{Location, LocationChange, Url};
|
use super::{Location, LocationChange, Url};
|
||||||
|
use alloc::string::{String, ToString};
|
||||||
|
use core::fmt::Display;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct RequestUrl(String);
|
pub struct RequestUrl(String);
|
||||||
|
|
||||||
impl RequestUrl {
|
impl RequestUrl {
|
||||||
/// Creates a server-side request URL from a path, with an optional initial slash.
|
/// Creates a server-side request URL from a path.
|
||||||
pub fn from_path(path: impl AsRef<str>) -> Self {
|
pub fn new(path: impl Display) -> Self {
|
||||||
let path = path.as_ref().trim_start_matches('/');
|
Self(path.to_string())
|
||||||
let mut string = String::with_capacity(path.len());
|
|
||||||
string.push_str(path);
|
|
||||||
Self(string)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,25 +23,27 @@ impl Location for RequestUrl {
|
||||||
|
|
||||||
fn init(&self) {}
|
fn init(&self) {}
|
||||||
|
|
||||||
fn try_to_url_with_base(&self, base: &str) -> Result<Url, Self::Error> {
|
fn set_navigation_hook(&mut self, _cb: impl FnMut(Url) + 'static) {}
|
||||||
let url = String::with_capacity(self.0.len() + 1 + base);
|
|
||||||
let url = url::Url::parse(&self.0)?;
|
fn navigate(&self, _loc: &LocationChange) {}
|
||||||
|
|
||||||
|
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error> {
|
||||||
|
let base = url::Url::parse(base)?;
|
||||||
|
let url = url::Url::options().base_url(Some(&base)).parse(&url)?;
|
||||||
|
|
||||||
let search_params = url
|
let search_params = url
|
||||||
.query_pairs()
|
.query_pairs()
|
||||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Url {
|
Ok(Url {
|
||||||
origin: url.origin().unicode_serialization(),
|
origin: url.origin().unicode_serialization(),
|
||||||
pathname: url.path().to_string(),
|
path: url.path().to_string(),
|
||||||
search: url.query().unwrap_or_default().to_string(),
|
search: url.query().unwrap_or_default().to_string(),
|
||||||
search_params,
|
search_params,
|
||||||
hash: Default::default(),
|
hash: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_navigation_hook(&mut self, _cb: impl FnMut(Url) + 'static) {}
|
|
||||||
|
|
||||||
fn navigate(&self, _loc: &LocationChange) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -50,8 +51,22 @@ mod tests {
|
||||||
use super::RequestUrl;
|
use super::RequestUrl;
|
||||||
use crate::location::Location;
|
use crate::location::Location;
|
||||||
|
|
||||||
|
#[test]
|
||||||
pub fn should_parse_url_without_origin() {
|
pub fn should_parse_url_without_origin() {
|
||||||
let req = RequestUrl::from_path("/foo/bar");
|
let url = RequestUrl::parse("/foo/bar").unwrap();
|
||||||
let url = req.try_to_url().expect("could not parse URL");
|
assert_eq!(url.path(), "/foo/bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn should_not_parse_url_without_slash() {
|
||||||
|
let url = RequestUrl::parse("foo/bar").unwrap();
|
||||||
|
assert_eq!(url.path(), "/foo/bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn should_parse_with_base() {
|
||||||
|
let url = RequestUrl::parse("https://www.example.com/foo/bar").unwrap();
|
||||||
|
assert_eq!(url.origin(), "https://www.example.com");
|
||||||
|
assert_eq!(url.path(), "/foo/bar");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
28
routing/src/matching/horizontal/mod.rs
Normal file
28
routing/src/matching/horizontal/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use crate::PathSegment;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use core::str::Chars;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fn matches(&self, path: &str) -> bool {
|
||||||
|
self.matches_iter(&mut path.chars())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_iter(&self, path: &mut Chars) -> bool;
|
||||||
|
|
||||||
|
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
|
||||||
|
|
||||||
|
fn generate_path(&self, path: &mut Vec<PathSegment>);
|
||||||
|
}
|
148
routing/src/matching/horizontal/param_segments.rs
Normal file
148
routing/src/matching/horizontal/param_segments.rs
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
use super::{PartialPathMatch, PossibleRouteMatch};
|
||||||
|
use crate::PathSegment;
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
use core::str::Chars;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ParamSegment(pub &'static str);
|
||||||
|
|
||||||
|
impl PossibleRouteMatch for ParamSegment {
|
||||||
|
fn matches_iter(&self, test: &mut Chars) -> bool {
|
||||||
|
let mut test = test.peekable();
|
||||||
|
// match an initial /
|
||||||
|
if test.peek() == Some(&'/') {
|
||||||
|
test.next();
|
||||||
|
}
|
||||||
|
for char in test {
|
||||||
|
// when we get a closing /, stop matching
|
||||||
|
if char == '/' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||||
|
let mut matched = String::new();
|
||||||
|
let mut param_value = String::new();
|
||||||
|
let mut test = path.chars();
|
||||||
|
|
||||||
|
// match an initial /
|
||||||
|
if let Some('/') = test.next() {
|
||||||
|
matched.push('/');
|
||||||
|
}
|
||||||
|
for char in test {
|
||||||
|
// when we get a closing /, stop matching
|
||||||
|
if char == '/' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// otherwise, push into the matched param
|
||||||
|
else {
|
||||||
|
matched.push(char);
|
||||||
|
param_value.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_index = matched.len();
|
||||||
|
Some(PartialPathMatch::new(
|
||||||
|
&path[next_index..],
|
||||||
|
vec![(self.0, 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 {
|
||||||
|
fn matches_iter(&self, _path: &mut Chars) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||||
|
let mut matched = String::new();
|
||||||
|
let mut param_value = String::new();
|
||||||
|
let mut test = path.chars();
|
||||||
|
|
||||||
|
// match an initial /
|
||||||
|
if let Some('/') = test.next() {
|
||||||
|
matched.push('/');
|
||||||
|
}
|
||||||
|
for char in test {
|
||||||
|
matched.push(char);
|
||||||
|
param_value.push(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_index = matched.len();
|
||||||
|
Some(PartialPathMatch::new(
|
||||||
|
&path[next_index..],
|
||||||
|
vec![(self.0, 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::matching::{ParamSegment, StaticSegment, WildcardSegment};
|
||||||
|
use alloc::string::ToString;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_param_match() {
|
||||||
|
let path = "/foo";
|
||||||
|
let def = ParamSegment("a");
|
||||||
|
assert!(def.matches(path));
|
||||||
|
let matched = def.test(path).expect("couldn't match route");
|
||||||
|
assert_eq!(matched.matched(), "/foo");
|
||||||
|
assert_eq!(matched.remaining(), "");
|
||||||
|
assert_eq!(matched.params()[0], ("a", "foo".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_param_match_with_trailing_slash() {
|
||||||
|
let path = "/foo/";
|
||||||
|
let def = ParamSegment("a");
|
||||||
|
assert!(def.matches(path));
|
||||||
|
let matched = def.test(path).expect("couldn't match route");
|
||||||
|
assert_eq!(matched.matched(), "/foo");
|
||||||
|
assert_eq!(matched.remaining(), "/");
|
||||||
|
assert_eq!(matched.params()[0], ("a", "foo".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tuple_of_param_matches() {
|
||||||
|
let path = "/foo/bar";
|
||||||
|
let def = (ParamSegment("a"), ParamSegment("b"));
|
||||||
|
assert!(def.matches(path));
|
||||||
|
let matched = def.test(path).expect("couldn't match route");
|
||||||
|
assert_eq!(matched.matched(), "/foo/bar");
|
||||||
|
assert_eq!(matched.remaining(), "");
|
||||||
|
assert_eq!(matched.params()[0], ("a", "foo".to_string()));
|
||||||
|
assert_eq!(matched.params()[1], ("b", "bar".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn splat_should_match_all() {
|
||||||
|
let path = "/foo/bar/////";
|
||||||
|
let def = (
|
||||||
|
StaticSegment("foo"),
|
||||||
|
StaticSegment("bar"),
|
||||||
|
WildcardSegment("rest"),
|
||||||
|
);
|
||||||
|
assert!(def.matches(path));
|
||||||
|
let matched = def.test(path).expect("couldn't match route");
|
||||||
|
assert_eq!(matched.matched(), "/foo/bar/////");
|
||||||
|
assert_eq!(matched.remaining(), "");
|
||||||
|
assert_eq!(matched.params()[0], ("rest", "////".to_string()));
|
||||||
|
}
|
||||||
|
}
|
159
routing/src/matching/horizontal/static_segment.rs
Normal file
159
routing/src/matching/horizontal/static_segment.rs
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
use super::{PartialPathMatch, PossibleRouteMatch};
|
||||||
|
use crate::PathSegment;
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
use core::str::Chars;
|
||||||
|
|
||||||
|
impl PossibleRouteMatch for () {
|
||||||
|
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||||
|
Some(PartialPathMatch::new(path, [], ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_iter(&self, _path: &mut Chars) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fn matches_iter(&self, test: &mut Chars) -> bool {
|
||||||
|
let mut this = self.0.chars();
|
||||||
|
let mut test = test.peekable();
|
||||||
|
if test.peek() == Some(&'/') {
|
||||||
|
test.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// unless this segment is empty, we start by
|
||||||
|
// assuming that it has not actually matched
|
||||||
|
let mut has_matched = self.0.is_empty();
|
||||||
|
for char in test {
|
||||||
|
// when we get a closing /, stop matching
|
||||||
|
if char == '/' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// if the next character in the path doesn't match the
|
||||||
|
// next character in the segment, we don't match
|
||||||
|
else if this.next() != Some(char) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
has_matched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has_matched
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||||
|
let mut matched = String::new();
|
||||||
|
let mut test = path.chars();
|
||||||
|
let mut this = self.0.chars();
|
||||||
|
|
||||||
|
// match an initial /
|
||||||
|
if let Some('/') = test.next() {
|
||||||
|
matched.push('/');
|
||||||
|
}
|
||||||
|
for char in test {
|
||||||
|
// when we get a closing /, stop matching
|
||||||
|
if char == '/' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// if the next character in the path matches the
|
||||||
|
// next character in the segment, add it to the match
|
||||||
|
else if Some(char) == this.next() {
|
||||||
|
matched.push(char);
|
||||||
|
}
|
||||||
|
// 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 next_index = matched.len();
|
||||||
|
Some(PartialPathMatch::new(
|
||||||
|
&path[next_index..],
|
||||||
|
Vec::new(),
|
||||||
|
matched,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||||
|
path.push(PathSegment::Static(self.0.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{PossibleRouteMatch, StaticSegment};
|
||||||
|
|
||||||
|
#[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(), "");
|
||||||
|
assert!(matched.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");
|
||||||
|
assert!(def.matches(path));
|
||||||
|
let matched = def.test(path).expect("couldn't match route");
|
||||||
|
assert_eq!(matched.matched(), "/foo");
|
||||||
|
assert_eq!(matched.remaining(), "/");
|
||||||
|
assert!(matched.params().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tuple_of_static_matches() {
|
||||||
|
let path = "/foo/bar";
|
||||||
|
let def = (StaticSegment("foo"), StaticSegment("bar"));
|
||||||
|
assert!(def.matches(path));
|
||||||
|
let matched = def.test(path).expect("couldn't match route");
|
||||||
|
assert_eq!(matched.matched(), "/foo/bar");
|
||||||
|
assert_eq!(matched.remaining(), "");
|
||||||
|
assert!(matched.params().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tuple_static_mismatch() {
|
||||||
|
let path = "/foo/baz";
|
||||||
|
let def = (StaticSegment("foo"), StaticSegment("bar"));
|
||||||
|
assert!(!def.matches(path));
|
||||||
|
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"),
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
assert!(def.matches(path));
|
||||||
|
let matched = def.test(path).expect("couldn't match route");
|
||||||
|
assert_eq!(matched.matched(), "/foo/bar");
|
||||||
|
assert_eq!(matched.remaining(), "");
|
||||||
|
assert!(matched.params().is_empty());
|
||||||
|
}
|
||||||
|
}
|
81
routing/src/matching/horizontal/tuples.rs
Normal file
81
routing/src/matching/horizontal/tuples.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
use core::str::Chars;
|
||||||
|
|
||||||
|
macro_rules! tuples {
|
||||||
|
($($ty:ident),*) => {
|
||||||
|
impl<$($ty),*> PossibleRouteMatch for ($($ty,)*)
|
||||||
|
where
|
||||||
|
$($ty: PossibleRouteMatch),*,
|
||||||
|
{
|
||||||
|
fn matches_iter(&self, path: &mut Chars) -> bool
|
||||||
|
{
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let ($($ty,)*) = &self;
|
||||||
|
$($ty.matches_iter(path) &&)* true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>
|
||||||
|
{
|
||||||
|
let mut full_params = Vec::new();
|
||||||
|
let mut full_matched = String::new();
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let ($($ty,)*) = &self;
|
||||||
|
$(
|
||||||
|
let PartialPathMatch {
|
||||||
|
remaining,
|
||||||
|
matched,
|
||||||
|
params
|
||||||
|
} = $ty.test(path)?;
|
||||||
|
let path = remaining;
|
||||||
|
full_matched.push_str(&matched);
|
||||||
|
full_params.extend(params);
|
||||||
|
)*
|
||||||
|
Some(PartialPathMatch {
|
||||||
|
remaining: path,
|
||||||
|
matched: full_matched,
|
||||||
|
params: full_params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let ($($ty,)*) = &self;
|
||||||
|
$(
|
||||||
|
$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
|
||||||
|
);
|
65
routing/src/matching/mod.rs
Normal file
65
routing/src/matching/mod.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
mod horizontal;
|
||||||
|
mod vertical;
|
||||||
|
use crate::params::Params;
|
||||||
|
use alloc::{borrow::Cow, string::String, vec::Vec};
|
||||||
|
pub use horizontal::*;
|
||||||
|
pub use vertical::*;
|
||||||
|
|
||||||
|
pub struct Routes<Children> {
|
||||||
|
base: Cow<'static, str>,
|
||||||
|
children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RouteMatch<'a> {
|
||||||
|
matched_nested_routes: Vec<NestedRouteMatch<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NestedRouteMatch<'a> {
|
||||||
|
/// The portion of the full path matched by this nested route.
|
||||||
|
matched_path: String,
|
||||||
|
/// The map of params matched by this route.
|
||||||
|
params: Params<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NestedRoute<Segments, Children> {
|
||||||
|
segments: Segments,
|
||||||
|
children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PartialPathMatch<'a> {
|
||||||
|
pub(crate) remaining: &'a str,
|
||||||
|
pub(crate) params: Params<&'static str>,
|
||||||
|
pub(crate) matched: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PartialPathMatch<'a> {
|
||||||
|
pub fn new(
|
||||||
|
remaining: &'a str,
|
||||||
|
params: impl Into<Params<&'static str>>,
|
||||||
|
matched: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
remaining,
|
||||||
|
params: params.into(),
|
||||||
|
matched: matched.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_complete(&self) -> bool {
|
||||||
|
self.remaining.is_empty() || self.remaining == "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remaining(&self) -> &str {
|
||||||
|
self.remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn params(&self) -> &[(&'static str, String)] {
|
||||||
|
&self.params
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matched(&self) -> &str {
|
||||||
|
self.matched.as_str()
|
||||||
|
}
|
||||||
|
}
|
5
routing/src/matching/vertical/mod.rs
Normal file
5
routing/src/matching/vertical/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use super::PartialPathMatch;
|
||||||
|
|
||||||
|
pub trait ChooseRoute {
|
||||||
|
fn choose_route<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
|
||||||
|
}
|
|
@ -1 +1,4 @@
|
||||||
|
extern crate alloc;
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
pub(crate) type Params<K> = Vec<(K, String)>;
|
pub(crate) type Params<K> = Vec<(K, String)>;
|
||||||
|
|
8
routing/src/path_segment.rs
Normal file
8
routing/src/path_segment.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
use alloc::borrow::Cow;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum PathSegment {
|
||||||
|
Static(Cow<'static, str>),
|
||||||
|
Param(Cow<'static, str>),
|
||||||
|
Splat(Cow<'static, str>),
|
||||||
|
}
|
Loading…
Reference in a new issue