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)
|
||||
- router
|
||||
- nested routes
|
||||
- trailing slashes
|
||||
- \_meta package (and use in hackernews)
|
||||
- integrations
|
||||
- update tests
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<meta name="color-scheme" content="light dark"/>
|
||||
</head>
|
||||
<style>
|
||||
img {
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
use futures::StreamExt;
|
||||
use gloo_timers::{
|
||||
callback::Interval,
|
||||
future::{IntervalStream, TimeoutFuture},
|
||||
};
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::{
|
||||
prelude::*,
|
||||
reactive_graph::{computed::AsyncDerived, owner::Stored, signal::RwSignal},
|
||||
reactive_graph::{computed::AsyncDerived, signal::RwSignal},
|
||||
tachys::log,
|
||||
view, Executor, IntoView,
|
||||
view, IntoView,
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::future::Future;
|
||||
use std::future::{Future, IntoFuture};
|
||||
|
||||
fn wait(
|
||||
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 {
|
||||
let a = RwSignal::new(0);
|
||||
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 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 a2 = wait('A', 1, a.get());
|
||||
//let b2 = wait('B', 3, b.get());
|
||||
let a_and_b = move || {
|
||||
async move { (a2.await, " + ", b2.await) }
|
||||
//async move { (a2.await, " + ", b2.await, " = ", c.await) }
|
||||
.suspend()
|
||||
.with_fallback("Loading...")
|
||||
.with_fallback("Loading A and B...")
|
||||
.track()
|
||||
};
|
||||
|
||||
let c = move || {
|
||||
c.into_future()
|
||||
.suspend()
|
||||
.with_fallback("Loading C...")
|
||||
.track()
|
||||
};
|
||||
|
||||
|
@ -53,8 +118,7 @@ pub fn async_example() -> impl IntoView {
|
|||
}>
|
||||
{b}
|
||||
</button>
|
||||
<p>
|
||||
{times}
|
||||
</p>
|
||||
<p> {a_and_b} </p>
|
||||
<p> {c} </p>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ tracing = { version = "0.1", optional = true }
|
|||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Document",
|
||||
"Window",
|
||||
"console",
|
||||
# History/Routing
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
|
@ -38,7 +41,6 @@ features = [
|
|||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"Window",
|
||||
]
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
#![no_std]
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
//mod generate_route_list;
|
||||
pub mod location;
|
||||
pub mod matching;
|
||||
pub mod params;
|
||||
mod path_segment;
|
||||
pub use path_segment::*;
|
||||
//pub mod matching;
|
||||
//cfg(feature = "reaccy")]
|
||||
//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 std::fmt::Debug;
|
||||
use alloc::string::String;
|
||||
use core::fmt::Debug;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
mod browser;
|
||||
mod server;
|
||||
pub use browser::*;
|
||||
pub use server::*;
|
||||
|
||||
pub(crate) const BASE: &str = "https://leptos.dev";
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Url {
|
||||
pub origin: String,
|
||||
pub pathname: String,
|
||||
pub search: String,
|
||||
pub search_params: Params<String>,
|
||||
pub hash: String,
|
||||
origin: String,
|
||||
path: String,
|
||||
search: String,
|
||||
search_params: Params<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.
|
||||
|
@ -47,13 +72,16 @@ pub trait Location {
|
|||
/// Sets up any global event listeners or other initialization needed.
|
||||
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);
|
||||
|
||||
/// Navigate to a new location.
|
||||
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)]
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
use super::{Location, LocationChange, Url};
|
||||
use alloc::string::{String, ToString};
|
||||
use core::fmt::Display;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RequestUrl(String);
|
||||
|
||||
impl RequestUrl {
|
||||
/// Creates a server-side request URL from a path, with an optional initial slash.
|
||||
pub fn from_path(path: impl AsRef<str>) -> Self {
|
||||
let path = path.as_ref().trim_start_matches('/');
|
||||
let mut string = String::with_capacity(path.len());
|
||||
string.push_str(path);
|
||||
Self(string)
|
||||
/// Creates a server-side request URL from a path.
|
||||
pub fn new(path: impl Display) -> Self {
|
||||
Self(path.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,25 +23,27 @@ impl Location for RequestUrl {
|
|||
|
||||
fn init(&self) {}
|
||||
|
||||
fn try_to_url_with_base(&self, base: &str) -> Result<Url, Self::Error> {
|
||||
let url = String::with_capacity(self.0.len() + 1 + base);
|
||||
let url = url::Url::parse(&self.0)?;
|
||||
fn set_navigation_hook(&mut self, _cb: impl FnMut(Url) + 'static) {}
|
||||
|
||||
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
|
||||
.query_pairs()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
Ok(Url {
|
||||
origin: url.origin().unicode_serialization(),
|
||||
pathname: url.path().to_string(),
|
||||
path: url.path().to_string(),
|
||||
search: url.query().unwrap_or_default().to_string(),
|
||||
search_params,
|
||||
hash: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn set_navigation_hook(&mut self, _cb: impl FnMut(Url) + 'static) {}
|
||||
|
||||
fn navigate(&self, _loc: &LocationChange) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -50,8 +51,22 @@ mod tests {
|
|||
use super::RequestUrl;
|
||||
use crate::location::Location;
|
||||
|
||||
#[test]
|
||||
pub fn should_parse_url_without_origin() {
|
||||
let req = RequestUrl::from_path("/foo/bar");
|
||||
let url = req.try_to_url().expect("could not parse URL");
|
||||
let url = RequestUrl::parse("/foo/bar").unwrap();
|
||||
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)>;
|
||||
|
|
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