work on routing

This commit is contained in:
Greg Johnston 2024-02-24 11:52:40 -05:00
parent c1f4616a31
commit 1454c5d272
16 changed files with 939 additions and 44 deletions

View file

@ -27,6 +27,7 @@
- escaping HTML correctly (attributes + text nodes)
- router
- nested routes
- trailing slashes
- \_meta package (and use in hackernews)
- integrations
- update tests

View file

@ -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 {

View file

@ -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>
}
}

View file

@ -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]

View file

@ -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;

View 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)
}

View file

@ -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)]

View file

@ -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");
}
}

View 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>);
}

View 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()));
}
}

View 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());
}
}

View 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
);

View 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()
}
}

View file

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

View file

@ -1 +1,4 @@
extern crate alloc;
use alloc::{string::String, vec::Vec};
pub(crate) type Params<K> = Vec<(K, String)>;

View 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>),
}