Rewrite router (#1791)

* rewrite router

* add support for 404 routes

* support base urls

* parse query params

* don't use js snippets lol

* cleanup code, update example

* bruh fmt

* test router

* add more tests

* wasm_test feature, CI

* Add rustdocs

* update docs on website

* use enum for routes, add derive macro for it

* fix 404 handling

* fix tests

* formatting

* update docs, little cleanup

* fix example

* misc fixes

* add routable macro tests

* document routable macro, rustfmt

* fix test, add makefile

* Replace the children based API with callback based one

* update example

* update docs

* update Cargo.toml

* clippy & fmt

* cleanup code

* apply review

* more fixes from review

* fix warnings

* replace function component with struct component, update docs

* formatting

* use `href` getter instead of reading attribute

* apply review

* use serde to parse query parameters

* use js_sys::Array for search_params + formatting

* fix doc test

* Apply suggestions from code review

Co-authored-by: Simon <simon@siku2.io>

* Update docs/concepts/router.md

apply suggestion

Co-authored-by: Simon <simon@siku2.io>

* apply review (part 2)

* use serde for parsing query

* a more modular implementation

* docs for query parameters

* fix doc

* Apply suggestions from code review

Co-authored-by: Simon <simon@siku2.io>

* fixes (from review)

* formatting

* use new functions

* not_found returns `Option<Self>`, to_route -> to_path

* Apply suggestions from code review

Co-authored-by: Simon <simon@siku2.io>

* remove PartialEq + Clone bound

* docs

* fix example

Co-authored-by: Simon <simon@siku2.io>
This commit is contained in:
Muhammad Hamza 2021-05-17 20:39:12 +05:00 committed by GitHub
parent 09a41d6903
commit af440761ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1103 additions and 5283 deletions

View File

@ -158,6 +158,11 @@ jobs:
cd packages/yew
wasm-pack test --chrome --firefox --headless -- --features "wasm_test"
- name: Run tests - yew-router
run: |
cd packages/yew-router
wasm-pack test --chrome --firefox --headless
- name: Run tests - yew-functional
run: |
cd packages/yew-functional

View File

@ -9,7 +9,6 @@ members = [
# Router
"packages/yew-router",
"packages/yew-router-macro",
"packages/yew-router-route-parser",
# Function components
"packages/yew-functional",

View File

@ -5,87 +5,79 @@ description: Yew's official router
[The router on crates.io](https://crates.io/crates/yew-router)
Routers in Single Page Applications \(SPA\) handle displaying different pages depending on what the URL is. Instead of the default behavior of requesting a different remote resource when a link is clicked, the router instead sets the URL locally to point to a valid route in your application. The router then detects this change and then decides what to render.
Routers in Single Page Applications (SPA) handle displaying different pages depending on what the URL is.
Instead of the default behavior of requesting a different remote resource when a link is clicked,
the router instead sets the URL locally to point to a valid route in your application.
The router then detects this change and then decides what to render.
## Core Elements
## Usage
### `Route`
Contains a String representing everything after the domain in the url and optionally the state stored in the history API.
### `RouteService`
Communicates with the browser to get and set Routes.
### `RouteAgent`
Owns a RouteService and is used to coordinate updates when the route changes, either from within the application logic or from an event fired from the browser.
### `Switch`
The `Switch` trait is used to convert a `Route` to and from the implementer of this trait.
### `Router`
The Router component communicates with `RouteAgent` and will automatically resolve Routes it gets from the agent into switches, which it will expose via a `render` prop that allows specifying how the resulting switch gets converted to `Html`.
## How to use the router
First, you want to create a type that represents all the states of your application. Do note that while this typically is an enum, structs are supported as well, and that you can nest other items that implement `Switch` inside.
Then you should derive `Switch` for your type. For enums, every variant must be annotated with `
#[at = "/some/route"]` (or `#[at = "/some/route"]`, but this is being phased out in favor of "at"),
or if you use a struct instead, that must appear outside the struct declaration.
The Router component. It takes in a callback and renders the HTML based on the returned value of the callback. It is usually placed
at the top of the application.
Routes are defined by an `enum` which derives `Routable`:
```rust
#[derive(Switch)]
enum AppRoute {
#[at = "/login"]
Login,
#[at = "/register"]
Register,
#[at = "/delete_account"]
Delete,
#[at = "/posts/{id}"]
ViewPost(i32),
#[at = "/posts/view"]
ViewPosts,
#[at = "/"]
Home
#[derive(Routable)]
enum Route {
#[at("/")]
Home,
#[at("/secure")]
Secure,
#[not_found]
#[at("/404")]
NotFound,
}
```
:::caution
Do note that the implementation generated by the derive macro for `Switch` will try to match each
variant in order from first to last, so if any route could possibly match two of your specified
`to` annotations, then the first one will match, and the second will never be tried. For example,
if you defined the following `Switch`, the only route that would be matched would be
`AppRoute::Home`.
The `Router` component takes the `Routable` enum as its type parameter, finds the first variant whose path matches the
browser's current URL and passes it to the `render` callback. The callback then decides what to render.
In case no path is matched, the router navigates to the path with `not_found` attribute. If no route is specified,
nothing is rendered, and a message is logged to console stating that no route was matched.
`yew_router::current_route` is used to programmatically obtain the current route.
`yew_router::attach_route_listener` is used to attach a listener which is called every time route is changed.
```rust
#[derive(Switch)]
enum AppRoute {
#[at = "/"]
Home,
#[at = "/login"]
Login,
#[at = "/register"]
Register,
#[at = "/delete_account"]
Delete,
#[at = "/posts/{id}"]
ViewPost(i32),
#[at = "/posts/view"]
ViewPosts,
#[function_component(Main)]
fn app() -> Html {
html! {
<Router<Route> render=Router::render(switch) />
}
}
fn switch(route: &Route) -> Html {
match route {
Route::Home => html! { <h1>{ "Home" }</h1> },
Route::Secure => {
let callback = Callback::from(|_| yew_router::push_route(Routes::Home));
html! {
<div>
<h1>{ "Secure" }</h1>
<button onclick=callback>{ "Go Home" }</button>
</div>
}
},
Route::NotFound => html! { <h1>{ "404" }</h1> },
}
}
```
:::
You can also capture sections using variations of `{}` within your `#[at = ""]` annotation. `{}` means capture text until the next separator \(either "/", "?", "&", or "\#" depending on the context\). `{*}` means capture text until the following characters match, or if no characters are present, it will match anything. `{<number>}` means capture text until the specified number of separators are encountered \(example: `{2}` will capture until two separators are encountered\).
### Navigation
For structs and enums with named fields, you must specify the field's name within the capture group like so: `{user_name}` or `{*:age}`.
To navigate between pages, use either a `Link` component (which renders a `<a>` element) or the `yew_router::push_route` function.
The Switch trait works with capture groups that are more structured than just Strings. You can specify any type that implements `Switch`. So you can specify that the capture group is a `usize`, and if the captured section of the URL can't be converted to it, then the variant won't match.
### Query Parameters
#### Specifying query parameters when navigating
In order to specify query parameters when navigating to a new route, use `yew_router::push_route_with_query` function.
It uses `serde` to serialize the parameters into query string for the URL so any type that implements `Serialize` can be passed.
In its simplest form this is just a `HashMap` containing string pairs.
#### Obtaining query parameters for current route
`yew_router::parse_query` is used to obtain the query parameters.
It uses `serde` to deserialize the parameters from query string in the URL.
## Relevant examples
- [Router](https://github.com/yewstack/yew/tree/master/examples/router)

View File

@ -15,3 +15,4 @@ yew = { path = "../../packages/yew" }
yew-router = { path = "../../packages/yew-router" }
yewtil = { path = "../../packages/yewtil" }
yew-services = { path = "../../packages/yew-services" }
serde = { version = "1.0", features = ["derive"] }

View File

@ -1,9 +1,6 @@
use crate::{
content::Author,
generator::Generated,
switch::{AppAnchor, AppRoute},
};
use crate::{content::Author, generator::Generated, Route};
use yew::prelude::*;
use yew_router::prelude::*;
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct Props {
@ -57,9 +54,9 @@ impl Component for AuthorCard {
</div>
</div>
<footer class="card-footer">
<AppAnchor classes="card-footer-item" route=AppRoute::Author(author.seed)>
<Link<Route> classes=classes!("card-footer-item") route=Route::Author { id: author.seed }>
{ "Profile" }
</AppAnchor>
</Link<Route>>
</footer>
</div>
}

View File

@ -1,9 +1,6 @@
use crate::{
content::Post,
generator::Generated,
switch::{AppAnchor, AppRoute},
};
use crate::{content::Post, generator::Generated, Route};
use yew::prelude::*;
use yew_router::components::Link;
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct Props {
@ -46,12 +43,12 @@ impl Component for PostCard {
</figure>
</div>
<div class="card-content">
<AppAnchor classes="title is-block" route=AppRoute::Post(post.seed)>
<Link<Route> classes=classes!("title", "is-block") route=Route::Post { id: post.seed }>
{ &post.title }
</AppAnchor>
<AppAnchor classes="subtitle is-block" route=AppRoute::Author(post.author.seed)>
</Link<Route>>
<Link<Route> classes=classes!("subtitle", "is-block") route=Route::Author { id: post.author.seed }>
{ &post.author.name }
</AppAnchor>
</Link<Route>>
</div>
</div>
}

View File

@ -1,5 +1,5 @@
use yew::prelude::*;
use yew_router::{route::Route, switch::Permissive};
use yew_router::prelude::*;
mod components;
mod content;
@ -9,8 +9,23 @@ use pages::{
author::Author, author_list::AuthorList, home::Home, page_not_found::PageNotFound, post::Post,
post_list::PostList,
};
mod switch;
use switch::{AppAnchor, AppRoute, AppRouter, PublicUrlSwitch};
#[derive(Routable, PartialEq, Clone, Debug)]
pub enum Route {
#[at("/posts/:id")]
Post { id: u64 },
#[at("/posts")]
Posts,
#[at("/authors/:id")]
Author { id: u64 },
#[at("/authors")]
Authors,
#[at("/")]
Home,
#[not_found]
#[at("/404")]
NotFound,
}
pub enum Msg {
ToggleNavbar,
@ -50,12 +65,7 @@ impl Component for Model {
{ self.view_nav() }
<main>
<AppRouter
render=AppRouter::render(Self::switch)
redirect=AppRouter::redirect(|route: Route| {
AppRoute::PageNotFound(Permissive(Some(route.route))).into_public()
})
/>
<Router<Route> render=Router::render(switch) />
</main>
<footer class="footer">
<div class="content has-text-centered">
@ -98,12 +108,12 @@ impl Model {
</div>
<div class=classes!("navbar-menu", active_class)>
<div class="navbar-start">
<AppAnchor classes="navbar-item" route=AppRoute::Home>
<Link<Route> classes=classes!("navbar-item") route=Route::Home>
{ "Home" }
</AppAnchor>
<AppAnchor classes="navbar-item" route=AppRoute::PostList>
</Link<Route>>
<Link<Route> classes=classes!("navbar-item") route=Route::Posts>
{ "Posts" }
</AppAnchor>
</Link<Route>>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
@ -111,9 +121,9 @@ impl Model {
</a>
<div class="navbar-dropdown">
<a class="navbar-item">
<AppAnchor classes="navbar-item" route=AppRoute::AuthorList>
<Link<Route> classes=classes!("navbar-item") route=Route::Authors>
{ "Meet the authors" }
</AppAnchor>
</Link<Route>>
</a>
</div>
</div>
@ -122,30 +132,27 @@ impl Model {
</nav>
}
}
}
fn switch(switch: PublicUrlSwitch) -> Html {
match switch.route() {
AppRoute::Post(id) => {
html! { <Post seed=id /> }
}
AppRoute::PostListPage(page) => {
html! { <PostList page=page.max(1) /> }
}
AppRoute::PostList => {
html! { <PostList page=1 /> }
}
AppRoute::Author(id) => {
html! { <Author seed=id /> }
}
AppRoute::AuthorList => {
html! { <AuthorList /> }
}
AppRoute::Home => {
html! { <Home /> }
}
AppRoute::PageNotFound(Permissive(route)) => {
html! { <PageNotFound route=route /> }
}
fn switch(routes: &Route) -> Html {
match routes {
Route::Post { id } => {
html! { <Post seed=*id /> }
}
Route::Posts => {
html! { <PostList /> }
}
Route::Author { id } => {
html! { <Author seed=*id /> }
}
Route::Authors => {
html! { <AuthorList /> }
}
Route::Home => {
html! { <Home /> }
}
Route::NotFound => {
html! { <PageNotFound /> }
}
}
}

View File

@ -1,28 +1,21 @@
use yew::prelude::*;
use yewtil::NeqAssign;
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
pub struct Props {
pub route: Option<String>,
}
pub struct PageNotFound;
pub struct PageNotFound {
props: Props,
}
impl Component for PageNotFound {
type Message = ();
type Properties = Props;
type Properties = ();
fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
Self { props }
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
Self
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
unimplemented!()
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props.neq_assign(props)
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
@ -34,7 +27,7 @@ impl Component for PageNotFound {
{ "Page not found" }
</h1>
<h2 class="subtitle">
{ "This page does not seem to exist" }
{ "Page page does not seem to exist" }
</h2>
</div>
</div>

View File

@ -1,10 +1,7 @@
use crate::{
content,
generator::Generated,
switch::{AppAnchor, AppRoute},
};
use crate::{content, generator::Generated, Route};
use content::PostPart;
use yew::prelude::*;
use yew_router::prelude::*;
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
pub struct Props {
@ -56,9 +53,9 @@ impl Component for Post {
</h1>
<h2 class="subtitle">
{ "by " }
<AppAnchor classes="has-text-weight-semibold" route=AppRoute::Author(post.author.seed)>
<Link<Route> classes=classes!("has-text-weight-semibold") route=Route::Author { id: post.author.seed }>
{ &post.author.name }
</AppAnchor>
</Link<Route>>
</h2>
<div class="tags">
{ for keywords }
@ -84,9 +81,9 @@ impl Post {
</figure>
<div class="media-content">
<div class="content">
<AppAnchor classes="is-size-5" route=AppRoute::Author(quote.author.seed)>
<Link<Route> classes=classes!("is-size-5") route=Route::Author { id: quote.author.seed }>
<strong>{ &quote.author.name }</strong>
</AppAnchor>
</Link<Route>>
<p class="is-family-secondary">
{ &quote.content }
</p>

View File

@ -1,57 +1,46 @@
use crate::{
components::{pagination::Pagination, post_card::PostCard},
switch::AppRoute,
};
use crate::components::{pagination::Pagination, post_card::PostCard};
use crate::Route;
use serde::{Deserialize, Serialize};
use yew::prelude::*;
use yew_router::agent::{RouteAgentDispatcher, RouteRequest};
use yewtil::NeqAssign;
const ITEMS_PER_PAGE: u64 = 10;
const TOTAL_PAGES: u64 = std::u64::MAX / ITEMS_PER_PAGE;
const TOTAL_PAGES: u64 = u64::MAX / ITEMS_PER_PAGE;
pub enum Msg {
ShowPage(u64),
}
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
pub struct Props {
pub page: u64,
#[derive(Serialize, Deserialize)]
struct PageQuery {
page: u64,
}
pub struct PostList {
props: Props,
link: ComponentLink<Self>,
route_dispatcher: RouteAgentDispatcher,
}
impl Component for PostList {
type Message = Msg;
type Properties = Props;
type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
props,
link,
route_dispatcher: RouteAgentDispatcher::new(),
}
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self { link }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::ShowPage(page) => {
let route = AppRoute::PostListPage(page);
self.route_dispatcher
.send(RouteRequest::ChangeRoute(route.into_route()));
false
yew_router::push_route_with_query(Route::Posts, PageQuery { page }).unwrap();
true
}
}
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props.neq_assign(props)
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
let Props { page } = self.props;
let page = self.current_page();
html! {
<div class="section container">
@ -69,7 +58,7 @@ impl Component for PostList {
}
impl PostList {
fn view_posts(&self) -> Html {
let start_seed = (self.props.page - 1) * ITEMS_PER_PAGE;
let start_seed = (self.current_page() - 1) * ITEMS_PER_PAGE;
let mut cards = (0..ITEMS_PER_PAGE).map(|seed_offset| {
html! {
<li class="list-item mb-5">
@ -92,4 +81,10 @@ impl PostList {
</div>
}
}
fn current_page(&self) -> u64 {
yew_router::parse_query::<PageQuery>()
.map(|it| it.page)
.unwrap_or(1)
}
}

View File

@ -1,96 +0,0 @@
use yew::{
virtual_dom::{Transformer, VComp},
web_sys::Url,
};
use yew_router::{components::RouterAnchor, prelude::*, switch::Permissive};
#[derive(Clone, Debug, Switch)]
pub enum AppRoute {
#[at = "/posts/{}"]
Post(u64),
#[at = "/posts/?page={}"]
PostListPage(u64),
#[at = "/posts/"]
PostList,
#[at = "/authors/{id}"]
Author(u64),
#[at = "/authors/"]
AuthorList,
#[at = "/page-not-found"]
PageNotFound(Permissive<String>),
#[at = "/!"]
Home,
}
impl AppRoute {
pub fn into_public(self) -> PublicUrlSwitch {
PublicUrlSwitch(self)
}
pub fn into_route(self) -> Route {
Route::from(self.into_public())
}
}
/// Helper type which just wraps around the actual `AppRoute` but handles a public url prefix.
/// We need to have this because we're hosting the example at `/router/` instead of `/`.
/// This type allows us have the best of both worlds.
///
/// IMPORTANT: You *must* specify a `<base>` tag on your webpage in order for this to work!
/// For more information, see the
/// [Mozilla Developer Network docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)
#[derive(Clone, Debug)]
pub struct PublicUrlSwitch(AppRoute);
impl PublicUrlSwitch {
fn base_url() -> Url {
if let Ok(Some(href)) = yew::utils::document().base_uri() {
// since this always returns an absolute URL we turn it into `Url`
// so we can more easily get the path.
Url::new(&href).unwrap()
} else {
Url::new("/").unwrap()
}
}
fn base_path() -> String {
let mut path = Self::base_url().pathname();
if path.ends_with('/') {
// pop the trailing slash because AppRoute already accounts for it
path.pop();
}
path
}
pub fn route(self) -> AppRoute {
self.0
}
}
impl Switch for PublicUrlSwitch {
fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>) {
if let Some(part) = part.strip_prefix(&Self::base_path()) {
let (route, state) = AppRoute::from_route_part(part.to_owned(), state);
(route.map(Self), state)
} else {
(None, None)
}
}
fn build_route_section<STATE>(self, route: &mut String) -> Option<STATE> {
route.push_str(&Self::base_path());
self.0.build_route_section(route)
}
}
// this allows us to pass `AppRoute` to components which take `PublicUrlSwitch`.
impl Transformer<AppRoute, PublicUrlSwitch> for VComp {
fn transform(from: AppRoute) -> PublicUrlSwitch {
from.into_public()
}
}
// type aliases to make life just a bit easier
pub type AppRouter = Router<PublicUrlSwitch>;
pub type AppAnchor = RouterAnchor<PublicUrlSwitch>;

View File

@ -1,7 +1,7 @@
[package]
name = "yew-router-macro"
version = "0.14.0"
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
authors = ["Hamza <muhammadhamza1311@gmail.com>"]
edition = "2018"
license = "MIT OR Apache-2.0"
description = "Contains macros used with yew-router"
@ -11,10 +11,12 @@ repository = "https://github.com/yewstack/yew"
proc-macro = true
[dependencies]
syn = "1.0.2"
quote = "1.0.1"
yew-router-route-parser = { version = "0.14.0", path = "../yew-router-route-parser"}
proc-macro2 = "1.0.1"
heck = "0.3.2"
proc-macro2 = "1.0.24"
quote = "1.0.9"
syn = { version = "1.0.64", features = ["full","extra-traits"] }
[dev-dependencies]
yew-router = { version = "0.14.0", path = "../yew-router" } # This should probably be removed, it makes the deploy process much more annoying.
rustversion = "1.0"
trybuild = "1.0"
yew-router = { path = "../yew-router" }

View File

@ -0,0 +1,9 @@
[tasks.test]
clear = true
toolchain = "1.51"
command = "cargo"
args = ["test"]
[tasks.test-overwrite]
extend = "test"
env = { TRYBUILD = "overwrite" }

View File

@ -1,106 +1,30 @@
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
mod routable_derive;
use routable_derive::{routable_derive_impl, Routable};
use syn::parse_macro_input;
mod switch;
/// Implements the `Switch` trait based on attributes present on the struct or enum variants.
/// Derive macro used to mark an enum as Routable.
///
/// If deriving an enum, each variant should have a `#[at = ""]` attribute,
/// and if deriving a struct, the struct itself should have a `#[at = ""]` attribute.
/// This macro can only be used on enums. Every varient of the macro needs to be marked
/// with the `at` attribute to specify the URL of the route. It generates an implementation of
/// `yew_router::Routable` trait and `const`s for the routes passed which are used with `Route`
/// component.
///
/// Inside the `""` you should put your **route matcher string**.
/// At its simplest, the route matcher string will create your variant/struct if it exactly matches the browser's route.
/// If the route in the url bar is `http://yoursite.com/some/route` and your route matcher string
/// for an enum variant is `/some/route`, then that variant will be created when `switch()` is called with the route.
///
/// But the route matcher has other capabilities.
/// If you want to capture data from the route matcher string, for example, extract an id or user name from the route,
/// you can use `{field_name}` to capture data from the route.
/// For example, `#[at = "/route/{id}"]` will capture the content after "/route/",
/// and if the associated variant is defined as `Route{id: usize}`, then the string that was captured will be
/// transformed into a `usize`.
/// If the conversion fails, then the match won't succeed and the next variant will be tried instead.
///
/// There are also `{*:field_name}` and `{3:field_name}` types of capture sections that will capture
/// _everything_, and the next 3 path sections respectively.
/// `{1:field_name}` is the same as `{field_name}`.
///
/// Tuple-structs and Tuple-enum-variants are also supported.
/// If you don't want to specify keys that don't correspond to any specific field,
/// `{}`, `{*}`, and `{4}` also denote valid capture sections when used on structs and variants without named fields.
/// In datastructures without field names, the captures will be assigned in order - left to right.
///
/// # Note
/// It should be mentioned that the derived function for matching will try enum variants in order,
/// from top to bottom, and that the whole route doesn't need to be matched by the route
/// matcher string in order for the match to succeed.
/// What is meant by this is that `[to = "/"]` will match "/", but also "/anything/else",
/// because as soon as the "/" is satisfied, that is considered a match.
///
/// This can be mitigated by specifying a `!` at the end of your route to inform the matcher that if
/// any characters are left after matching the route matcher string, the match should fail.
/// This means that `[to = "/!"]` will match "/" and _only_ "/".
///
/// -----
/// There are other attributes as well.
/// `#[rest]`, `#[rest="field_name"]` and `#[end]` attributes exist as well.
/// `#[rest]` and `#[rest="field_name"]` are equivalent to `{*}` and `{*:field_name}` respectively.
/// `#[end]` is equivalent to `!`.
/// The `#[rest]` attributes are good if you just want to delegate the whole matching of a variant to a specific
/// wrapped struct or enum that also implements `Switch`.
///
/// ------
/// # Example
///
/// ```
/// use yew_router::Switch;
///
/// #[derive(Switch, Clone)]
/// enum AppRoute {
/// #[at = "/some/simple/route"]
/// SomeSimpleRoute,
/// #[at = "/capture/{}"]
/// Capture(String),
/// #[at = "/named/capture/{name}"]
/// NamedCapture { name: String },
/// #[at = "/convert/{id}"]
/// Convert { id: usize },
/// #[rest] // shorthand for #[at = "{*}"]
/// Inner(InnerRoute),
/// }
///
/// #[derive(Switch, Clone)]
/// #[at = "/inner/route/{first}/{second}"]
/// struct InnerRoute {
/// first: String,
/// second: String,
/// # use yew_router::Routable;
/// #[derive(Debug, Clone, Copy, PartialEq, Routable)]
/// enum Routes {
/// #[at("/")]
/// Home,
/// #[at("/secure")]
/// Secure,
/// #[at("/404")]
/// NotFound,
/// }
/// ```
/// Check out the examples directory in the repository to see some more usages of the routing syntax.
#[proc_macro_derive(Switch, attributes(to, at, rest, end))]
pub fn switch(tokens: TokenStream) -> TokenStream {
let input: DeriveInput = parse_macro_input!(tokens as DeriveInput);
crate::switch::switch_impl(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
#[proc_macro_attribute]
pub fn to(_: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
}
#[proc_macro_attribute]
pub fn at(_: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
}
#[proc_macro_attribute]
pub fn rest(_: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
}
#[proc_macro_attribute]
pub fn end(_: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
#[proc_macro_derive(Routable, attributes(at, not_found))]
pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as Routable);
routable_derive_impl(input).into()
}

View File

@ -0,0 +1,219 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant};
const AT_ATTR_IDENT: &str = "at";
const NOT_FOUND_ATTR_IDENT: &str = "not_found";
pub struct Routable {
ident: Ident,
ats: Vec<LitStr>,
variants: Punctuated<Variant, syn::token::Comma>,
not_found_route: Option<Ident>,
}
impl Parse for Routable {
fn parse(input: ParseStream) -> syn::Result<Self> {
let DeriveInput { ident, data, .. } = input.parse()?;
let data = match data {
Data::Enum(data) => data,
Data::Struct(s) => {
return Err(syn::Error::new(
s.struct_token.span(),
"expected enum, found struct",
))
}
Data::Union(u) => {
return Err(syn::Error::new(
u.union_token.span(),
"expected enum, found union",
))
}
};
let (not_found_route, ats) = parse_variants_attributes(&data.variants)?;
Ok(Self {
ident,
variants: data.variants,
ats,
not_found_route,
})
}
}
fn parse_variants_attributes(
variants: &Punctuated<Variant, syn::token::Comma>,
) -> syn::Result<(Option<Ident>, Vec<LitStr>)> {
let mut not_founds = vec![];
let mut ats: Vec<LitStr> = vec![];
let mut not_found_attrs = vec![];
for variant in variants.iter() {
if let Fields::Unnamed(ref field) = variant.fields {
return Err(syn::Error::new(
field.span(),
"only named fields are supported",
));
}
let attrs = &variant.attrs;
let at_attrs = attrs
.iter()
.filter(|attr| attr.path.is_ident(AT_ATTR_IDENT))
.collect::<Vec<_>>();
let attr = match at_attrs.len() {
1 => *at_attrs.first().unwrap(),
0 => {
return Err(syn::Error::new(
variant.span(),
format!(
"{} attribute must be present on every variant",
AT_ATTR_IDENT
),
))
}
_ => {
return Err(syn::Error::new_spanned(
quote! { #(#at_attrs)* },
format!("only one {} attribute must be present", AT_ATTR_IDENT),
))
}
};
let lit = attr.parse_args::<LitStr>()?;
ats.push(lit);
for attr in attrs.iter() {
if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) {
not_found_attrs.push(attr);
not_founds.push(variant.ident.clone())
}
}
}
if not_founds.len() > 1 {
return Err(syn::Error::new_spanned(
quote! { #(#not_found_attrs)* },
format!("there can only be one {}", NOT_FOUND_ATTR_IDENT),
));
}
Ok((not_founds.into_iter().next(), ats))
}
impl Routable {
fn build_from_path(&self) -> TokenStream {
let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
let ident = &variant.ident;
let right = match &variant.fields {
Fields::Unit => quote! { Self::#ident },
Fields::Named(field) => {
let fields = field.named.iter().map(|it| {
//named fields have idents
it.ident.as_ref().unwrap()
});
quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?)*, } }
}
Fields::Unnamed(_) => unreachable!(), // already checked
};
let left = self.ats.get(i).unwrap();
quote! {
#left => ::std::option::Option::Some(#right)
}
});
quote! {
fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option<Self> {
match path {
#(#from_path_matches),*,
_ => ::std::option::Option::None,
}
}
}
}
fn build_to_path(&self) -> TokenStream {
let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
let ident = &variant.ident;
let mut right = self.ats.get(i).unwrap().value();
match &variant.fields {
Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) },
Fields::Named(field) => {
let fields = field
.named
.iter()
.map(|it| it.ident.as_ref().unwrap())
.collect::<Vec<_>>();
for field in fields.iter() {
// :param -> {param}
// so we can pass it to `format!("...", param)`
right = right.replace(&format!(":{}", field), &format!("{{{}}}", field))
}
quote! {
Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*)
}
}
Fields::Unnamed(_) => unreachable!(), // already checked
}
});
quote! {
fn to_path(&self) -> ::std::string::String {
match self {
#(#to_path_matches),*,
}
}
}
}
}
pub fn routable_derive_impl(input: Routable) -> TokenStream {
let Routable {
ats,
not_found_route,
ident,
..
} = &input;
let from_path = input.build_from_path();
let to_path = input.build_to_path();
let not_found_route = match not_found_route {
Some(route) => quote! { ::std::option::Option::Some(Self::#route) },
None => quote! { ::std::option::Option::None },
};
quote! {
#[automatically_derived]
impl ::yew_router::Routable for #ident {
#from_path
#to_path
fn routes() -> ::std::vec::Vec<&'static str> {
::std::vec![#(#ats),*]
}
fn not_found_route() -> ::std::option::Option<Self> {
#not_found_route
}
fn recognize(pathname: &str) -> ::std::option::Option<Self> {
::std::thread_local! {
static ROUTER: ::yew_router::__macro::Router = ::yew_router::__macro::build_router::<#ident>();
}
ROUTER.with(|router| ::yew_router::__macro::recognize_with_router(router, pathname))
}
}
}
}

View File

@ -1,180 +0,0 @@
use crate::switch::shadow::{ShadowCaptureVariant, ShadowMatcherToken};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{Data, DeriveInput, Fields, Ident, Variant};
mod attribute;
mod enum_impl;
mod shadow;
mod struct_impl;
mod switch_impl;
use self::{attribute::AttrToken, switch_impl::SwitchImpl};
use crate::switch::{enum_impl::EnumInner, struct_impl::StructInner};
use yew_router_route_parser::FieldNamingScheme;
/// Holds data that is required to derive Switch for a struct or a single enum variant.
pub struct SwitchItem {
pub matcher: Vec<ShadowMatcherToken>,
pub ident: Ident,
pub fields: Fields,
}
pub fn switch_impl(input: DeriveInput) -> syn::Result<TokenStream> {
let ident: Ident = input.ident;
let generics = input.generics;
Ok(match input.data {
Data::Struct(ds) => {
let field_naming_scheme = match ds.fields {
Fields::Unnamed(_) => FieldNamingScheme::Unnamed,
Fields::Unit => FieldNamingScheme::Unit,
Fields::Named(_) => FieldNamingScheme::Named,
};
let matcher = AttrToken::convert_attributes_to_tokens(input.attrs)?
.into_iter()
.enumerate()
.map(|(index, at)| at.into_shadow_matcher_tokens(index, field_naming_scheme))
.flatten()
.collect::<Vec<_>>();
let item = SwitchItem {
matcher,
ident: ident.clone(), // TODO make SwitchItem take references instead.
fields: ds.fields,
};
SwitchImpl {
target_ident: &ident,
generics: &generics,
inner: StructInner {
from_route_part: struct_impl::FromRoutePart(&item),
build_route_section: struct_impl::BuildRouteSection {
switch_item: &item,
item: &Ident::new("self", Span::call_site()),
},
},
}
.to_token_stream()
}
Data::Enum(de) => {
let switch_variants = de
.variants
.into_iter()
.map(|variant: Variant| {
let field_type = match variant.fields {
Fields::Unnamed(_) => yew_router_route_parser::FieldNamingScheme::Unnamed,
Fields::Unit => FieldNamingScheme::Unit,
Fields::Named(_) => yew_router_route_parser::FieldNamingScheme::Named,
};
let matcher = AttrToken::convert_attributes_to_tokens(variant.attrs)?
.into_iter()
.enumerate()
.map(|(index, at)| at.into_shadow_matcher_tokens(index, field_type))
.flatten()
.collect::<Vec<_>>();
Ok(SwitchItem {
matcher,
ident: variant.ident,
fields: variant.fields,
})
})
.collect::<syn::Result<Vec<_>>>()?;
SwitchImpl {
target_ident: &ident,
generics: &generics,
inner: EnumInner {
from_route_part: enum_impl::FromRoutePart {
switch_variants: &switch_variants,
enum_ident: &ident,
},
build_route_section: enum_impl::BuildRouteSection {
switch_items: &switch_variants,
enum_ident: &ident,
match_item: &Ident::new("self", Span::call_site()),
},
},
}
.to_token_stream()
}
Data::Union(_du) => panic!("Deriving FromCaptures not supported for Unions."),
})
}
trait Flatten<T> {
/// Because flatten is a nightly feature. I'm making a new variant of the function here for
/// stable use. The naming is changed to avoid this getting clobbered when object_flattening
/// 60258 is stabilized.
fn flatten_stable(self) -> Option<T>;
}
impl<T> Flatten<T> for Option<Option<T>> {
fn flatten_stable(self) -> Option<T> {
match self {
None => None,
Some(v) => v,
}
}
}
fn build_matcher_from_tokens(tokens: &[ShadowMatcherToken]) -> TokenStream {
quote! {
let settings = ::yew_router::matcher::MatcherSettings {
case_insensitive: true,
};
let matcher = ::yew_router::matcher::RouteMatcher {
tokens: ::std::vec![#(#tokens),*],
settings
};
}
}
/// Enum indicating which sort of writer is needed.
pub(crate) enum FieldType {
Named,
Unnamed { index: usize },
Unit,
}
/// This assumes that the variant/struct has been destructured.
fn write_for_token(token: &ShadowMatcherToken, naming_scheme: FieldType) -> TokenStream {
match token {
ShadowMatcherToken::Exact(lit) => {
quote! {
write!(buf, "{}", #lit).unwrap();
}
}
ShadowMatcherToken::Capture(capture) => match naming_scheme {
FieldType::Named | FieldType::Unit => match &capture {
ShadowCaptureVariant::Named(name)
| ShadowCaptureVariant::ManyNamed(name)
| ShadowCaptureVariant::NumberedNamed { name, .. } => {
let name = Ident::new(&name, Span::call_site());
quote! {
state = state.or_else(|| #name.build_route_section(buf));
}
}
ShadowCaptureVariant::Unnamed
| ShadowCaptureVariant::ManyUnnamed
| ShadowCaptureVariant::NumberedUnnamed { .. } => {
panic!("Unnamed matcher sections not allowed for named field types")
}
},
FieldType::Unnamed { index } => {
let name = unnamed_field_index_item(index);
quote! {
state = state.or_else(|| #name.build_route_section(&mut buf));
}
}
},
ShadowMatcherToken::End => quote! {},
}
}
/// Creates an ident used for destructuring unnamed fields.
///
/// There needs to be a unified way to "mangle" the unnamed fields so they can be destructured,
fn unnamed_field_index_item(index: usize) -> Ident {
Ident::new(&format!("__field_{}", index), Span::call_site())
}

View File

@ -1,93 +0,0 @@
use crate::switch::shadow::{ShadowCaptureVariant, ShadowMatcherToken};
use syn::{spanned::Spanned, Attribute, Lit, Meta, MetaNameValue};
use yew_router_route_parser::FieldNamingScheme;
pub enum AttrToken {
ToOrAt(String),
End,
Rest(Option<String>),
}
impl AttrToken {
pub fn convert_attributes_to_tokens(attributes: Vec<Attribute>) -> syn::Result<Vec<Self>> {
fn get_meta_name_value_str(mnv: &MetaNameValue) -> syn::Result<String> {
match &mnv.lit {
Lit::Str(s) => Ok(s.value()),
lit => Err(syn::Error::new_spanned(lit, "expected a string literal")),
}
}
attributes
.iter()
.filter_map(|attr: &Attribute| attr.parse_meta().ok())
.filter_map(|meta: Meta| {
let meta_span = meta.span();
match meta {
Meta::NameValue(mnv) => {
mnv.path
.get_ident()
.and_then(|ident| match ident.to_string().as_str() {
"to" | "at" => {
Some(get_meta_name_value_str(&mnv).map(AttrToken::ToOrAt))
}
"rest" => Some(
get_meta_name_value_str(&mnv).map(|s| AttrToken::Rest(Some(s))),
),
_ => None,
})
}
Meta::Path(path) => {
path.get_ident()
.and_then(|ident| match ident.to_string().as_str() {
"end" => Some(Ok(AttrToken::End)),
"rest" => Some(Ok(AttrToken::Rest(None))),
_ => None,
})
}
Meta::List(list) => {
list.path
.get_ident()
.and_then(|ident| match ident.to_string().as_str() {
id @ "to" | id @ "at" | id @ "rest" => Some(Err(syn::Error::new(
meta_span,
&format!(
"This syntax is not supported, did you mean `#[{} = ...]`?",
id
),
))),
_ => None,
})
}
}
})
.collect()
}
/// The id is an unique identifier that allows otherwise unnamed captures to still be captured
/// with unique names.
pub fn into_shadow_matcher_tokens(
self,
id: usize,
field_naming_scheme: FieldNamingScheme,
) -> Vec<ShadowMatcherToken> {
match self {
AttrToken::ToOrAt(matcher_string) => {
yew_router_route_parser::parse_str_and_optimize_tokens(
&matcher_string,
field_naming_scheme,
)
.expect("Invalid Matcher") // This is the point where users should see an error message if their matcher string has some syntax error.
.into_iter()
.map(crate::switch::shadow::ShadowMatcherToken::from)
.collect()
}
AttrToken::End => vec![ShadowMatcherToken::End],
AttrToken::Rest(Some(capture_name)) => vec![ShadowMatcherToken::Capture(
ShadowCaptureVariant::ManyNamed(capture_name),
)],
AttrToken::Rest(None) => vec![ShadowMatcherToken::Capture(
ShadowCaptureVariant::ManyNamed(id.to_string()),
)],
}
}
}

View File

@ -1,25 +0,0 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
pub use self::{build_route_section::BuildRouteSection, from_route_part::FromRoutePart};
mod build_route_section;
mod from_route_part;
pub struct EnumInner<'a> {
pub from_route_part: FromRoutePart<'a>,
pub build_route_section: BuildRouteSection<'a>,
}
impl<'a> ToTokens for EnumInner<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let EnumInner {
from_route_part,
build_route_section,
} = self;
tokens.extend(quote! {
#from_route_part
#build_route_section
});
}
}

View File

@ -1,98 +0,0 @@
use crate::switch::{
shadow::ShadowMatcherToken, unnamed_field_index_item, write_for_token, FieldType, SwitchItem,
};
use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use syn::Fields;
pub struct BuildRouteSection<'a> {
pub switch_items: &'a [SwitchItem],
pub enum_ident: &'a Ident,
pub match_item: &'a Ident,
}
impl<'a> ToTokens for BuildRouteSection<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let serializer =
build_serializer_for_enum(self.switch_items, self.enum_ident, self.match_item);
tokens.extend(quote!{
fn build_route_section<__T>(self, mut buf: &mut ::std::string::String) -> ::std::option::Option<__T> {
#serializer
}
});
}
}
/// The serializer makes up the body of `build_route_section`.
pub fn build_serializer_for_enum(
switch_items: &[SwitchItem],
enum_ident: &Ident,
match_item: &Ident,
) -> TokenStream {
let variants = switch_items.iter().map(|switch_item: &SwitchItem| {
let SwitchItem {
matcher,
ident,
fields,
} = switch_item;
match fields {
Fields::Named(fields_named) => {
let field_names = fields_named
.named
.iter()
.filter_map(|named| named.ident.as_ref());
let writers = matcher
.iter()
.map(|token| write_for_token(token, FieldType::Named));
quote! {
#enum_ident::#ident{#(#field_names),*} => {
#(#writers)*
}
}
}
Fields::Unnamed(fields_unnamed) => {
let field_names = fields_unnamed
.unnamed
.iter()
.enumerate()
.map(|(index, _)| unnamed_field_index_item(index));
let mut item_count = 0;
let writers = matcher.iter().map(|token| {
if let ShadowMatcherToken::Capture(_) = &token {
let ts = write_for_token(token, FieldType::Unnamed { index: item_count });
item_count += 1;
ts
} else {
// Its either a literal, or something that will panic currently
write_for_token(token, FieldType::Unit)
}
});
quote! {
#enum_ident::#ident(#(#field_names),*) => {
#(#writers)*
}
}
}
Fields::Unit => {
let writers = matcher
.iter()
.map(|token| write_for_token(token, FieldType::Unit));
quote! {
#enum_ident::#ident => {
#(#writers)*
}
}
}
}
});
quote! {
use ::std::fmt::Write as __Write;
let mut state: Option<__T> = None;
match #match_item {
#(#variants)*,
}
state
}
}

View File

@ -1,193 +0,0 @@
use crate::switch::SwitchItem;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{Field, Fields, Type};
pub struct FromRoutePart<'a> {
pub switch_variants: &'a [SwitchItem],
pub enum_ident: &'a Ident,
}
impl<'a> ToTokens for FromRoutePart<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let variant_matchers = self.switch_variants.iter().map(|sv| {
let SwitchItem {
matcher,
ident,
fields,
} = sv;
let build_from_captures = build_variant_from_captures(&self.enum_ident, ident, fields);
let matcher = super::super::build_matcher_from_tokens(&matcher);
quote! {
#matcher
#build_from_captures
}
});
tokens.extend(quote!{
fn from_route_part<__T>(route: String, mut state: Option<__T>) -> (::std::option::Option<Self>, ::std::option::Option<__T>) {
let route_string = route;
#(#variant_matchers)*
(::std::option::Option::None, state)
}
});
}
}
/// Once the 'captures' exists, attempt to populate the fields from the list of captures.
fn build_variant_from_captures(
enum_ident: &Ident,
variant_ident: &Ident,
fields: &Fields,
) -> TokenStream {
match fields {
Fields::Named(named_fields) => {
let (field_declarations, fields): (Vec<_>, Vec<_>) = named_fields
.named
.iter()
.filter_map(|field: &Field| {
let field_ty: &Type = &field.ty;
field.ident.as_ref().map(|i: &Ident| {
let key = i.to_string();
(i, key, field_ty)
})
})
.map(|(field_name, key, field_ty): (&Ident, String, &Type)| {
let field_decl = quote! {
let #field_name = {
let (v, s) = match captures.remove(#key) {
::std::option::Option::Some(value) => {
<#field_ty as ::yew_router::Switch>::from_route_part(
value,
state,
)
}
::std::option::Option::None => {
(
<#field_ty as ::yew_router::Switch>::key_not_available(),
state,
)
}
};
match v {
::std::option::Option::Some(val) => {
state = s; // Set state for the next var.
val
},
::std::option::Option::None => return (None, s) // Failed
}
};
};
(field_decl, field_name)
})
.unzip();
quote! {
let mut state = if let ::std::option::Option::Some(mut captures) = matcher
.capture_route_into_map(&route_string)
.ok()
.map(|x| x.1)
{
let create_item = || {
#(#field_declarations)*
let val = ::std::option::Option::Some(
#enum_ident::#variant_ident {
#(#fields),*
}
);
(val, state)
};
let (val, state) = create_item();
if val.is_some() {
return (val, state);
}
state
} else {
state
};
}
}
Fields::Unnamed(unnamed_fields) => {
let (field_declarations, fields): (Vec<_>, Vec<_>) = unnamed_fields
.unnamed
.iter()
.enumerate()
.map(|(idx, f)| {
let field_ty = &f.ty;
let field_var_name = Ident::new(&format!("field_{}", idx), Span::call_site());
let field_decl = quote! {
let #field_var_name = {
let (v, s) = match drain.next() {
::std::option::Option::Some(value) => {
<#field_ty as ::yew_router::Switch>::from_route_part(
value,
state,
)
},
::std::option::Option::None => {
(
<#field_ty as ::yew_router::Switch>::key_not_available(),
state,
)
}
};
match v {
::std::option::Option::Some(val) => {
state = s; // Set state for the next var.
val
},
::std::option::Option::None => return (None, s) // Failed
}
};
};
(field_decl, field_var_name)
})
.unzip();
quote! {
let mut state = if let ::std::option::Option::Some(mut captures) = matcher
.capture_route_into_vec(&route_string)
.ok()
.map(|x| x.1)
{
let mut drain = captures.drain(..);
let create_item = || {
#(#field_declarations)*
(
::std::option::Option::Some(
#enum_ident::#variant_ident(
#(#fields),*
)
),
state
)
};
let (val, state) = create_item();
if val.is_some() {
return (val, state);
}
state
} else {
state
};
}
}
Fields::Unit => {
quote! {
let mut state = if let ::std::option::Option::Some(_captures) = matcher.capture_route_into_map(&route_string).ok().map(|x| x.1) {
return (::std::option::Option::Some(#enum_ident::#variant_ident), state);
} else {
state
};
}
}
}
}

View File

@ -1,101 +0,0 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use yew_router_route_parser::{CaptureVariant, MatcherToken};
impl ToTokens for ShadowMatcherToken {
fn to_tokens(&self, ts: &mut TokenStream) {
use ShadowMatcherToken as SOT;
let t: TokenStream = match self {
SOT::Exact(s) => quote! {
::yew_router::matcher::MatcherToken::Exact(#s.to_string())
},
SOT::Capture(variant) => quote! {
::yew_router::matcher::MatcherToken::Capture(#variant)
},
SOT::End => quote! {
::yew_router::matcher::MatcherToken::End
},
};
ts.extend(t)
}
}
/// A shadow of the OptimizedToken type.
/// It should match it exactly so that this macro can expand to the original.
pub enum ShadowMatcherToken {
Exact(String),
Capture(ShadowCaptureVariant),
End,
}
pub enum ShadowCaptureVariant {
/// {}
Unnamed,
/// {*}
ManyUnnamed,
/// {5}
NumberedUnnamed {
/// Number of sections to match.
sections: usize,
},
/// {name} - captures a section and adds it to the map with a given name
Named(String),
/// {*:name} - captures over many sections and adds it to the map with a given name.
ManyNamed(String),
/// {2:name} - captures a fixed number of sections with a given name.
NumberedNamed { sections: usize, name: String },
}
impl ToTokens for ShadowCaptureVariant {
fn to_tokens(&self, ts: &mut TokenStream) {
let t = match self {
ShadowCaptureVariant::Named(name) => {
quote! {::yew_router::matcher::CaptureVariant::Named(#name.to_string())}
}
ShadowCaptureVariant::ManyNamed(name) => {
quote! {::yew_router::matcher::CaptureVariant::ManyNamed(#name.to_string())}
}
ShadowCaptureVariant::NumberedNamed { sections, name } => {
quote! {::yew_router::matcher::CaptureVariant::NumberedNamed{sections: #sections, name: #name.to_string()}}
}
ShadowCaptureVariant::Unnamed => {
quote! {::yew_router::matcher::CaptureVariant::Unnamed}
}
ShadowCaptureVariant::ManyUnnamed => {
quote! {::yew_router::matcher::CaptureVariant::ManyUnnamed}
}
ShadowCaptureVariant::NumberedUnnamed { sections } => {
quote! {::yew_router::matcher::CaptureVariant::NumberedUnnamed{sections: #sections}}
}
};
ts.extend(t)
}
}
impl From<MatcherToken> for ShadowMatcherToken {
fn from(mt: MatcherToken) -> Self {
use MatcherToken as MT;
use ShadowMatcherToken as SOT;
match mt {
MT::Exact(s) => SOT::Exact(s),
MT::Capture(capture) => SOT::Capture(capture.into()),
MT::End => SOT::End,
}
}
}
impl From<CaptureVariant> for ShadowCaptureVariant {
fn from(cv: CaptureVariant) -> Self {
use ShadowCaptureVariant as SCV;
match cv {
CaptureVariant::Named(name) => SCV::Named(name),
CaptureVariant::ManyNamed(name) => SCV::ManyNamed(name),
CaptureVariant::NumberedNamed { sections, name } => {
SCV::NumberedNamed { sections, name }
}
CaptureVariant::Unnamed => SCV::Unnamed,
CaptureVariant::ManyUnnamed => SCV::ManyUnnamed,
CaptureVariant::NumberedUnnamed { sections } => SCV::NumberedUnnamed { sections },
}
}
}

View File

@ -1,24 +0,0 @@
pub use self::{build_route_section::BuildRouteSection, from_route_part::FromRoutePart};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
mod build_route_section;
mod from_route_part;
pub struct StructInner<'a> {
pub from_route_part: FromRoutePart<'a>,
pub build_route_section: BuildRouteSection<'a>,
}
impl<'a> ToTokens for StructInner<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let StructInner {
from_route_part,
build_route_section,
} = self;
tokens.extend(quote! {
#from_route_part
#build_route_section
})
}
}

View File

@ -1,82 +0,0 @@
use crate::switch::{
shadow::ShadowMatcherToken, unnamed_field_index_item, write_for_token, FieldType, SwitchItem,
};
use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use syn::Fields;
pub struct BuildRouteSection<'a> {
pub switch_item: &'a SwitchItem,
pub item: &'a Ident,
}
impl<'a> ToTokens for BuildRouteSection<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let serializer = build_serializer_for_struct(self.switch_item, self.item);
tokens.extend(quote! {
fn build_route_section<__T>(self, mut buf: &mut ::std::string::String) -> ::std::option::Option<__T> {
#serializer
}
})
}
}
pub fn build_serializer_for_struct(switch_item: &SwitchItem, item: &Ident) -> TokenStream {
let SwitchItem {
matcher,
ident,
fields,
} = switch_item;
let destructor_and_writers = match fields {
Fields::Named(fields_named) => {
let field_names = fields_named
.named
.iter()
.filter_map(|named| named.ident.as_ref());
let writers = matcher
.iter()
.map(|token| write_for_token(token, FieldType::Named));
quote! {
let #ident{#(#field_names),*} = #item;
#(#writers)*
}
}
Fields::Unnamed(fields_unnamed) => {
let field_names = fields_unnamed
.unnamed
.iter()
.enumerate()
.map(|(index, _)| unnamed_field_index_item(index));
let mut item_count = 0;
let writers = matcher.iter().map(|token| {
if let ShadowMatcherToken::Capture(_) = &token {
let ts = write_for_token(token, FieldType::Unnamed { index: item_count });
item_count += 1;
ts
} else {
// Its either a literal, or something that will panic currently
write_for_token(token, FieldType::Unit)
}
});
quote! {
let #ident(#(#field_names),*) = #item;
#(#writers)*
}
}
Fields::Unit => {
let writers = matcher
.iter()
.map(|token| write_for_token(token, FieldType::Unit));
quote! {
#(#writers)*
}
}
};
quote! {
use ::std::fmt::Write as _;
let mut state: Option<__T> = None;
#destructor_and_writers
state
}
}

View File

@ -1,162 +0,0 @@
// use crate::switch::{SwitchItem, write_for_token, FieldType, unnamed_field_index_item};
use crate::switch::SwitchItem;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{Field, Fields, Type};
pub struct FromRoutePart<'a>(pub &'a SwitchItem);
impl<'a> ToTokens for FromRoutePart<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let SwitchItem {
matcher,
ident,
fields,
} = &self.0;
let matcher = super::super::build_matcher_from_tokens(&matcher);
let build_from_captures = build_struct_from_captures(ident, fields);
tokens.extend(quote! {
fn from_route_part<__T>(
route: String, mut state: Option<__T>
) -> (::std::option::Option<Self>, ::std::option::Option<__T>) {
#matcher
let route_string = route;
#build_from_captures
(::std::option::Option::None, state)
}
})
}
}
fn build_struct_from_captures(ident: &Ident, fields: &Fields) -> TokenStream {
match fields {
Fields::Named(named_fields) => {
let (field_declarations, fields): (Vec<_>, Vec<_>) = named_fields
.named
.iter()
.filter_map(|field: &Field| {
let field_ty: &Type = &field.ty;
field.ident.as_ref().map(|i| {
let key = i.to_string();
(i, key, field_ty)
})
})
.map(|(field_name, key, field_ty): (&Ident, String, &Type)| {
let field_decl = quote! {
let #field_name = {
let (v, s) = match captures.remove(#key) {
::std::option::Option::Some(value) => {
<#field_ty as ::yew_router::Switch>::from_route_part(
value,
state,
)
}
::std::option::Option::None => {
(
<#field_ty as ::yew_router::Switch>::key_not_available(),
state,
)
}
};
match v {
::std::option::Option::Some(val) => {
state = s; // Set state for the next var.
val
},
::std::option::Option::None => return (::std::option::Option::None, s) // Failed
}
};
};
(field_decl, field_name)
})
.unzip();
quote! {
if let ::std::option::Option::Some(mut captures) = matcher
.capture_route_into_map(&route_string)
.ok()
.map(|x| x.1)
{
#(#field_declarations)*
return (
::std::option::Option::Some(
#ident {
#(#fields),*
}
),
state
);
}
}
}
Fields::Unnamed(unnamed_fields) => {
let (field_declarations, fields): (Vec<_>, Vec<_>) = unnamed_fields
.unnamed
.iter()
.enumerate()
.map(|(idx, f)| {
let field_ty = &f.ty;
let field_var_name = Ident::new(&format!("field_{}", idx), Span::call_site());
let field_decl = quote! {
let #field_var_name = {
let (v, s) = match drain.next() {
::std::option::Option::Some(value) => {
<#field_ty as ::yew_router::Switch>::from_route_part(
value,
state,
)
},
::std::option::Option::None => {
(
<#field_ty as ::yew_router::Switch>::key_not_available(),
state,
)
}
};
match v {
::std::option::Option::Some(val) => {
state = s; // Set state for the next var.
val
},
::std::option::Option::None => return (::std::option::Option::None, s) // Failed
}
};
};
(field_decl, field_var_name)
})
.unzip();
quote! {
if let Some(mut captures) = matcher.capture_route_into_vec(&route_string).ok().map(|x| x.1) {
let mut drain = captures.drain(..);
#(#field_declarations)*
return (
::std::option::Option::Some(
#ident(
#(#fields),*
)
),
state
);
};
}
}
Fields::Unit => {
return quote! {
let mut state = if let ::std::option::Option::Some(_captures) = matcher.capture_route_into_map(&route_string).ok().map(|x| x.1) {
return (::std::option::Option::Some(#ident), state);
} else {
state
};
}
}
}
}

View File

@ -1,49 +0,0 @@
use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use syn::{punctuated::Punctuated, GenericParam, Generics};
// Todo, consider removing the T here and replacing it with an enum.
/// Creates the "impl <X,Y,Z> ::yew_router::Switch for TypeName<X,Y,Z> where etc.." line.
///
/// Then populates the body of the implementation with the specified `T`.
pub struct SwitchImpl<'a, T> {
pub target_ident: &'a Ident,
pub generics: &'a Generics,
pub inner: T,
}
impl<'a, T: ToTokens> ToTokens for SwitchImpl<'a, T> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ident = self.target_ident;
let inner = &self.inner;
let line_tokens = if self.generics.params.is_empty() {
quote! {
impl ::yew_router::Switch for #ident {
#inner
}
}
} else {
let params = &self.generics.params;
let param_idents = params
.iter()
.map(|p: &GenericParam| {
match p {
GenericParam::Type(ty) => ty.ident.clone(),
// GenericParam::Lifetime(lt) => lt.lifetime, // TODO different type here, must be handled by collecting into a new enum and defining how to convert _that_ to tokens.
_ => unimplemented!("Not all type parameter variants (lifetimes and consts) are supported in Switch")
}
})
.collect::<Punctuated<_,syn::token::Comma>>();
let where_clause = &self.generics.where_clause;
quote! {
impl <#params> ::yew_router::Switch for #ident <#param_idents> #where_clause
{
#inner
}
}
};
tokens.extend(line_tokens)
}
}

View File

@ -0,0 +1,13 @@
#[derive(yew_router::Routable)]
enum Routes {
One,
}
#[derive(yew_router::Routable)]
enum RoutesTwo {
#[at("/one")]
#[at("/two")]
One,
}
fn main() {}

View File

@ -0,0 +1,12 @@
error: at attribute must be present on every variant
--> $DIR/bad-ats-fail.rs:3:5
|
3 | One,
| ^^^
error: only one at attribute must be present
--> $DIR/bad-ats-fail.rs:8:5
|
8 | / #[at("/one")]
9 | | #[at("/two")]
| |_________________^

View File

@ -0,0 +1,18 @@
#[derive(Debug, PartialEq, yew_router::Routable)]
enum RoutesOne {
#[at("/")]
#[not_found]
Home,
#[at("/404")]
#[not_found]
NotFound,
}
#[derive(Debug, PartialEq, yew_router::Routable)]
enum RoutesTwo {
#[at("/404")]
#[not_found]
#[not_found]
NotFound,
}
fn main() {}

View File

@ -0,0 +1,15 @@
error: there can only be one not_found
--> $DIR/invalid-not-found-fail.rs:4:5
|
4 | / #[not_found]
5 | | Home,
6 | | #[at("/404")]
7 | | #[not_found]
| |________________^
error: there can only be one not_found
--> $DIR/invalid-not-found-fail.rs:14:5
|
14 | / #[not_found]
15 | | #[not_found]
| |________________^

View File

@ -0,0 +1,4 @@
#[derive(yew_router::Routable)]
struct Test {}
fn main() {}

View File

@ -0,0 +1,5 @@
error: expected enum, found struct
--> $DIR/struct-fail.rs:2:1
|
2 | struct Test {}
| ^^^^^^

View File

@ -0,0 +1,7 @@
#[derive(yew_router::Routable)]
enum Routes {
#[at("/one/:two")]
One(u32),
}
fn main() {}

View File

@ -0,0 +1,5 @@
error: only named fields are supported
--> $DIR/unnamed-fields-fail.rs:4:8
|
4 | One(u32),
| ^^^^^

View File

@ -0,0 +1,14 @@
#![no_implicit_prelude]
#[derive(Debug, PartialEq, ::yew_router::Routable)]
enum Routes {
#[at("/")]
One,
#[at("/two/:id")]
Two { id: u32 },
#[at("/404")]
#[not_found]
NotFound,
}
fn main() {}

View File

@ -0,0 +1,9 @@
#[allow(dead_code)]
#[rustversion::attr(stable(1.51), test)]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/routable_derive/*-pass.rs");
t.compile_fail("tests/routable_derive/*-fail.rs");
}
fn main() {}

View File

@ -1,11 +0,0 @@
[package]
name = "yew-router-route-parser"
version = "0.14.0"
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
edition = "2018"
license = "MIT OR Apache-2.0"
description = "The parser for the routing syntax used with yew-router"
repository = "https://github.com/yewstack/yew"
[dependencies]
nom = "6.1.2"

View File

@ -1,367 +0,0 @@
use crate::{
error::{ExpectedToken, ParserErrorReason},
parser::{CaptureOrExact, RefCaptureVariant, RouteParserToken},
ParseError,
};
use nom::{
branch::alt,
bytes::complete::{tag, take_till1},
character::{
complete::{char, digit1},
is_digit,
},
combinator::{map, map_parser},
sequence::{delimited, separated_pair},
IResult,
};
/// Indicates if the parser is working to create a matcher for a datastructure with named or unnamed fields.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Ord, PartialOrd)]
pub enum FieldNamingScheme {
/// For Thing { field: String }
Named,
/// for Thing(String)
Unnamed,
/// for Thing
Unit,
}
pub fn get_slash(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
map(char('/'), |_: char| RouteParserToken::Separator)(i)
.map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::Separator)))
}
pub fn get_question(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
map(char('?'), |_: char| RouteParserToken::QueryBegin)(i)
.map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::QueryBegin)))
}
pub fn get_and(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
map(char('&'), |_: char| RouteParserToken::QuerySeparator)(i).map_err(|_: nom::Err<()>| {
nom::Err::Error(ParseError::expected(ExpectedToken::QuerySeparator))
})
}
/// Returns a FragmentBegin variant if the next character is '\#'.
pub fn get_hash(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
map(char('#'), |_: char| RouteParserToken::FragmentBegin)(i).map_err(|_: nom::Err<()>| {
nom::Err::Error(ParseError::expected(ExpectedToken::FragmentBegin))
})
}
/// Returns an End variant if the next character is a '!`.
pub fn get_end(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
map(char('!'), |_: char| RouteParserToken::End)(i)
.map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::End)))
}
/// Returns an End variant if the next character is a '!`.
fn get_open_bracket(i: &str) -> IResult<&str, (), ParseError> {
map(char('{'), |_: char| ())(i).map_err(|_: nom::Err<()>| {
nom::Err::Error(ParseError::expected(ExpectedToken::OpenBracket))
})
}
fn get_close_bracket(i: &str) -> IResult<&str, (), ParseError> {
map(char('}'), |_: char| ())(i).map_err(|_: nom::Err<()>| {
nom::Err::Error(ParseError::expected(ExpectedToken::CloseBracket))
})
}
fn get_eq(i: &str) -> IResult<&str, (), ParseError> {
map(char('='), |_: char| ())(i)
.map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::Equals)))
}
fn get_star(i: &str) -> IResult<&str, (), ParseError> {
map(char('*'), |_: char| ())(i)
.map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::Star)))
}
fn get_colon(i: &str) -> IResult<&str, (), ParseError> {
map(char(':'), |_: char| ())(i)
.map_err(|_: nom::Err<()>| nom::Err::Error(ParseError::expected(ExpectedToken::Colon)))
}
fn rust_ident<'a>(i: &'a str) -> IResult<&'a str, &'a str, ParseError> {
let invalid_ident_chars = r##" \|/{[]()?+=-!@#$%^&*~`'";:"##;
// Detect an ident by first reading until a } is found,
// then validating the captured section against invalid characters that can't be in rust idents.
map_parser(take_till1(move |c| c == '}'), move |i: &'a str| {
match take_till1::<_, _, ()>(|c| invalid_ident_chars.contains(c))(i) {
Ok((remain, got)) => {
// Detects if the first character is a digit.
if !got.is_empty() && got.starts_with(|c: char| is_digit(c as u8)) {
Err(nom::Err::Failure(ParseError {
reason: Some(ParserErrorReason::BadRustIdent(got.chars().next().unwrap())),
expected: vec![ExpectedToken::Ident],
offset: 1,
}))
} else if !remain.is_empty() {
Err(nom::Err::Failure(ParseError {
reason: Some(ParserErrorReason::BadRustIdent(
remain.chars().next().unwrap(),
)),
expected: vec![ExpectedToken::CloseBracket, ExpectedToken::Ident],
offset: got.len() + 1,
}))
} else {
Ok((i, i))
}
}
Err(_) => Ok((i, i)),
}
})(i)
}
/// Matches escaped items
fn escaped_item_impl(i: &str) -> IResult<&str, &str> {
map(alt((tag("!!"), tag("{{"), tag("}}"))), |s| match s {
"!!" => "!",
"}}" => "}",
"{{" => "{",
_ => unreachable!(),
})(i)
}
/// Matches "".
pub fn nothing(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
if i.is_empty() {
Ok((i, RouteParserToken::Nothing))
} else {
Err(nom::Err::Error(ParseError {
reason: None, // This should never actually report an error.
expected: vec![],
offset: 0,
}))
}
}
/// The provided string of special characters will be used to terminate this parser.
///
/// Due to escaped character parser, the list of special characters MUST contain the characters:
/// "!{}" within it.
fn exact_impl(special_chars: &'static str) -> impl Fn(&str) -> IResult<&str, &str, ParseError> {
// Detect either an exact ident, or an escaped item.
// At higher levels, this can be called multiple times in a row,
// and that results of those multiple parse attempts will be stitched together into one literal.
move |i: &str| {
alt((
take_till1(move |c| special_chars.contains(c)),
escaped_item_impl,
))(i)
.map_err(|x: nom::Err<nom::error::Error<&str>>| {
let s = match x {
nom::Err::Error(e) | nom::Err::Failure(e) => e.input,
nom::Err::Incomplete(_) => panic!(),
};
nom::Err::Error(ParseError {
reason: Some(ParserErrorReason::BadLiteral),
expected: vec![ExpectedToken::Literal],
offset: 1 + i.len() - s.len(),
})
})
}
}
const SPECIAL_CHARS: &str = r##"/?&#={}!"##;
const FRAGMENT_SPECIAL_CHARS: &str = r##"{}!"##;
pub fn exact(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
map(exact_impl(SPECIAL_CHARS), RouteParserToken::Exact)(i)
}
/// More permissive exact matchers
pub fn fragment_exact(i: &str) -> IResult<&str, RouteParserToken, ParseError> {
map(exact_impl(FRAGMENT_SPECIAL_CHARS), RouteParserToken::Exact)(i)
}
pub fn capture<'a>(
field_naming_scheme: FieldNamingScheme,
) -> impl FnMut(&'a str) -> IResult<&'a str, RouteParserToken<'a>, ParseError> {
map(capture_impl(field_naming_scheme), RouteParserToken::Capture)
}
fn capture_single_impl<'a>(
field_naming_scheme: FieldNamingScheme,
) -> impl Fn(&'a str) -> IResult<&'a str, RefCaptureVariant<'a>, ParseError> {
move |i: &str| match field_naming_scheme {
FieldNamingScheme::Named => delimited(
get_open_bracket,
named::single_capture_impl,
get_close_bracket,
)(i),
FieldNamingScheme::Unnamed => delimited(
get_open_bracket,
alt((named::single_capture_impl, unnamed::single_capture_impl)),
get_close_bracket,
)(i),
FieldNamingScheme::Unit => {
println!("Unit encountered, erroring in capture single");
Err(nom::Err::Failure(ParseError {
reason: Some(ParserErrorReason::CapturesInUnit),
expected: vec![],
offset: 0,
}))
}
}
}
/// Captures {ident}, {*:ident}, {<number>:ident}
///
/// Depending on the provided field naming, it may also match {}, {*}, and {<number>} for unnamed fields, or none at all for units.
fn capture_impl<'a>(
field_naming_scheme: FieldNamingScheme,
) -> impl Fn(&'a str) -> IResult<&'a str, RefCaptureVariant, ParseError> {
move |i: &str| match field_naming_scheme {
FieldNamingScheme::Named => {
let inner = alt((
named::many_capture_impl,
named::numbered_capture_impl,
named::single_capture_impl,
));
delimited(get_open_bracket, inner, get_close_bracket)(i)
}
FieldNamingScheme::Unnamed => {
let inner = alt((
named::many_capture_impl,
unnamed::many_capture_impl,
named::numbered_capture_impl,
unnamed::numbered_capture_impl,
named::single_capture_impl,
unnamed::single_capture_impl,
));
delimited(get_open_bracket, inner, get_close_bracket)(i)
}
FieldNamingScheme::Unit => Err(nom::Err::Error(ParseError {
reason: Some(ParserErrorReason::CapturesInUnit),
expected: vec![],
offset: 0,
})),
}
}
mod named {
use super::*;
pub fn single_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
map(rust_ident, |key| RefCaptureVariant::Named(key))(i)
}
pub fn many_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
map(
separated_pair(get_star, get_colon, rust_ident),
|(_, key)| RefCaptureVariant::ManyNamed(key),
)(i)
}
pub fn numbered_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
map(
separated_pair(digit1, get_colon, rust_ident),
|(number, key)| RefCaptureVariant::NumberedNamed {
sections: number.parse().unwrap(),
name: key,
},
)(i)
}
}
mod unnamed {
use super::*;
/// #Note
/// because this always succeeds, try this last
pub fn single_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
Ok((i, RefCaptureVariant::Unnamed))
}
pub fn many_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
map(get_star, |_| RefCaptureVariant::ManyUnnamed)(i)
}
pub fn numbered_capture_impl(i: &str) -> IResult<&str, RefCaptureVariant, ParseError> {
map(digit1, |number: &str| RefCaptureVariant::NumberedUnnamed {
sections: number.parse().unwrap(),
})(i)
}
}
/// Gets a capture or exact, mapping it to the CaptureOrExact enum - to provide a limited subset.
fn cap_or_exact<'a>(
field_naming_scheme: FieldNamingScheme,
) -> impl Fn(&'a str) -> IResult<&'a str, CaptureOrExact<'a>, ParseError> {
move |i: &str| {
alt((
map(
capture_single_impl(field_naming_scheme),
CaptureOrExact::Capture,
),
map(exact_impl(SPECIAL_CHARS), CaptureOrExact::Exact),
))(i)
}
}
/// Matches a query
pub fn query<'a>(
field_naming_scheme: FieldNamingScheme,
) -> impl Fn(&'a str) -> IResult<&'a str, RouteParserToken<'a>, ParseError> {
move |i: &str| {
map(
separated_pair(
exact_impl(SPECIAL_CHARS),
get_eq,
cap_or_exact(field_naming_scheme),
),
|(ident, capture_or_exact)| RouteParserToken::Query {
ident,
capture_or_exact,
},
)(i)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn lit() {
let x = exact("hello").expect("Should parse");
assert_eq!(x.1, RouteParserToken::Exact("hello"))
}
#[test]
fn cap_or_exact_match_lit() {
cap_or_exact(FieldNamingScheme::Named)("lorem").expect("Should parse");
}
#[test]
fn cap_or_exact_match_cap() {
cap_or_exact(FieldNamingScheme::Named)("{lorem}").expect("Should parse");
}
#[test]
fn query_section_exact() {
query(FieldNamingScheme::Named)("lorem=ipsum").expect("should parse");
}
#[test]
fn query_section_capture_named() {
query(FieldNamingScheme::Named)("lorem={ipsum}").expect("should parse");
}
#[test]
fn query_section_capture_named_fails_without_key() {
query(FieldNamingScheme::Named)("lorem={}").expect_err("should not parse");
}
#[test]
fn query_section_capture_unnamed_succeeds_without_key() {
query(FieldNamingScheme::Unnamed)("lorem={}").expect("should parse");
}
#[test]
fn non_leading_numbers_in_ident() {
rust_ident("hello5").expect("sholud parse");
}
#[test]
fn leading_numbers_in_ident_fails() {
rust_ident("5hello").expect_err("sholud not parse");
}
}

View File

@ -1,239 +0,0 @@
use nom::error::ErrorKind;
use std::fmt;
/// Parser error that can print itself in a human-readable format.
#[derive(Clone, PartialEq)]
pub struct PrettyParseError<'a> {
/// Inner error
pub error: ParseError,
/// Input to the parser
pub input: &'a str,
/// Remaining input after partially tokenizing
pub remaining: &'a str,
}
/// Simple offset calculator to determine where to place the carrot for indicating an error.
fn offset(input: &str, substring: &str) -> usize {
input.len() - substring.len()
}
impl<'a> fmt::Debug for PrettyParseError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Could not parse route.")?;
f.write_str("\n")?;
let route_str: &str = "Route: ";
f.write_str(route_str)?;
f.write_str(self.input)?;
f.write_str("\n")?;
let offset = offset(self.input, self.remaining);
let offset = offset + self.error.offset;
let pad = (0..offset + route_str.len())
.map(|_| '-')
.collect::<String>();
f.write_str(&format!("{}^", pad))?;
f.write_str("\n")?;
if !self.error.expected.is_empty() {
f.write_str("Expected: ")?;
self.error.expected[..self.error.expected.len() - 1]
.iter()
.try_for_each(|expected| {
<ExpectedToken as fmt::Display>::fmt(expected, f)
.and_then(|_| f.write_str(", "))
})?;
self.error
.expected
.last()
.map(|expected| <ExpectedToken as fmt::Display>::fmt(expected, f))
.transpose()?;
f.write_str("\n")?;
}
if let Some(reason) = self.error.reason {
f.write_str("Reason: ")?;
<ParserErrorReason as fmt::Display>::fmt(&reason, f)?;
}
Ok(())
}
}
/// Error for parsing the route
#[derive(Debug, Clone, PartialEq)]
pub struct ParseError {
/// A concrete reason why the parse failed.
pub reason: Option<ParserErrorReason>,
/// Expected token sequences
pub expected: Vec<ExpectedToken>,
/// Additional offset for failures within sub-parsers.
/// Eg. if `{` parses, but then a bad ident is presented, some offset is needed here then.
pub offset: usize,
}
impl ParseError {
pub(crate) fn expected(expected: ExpectedToken) -> Self {
ParseError {
reason: None,
expected: vec![expected],
offset: 0,
}
}
}
impl nom::error::ParseError<&str> for ParseError {
fn from_error_kind(_input: &str, _kind: ErrorKind) -> Self {
ParseError {
reason: None,
expected: vec![],
offset: 0,
}
}
fn append(_input: &str, _kind: ErrorKind, other: Self) -> Self {
other
}
fn or(mut self, other: Self) -> Self {
// It is assumed that there aren't duplicates.
self.expected.extend(other.expected);
ParseError {
reason: other.reason.or(self.reason), // Take the right most reason
expected: self.expected,
offset: other.offset, /* Defer to the "other"'s offset. TODO it might make sense if the offsets are different, only show the other's "expected". */
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExpectedToken {
/// /
Separator,
/// specific string.
Literal,
/// ?
QueryBegin,
/// &
QuerySeparator,
/// \#
FragmentBegin,
/// !
End,
/// identifier within {}
Ident,
/// {
OpenBracket,
/// }
CloseBracket,
/// =
Equals,
/// *
Star,
/// :
Colon,
}
impl fmt::Display for ExpectedToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExpectedToken::Separator => f.write_str("/"),
ExpectedToken::Literal => f.write_str("<literal>"),
ExpectedToken::QueryBegin => f.write_str("?"),
ExpectedToken::QuerySeparator => f.write_str("&"),
ExpectedToken::FragmentBegin => f.write_str("#"),
ExpectedToken::End => f.write_str("!"),
ExpectedToken::Ident => f.write_str("<ident>"),
ExpectedToken::OpenBracket => f.write_str("{"),
ExpectedToken::CloseBracket => f.write_str("}"),
ExpectedToken::Equals => f.write_str("="),
ExpectedToken::Star => f.write_str("*"),
ExpectedToken::Colon => f.write_str(":"),
}
}
}
/// A concrete reason why a parse failed
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ParserErrorReason {
/// Some token encountered after the end token.
TokensAfterEndToken,
/// Two slashes are able to occur next to each other.
DoubleSlash,
/// End after a {}
EndAfterCapture,
/// A & appears before a ?
AndBeforeQuestion,
/// Captures can't be next to each other
AdjacentCaptures,
/// There can only be one question mark in the query section
MultipleQuestions,
/// The provided ident within a capture group could never match with a valid rust identifier.
BadRustIdent(char),
/// A bad literal.
BadLiteral,
/// Invalid state
InvalidState,
/// Can't have capture sections for unit structs/variants
CapturesInUnit,
/// Internal check on valid state transitions
/// This should never actually be created.
NotAllowedStateTransition,
/// Expected a specific token
Expected(ExpectedToken),
}
impl fmt::Display for ParserErrorReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParserErrorReason::TokensAfterEndToken => {
f.write_str("Characters appeared after the end token (!).")?;
}
ParserErrorReason::DoubleSlash => {
f.write_str("Two slashes are not allowed to be next to each other (//).")?;
}
ParserErrorReason::AndBeforeQuestion => {
f.write_str("The first query must be indicated with a '?', not a '&'.")?;
}
ParserErrorReason::AdjacentCaptures => {
f.write_str("Capture groups can't be next to each other. There must be some character in between the '}' and '{' characters.")?;
}
ParserErrorReason::InvalidState => {
f.write_str("Library Error: The parser was able to enter into an invalid state.")?;
}
ParserErrorReason::NotAllowedStateTransition => {
f.write_str("Library Error: A state transition was attempted that would put the parser in an invalid state")?;
}
ParserErrorReason::MultipleQuestions => {
f.write_str("There can only be one question mark in the query section. `&` should be used to separate other queries.")?;
}
ParserErrorReason::BadRustIdent(c) => {
f.write_str(&format!(
"The character: '{}' could not be used as a Rust identifier.",
c
))?;
}
ParserErrorReason::EndAfterCapture => {
f.write_str("The end token (!) can't appear after a capture ({}).")?;
}
ParserErrorReason::Expected(expected) => {
f.write_str(&format!("Expected: {}", expected))?;
}
ParserErrorReason::BadLiteral => {
f.write_str("Malformed literal.")?;
}
ParserErrorReason::CapturesInUnit => {
f.write_str("Cannot have a capture section for a unit struct or variant.")?;
}
}
Ok(())
}
}
pub(crate) fn get_reason(err: &mut nom::Err<ParseError>) -> &mut Option<ParserErrorReason> {
match err {
nom::Err::Error(err) | nom::Err::Failure(err) => &mut err.reason,
nom::Err::Incomplete(_) => panic!("Incomplete not possible"),
}
}

View File

@ -1,67 +0,0 @@
//! Parser for yew-router's matcher syntax.
//! This syntax allows specifying if a route should produce an enum variant or struct,
//! and allows capturing sections from the route to be incorporated into its associated variant or struct.
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_qualifications
)]
mod core;
mod error;
pub mod parser;
pub use crate::core::FieldNamingScheme;
pub use error::{ParseError, PrettyParseError};
mod optimizer;
pub use optimizer::{convert_tokens, parse_str_and_optimize_tokens};
use std::collections::HashMap;
/// Alias of `HashMap<&'a str, String>` that represent strings captured from a route.
///
/// Captures contain keys corresponding to named match sections,
/// and values containing the content captured by those sections.
pub type Captures<'a> = HashMap<&'a str, String>;
/// Tokens used to determine how to match and capture sections from a URL.
#[derive(Debug, PartialEq, Clone)]
pub enum MatcherToken {
/// Section-related tokens can be condensed into a match.
Exact(String),
/// Capture section.
Capture(CaptureVariant),
/// End token - if the string hasn't been consumed entirely, then the parse will fail.
/// This is useful for being able to specify more general matchers for variants that would
/// otherwise match above more specific variants.
End,
}
/// Variants that indicate how part of a string should be captured.
#[derive(Debug, PartialEq, Clone)]
pub enum CaptureVariant {
/// {}
Unnamed,
/// {*}
ManyUnnamed,
/// {5}
NumberedUnnamed {
/// Number of sections to match.
sections: usize,
},
/// {name} - captures a section and adds it to the map with a given name.
Named(String),
/// {*:name} - captures over many sections and adds it to the map with a given name.
ManyNamed(String),
/// {2:name} - captures a fixed number of sections with a given name.
NumberedNamed {
/// Number of sections to match.
sections: usize,
/// The key to be entered in the `Matches` map.
name: String,
},
}

View File

@ -1,150 +0,0 @@
use crate::{
error::PrettyParseError,
parser::{parse, CaptureOrExact, RefCaptureVariant, RouteParserToken},
};
use crate::{core::FieldNamingScheme, CaptureVariant, MatcherToken};
impl<'a> From<RefCaptureVariant<'a>> for CaptureVariant {
fn from(v: RefCaptureVariant<'a>) -> Self {
match v {
RefCaptureVariant::Named(s) => CaptureVariant::Named(s.to_string()),
RefCaptureVariant::ManyNamed(s) => CaptureVariant::ManyNamed(s.to_string()),
RefCaptureVariant::NumberedNamed { sections, name } => CaptureVariant::NumberedNamed {
sections,
name: name.to_string(),
},
RefCaptureVariant::Unnamed => CaptureVariant::Unnamed,
RefCaptureVariant::ManyUnnamed => CaptureVariant::ManyUnnamed,
RefCaptureVariant::NumberedUnnamed { sections } => {
CaptureVariant::NumberedUnnamed { sections }
}
}
}
}
impl<'a> From<CaptureOrExact<'a>> for MatcherToken {
fn from(value: CaptureOrExact<'a>) -> Self {
match value {
CaptureOrExact::Exact(m) => MatcherToken::Exact(m.to_string()),
CaptureOrExact::Capture(v) => MatcherToken::Capture(v.into()),
}
}
}
impl<'a> RouteParserToken<'a> {
fn as_str(&self) -> &str {
match self {
RouteParserToken::Separator => "/",
RouteParserToken::Exact(literal) => &literal,
RouteParserToken::QueryBegin => "?",
RouteParserToken::QuerySeparator => "&",
RouteParserToken::FragmentBegin => "#",
RouteParserToken::Nothing
| RouteParserToken::Capture { .. }
| RouteParserToken::Query { .. }
| RouteParserToken::End => unreachable!(),
}
}
}
/// Parse the provided "matcher string" and then optimize the tokens.
pub fn parse_str_and_optimize_tokens(
i: &str,
field_naming_scheme: FieldNamingScheme,
) -> Result<Vec<MatcherToken>, PrettyParseError> {
let tokens = parse(i, field_naming_scheme)?;
Ok(convert_tokens(&tokens))
}
/// Converts a slice of `RouteParserToken` into a Vec of MatcherTokens.
///
/// In the process of converting the tokens, this function will condense multiple RouteParserTokens
/// that represent literals into one Exact variant if multiple reducible tokens happen to occur in a row.
pub fn convert_tokens(tokens: &[RouteParserToken]) -> Vec<MatcherToken> {
let mut new_tokens: Vec<MatcherToken> = vec![];
let mut run: Vec<RouteParserToken> = vec![];
fn empty_run(run: &mut Vec<RouteParserToken>) -> Option<MatcherToken> {
let segment = run.iter().map(RouteParserToken::as_str).collect::<String>();
run.clear();
if !segment.is_empty() {
Some(MatcherToken::Exact(segment))
} else {
None
}
}
fn empty_run_with_query_cap_at_end(
run: &mut Vec<RouteParserToken>,
query_lhs: &str,
) -> MatcherToken {
let segment = run
.iter()
.map(RouteParserToken::as_str)
.chain(Some(query_lhs))
.chain(Some("="))
.collect::<String>();
run.clear();
MatcherToken::Exact(segment)
}
for token in tokens.iter() {
match token {
RouteParserToken::QueryBegin
| RouteParserToken::FragmentBegin
| RouteParserToken::Separator
| RouteParserToken::QuerySeparator
| RouteParserToken::Exact(_) => run.push(*token),
RouteParserToken::Capture(cap) => {
if let Some(current_run) = empty_run(&mut run) {
new_tokens.push(current_run);
}
new_tokens.push(MatcherToken::Capture(CaptureVariant::from(*cap)))
}
RouteParserToken::Query {
ident,
capture_or_exact,
} => match capture_or_exact {
CaptureOrExact::Exact(s) => {
run.push(RouteParserToken::Exact(ident));
run.push(RouteParserToken::Exact("="));
run.push(RouteParserToken::Exact(s));
}
CaptureOrExact::Capture(cap) => {
new_tokens.push(empty_run_with_query_cap_at_end(&mut run, *ident));
new_tokens.push(MatcherToken::Capture(CaptureVariant::from(*cap)))
}
},
RouteParserToken::End => {
if let Some(current_run) = empty_run(&mut run) {
new_tokens.push(current_run);
}
new_tokens.push(MatcherToken::End);
}
RouteParserToken::Nothing => {}
}
}
// Empty the run at the end.
if !run.is_empty() {
if let Some(current_run) = empty_run(&mut run) {
new_tokens.push(current_run);
}
}
new_tokens
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_creates_empty_token_list() {
let tokens = parse_str_and_optimize_tokens("", FieldNamingScheme::Unit).unwrap();
assert_eq!(tokens, vec![])
}
}

View File

@ -1,781 +0,0 @@
//! Parser that consumes a string and produces the first representation of the matcher.
use crate::{
core::{
capture, exact, fragment_exact, get_and, get_end, get_hash, get_question, get_slash,
nothing, query,
},
error::{get_reason, ParseError, ParserErrorReason, PrettyParseError},
FieldNamingScheme,
};
use nom::{branch::alt, IResult};
// use crate::core::escaped_item;
/// Tokens generated from parsing a route matcher string.
/// They will be optimized to another token type that is used to match URLs.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RouteParserToken<'a> {
/// Generated by the empty string `""`.
Nothing,
/// Match /
Separator,
/// Match a specific string.
Exact(&'a str),
/// Match {_}. See `RefCaptureVariant` for more.
Capture(RefCaptureVariant<'a>),
/// Match ?
QueryBegin,
/// Match &
QuerySeparator,
/// Match x=y
Query {
/// Identifier
ident: &'a str,
/// Capture or match
capture_or_exact: CaptureOrExact<'a>,
},
/// Match \#
FragmentBegin,
/// Match !
End,
}
/// Token representing various types of captures.
///
/// It can capture and discard for unnamed variants, or capture and store in the `Matches` for the
/// named variants.
///
/// Its name stems from the fact that it does not have ownership over all its values.
/// It gets converted to CaptureVariant, a nearly identical enum that has owned Strings instead.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RefCaptureVariant<'a> {
/// {}
Unnamed,
/// {*}
ManyUnnamed,
/// {5}
NumberedUnnamed {
/// Number of sections to match.
sections: usize,
},
/// {name} - captures a section and adds it to the map with a given name.
Named(&'a str),
/// {*:name} - captures over many sections and adds it to the map with a given name.
ManyNamed(&'a str),
/// {2:name} - captures a fixed number of sections with a given name.
NumberedNamed {
/// Number of sections to match.
sections: usize,
/// The key to be entered in the `Matches` map.
name: &'a str,
},
}
/// Either a Capture, or an Exact match
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CaptureOrExact<'a> {
/// Match a specific string.
Exact(&'a str),
/// Match a capture variant.
Capture(RefCaptureVariant<'a>),
}
/// Represents the states the parser can be in.
#[derive(Clone, PartialEq)]
enum ParserState<'a> {
None,
Path { prev_token: RouteParserToken<'a> },
FirstQuery { prev_token: RouteParserToken<'a> },
NthQuery { prev_token: RouteParserToken<'a> },
Fragment { prev_token: RouteParserToken<'a> },
End,
}
impl<'a> ParserState<'a> {
/// Given a new route parser token, transition to a new state.
///
/// This will set the prev token to a token able to be handled by the new state,
/// so the new state does not need to handle arbitrary "from" states.
///
/// This function represents the valid state transition graph.
fn transition(self, token: RouteParserToken<'a>) -> Result<Self, ParserErrorReason> {
match self {
ParserState::None => match token {
RouteParserToken::Separator
| RouteParserToken::Exact(_)
| RouteParserToken::Capture(_) => Ok(ParserState::Path { prev_token: token }),
RouteParserToken::QueryBegin => Ok(ParserState::FirstQuery { prev_token: token }),
RouteParserToken::QuerySeparator => Ok(ParserState::NthQuery { prev_token: token }),
RouteParserToken::Query { .. } => Err(ParserErrorReason::NotAllowedStateTransition),
RouteParserToken::FragmentBegin => Ok(ParserState::Fragment { prev_token: token }),
RouteParserToken::Nothing | RouteParserToken::End => Ok(ParserState::End),
},
ParserState::Path { prev_token } => {
match prev_token {
RouteParserToken::Separator => match token {
RouteParserToken::Exact(_) | RouteParserToken::Capture(_) => {
Ok(ParserState::Path { prev_token: token })
}
RouteParserToken::QueryBegin => {
Ok(ParserState::FirstQuery { prev_token: token })
}
RouteParserToken::FragmentBegin => {
Ok(ParserState::Fragment { prev_token: token })
}
RouteParserToken::End => Ok(ParserState::End),
_ => Err(ParserErrorReason::NotAllowedStateTransition),
},
RouteParserToken::Exact(_) => match token {
RouteParserToken::Exact(_)
| RouteParserToken::Separator
| RouteParserToken::Capture(_) => {
Ok(ParserState::Path { prev_token: token })
}
RouteParserToken::QueryBegin => {
Ok(ParserState::FirstQuery { prev_token: token })
}
RouteParserToken::FragmentBegin => {
Ok(ParserState::Fragment { prev_token: token })
}
RouteParserToken::End => Ok(ParserState::End),
_ => Err(ParserErrorReason::NotAllowedStateTransition),
},
RouteParserToken::Capture(_) => match token {
RouteParserToken::Separator | RouteParserToken::Exact(_) => {
Ok(ParserState::Path { prev_token: token })
}
RouteParserToken::QueryBegin => {
Ok(ParserState::FirstQuery { prev_token: token })
}
RouteParserToken::FragmentBegin => {
Ok(ParserState::Fragment { prev_token: token })
}
RouteParserToken::End => Ok(ParserState::End),
_ => Err(ParserErrorReason::NotAllowedStateTransition),
},
_ => Err(ParserErrorReason::InvalidState), /* Other previous token types are
* invalid within a Path state. */
}
}
ParserState::FirstQuery { prev_token } => match prev_token {
RouteParserToken::QueryBegin => match token {
RouteParserToken::Query { .. } => {
Ok(ParserState::FirstQuery { prev_token: token })
}
_ => Err(ParserErrorReason::NotAllowedStateTransition),
},
RouteParserToken::Query { .. } => match token {
RouteParserToken::QuerySeparator => {
Ok(ParserState::NthQuery { prev_token: token })
}
RouteParserToken::FragmentBegin => {
Ok(ParserState::Fragment { prev_token: token })
}
RouteParserToken::End => Ok(ParserState::End),
_ => Err(ParserErrorReason::NotAllowedStateTransition),
},
_ => Err(ParserErrorReason::InvalidState),
},
ParserState::NthQuery { prev_token } => match prev_token {
RouteParserToken::QuerySeparator => match token {
RouteParserToken::Query { .. } => {
Ok(ParserState::NthQuery { prev_token: token })
}
_ => Err(ParserErrorReason::NotAllowedStateTransition),
},
RouteParserToken::Query { .. } => match token {
RouteParserToken::QuerySeparator => {
Ok(ParserState::NthQuery { prev_token: token })
}
RouteParserToken::FragmentBegin => {
Ok(ParserState::Fragment { prev_token: token })
}
RouteParserToken::End => Ok(ParserState::End),
_ => Err(ParserErrorReason::NotAllowedStateTransition),
},
_ => Err(ParserErrorReason::InvalidState),
},
ParserState::Fragment { prev_token } => match prev_token {
RouteParserToken::FragmentBegin
| RouteParserToken::Exact(_)
| RouteParserToken::Capture(_) => Ok(ParserState::Fragment { prev_token: token }),
RouteParserToken::End => Ok(ParserState::End),
_ => Err(ParserErrorReason::InvalidState),
},
ParserState::End => Err(ParserErrorReason::TokensAfterEndToken),
}
}
}
/// Parse a matching string into a vector of RouteParserTokens.
///
/// The parsing logic involves using a state machine.
/// After a token is read, this token is fed into the state machine, causing it to transition to a new state or throw an error.
/// Because the tokens that can be parsed in each state are limited, errors are not actually thrown in the state transition,
/// due to the fact that erroneous tokens can't be fed into the transition function.
///
/// This continues until the string is exhausted, or none of the parsers for the current state can parse the current input.
pub fn parse(
mut i: &str,
field_naming_scheme: FieldNamingScheme,
) -> Result<Vec<RouteParserToken>, PrettyParseError> {
let input = i;
let mut tokens: Vec<RouteParserToken> = vec![];
let mut state = ParserState::None;
loop {
let (ii, token) = parse_impl(i, &state, field_naming_scheme).map_err(|e| match e {
nom::Err::Error(e) | nom::Err::Failure(e) => PrettyParseError {
error: e,
input,
remaining: i,
},
_ => panic!("parser should not be incomplete"),
})?;
i = ii;
state = state.transition(token).map_err(|reason| {
let error = ParseError {
reason: Some(reason),
expected: vec![],
offset: 0,
};
PrettyParseError {
error,
input,
remaining: i,
}
})?;
tokens.push(token);
// If there is no more input, break out of the loop
if i.is_empty() {
break;
}
}
Ok(tokens)
}
fn parse_impl<'a>(
i: &'a str,
state: &ParserState,
field_naming_scheme: FieldNamingScheme,
) -> IResult<&'a str, RouteParserToken<'a>, ParseError> {
match state {
ParserState::None => alt((
get_slash,
get_question,
get_and,
get_hash,
capture(field_naming_scheme),
exact,
get_end,
nothing,
))(i),
ParserState::Path { prev_token } => match prev_token {
RouteParserToken::Separator => {
alt((
exact,
capture(field_naming_scheme),
get_question,
get_hash,
get_end,
))(i)
.map_err(|mut e: nom::Err<ParseError>| {
// Detect likely failures if the above failed to match.
let reason: &mut Option<ParserErrorReason> = get_reason(&mut e);
*reason = get_slash(i)
.map(|_| ParserErrorReason::DoubleSlash)
.or_else(|_| get_and(i).map(|_| ParserErrorReason::AndBeforeQuestion))
.ok()
.or(*reason);
e
})
}
RouteParserToken::Exact(_) => {
alt((
get_slash,
exact, // This will handle escaped items
capture(field_naming_scheme),
get_question,
get_hash,
get_end,
))(i)
.map_err(|mut e: nom::Err<ParseError>| {
// Detect likely failures if the above failed to match.
let reason: &mut Option<ParserErrorReason> = get_reason(&mut e);
*reason = get_and(i)
.map(|_| ParserErrorReason::AndBeforeQuestion)
.ok()
.or(*reason);
e
})
}
RouteParserToken::Capture(_) => {
alt((get_slash, exact, get_question, get_hash, get_end))(i).map_err(
|mut e: nom::Err<ParseError>| {
// Detect likely failures if the above failed to match.
let reason: &mut Option<ParserErrorReason> = get_reason(&mut e);
*reason = capture(field_naming_scheme)(i)
.map(|_| ParserErrorReason::AdjacentCaptures)
.or_else(|_| get_and(i).map(|_| ParserErrorReason::AndBeforeQuestion))
.ok()
.or(*reason);
e
},
)
}
_ => Err(nom::Err::Failure(ParseError {
reason: Some(ParserErrorReason::InvalidState),
expected: vec![],
offset: 0,
})),
},
ParserState::FirstQuery { prev_token } => match prev_token {
RouteParserToken::QueryBegin => {
query(field_naming_scheme)(i).map_err(|mut e: nom::Err<ParseError>| {
// Detect likely failures if the above failed to match.
let reason: &mut Option<ParserErrorReason> = get_reason(&mut e);
*reason = get_question(i)
.map(|_| ParserErrorReason::MultipleQuestions)
.ok()
.or(*reason);
e
})
}
RouteParserToken::Query { .. } => {
alt((get_and, get_hash, get_end))(i).map_err(|mut e: nom::Err<ParseError>| {
// Detect likely failures if the above failed to match.
let reason: &mut Option<ParserErrorReason> = get_reason(&mut e);
*reason = get_question(i)
.map(|_| ParserErrorReason::MultipleQuestions)
.ok()
.or(*reason);
e
})
}
_ => Err(nom::Err::Failure(ParseError {
reason: Some(ParserErrorReason::InvalidState),
expected: vec![],
offset: 0,
})),
},
ParserState::NthQuery { prev_token } => match prev_token {
RouteParserToken::QuerySeparator => {
query(field_naming_scheme)(i).map_err(|mut e: nom::Err<ParseError>| {
// Detect likely failures if the above failed to match.
let reason: &mut Option<ParserErrorReason> = get_reason(&mut e);
*reason = get_question(i)
.map(|_| ParserErrorReason::MultipleQuestions)
.ok()
.or(*reason);
e
})
}
RouteParserToken::Query { .. } => {
alt((get_and, get_hash, get_end))(i).map_err(|mut e: nom::Err<ParseError>| {
// Detect likely failures if the above failed to match.
let reason: &mut Option<ParserErrorReason> = get_reason(&mut e);
*reason = get_question(i)
.map(|_| ParserErrorReason::MultipleQuestions)
.ok()
.or(*reason);
e
})
}
_ => Err(nom::Err::Failure(ParseError {
reason: Some(ParserErrorReason::InvalidState),
expected: vec![],
offset: 0,
})),
},
ParserState::Fragment { prev_token } => match prev_token {
RouteParserToken::FragmentBegin => {
alt((fragment_exact, capture(field_naming_scheme), get_end))(i)
}
RouteParserToken::Exact(_) => alt((capture(field_naming_scheme), get_end))(i),
RouteParserToken::Capture(_) => alt((fragment_exact, get_end))(i),
_ => Err(nom::Err::Failure(ParseError {
reason: Some(ParserErrorReason::InvalidState),
expected: vec![],
offset: 0,
})),
},
ParserState::End => Err(nom::Err::Failure(ParseError {
reason: Some(ParserErrorReason::TokensAfterEndToken),
expected: vec![],
offset: 0,
})),
}
}
#[cfg(test)]
mod test {
// use super::*;
use super::parse as actual_parse;
use crate::{parser::RouteParserToken, FieldNamingScheme, PrettyParseError};
// Call all tests to parse with the Unnamed variant
fn parse(i: &str) -> Result<Vec<RouteParserToken>, PrettyParseError> {
actual_parse(i, FieldNamingScheme::Unnamed)
}
mod does_parse {
use super::*;
#[test]
fn empty() {
let x = parse("").expect("Should parse");
assert_eq!(x, vec![RouteParserToken::Nothing])
}
#[test]
fn slash() {
parse("/").expect("should parse");
}
#[test]
fn slash_exact() {
parse("/hello").expect("should parse");
}
#[test]
fn multiple_exact() {
parse("/lorem/ipsum").expect("should parse");
}
#[test]
fn capture_in_path() {
parse("/lorem/{ipsum}").expect("should parse");
}
#[test]
fn capture_rest_in_path() {
parse("/lorem/{*:ipsum}").expect("should parse");
}
#[test]
fn capture_numbered_in_path() {
parse("/lorem/{5:ipsum}").expect("should parse");
}
#[test]
fn exact_query_after_path() {
parse("/lorem?ipsum=dolor").expect("should parse");
}
#[test]
fn leading_query_separator() {
parse("&lorem=ipsum").expect("Should parse");
}
#[test]
fn exact_query() {
parse("?lorem=ipsum").expect("should parse");
}
#[test]
fn capture_query() {
parse("?lorem={ipsum}").expect("should parse");
}
#[test]
fn multiple_queries() {
parse("?lorem=ipsum&dolor=sit").expect("should parse");
}
#[test]
fn query_and_exact_fragment() {
parse("?lorem=ipsum#dolor").expect("should parse");
}
#[test]
fn query_with_exact_and_capture_fragment() {
parse("?lorem=ipsum#dolor{sit}").expect("should parse");
}
#[test]
fn query_with_capture_fragment() {
parse("?lorem=ipsum#{dolor}").expect("should parse");
}
#[test]
fn escaped_backslash() {
let tokens = parse(r#"/escaped\\backslash"#).expect("should parse");
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact(r#"escaped\\backslash"#),
];
assert_eq!(tokens, expected);
}
#[test]
fn escaped_exclamation() {
let tokens = parse(r#"/escaped!!exclamation"#).expect("should parse");
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact(r#"escaped"#),
RouteParserToken::Exact(r#"!"#),
RouteParserToken::Exact(r#"exclamation"#),
];
assert_eq!(tokens, expected);
}
#[test]
fn escaped_open_bracket() {
let tokens = parse(r#"/escaped{{bracket"#).expect("should parse");
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact(r#"escaped"#),
RouteParserToken::Exact(r#"{"#),
RouteParserToken::Exact(r#"bracket"#),
];
assert_eq!(tokens, expected);
}
#[test]
fn escaped_close_bracket() {
let tokens = parse(r#"/escaped}}bracket"#).expect("should parse");
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact(r#"escaped"#),
RouteParserToken::Exact(r#"}"#),
RouteParserToken::Exact(r#"bracket"#),
];
assert_eq!(tokens, expected);
}
}
mod does_not_parse {
use super::*;
use crate::error::{ExpectedToken, ParserErrorReason};
#[test]
fn double_slash() {
let x = parse("//").expect_err("Should not parse");
assert_eq!(x.error.reason, Some(ParserErrorReason::DoubleSlash))
}
#[test]
fn slash_ampersand() {
let x = parse("/&lorem=ipsum").expect_err("Should not parse");
assert_eq!(x.error.reason, Some(ParserErrorReason::AndBeforeQuestion))
}
#[test]
fn non_ident_capture() {
let x = parse("/{lor#m}").expect_err("Should not parse");
assert_eq!(x.error.reason, Some(ParserErrorReason::BadRustIdent('#')));
assert_eq!(
x.error.expected,
vec![ExpectedToken::CloseBracket, ExpectedToken::Ident]
)
}
#[test]
fn after_end() {
let x = parse("/lorem/ipsum!/dolor").expect_err("Should not parse");
assert_eq!(x.error.reason, Some(ParserErrorReason::TokensAfterEndToken));
}
}
mod correct_parse {
use super::*;
use crate::parser::{CaptureOrExact, RefCaptureVariant};
#[test]
fn starting_literal() {
let parsed = parse("lorem").unwrap();
let expected = vec![RouteParserToken::Exact("lorem")];
assert_eq!(parsed, expected);
}
#[test]
fn minimal_path() {
let parsed = parse("/lorem").unwrap();
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact("lorem"),
];
assert_eq!(parsed, expected);
}
#[test]
fn multiple_path() {
let parsed = parse("/lorem/ipsum/dolor/sit").unwrap();
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact("lorem"),
RouteParserToken::Separator,
RouteParserToken::Exact("ipsum"),
RouteParserToken::Separator,
RouteParserToken::Exact("dolor"),
RouteParserToken::Separator,
RouteParserToken::Exact("sit"),
];
assert_eq!(parsed, expected);
}
#[test]
fn capture_path() {
let parsed = parse("/{lorem}/{ipsum}").unwrap();
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::Named("ipsum")),
];
assert_eq!(parsed, expected);
}
#[test]
fn query() {
let parsed = parse("?query=this").unwrap();
let expected = vec![
RouteParserToken::QueryBegin,
RouteParserToken::Query {
ident: "query",
capture_or_exact: CaptureOrExact::Exact("this"),
},
];
assert_eq!(parsed, expected);
}
#[test]
fn query_2_part() {
let parsed = parse("?lorem=ipsum&dolor=sit").unwrap();
let expected = vec![
RouteParserToken::QueryBegin,
RouteParserToken::Query {
ident: "lorem",
capture_or_exact: CaptureOrExact::Exact("ipsum"),
},
RouteParserToken::QuerySeparator,
RouteParserToken::Query {
ident: "dolor",
capture_or_exact: CaptureOrExact::Exact("sit"),
},
];
assert_eq!(parsed, expected);
}
#[test]
fn query_3_part() {
let parsed = parse("?lorem=ipsum&dolor=sit&amet=consectetur").unwrap();
let expected = vec![
RouteParserToken::QueryBegin,
RouteParserToken::Query {
ident: "lorem",
capture_or_exact: CaptureOrExact::Exact("ipsum"),
},
RouteParserToken::QuerySeparator,
RouteParserToken::Query {
ident: "dolor",
capture_or_exact: CaptureOrExact::Exact("sit"),
},
RouteParserToken::QuerySeparator,
RouteParserToken::Query {
ident: "amet",
capture_or_exact: CaptureOrExact::Exact("consectetur"),
},
];
assert_eq!(parsed, expected);
}
#[test]
fn exact_fragment() {
let parsed = parse("#lorem").unwrap();
let expected = vec![
RouteParserToken::FragmentBegin,
RouteParserToken::Exact("lorem"),
];
assert_eq!(parsed, expected);
}
#[test]
fn capture_fragment() {
let parsed = parse("#{lorem}").unwrap();
let expected = vec![
RouteParserToken::FragmentBegin,
RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
];
assert_eq!(parsed, expected);
}
#[test]
fn mixed_fragment() {
let parsed = parse("#{lorem}ipsum{dolor}").unwrap();
let expected = vec![
RouteParserToken::FragmentBegin,
RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
RouteParserToken::Exact("ipsum"),
RouteParserToken::Capture(RefCaptureVariant::Named("dolor")),
];
assert_eq!(parsed, expected);
}
#[test]
fn end_after_path() {
let parsed = parse("/lorem!").unwrap();
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact("lorem"),
RouteParserToken::End,
];
assert_eq!(parsed, expected);
}
#[test]
fn end_after_path_separator() {
let parsed = parse("/lorem/!").unwrap();
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact("lorem"),
RouteParserToken::Separator,
RouteParserToken::End,
];
assert_eq!(parsed, expected);
}
#[test]
fn end_after_path_capture() {
let parsed = parse("/lorem/{cap}!").unwrap();
let expected = vec![
RouteParserToken::Separator,
RouteParserToken::Exact("lorem"),
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::Named("cap")),
RouteParserToken::End,
];
assert_eq!(parsed, expected);
}
#[test]
fn end_after_query_capture() {
let parsed = parse("?lorem={cap}!").unwrap();
let expected = vec![
RouteParserToken::QueryBegin,
RouteParserToken::Query {
ident: "lorem",
capture_or_exact: CaptureOrExact::Capture(RefCaptureVariant::Named("cap")),
},
RouteParserToken::End,
];
assert_eq!(parsed, expected);
}
#[test]
fn end_after_frag_capture() {
let parsed = parse("#{cap}!").unwrap();
let expected = vec![
RouteParserToken::FragmentBegin,
RouteParserToken::Capture(RefCaptureVariant::Named("cap")),
RouteParserToken::End,
];
assert_eq!(parsed, expected);
}
#[test]
fn just_end() {
let parsed = parse("!").unwrap();
assert_eq!(parsed, vec![RouteParserToken::End]);
}
}
}

View File

@ -1,7 +1,7 @@
[package]
name = "yew-router"
version = "0.14.0"
authors = ["Henry Zimmerman <zimhen7@gmail.com>", "Sascha Grunert <mail@saschagrunert.de>"]
authors = ["Hamza <muhammadhamza1311@gmail.com>"]
edition = "2018"
license = "MIT OR Apache-2.0"
readme = "README.md"
@ -10,38 +10,39 @@ categories = ["gui", "web-programming"]
description = "A router implementation for the Yew framework"
repository = "https://github.com/yewstack/yew"
[features]
default = ["core", "unit_alias"]
core = ["router", "components"] # Most everything
unit_alias = [] # TODO remove this
router = ["agent"] # The Router component
components = ["agent" ] # The button and anchor
agent = ["service"] # The RouteAgent
service = ["yew"] # The RouteService
[dependencies]
yew = { version = "0.17.0", path = "../yew", features = ["agent"], default-features= false, optional = true }
yew-router-macro = { version = "0.14.0", path = "../yew-router-macro" }
yew-router-route-parser = { version = "0.14.0", path = "../yew-router-route-parser" }
yew = { path = "../yew", default-features= false }
yew-router-macro = { path = "../yew-router-macro" }
gloo = "0.2.0"
js-sys = "0.3.35"
log = "0.4.8"
nom = "5.1.1"
serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.48"
wasm-bindgen = "0.2.58"
wasm-bindgen = "0.2"
js-sys = "0.3"
weblog = "0.3.0"
gloo = "0.2.1"
route-recognizer = "0.3.0"
serde = "1.0"
serde_urlencoded = "0.7"
[dependencies.web-sys]
version = "0.3"
features = [
'History',
'HtmlLinkElement',
'Location',
'MouseEvent',
'PopStateEvent',
'Window',
"Attr",
"Document",
"History",
"HtmlBaseElement",
"Event",
"NamedNodeMap",
"Url",
"UrlSearchParams",
"Window",
]
[dev-dependencies]
uuid = "0.8.1"
wasm-bindgen-test = "0.3"
yew-functional = { path = "../yew-functional" }
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies.web-sys]
version = "0.3"
features = [
"HtmlHeadElement",
]

View File

@ -3,35 +3,40 @@ A routing library for the [Yew](https://github.com/yewstack/yew) frontend framew
### Example
```rust
#[derive(Switch, Debug, Clone)]
pub enum AppRoute {
#[at = "/profile/{id}"]
Profile(u32),
#[at = "/forum{*:rest}"]
Forum(ForumRoute),
#[at = "/"]
Index,
use yew::prelude::*;
use yew_functional::*;
use yew_router::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Routable)]
enum Route {
#[at("/")]
Home,
#[at("/secure")]
Secure,
#[not_found]
#[at("/404")]
NotFound,
}
#[derive(Switch, Debug, Clone)]
pub enum ForumRoute {
#[at = "/{subforum}/{thread_slug}"]
SubForumAndThread{subforum: String, thread_slug: String}
#[at = "/{subforum}"]
SubForum{subforum: String}
fn switch(routes: &Route) -> Html {
let onclick_callback = Callback::from(|_| yew_router::service::push(Route::Home, None));
match routes {
Route::Home => html! { <h1>{ "Home" }</h1> },
Route::Secure => html! {
<div>
<h1>{ "Secure" }</h1>
<button onclick=onclick_callback>{ "Go Home" }</button>
</div>
},
Route::NotFound => html! { <h1>{ "404" }</h1> },
}
}
// Component's `view` method
html! {
<Router<AppRoute, ()>
render = Router::render(|switch: AppRoute| {
match switch {
AppRoute::Profile(id) => html!{<ProfileComponent id = id/>},
AppRoute::Index => html!{<IndexComponent/>},
AppRoute::Forum(forum_route) => html!{<ForumComponent route = forum_route/>},
}
})
/>
<Router<Route> render=Router::render(switch) />
}
```

View File

@ -1,56 +0,0 @@
//! Bridge to RouteAgent.
use crate::{agent::RouteAgent, route::Route, RouteState};
use std::{
fmt::{Debug, Error as FmtError, Formatter},
ops::{Deref, DerefMut},
};
use yew::{
agent::{Bridged, Context},
Bridge, Callback,
};
/// A wrapped bridge to the route agent.
///
/// A component that owns this can send and receive messages from the agent.
pub struct RouteAgentBridge<STATE = ()>(Box<dyn Bridge<RouteAgent<STATE>>>)
where
STATE: RouteState;
impl<STATE> RouteAgentBridge<STATE>
where
STATE: RouteState,
{
/// Creates a new bridge.
pub fn new(callback: Callback<Route<STATE>>) -> Self {
let router_agent = RouteAgent::bridge(callback);
RouteAgentBridge(router_agent)
}
/// Experimental, may be removed
///
/// Directly spawn a new Router
pub fn spawn(callback: Callback<Route<STATE>>) -> Self {
use yew::agent::Discoverer;
let router_agent = Context::spawn_or_join(Some(callback));
RouteAgentBridge(router_agent)
}
}
impl<STATE: RouteState> Debug for RouteAgentBridge<STATE> {
fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
f.debug_tuple("RouteAgentBridge").finish()
}
}
impl<STATE: RouteState> Deref for RouteAgentBridge<STATE> {
type Target = Box<dyn Bridge<RouteAgent<STATE>>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<STATE: RouteState> DerefMut for RouteAgentBridge<STATE> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@ -1,53 +0,0 @@
//! Dispatcher to RouteAgent.
use crate::{agent::RouteAgent, RouteState};
use std::{
fmt::{Debug, Error as FmtError, Formatter},
ops::{Deref, DerefMut},
};
use yew::agent::{Dispatched, Dispatcher};
/// A wrapped dispatcher to the route agent.
///
/// A component that owns and instance of this can send messages to the RouteAgent, but not receive them.
pub struct RouteAgentDispatcher<STATE = ()>(Dispatcher<RouteAgent<STATE>>)
where
STATE: RouteState;
impl<STATE> RouteAgentDispatcher<STATE>
where
STATE: RouteState,
{
/// Creates a new bridge.
pub fn new() -> Self {
let dispatcher = RouteAgent::dispatcher();
RouteAgentDispatcher(dispatcher)
}
}
impl<STATE> Default for RouteAgentDispatcher<STATE>
where
STATE: RouteState,
{
fn default() -> Self {
Self::new()
}
}
impl<STATE: RouteState> Debug for RouteAgentDispatcher<STATE> {
fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
f.debug_tuple("RouteAgentDispatcher").finish()
}
}
impl<STATE: RouteState> Deref for RouteAgentDispatcher<STATE> {
type Target = Dispatcher<RouteAgent<STATE>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: RouteState> DerefMut for RouteAgentDispatcher<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@ -1,158 +0,0 @@
//! Routing agent.
//!
//! It wraps a route service and allows calls to be sent to it to update every subscriber,
//! or just the element that made the request.
use crate::service::RouteService;
use yew::prelude::worker::*;
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Error as FmtError, Formatter};
use crate::route::{Route, RouteState};
use log::trace;
mod bridge;
pub use bridge::RouteAgentBridge;
mod dispatcher;
pub use dispatcher::RouteAgentDispatcher;
/// Internal Message used for the RouteAgent.
#[derive(Debug)]
pub enum Msg<STATE> {
/// Message for when the route is changed.
BrowserNavigationRouteChanged(Route<STATE>), // TODO make this a route?
}
/// Input message type for interacting with the `RouteAgent'.
#[derive(Serialize, Deserialize, Debug)]
pub enum RouteRequest<T = ()> {
/// Replaces the most recent Route with a new one and alerts connected components to the route
/// change.
ReplaceRoute(Route<T>),
/// Replaces the most recent Route with a new one, but does not alert connected components to
/// the route change.
ReplaceRouteNoBroadcast(Route<T>),
/// Changes the route using a Route struct and alerts connected components to the route change.
ChangeRoute(Route<T>),
/// Changes the route using a Route struct, but does not alert connected components to the
/// route change.
ChangeRouteNoBroadcast(Route<T>),
/// Gets the current route.
GetCurrentRoute,
}
/// The RouteAgent holds on to the RouteService singleton and mediates access to it.
///
/// It serves as a means to propagate messages to components interested in the state of the current
/// route.
///
/// # Warning
/// All routing-related components/agents/services should use the same type parameter across your application.
///
/// If you use multiple agents with different types, then the Agents won't be able to communicate to
/// each other and associated components may not work as intended.
pub struct RouteAgent<STATE = ()>
where
STATE: RouteState,
{
// In order to have the AgentLink<Self> below, apparently T must be constrained like this.
// Unfortunately, this means that everything related to an agent requires this constraint.
link: AgentLink<RouteAgent<STATE>>,
/// The service through which communication with the browser happens.
route_service: RouteService<STATE>,
/// A list of all entities connected to the router.
/// When a route changes, either initiated by the browser or by the app,
/// the route change will be broadcast to all listening entities.
subscribers: HashSet<HandlerId>,
}
impl<STATE: RouteState> Debug for RouteAgent<STATE> {
fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
f.debug_struct("RouteAgent")
.field("link", &"-")
.field("route_service", &self.route_service)
.field("subscribers", &self.subscribers.len())
.finish()
}
}
impl<STATE> Agent for RouteAgent<STATE>
where
STATE: RouteState,
{
type Input = RouteRequest<STATE>;
type Message = Msg<STATE>;
type Output = Route<STATE>;
type Reach = Context<Self>;
fn create(link: AgentLink<RouteAgent<STATE>>) -> Self {
let callback = link.callback(Msg::BrowserNavigationRouteChanged);
let mut route_service = RouteService::new();
route_service.register_callback(callback);
RouteAgent {
link,
route_service,
subscribers: HashSet::new(),
}
}
fn update(&mut self, msg: Self::Message) {
match msg {
Msg::BrowserNavigationRouteChanged(route) => {
trace!("Browser navigated");
for sub in &self.subscribers {
self.link.respond(*sub, route.clone());
}
}
}
}
fn connected(&mut self, id: HandlerId) {
self.subscribers.insert(id);
}
fn handle_input(&mut self, msg: Self::Input, who: HandlerId) {
match msg {
RouteRequest::ReplaceRoute(route) => {
let route_string: String = route.to_string();
self.route_service.replace_route(&route_string, route.state);
let route = self.route_service.get_route();
for sub in &self.subscribers {
self.link.respond(*sub, route.clone());
}
}
RouteRequest::ReplaceRouteNoBroadcast(route) => {
let route_string: String = route.to_string();
self.route_service.replace_route(&route_string, route.state);
}
RouteRequest::ChangeRoute(route) => {
let route_string: String = route.to_string();
// set the route
self.route_service.set_route(&route_string, route.state);
// get the new route.
let route = self.route_service.get_route();
// broadcast it to all listening components
for sub in &self.subscribers {
self.link.respond(*sub, route.clone());
}
}
RouteRequest::ChangeRouteNoBroadcast(route) => {
let route_string: String = route.to_string();
self.route_service.set_route(&route_string, route.state);
}
RouteRequest::GetCurrentRoute => {
let route = self.route_service.get_route();
self.link.respond(who, route);
}
}
}
fn disconnected(&mut self, id: HandlerId) {
self.subscribers.remove(&id);
}
}

View File

@ -1,85 +0,0 @@
/// Generates a module named `router_state` containing aliases to common structures within
/// yew_router that deal with operating with Route and its state values as well as functions for
/// rendering routes.
///
/// Because they should be the same across a given application,
/// its a handy way to make sure that every type that could be needed is generated.
///
/// This macro is used to generate aliases for the state type of `()` within yew_router.
/// Instead of doing these yourself, use this macro if you need to store state in the browser.
///
/// # Example
/// ```
/// # use yew_router::define_router_state;
/// define_router_state!(Option<String>);
/// use router_state::Route; // alias to Route<Option<String>>
/// # fn main() {}
/// ```
#[macro_export]
macro_rules! define_router_state {
($StateT:ty) => {
define_router_state!($StateT, stringify!($StateT));
};
($StateT:ty, $StateName:expr) => {
#[doc = "A set of aliases to commonly used structures and functions used for routing."]
mod router_state {
#[doc = "The state that can be stored by the router service."]
pub type State = $StateT;
#[doc = "Alias to [Route<"]
#[doc = $StateName]
#[doc = ">](route/struct.Route.html)."]
pub type Route = $crate::route::Route<$StateT>;
#[doc = "Alias to [RouteService<"]
#[doc = $StateName]
#[doc = ">](route_service/struct.RouteService.html)."]
pub type RouteService = $crate::service::RouteService<$StateT>;
#[cfg(feature = "agent")]
#[doc = "Alias to [RouteAgent<"]
#[doc = $StateName]
#[doc = ">](agent/struct.RouteAgent.html)."]
pub type RouteAgent = $crate::agent::RouteAgent<$StateT>;
#[cfg(feature = "agent")]
#[doc = "Alias to [RouteAgentBridge<"]
#[doc = $StateName]
#[doc = ">](agent/bridge/struct.RouteAgentBridge.html)`."]
pub type RouteAgentBridge = $crate::agent::RouteAgentBridge<$StateT>;
#[cfg(feature = "agent")]
#[doc = "Alias to [RouteAgentDispatcher<"]
#[doc = $StateName]
#[doc = ">](agent/struct.RouteAgentDispatcher.html)`."]
pub type RouteAgentDispatcher = $crate::agent::RouteAgentDispatcher<$StateT>;
#[allow(deprecated)]
#[deprecated(note = "Has been renamed to RouterAnchor")]
#[cfg(feature = "components")]
#[doc = "Alias to [RouterLink<"]
#[doc = $StateName]
#[doc = ">](components/struct.RouterLink.html)`."]
pub type RouterLink = $crate::components::RouterLink<$StateT>;
#[cfg(feature = "components")]
#[doc = "Alias to [RouterAnchor<"]
#[doc = $StateName]
#[doc = ">](components/struct.RouterAnchor.html)`."]
pub type RouterAnchor = $crate::components::RouterAnchor<$StateT>;
#[cfg(feature = "components")]
#[doc = "Alias to [RouterButton<"]
#[doc = $StateName]
#[doc = ">](components/struct.RouterButton.html)`."]
pub type RouterButton = $crate::components::RouterButton<$StateT>;
#[cfg(feature = "router")]
#[doc = "Alias to [Router<"]
#[doc = $StateName]
#[doc = ">](router/router/struct.Router.html)."]
pub type Router<SW> = $crate::router::Router<$StateT, SW>;
}
};
}

View File

@ -0,0 +1,52 @@
use crate::{service, Routable};
use yew::prelude::*;
/// Props for [`Link`]
#[derive(Properties, Clone, PartialEq)]
pub struct LinkProps<R: Routable + Clone> {
/// CSS classes to add to the anchor element (optional).
#[prop_or_default]
pub classes: Classes,
/// Route that will be pushed when the anchor is clicked.
pub route: R,
pub children: Children,
}
/// A wrapper around `<a>` tag to be used with [`Router`](crate::Router)
pub struct Link<R: Routable + Clone + PartialEq + 'static> {
link: ComponentLink<Self>,
props: LinkProps<R>,
}
pub enum Msg {
OnClick,
}
impl<R: Routable + Clone + PartialEq + 'static> Component for Link<R> {
type Message = Msg;
type Properties = LinkProps<R>;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self { link, props }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::OnClick => {
service::push_route(self.props.route.clone());
false
}
}
}
fn change(&mut self, mut props: Self::Properties) -> ShouldRender {
std::mem::swap(&mut self.props, &mut props);
props != self.props
}
fn view(&self) -> Html {
html! {
<a class=self.props.classes.clone() onclick=self.link.callback(|_| Msg::OnClick)>{ self.props.children.clone() }</a>
}
}
}

View File

@ -1,45 +1,4 @@
//! Components that integrate with the [route agent](agent/struct.RouteAgent.html).
//!
//! At least one bridge to the agent needs to exist for these to work.
//! This can be done transitively by using a `Router` component, which owns a bridge to the agent.
//! Components to interface with [Router][crate::Router].
mod router_button;
mod router_link;
use yew::{Children, Properties};
#[allow(deprecated)]
pub use self::{router_button::RouterButton, router_link::RouterAnchor, router_link::RouterLink};
use crate::Switch;
// TODO This should also be PartialEq and Clone. Its blocked on Children not supporting that.
// TODO This should no longer take link & String, and instead take a route: SW implementing Switch
/// Properties for `RouterButton` and `RouterLink`.
#[derive(Properties, Clone, Default, Debug)]
pub struct Props<SW>
where
SW: Switch + Clone,
{
/// The Switched item representing the route.
pub route: SW,
#[deprecated(note = "Use children field instead (nested html)")]
/// The text to display.
#[prop_or_default]
pub text: String,
/// Html inside the component.
#[prop_or_default]
pub children: Children,
/// Disable the component.
#[prop_or_default]
pub disabled: bool,
/// Classes to be added to component.
#[prop_or_default]
pub classes: String,
}
/// Message for `RouterButton` and `RouterLink`.
#[derive(Clone, Copy, Debug)]
pub enum Msg {
/// Tell the router to navigate the application to the Component's pre-defined route.
Clicked,
}
mod link;
pub use link::*;

View File

@ -1,68 +0,0 @@
//! A component wrapping a `<button>` tag that changes the route.
use crate::{
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
Switch,
};
use yew::prelude::*;
use super::{Msg, Props};
use crate::RouterState;
use yew::virtual_dom::VNode;
/// Changes the route when clicked.
#[derive(Debug)]
pub struct RouterButton<SW: Switch + Clone + 'static, STATE: RouterState = ()> {
link: ComponentLink<Self>,
router: RouteAgentDispatcher<STATE>,
props: Props<SW>,
}
impl<SW: Switch + Clone + 'static, STATE: RouterState> Component for RouterButton<SW, STATE> {
type Message = Msg;
type Properties = Props<SW>;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let router = RouteAgentDispatcher::new();
RouterButton {
link,
router,
props,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Clicked => {
let route = Route::from(self.props.route.clone());
self.router.send(RouteRequest::ChangeRoute(route));
false
}
}
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props = props;
true
}
fn view(&self) -> VNode {
let cb = self.link.callback(|event: MouseEvent| {
event.prevent_default();
Msg::Clicked
});
html! {
<button
class=self.props.classes.clone()
onclick=cb
disabled=self.props.disabled
>
{
#[allow(deprecated)]
&self.props.text
}
{self.props.children.iter().collect::<VNode>()}
</button>
}
}
}

View File

@ -1,79 +0,0 @@
//! A component wrapping an `<a>` tag that changes the route.
use crate::{
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
Switch,
};
use yew::prelude::*;
use super::{Msg, Props};
use crate::RouterState;
use yew::virtual_dom::VNode;
/// An anchor tag Component that when clicked, will navigate to the provided route.
///
/// Alias to RouterAnchor.
#[deprecated(note = "Has been renamed to RouterAnchor")]
pub type RouterLink<T> = RouterAnchor<T>;
/// An anchor tag Component that when clicked, will navigate to the provided route.
#[derive(Debug)]
pub struct RouterAnchor<SW: Switch + Clone + 'static, STATE: RouterState = ()> {
link: ComponentLink<Self>,
router: RouteAgentDispatcher<STATE>,
props: Props<SW>,
}
impl<SW: Switch + Clone + 'static, STATE: RouterState> Component for RouterAnchor<SW, STATE> {
type Message = Msg;
type Properties = Props<SW>;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let router = RouteAgentDispatcher::new();
RouterAnchor {
link,
router,
props,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Clicked => {
let route = Route::from(self.props.route.clone());
self.router.send(RouteRequest::ChangeRoute(route));
false
}
}
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props = props;
true
}
fn view(&self) -> VNode {
let route: Route<STATE> = Route::from(self.props.route.clone());
let target: &str = route.as_str();
let cb = self.link.callback(|event: MouseEvent| {
event.prevent_default();
Msg::Clicked
});
html! {
<a
class=self.props.classes.clone()
onclick=cb
disabled=self.props.disabled
href=target
>
{
#[allow(deprecated)]
&self.props.text
}
{self.props.children.iter().collect::<VNode>()}
</a>
}
}
}

View File

@ -1,124 +1,79 @@
#![recursion_limit = "128"]
//! Provides routing faculties for the Yew web framework.
//! Provides routing faculties using the browser history API to build
//! Single Page Applications (SPAs) using [Yew web framework](https://yew.rs).
//!
//! ## Contents
//! This crate consists of multiple types, some independently useful on their own,
//! that are used together to facilitate routing within the Yew framework.
//! Among them are:
//! * RouteService - Hooks into the History API and listens to `PopStateEvent`s to respond to users
//! clicking the back/forwards buttons.
//! * RouteAgent - A singleton agent that owns a RouteService that provides an easy place for other
//! components and agents to hook into it.
//! * Switch - A trait/derive macro that allows specification of how enums or structs can be constructed
//! from Routes.
//! * Router - A component connected to the RouteAgent, and is capable of resolving Routes to
//! Switch implementors, so you can use them to render Html.
//! * Route - A struct containing an the route string and state.
//! * RouteButton & RouteLink - Wrapper components around buttons and anchor tags respectively that
//! allow users to change the route.
//! # Usage
//!
//! ## State and Aliases
//! Because the History API allows you to store data along with a route string,
//! most types have at type parameter that allows you to specify which type is being stored.
//! As this behavior is uncommon, aliases using the unit type (`()`) are provided to remove the
//! need to specify the storage type you likely aren't using.
//! ```rust
//! # use yew::prelude::*;
//! # use yew_functional::*;
//! # use yew_router::prelude::*;
//!
//! If you want to store state using the history API, it is recommended that you generate your own
//! aliases using the `define_router_state` macro.
//! Give it a typename, and it will generate a module containing aliases and functions useful for
//! routing. If you specify your own router_state aliases and functions, you will want to disable
//! the `unit_alias` feature to prevent the default `()` aliases from showing up in the prelude.
//! #[derive(Debug, Clone, Copy, PartialEq, Routable)]
//! enum Route {
//! #[at("/")]
//! Home,
//! #[at("/secure")]
//! Secure,
//! #[not_found]
//! #[at("/404")]
//! NotFound,
//! }
//!
//! ## Features
//! This crate has some feature-flags that allow you to not include some parts in your compilation.
//! * "default" - Everything is included by default.
//! * "core" - The fully feature complete ("router", "components", "matchers"), but without
//! unit_alias.
//! * "unit_alias" - If enabled, a module will be added to the route and expanded within the prelude
//! for aliases of Router types to their `()` variants.
//! * "router" - If enabled, the Router component and its dependent infrastructure (including
//! "agent") will be included.
//! * "agent" - If enabled, the RouteAgent and its associated types will be included.
//! * "components" - If enabled, the accessory components will be made available.
//! # #[function_component(Main)]
//! # fn app() -> Html {
//! html! {
//! <Router<Route> render=Router::render(switch) />
//! }
//! # }
//!
//! fn switch(routes: &Route) -> Html {
//! let onclick_callback = Callback::from(|_| yew_router::push_route(Route::Home));
//! match routes {
//! Route::Home => html! { <h1>{ "Home" }</h1> },
//! Route::Secure => html! {
//! <div>
//! <h1>{ "Secure" }</h1>
//! <button onclick=onclick_callback>{ "Go Home" }</button>
//! </div>
//! },
//! Route::NotFound => html! { <h1>{ "404" }</h1> },
//! }
//! }
//! ```
//!
//! # Internals
//!
//! The router keeps its own internal state which is used to store the current route and its associated data.
//! This allows the [Router] to be operated using the [service] with an API that
//! isn't cumbersome to use.
//!
//! # State
//!
//! The browser history API allows users to state associated with the route. This crate does
//! not expose or make use of it. It is instead recommended that a state management library like
//! [yewdux](https://github.com/intendednull/yewdux) be used.
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_qualifications
)]
// This will break the project at some point, but it will break yew as well.
// It can be dealt with at the same time.
#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)]
pub use yew_router_route_parser;
#[macro_use]
mod alias;
#[cfg(feature = "service")]
pub mod service;
#[cfg(feature = "agent")]
pub mod agent;
pub mod route;
#[cfg(feature = "components")]
#[doc(hidden)]
#[path = "macro_helpers.rs"]
pub mod __macro;
pub mod components;
#[cfg(feature = "router")]
mod routable;
pub mod router;
mod service;
pub mod utils;
pub use service::*;
pub use routable::Routable;
pub use router::{RenderFn, Router};
/// Prelude module that can be imported when working with the yew_router
pub mod prelude {
pub use super::matcher::Captures;
//! Prelude module to be imported when working with `yew-router`.
//!
//! This module re-exports the frequently used types from the crate.
#[cfg(feature = "service")]
pub use crate::route::RouteState;
#[cfg(feature = "service")]
pub use crate::service::RouteService;
#[cfg(feature = "agent")]
pub use crate::agent::RouteAgent;
#[cfg(feature = "agent")]
pub use crate::agent::RouteAgentBridge;
#[cfg(feature = "agent")]
pub use crate::agent::RouteAgentDispatcher;
#[cfg(feature = "components")]
pub use crate::components::RouterAnchor;
#[cfg(feature = "components")]
pub use crate::components::RouterButton;
#[cfg(feature = "router")]
pub use crate::router::Router;
#[cfg(feature = "router")]
pub use crate::router::RouterState;
pub use crate::{
route::Route,
switch::{Routable, Switch},
};
pub use yew_router_macro::Switch;
pub use crate::components::Link;
#[doc(no_inline)]
pub use crate::Routable;
pub use crate::Router;
}
pub use alias::*;
pub mod matcher;
pub use matcher::Captures;
#[cfg(feature = "service")]
pub use crate::route::RouteState;
#[cfg(feature = "router")]
pub use crate::router::RouterState;
pub mod switch;
pub use switch::Switch;
pub use yew_router_macro::Switch;

View File

@ -0,0 +1,30 @@
use crate::utils::{base_url, build_path_with_base};
use crate::Routable;
// re-export Router because the macro needs to access it
pub type Router = route_recognizer::Router<String>;
/// Build a `route_recognizer::Router` from a `Routable` type.
pub fn build_router<R: Routable>() -> Router {
let base = base_url();
let mut router = Router::new();
R::routes().iter().for_each(|path| {
let path = match &base {
Some(base) if base != "/" => build_path_with_base(path),
_ => path.to_string(),
};
router.add(&path, path.clone());
});
router
}
/// Use a `route_recognizer::Router` to build the route of a `Routable`
pub fn recognize_with_router<R: Routable>(router: &Router, pathname: &str) -> Option<R> {
let matched = router.recognize(&pathname.strip_suffix("/").unwrap_or(&pathname));
match matched {
Ok(matched) => R::from_path(matched.handler(), &matched.params().into_iter().collect()),
Err(_) => R::not_found_route(),
}
}

View File

@ -1,398 +0,0 @@
use crate::matcher::{
util::{consume_until, next_delimiter, tag_possibly_case_sensitive},
Captures, MatcherSettings,
};
use log::trace;
use nom::{
bytes::complete::{is_not, tag},
combinator::map,
error::ErrorKind,
sequence::terminated,
IResult,
};
use std::{iter::Peekable, slice::Iter};
use yew_router_route_parser::{CaptureVariant, MatcherToken};
/// Allows abstracting over capturing into a HashMap (Captures) or a Vec.
trait CaptureCollection<'a> {
fn new2() -> Self;
fn insert2(&mut self, key: &'a str, value: String);
fn extend2(&mut self, other: Self);
}
impl<'a> CaptureCollection<'a> for Captures<'a> {
fn new2() -> Self {
Captures::new()
}
fn insert2(&mut self, key: &'a str, value: String) {
self.insert(key, value);
}
fn extend2(&mut self, other: Self) {
self.extend(other)
}
}
impl<'a> CaptureCollection<'a> for Vec<String> {
fn new2() -> Self {
Vec::new()
}
fn insert2(&mut self, _key: &'a str, value: String) {
self.push(value)
}
fn extend2(&mut self, other: Self) {
self.extend(other)
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(super) fn match_into_map<'a, 'b: 'a>(
tokens: &'b [MatcherToken],
settings: &'b MatcherSettings,
) -> impl Fn(&'a str) -> IResult<&'a str, Captures<'b>> {
move |i: &str| matcher_impl(tokens, *settings, i)
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(super) fn match_into_vec<'a, 'b: 'a>(
tokens: &'b [MatcherToken],
settings: &'b MatcherSettings,
) -> impl Fn(&'a str) -> IResult<&'a str, Vec<String>> {
move |i: &str| matcher_impl(tokens, *settings, i)
}
fn matcher_impl<'a, 'b: 'a, CAP: CaptureCollection<'b>>(
tokens: &'b [MatcherToken],
settings: MatcherSettings,
mut i: &'a str,
) -> IResult<&'a str, CAP> {
trace!("Attempting to match route: {:?} using: {:?}", i, tokens);
let mut iter = tokens.iter().peekable();
let mut captures: CAP = CAP::new2();
while let Some(token) = iter.next() {
i = match token {
MatcherToken::Exact(literal) => {
trace!("Matching '{}' against literal: '{}'", i, literal);
tag_possibly_case_sensitive(literal.as_str(), !settings.case_insensitive)(i)?.0
}
MatcherToken::Capture(capture) => match &capture {
CaptureVariant::Named(name) => capture_named(i, &mut iter, &name, &mut captures)?,
CaptureVariant::ManyNamed(name) => {
capture_many_named(i, &mut iter, &name, &mut captures)?
}
CaptureVariant::NumberedNamed { sections, name } => {
capture_numbered_named(i, &mut iter, Some((&name, &mut captures)), *sections)?
}
CaptureVariant::Unnamed => capture_named(i, &mut iter, "", &mut captures)?,
CaptureVariant::ManyUnnamed => capture_many_named(i, &mut iter, "", &mut captures)?,
CaptureVariant::NumberedUnnamed { sections } => {
capture_numbered_named(i, &mut iter, Some(("", &mut captures)), *sections)?
}
},
MatcherToken::End => {
if !i.is_empty() {
// this is approximately correct, but ultimately doesn't matter
return Err(nom::Err::Failure((i, ErrorKind::Eof)));
} else {
i
}
}
};
}
trace!("Route Matched");
Ok((i, captures))
}
fn capture_named<'a, 'b: 'a, CAP: CaptureCollection<'b>>(
i: &'a str,
iter: &mut Peekable<Iter<MatcherToken>>,
capture_key: &'b str,
matches: &mut CAP,
) -> Result<&'a str, nom::Err<(&'a str, ErrorKind)>> {
log::trace!("Matching Named ({})", capture_key);
if let Some(_peaked_next_token) = iter.peek() {
let delimiter = next_delimiter(iter);
let (ii, captured) = consume_until(delimiter)(i)?;
matches.insert2(capture_key, captured);
Ok(ii)
} else {
let (ii, captured) = map(valid_capture_characters, String::from)(i)?;
matches.insert2(capture_key, captured);
Ok(ii)
}
}
fn capture_many_named<'a, 'b, CAP: CaptureCollection<'b>>(
i: &'a str,
iter: &mut Peekable<Iter<MatcherToken>>,
capture_key: &'b str,
matches: &mut CAP,
) -> Result<&'a str, nom::Err<(&'a str, ErrorKind)>> {
log::trace!("Matching ManyUnnamed ({})", capture_key);
if let Some(_peaked_next_token) = iter.peek() {
let delimiter = next_delimiter(iter);
let (ii, captured) = consume_until(delimiter)(i)?;
matches.insert2(&capture_key, captured);
Ok(ii)
} else if i.is_empty() {
// If the route string is empty, return an empty value.
matches.insert2(&capture_key, "".to_string());
Ok(i) // Match even if nothing is left
} else {
let (ii, c) = map(valid_many_capture_characters, String::from)(i)?;
matches.insert2(&capture_key, c);
Ok(ii)
}
}
fn capture_numbered_named<'a, 'b, CAP: CaptureCollection<'b>>(
mut i: &'a str,
iter: &mut Peekable<Iter<MatcherToken>>,
name_and_captures: Option<(&'b str, &mut CAP)>,
mut sections: usize,
) -> Result<&'a str, nom::Err<(&'a str, ErrorKind)>> {
log::trace!("Matching NumberedNamed ({})", sections);
let mut captured = "".to_string();
if let Some(_peaked_next_token) = iter.peek() {
while sections > 0 {
if sections > 1 {
let (ii, c) = terminated(valid_capture_characters, tag("/"))(i)?;
i = ii;
captured += c;
captured += "/";
} else {
let delimiter = next_delimiter(iter);
let (ii, c) = consume_until(delimiter)(i)?;
i = ii;
captured += &c;
}
sections -= 1;
}
} else {
while sections > 0 {
if sections > 1 {
let (ii, c) = terminated(valid_capture_characters, tag("/"))(i)?;
i = ii;
captured += c;
captured += "/";
} else {
// Don't consume the next character on the last section
let (ii, c) = valid_capture_characters(i)?;
i = ii;
captured += c;
}
sections -= 1;
}
}
if let Some((name, captures)) = name_and_captures {
captures.insert2(&name, captured);
}
Ok(i)
}
/// Characters that don't interfere with parsing logic for capturing characters
fn valid_capture_characters(i: &str) -> IResult<&str, &str> {
const INVALID_CHARACTERS: &str = " */#&?{}=";
is_not(INVALID_CHARACTERS)(i)
}
fn valid_many_capture_characters(i: &str) -> IResult<&str, &str> {
const INVALID_CHARACTERS: &str = " #&?=";
is_not(INVALID_CHARACTERS)(i)
}
#[cfg(test)]
mod integration_test {
use super::*;
use yew_router_route_parser::{self, FieldNamingScheme};
use super::super::Captures;
// use nom::combinator::all_consuming;
#[test]
fn match_query_after_path() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/a/path?lorem=ipsum",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "/a/path?lorem=ipsum")
.expect("should match");
}
#[test]
fn match_query_after_path_trailing_slash() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/a/path/?lorem=ipsum",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "/a/path/?lorem=ipsum")
.expect("should match");
}
#[test]
fn match_query() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"?lorem=ipsum",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "?lorem=ipsum")
.expect("should match");
}
#[test]
fn named_capture_query() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"?lorem={ipsum}",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
let (_, matches) = matcher_impl::<Captures>(&x, MatcherSettings::default(), "?lorem=ipsum")
.expect("should match");
assert_eq!(matches["ipsum"], "ipsum".to_string())
}
#[test]
fn match_n_paths_3() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/{*:cap}/thing",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
let matches: Captures =
matcher_impl(&x, MatcherSettings::default(), "/anything/other/thing")
.expect("should match")
.1;
assert_eq!(matches["cap"], "anything/other".to_string())
}
#[test]
fn match_n_paths_4() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/{*:cap}/thing",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
let matches: Captures =
matcher_impl(&x, MatcherSettings::default(), "/anything/thing/thing")
.expect("should match")
.1;
assert_eq!(matches["cap"], "anything".to_string())
}
#[test]
fn match_path_5() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/{cap}/thing",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
let matches: Captures =
matcher_impl(&x, MatcherSettings::default(), "/anything/thing/thing")
.expect("should match")
.1;
assert_eq!(matches["cap"], "anything".to_string())
}
#[test]
fn match_fragment() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"#test",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "#test").expect("should match");
}
#[test]
fn match_fragment_after_path() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/a/path/#test",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "/a/path/#test")
.expect("should match");
}
#[test]
fn match_fragment_after_path_no_slash() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/a/path#test",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "/a/path#test")
.expect("should match");
}
#[test]
fn match_fragment_after_query() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/a/path?query=thing#test",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "/a/path?query=thing#test")
.expect("should match");
}
#[test]
fn match_fragment_after_query_capture() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/a/path?query={capture}#test",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "/a/path?query=thing#test")
.expect("should match");
}
#[test]
fn capture_as_only_token() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"{any}",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, MatcherSettings::default(), "literally_anything")
.expect("should match");
}
#[test]
fn case_insensitive() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/hello",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
let settings = MatcherSettings {
case_insensitive: true,
};
matcher_impl::<Captures>(&x, settings, "/HeLLo").expect("should match");
}
#[test]
fn end_token() {
let x = yew_router_route_parser::parse_str_and_optimize_tokens(
"/lorem!",
FieldNamingScheme::Unnamed,
)
.expect("Should parse");
matcher_impl::<Captures>(&x, Default::default(), "/lorem/ipsum")
.expect_err("should not match");
}
}

View File

@ -1,280 +0,0 @@
//! Module for matching route strings based on tokens generated from the yew_router_route_parser
//! crate.
mod matcher_impl;
mod util;
use nom::IResult;
use std::collections::HashSet;
use yew_router_route_parser::{parse_str_and_optimize_tokens, PrettyParseError};
pub use yew_router_route_parser::{CaptureVariant, Captures, MatcherToken};
/// Attempts to match routes, transform the route to Component props and render that Component.
#[derive(Debug, PartialEq, Clone)]
pub struct RouteMatcher {
/// Tokens used to determine how the matcher will match a route string.
pub tokens: Vec<MatcherToken>,
/// Settings
pub settings: MatcherSettings,
}
/// Settings used for the matcher.
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct MatcherSettings {
/// All literal matches do not care about case.
pub case_insensitive: bool,
}
impl Default for MatcherSettings {
fn default() -> Self {
MatcherSettings {
case_insensitive: false,
}
}
}
impl RouteMatcher {
/// Attempt to create a RouteMatcher from a "matcher string".
pub fn try_from(i: &str) -> Result<Self, PrettyParseError> {
let settings = MatcherSettings::default();
Self::new(i, settings)
}
/// Creates a new Matcher with settings.
pub fn new(i: &str, settings: MatcherSettings) -> Result<Self, PrettyParseError> {
Ok(RouteMatcher {
tokens: parse_str_and_optimize_tokens(
i,
yew_router_route_parser::FieldNamingScheme::Unnamed, // The most permissive scheme
)?, /* TODO this field type should be a superset of Named, but it would be better to source this from settings, and make sure that the macro generates settings as such. */
settings,
})
}
/// Match a route string, collecting the results into a map.
pub fn capture_route_into_map<'a, 'b: 'a>(
&'b self,
i: &'a str,
) -> IResult<&'a str, Captures<'a>> {
matcher_impl::match_into_map(&self.tokens, &self.settings)(i)
}
/// Match a route string, collecting the results into a vector.
pub fn capture_route_into_vec<'a, 'b: 'a>(
&'b self,
i: &'a str,
) -> IResult<&'a str, Vec<String>> {
matcher_impl::match_into_vec(&self.tokens, &self.settings)(i)
}
/// Gets a set of all names that will be captured.
/// This is useful in determining if a given struct will be able to be populated by a given path
/// matcher before being given a concrete path to match.
pub fn capture_names(&self) -> HashSet<&str> {
fn capture_names_impl(tokens: &[MatcherToken]) -> HashSet<&str> {
tokens
.iter()
.fold(HashSet::new(), |mut acc: HashSet<&str>, token| {
match token {
MatcherToken::Exact(_) | MatcherToken::End => {}
MatcherToken::Capture(capture) => match &capture {
CaptureVariant::ManyNamed(name)
| CaptureVariant::Named(name)
| CaptureVariant::NumberedNamed { name, .. } => {
acc.insert(&name);
}
CaptureVariant::Unnamed
| CaptureVariant::ManyUnnamed
| CaptureVariant::NumberedUnnamed { .. } => {}
},
}
acc
})
}
capture_names_impl(&self.tokens)
}
}
#[cfg(test)]
mod tests {
use super::*;
use yew_router_route_parser::{
convert_tokens,
parser::{RefCaptureVariant, RouteParserToken},
};
impl<'a> From<Vec<RouteParserToken<'a>>> for RouteMatcher {
fn from(tokens: Vec<RouteParserToken<'a>>) -> Self {
let settings = MatcherSettings::default();
RouteMatcher {
tokens: convert_tokens(&tokens),
settings,
}
}
}
#[test]
fn basic_separator() {
let tokens = vec![RouteParserToken::Separator];
let path_matcher = RouteMatcher::from(tokens);
path_matcher
.capture_route_into_map("/")
.expect("should parse");
}
#[test]
fn multiple_tokens() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Exact("lorem"),
RouteParserToken::Separator,
];
let path_matcher = RouteMatcher::from(tokens);
path_matcher
.capture_route_into_map("/lorem/")
.expect("should parse");
}
#[test]
fn simple_capture() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
RouteParserToken::Separator,
];
let path_matcher = RouteMatcher::from(tokens);
let (_, matches) = path_matcher
.capture_route_into_map("/ipsum/")
.expect("should parse");
assert_eq!(matches["lorem"], "ipsum".to_string())
}
#[test]
fn simple_capture_with_no_trailing_separator() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
];
let path_matcher = RouteMatcher::from(tokens);
let (_, matches) = path_matcher
.capture_route_into_map("/ipsum")
.expect("should parse");
assert_eq!(matches["lorem"], "ipsum".to_string())
}
#[test]
fn match_with_trailing_match_many() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Exact("a"),
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::ManyNamed("lorem")),
];
let path_matcher = RouteMatcher::from(tokens);
let (_, _matches) = path_matcher
.capture_route_into_map("/a/")
.expect("should parse");
}
#[test]
fn fail_match_with_trailing_match_single() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Exact("a"),
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::Named("lorem")),
];
let path_matcher = RouteMatcher::from(tokens);
path_matcher
.capture_route_into_map("/a/")
.expect_err("should not parse");
}
#[test]
fn match_n() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::NumberedNamed {
sections: 3,
name: "lorem",
}),
RouteParserToken::Separator,
RouteParserToken::Exact("a"),
];
let path_matcher = RouteMatcher::from(tokens);
let (_, _matches) = path_matcher
.capture_route_into_map("/garbage1/garbage2/garbage3/a")
.expect("should parse");
}
#[test]
fn match_n_no_overrun() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::NumberedNamed {
sections: 3,
name: "lorem",
}),
];
let path_matcher = RouteMatcher::from(tokens);
let (s, _matches) = path_matcher
.capture_route_into_map("/garbage1/garbage2/garbage3")
.expect("should parse");
assert_eq!(s.len(), 0)
}
#[test]
fn match_n_named() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::NumberedNamed {
sections: 3,
name: "captured",
}),
RouteParserToken::Separator,
RouteParserToken::Exact("a"),
];
let path_matcher = RouteMatcher::from(tokens);
let (_, matches) = path_matcher
.capture_route_into_map("/garbage1/garbage2/garbage3/a")
.expect("should parse");
assert_eq!(
matches["captured"],
"garbage1/garbage2/garbage3".to_string()
)
}
#[test]
fn match_many() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::ManyNamed("lorem")),
RouteParserToken::Separator,
RouteParserToken::Exact("a"),
];
let path_matcher = RouteMatcher::from(tokens);
let (_, _matches) = path_matcher
.capture_route_into_map("/garbage1/garbage2/garbage3/a")
.expect("should parse");
}
#[test]
fn match_many_named() {
let tokens = vec![
RouteParserToken::Separator,
RouteParserToken::Capture(RefCaptureVariant::ManyNamed("captured")),
RouteParserToken::Separator,
RouteParserToken::Exact("a"),
];
let path_matcher = RouteMatcher::from(tokens);
let (_, matches) = path_matcher
.capture_route_into_map("/garbage1/garbage2/garbage3/a")
.expect("should parse");
assert_eq!(
matches["captured"],
"garbage1/garbage2/garbage3".to_string()
)
}
}

View File

@ -1,140 +0,0 @@
use nom::{
bytes::complete::{tag, tag_no_case},
character::complete::anychar,
combinator::{cond, map, peek, rest},
error::{ErrorKind, ParseError},
multi::many_till,
sequence::pair,
IResult,
};
use std::{iter::Peekable, rc::Rc, slice::Iter};
use yew_router_route_parser::MatcherToken;
/// Allows a configurable tag that can optionally be case insensitive.
pub fn tag_possibly_case_sensitive<'a, 'b: 'a>(
text: &'b str,
is_sensitive: bool,
) -> impl Fn(&'a str) -> IResult<&'a str, &'a str> {
map(
pair(
cond(is_sensitive, tag(text)),
cond(!is_sensitive, tag_no_case(text)),
),
|(x, y): (Option<&str>, Option<&str>)| x.xor(y).unwrap(),
)
}
/// Similar to alt, but works on a vector of tags.
#[allow(unused)]
pub fn alternative(alternatives: Vec<String>) -> impl Fn(&str) -> IResult<&str, &str> {
move |i: &str| {
for alternative in &alternatives {
if let done @ IResult::Ok(..) = tag(alternative.as_str())(i) {
return done;
}
}
Err(nom::Err::Error((i, ErrorKind::Tag))) // nothing found.
}
}
/// Consumes the input until the provided parser succeeds.
/// The consumed input is returned in the form of an allocated string.
/// # Note
/// `stop_parser` only peeks its input.
pub fn consume_until<'a, F, E>(stop_parser: F) -> impl Fn(&'a str) -> IResult<&'a str, String, E>
where
E: ParseError<&'a str>,
F: Fn(&'a str) -> IResult<&'a str, &'a str, E>,
{
// In order for the returned fn to be Fn instead of FnOnce, wrap the inner fn in an RC.
let f = Rc::new(many_till(
anychar,
peek(stop_parser), // once this succeeds, stop folding.
));
move |i: &str| {
let (i, (first, _stop)): (&str, (Vec<char>, &str)) = (f)(i)?;
let ret = first.into_iter().collect();
Ok((i, ret))
}
}
/// Produces a parser combinator that searches for the next possible set of strings of
/// characters used to terminate a forward search.
///
/// # Panics
/// This function assumes that the next item after a Capture must be an Exact.
/// If this is violated, this function will panic.
pub fn next_delimiter<'a>(
iter: &mut Peekable<Iter<MatcherToken>>,
) -> impl Fn(&'a str) -> IResult<&'a str, &'a str> {
let t: MatcherToken = iter
.peek()
.copied()
.cloned()
.expect("There must be at least one token to peak in next_delimiter");
move |i: &'a str| match &t {
MatcherToken::Exact(sequence) => tag(sequence.as_str())(i),
MatcherToken::End => rest(i),
MatcherToken::Capture(_) => {
panic!("underlying parser should not allow two captures in a row")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn consume_until_simple() {
let parser = consume_until::<_, ()>(tag("z"));
let parsed = parser("abcz").expect("Should parse");
assert_eq!(parsed, ("z", "abc".to_string()))
}
#[test]
fn consume_until_fail() {
let parser = consume_until(tag("z"));
let e = parser("abc").expect_err("Should parse");
assert_eq!(e, nom::Err::Error(("", ErrorKind::Eof)))
}
#[test]
fn alternative_simple() {
let parser = alternative(
vec!["c", "d", "abc"]
.into_iter()
.map(String::from)
.collect(),
);
let parsed = parser("abcz").expect("Should parse");
assert_eq!(parsed, ("z", "abc"))
}
#[test]
fn alternative_and_consume_until() {
let parser = consume_until(alternative(
vec!["c", "d", "abc"]
.into_iter()
.map(String::from)
.collect(),
));
let parsed = parser("first_stuff_abc").expect("should parse");
assert_eq!(parsed, ("abc", "first_stuff_".to_string()))
}
#[test]
fn case_sensitive() {
let parser = tag_possibly_case_sensitive("lorem", true);
parser("lorem").expect("Should match");
parser("LoReM").expect_err("Should not match");
}
#[test]
fn case_insensitive() {
let parser = tag_possibly_case_sensitive("lorem", false);
parser("lorem").expect("Should match");
parser("LoREm").expect("Should match");
}
}

View File

@ -0,0 +1,26 @@
use std::collections::HashMap;
pub use yew_router_macro::Routable;
/// Marks an `enum` as routable.
///
/// # Implementation
///
/// Use derive macro to implement it. Although it *is* possible to implement it manually,
/// it is discouraged.
pub trait Routable: Sized {
/// Converts path to an instance of the routes enum.
fn from_path(path: &str, params: &HashMap<&str, &str>) -> Option<Self>;
/// Converts the route to a string that can passed to the history API.
fn to_path(&self) -> String;
/// Lists all the available routes
fn routes() -> Vec<&'static str>;
/// The route to redirect to on 404
fn not_found_route() -> Option<Self>;
/// Match a route based on the path
fn recognize(pathname: &str) -> Option<Self>;
}

View File

@ -1,58 +0,0 @@
//! Wrapper around route url string, and associated history state.
#[cfg(feature = "service")]
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::{
fmt::{self, Debug},
ops::Deref,
};
/// Any state that can be used in the router agent must meet the criteria of this trait.
pub trait RouteState: Serialize + DeserializeOwned + Debug + Clone + Default + 'static {}
impl<T> RouteState for T where T: Serialize + DeserializeOwned + Debug + Clone + Default + 'static {}
/// The representation of a route, segmented into different sections for easy access.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Route<STATE = ()> {
/// The route string
pub route: String,
/// The state stored in the history api
pub state: STATE,
}
impl Route<()> {
/// Creates a new route with no state out of a string.
///
/// This Route will have `()` for its state.
pub fn new_no_state<T: AsRef<str>>(route: T) -> Self {
Route {
route: route.as_ref().to_string(),
state: (),
}
}
}
impl<STATE: Default> Route<STATE> {
/// Creates a new route out of a string, setting the state to its default value.
pub fn new_default_state<T: AsRef<str>>(route: T) -> Self {
Route {
route: route.as_ref().to_string(),
state: STATE::default(),
}
}
}
impl<STATE> fmt::Display for Route<STATE> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(&self.route, f)
}
}
impl<STATE> Deref for Route<STATE> {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.route
}
}

View File

@ -1,232 +1,125 @@
//! Router Component.
use crate::{
agent::{RouteAgentBridge, RouteRequest},
route::Route,
RouteState, Switch,
};
use std::{
fmt::{self, Debug, Error as FmtError, Formatter},
rc::Rc,
};
use yew::{html, virtual_dom::VNode, Component, ComponentLink, Html, Properties, ShouldRender};
use crate::{attach_route_listener, current_route, Routable, RouteListener};
use std::rc::Rc;
use yew::prelude::*;
/// Any state that can be managed by the `Router` must meet the criteria of this trait.
pub trait RouterState: RouteState + PartialEq {}
impl<STATE> RouterState for STATE where STATE: RouteState + PartialEq {}
/// Wraps `Rc` around `Fn` so it can be passed as a prop.
pub struct RenderFn<R>(Rc<dyn Fn(&R) -> Html>);
/// Rendering control flow component.
///
/// # Example
/// ```
/// use yew::{prelude::*, virtual_dom::VNode};
/// use yew_router::{router::Router, Switch};
///
/// pub enum Msg {}
///
/// pub struct Model {}
/// impl Component for Model {
/// //...
/// # type Message = Msg;
/// # type Properties = ();
/// # fn create(_: Self::Properties, _link: ComponentLink<Self>) -> Self {
/// # Model {}
/// # }
/// # fn update(&mut self, msg: Self::Message) -> ShouldRender {
/// # false
/// # }
/// # fn change(&mut self, props: Self::Properties) -> ShouldRender {
/// # false
/// # }
///
/// fn view(&self) -> VNode {
/// html! {
/// <Router<S>
/// render = Router::render(|switch: S| {
/// match switch {
/// S::Variant => html!{"variant route was matched"},
/// }
/// })
/// />
/// }
/// }
/// }
///
/// #[derive(Switch, Clone)]
/// enum S {
/// #[at = "/v"]
/// Variant,
/// }
/// ```
// TODO, can M just be removed due to not having to explicitly deal with callbacks anymore? - Just get rid of M
#[derive(Debug)]
pub struct Router<SW: Switch + Clone + 'static, STATE: RouterState = ()> {
switch: Option<SW>,
props: Props<STATE, SW>,
router_agent: RouteAgentBridge<STATE>,
}
impl<SW, STATE> Router<SW, STATE>
where
STATE: RouterState,
SW: Switch + Clone + 'static,
{
/// Wrap a render closure so that it can be used by the Router.
/// # Example
/// ```
/// # use yew_router::Switch;
/// # use yew_router::router::{Router, Render};
/// # use yew::{html, Html};
/// # #[derive(Switch, Clone)]
/// # enum S {
/// # #[at = "/route"]
/// # Variant
/// # }
/// # pub enum Msg {}
impl<R> RenderFn<R> {
/// Creates a new [`RenderFn`]
///
/// # fn dont_execute() {
/// let render: Render<S> = Router::render(|switch: S| -> Html {
/// match switch {
/// S::Variant => html! {"Variant"},
/// }
/// });
/// # }
/// ```
pub fn render<F: RenderFn<Router<SW, STATE>, SW> + 'static>(f: F) -> Render<SW, STATE> {
Render::new(f)
}
/// Wrap a redirect function so that it can be used by the Router.
pub fn redirect<F: RedirectFn<SW, STATE> + 'static>(f: F) -> Option<Redirect<SW, STATE>> {
Some(Redirect::new(f))
/// It is recommended that you use [`Router::render`] instead
pub fn new(value: impl Fn(&R) -> Html + 'static) -> Self {
Self(Rc::new(value))
}
}
/// Message for Router.
#[derive(Debug, Clone)]
pub enum Msg<STATE> {
/// Updates the route
UpdateRoute(Route<STATE>),
}
/// Render function that takes a switched route and converts it to HTML
pub trait RenderFn<CTX: Component, SW>: Fn(SW) -> Html {}
impl<T, CTX: Component, SW> RenderFn<CTX, SW> for T where T: Fn(SW) -> Html {}
/// Owned Render function.
#[derive(Clone)]
pub struct Render<SW: Switch + Clone + 'static, STATE: RouterState = ()>(
pub(crate) Rc<dyn RenderFn<Router<SW, STATE>, SW>>,
);
impl<STATE: RouterState, SW: Switch + Clone> Render<SW, STATE> {
/// New render function
fn new<F: RenderFn<Router<SW, STATE>, SW> + 'static>(f: F) -> Self {
Render(Rc::new(f))
}
}
impl<STATE: RouterState, SW: Switch + Clone> Debug for Render<SW, STATE> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Render").finish()
impl<T> Clone for RenderFn<T> {
fn clone(&self) -> Self {
Self(Rc::clone(&self.0))
}
}
/// Redirection function that takes a route that didn't match any of the Switch variants,
/// and converts it to a switch variant.
pub trait RedirectFn<SW, STATE>: Fn(Route<STATE>) -> SW {}
impl<T, SW, STATE> RedirectFn<SW, STATE> for T where T: Fn(Route<STATE>) -> SW {}
/// Clonable Redirect function
#[derive(Clone)]
pub struct Redirect<SW: Switch + 'static, STATE: RouterState>(
pub(crate) Rc<dyn RedirectFn<SW, STATE>>,
);
impl<STATE: RouterState, SW: Switch + 'static> Redirect<SW, STATE> {
fn new<F: RedirectFn<SW, STATE> + 'static>(f: F) -> Self {
Redirect(Rc::new(f))
}
}
impl<STATE: RouterState, SW: Switch> Debug for Redirect<SW, STATE> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Redirect").finish()
impl<T> PartialEq for RenderFn<T> {
fn eq(&self, other: &Self) -> bool {
// https://github.com/rust-lang/rust-clippy/issues/6524
#[allow(clippy::vtable_address_comparisons)]
Rc::ptr_eq(&self.0, &other.0)
}
}
/// Properties for Router.
#[derive(Properties, Clone)]
pub struct Props<STATE: RouterState, SW: Switch + Clone + 'static> {
/// Render function that takes a Switch and produces Html
pub render: Render<SW, STATE>,
/// Optional redirect function that will convert the route to a known switch variant if explicit matching fails.
/// This should mostly be used to handle 404s and redirection.
/// It is not strictly necessary as your Switch is capable of handling unknown routes using `#[at = "/{*:any}"]`.
#[prop_or_default]
pub redirect: Option<Redirect<SW, STATE>>,
/// Props for [`Router`]
#[derive(Properties)]
pub struct RouterProps<R> {
/// Callback which returns [`Html`] to be rendered for the current route.
pub render: RenderFn<R>,
}
impl<STATE: RouterState, SW: Switch + Clone> Debug for Props<STATE, SW> {
fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
f.debug_struct("Props").finish()
impl<R> Clone for RouterProps<R> {
fn clone(&self) -> Self {
Self {
render: self.render.clone(),
}
}
}
impl<STATE, SW> Component for Router<SW, STATE>
impl<R> PartialEq for RouterProps<R> {
fn eq(&self, other: &Self) -> bool {
self.render.eq(&other.render)
}
}
#[doc(hidden)]
pub enum Msg<R> {
UpdateRoute(Option<R>),
}
/// The router component.
///
/// When a route can't be matched, it looks for the route with `not_found` attribute.
/// If such a route is provided, it redirects to the specified route.
/// Otherwise `html! {}` is rendered and a message is logged to console
/// stating that no route can be matched.
/// See the [crate level document][crate] for more information.
pub struct Router<R: Routable + 'static> {
props: RouterProps<R>,
#[allow(dead_code)] // only exists to drop listener on component drop
route_listener: RouteListener,
route: Option<R>,
}
impl<R> Component for Router<R>
where
STATE: RouterState,
SW: Switch + Clone + 'static,
R: Routable + 'static,
{
type Message = Msg<STATE>;
type Properties = Props<STATE, SW>;
type Message = Msg<R>;
type Properties = RouterProps<R>;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let callback = link.callback(Msg::UpdateRoute);
let mut router_agent = RouteAgentBridge::new(callback);
router_agent.send(RouteRequest::GetCurrentRoute);
let route_listener = attach_route_listener(link.callback(Msg::UpdateRoute));
Router {
switch: Default::default(), /* This must be updated by immediately requesting a route
* update from the service bridge. */
Self {
props,
router_agent,
route_listener,
route: current_route(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, msg: Self::Message) -> bool {
match msg {
Msg::UpdateRoute(route) => {
let mut switch = SW::switch(route.clone());
if switch.is_none() {
if let Some(redirect) = &self.props.redirect {
let redirected: SW = (&redirect.0)(route);
log::trace!(
"Route failed to match, but redirecting route to a known switch."
);
// Replace the route in the browser with the redirected.
self.router_agent
.send(RouteRequest::ReplaceRouteNoBroadcast(
redirected.clone().into(),
));
switch = Some(redirected)
}
}
self.switch = switch;
self.route = route;
true
}
}
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props = props;
true
fn change(&mut self, mut props: Self::Properties) -> bool {
std::mem::swap(&mut self.props, &mut props);
props != self.props
}
fn view(&self) -> VNode {
match self.switch.clone() {
Some(switch) => (&self.props.render.0)(switch),
fn view(&self) -> Html {
match &self.route {
Some(route) => (self.props.render.0)(route),
None => {
log::warn!("No route matched, provide a redirect prop to the router to handle cases where no route can be matched");
html! {"No route matched"}
weblog::console_log!("no route matched");
html! {}
}
}
}
}
impl<R> Router<R>
where
R: Routable + Clone + 'static,
{
pub fn render<F>(func: F) -> RenderFn<R>
where
F: Fn(&R) -> Html + 'static,
{
RenderFn::new(func)
}
}

View File

@ -1,182 +1,79 @@
//! Service that interfaces with the browser to handle routing.
use yew::callback::Callback;
use crate::route::{Route, RouteState};
use std::marker::PhantomData;
use crate::utils::build_path_with_base;
use crate::Routable;
use gloo::events::EventListener;
use wasm_bindgen::{JsCast, JsValue as Value};
use web_sys::{History, Location, PopStateEvent};
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsValue;
use web_sys::Event;
use yew::Callback;
/// A service that facilitates manipulation of the browser's URL bar and responding to browser events
/// when users press 'forward' or 'back'.
/// Navigate to a specific route.
pub fn push_route(route: impl Routable) {
push_impl(route.to_path())
}
/// Navigate to a specific route with query parameters.
///
/// The `T` determines what route state can be stored in the route service.
#[derive(Debug)]
pub struct RouteService<STATE = ()> {
history: History,
location: Location,
event_listener: Option<EventListener>,
phantom_data: PhantomData<STATE>,
}
impl<STATE> Default for RouteService<STATE>
/// This should be used in cases where [`Link`](crate::prelude::Link) is insufficient.
pub fn push_route_with_query<S>(
route: impl Routable,
query: S,
) -> Result<(), serde_urlencoded::ser::Error>
where
STATE: RouteState,
S: Serialize,
{
fn default() -> Self {
RouteService::<STATE>::new()
let mut url = route.to_path();
let query = serde_urlencoded::to_string(query)?;
if !query.is_empty() {
url.push_str(&format!("?{}", query));
}
push_impl(url);
Ok(())
}
impl<T> RouteService<T> {
/// Creates the route service.
pub fn new() -> RouteService<T> {
let (history, location) = {
let window = web_sys::window().unwrap();
(
window
.history()
.expect("browser does not support history API"),
window.location(),
)
};
fn push_impl(url: String) {
let history = yew::utils::window().history().expect("no history");
RouteService {
history,
location,
event_listener: None,
phantom_data: PhantomData,
}
}
#[inline]
fn get_route_from_location(location: &Location) -> String {
let path = location.pathname().unwrap();
let query = location.search().unwrap();
let fragment = location.hash().unwrap();
format_route_string(&path, &query, &fragment)
}
/// Gets the path name of the current url.
pub fn get_path(&self) -> String {
self.location.pathname().unwrap()
}
/// Gets the query string of the current url.
pub fn get_query(&self) -> String {
self.location.search().unwrap()
}
/// Gets the fragment of the current url.
pub fn get_fragment(&self) -> String {
self.location.hash().unwrap()
}
history
.push_state_with_url(&JsValue::NULL, "", Some(&build_path_with_base(&url)))
.expect("push history");
let event = Event::new("popstate").unwrap();
yew::utils::window()
.dispatch_event(&event)
.expect("dispatch");
}
impl<STATE> RouteService<STATE>
pub fn parse_query<T>() -> Result<T, serde_urlencoded::de::Error>
where
STATE: RouteState,
T: for<'de> Deserialize<'de>,
{
/// Registers a callback to the route service.
/// Callbacks will be called when the History API experiences a change such as
/// popping a state off of its stack when the forward or back buttons are pressed.
pub fn register_callback(&mut self, callback: Callback<Route<STATE>>) {
let cb = move |event: PopStateEvent| {
let state_value: Value = event.state();
let state_string: String = state_value.as_string().unwrap_or_default();
let state: STATE = serde_json::from_str(&state_string).unwrap_or_else(|_| {
log::error!("Could not deserialize state string");
STATE::default()
});
// Can't use the existing location, because this is a callback, and can't move it in
// here.
let location: Location = web_sys::window().unwrap().location();
let route: String = Self::get_route_from_location(&location);
callback.emit(Route { route, state })
};
self.event_listener = Some(EventListener::new(
web_sys::window().unwrap().as_ref(),
"popstate",
move |event| {
let event: PopStateEvent = event.clone().dyn_into().unwrap();
cb(event)
},
));
}
/// Sets the browser's url bar to contain the provided route,
/// and creates a history entry that can be navigated via the forward and back buttons.
///
/// The route should be a relative path that starts with a `/`.
pub fn set_route(&mut self, route: &str, state: STATE) {
let state_string: String = serde_json::to_string(&state).unwrap_or_else(|_| {
log::error!("Could not serialize state string");
"".to_string()
});
let _ = self
.history
.push_state_with_url(&Value::from_str(&state_string), "", Some(route));
}
/// Replaces the route with another one removing the most recent history event and
/// creating another history event in its place.
pub fn replace_route(&mut self, route: &str, state: STATE) {
let state_string: String = serde_json::to_string(&state).unwrap_or_else(|_| {
log::error!("Could not serialize state string");
"".to_string()
});
let _ =
self.history
.replace_state_with_url(&Value::from_str(&state_string), "", Some(route));
}
/// Gets the concatenated path, query, and fragment.
pub fn get_route(&self) -> Route<STATE> {
let route_string = Self::get_route_from_location(&self.location);
let state: STATE = get_state_string(&self.history)
.or_else(|| {
log::trace!("History state is empty");
None
})
.and_then(|state_string| -> Option<STATE> {
serde_json::from_str(&state_string)
.ok()
.or_else(|| {
log::error!("Could not deserialize state string");
None
})
.and_then(std::convert::identity) // flatten
})
.unwrap_or_default();
Route {
route: route_string,
state,
}
}
let query = yew::utils::document().location().unwrap().search().unwrap();
serde_urlencoded::from_str(query.strip_prefix("?").unwrap_or(""))
}
/// Formats a path, query, and fragment into a string.
pub fn current_route<R: Routable>() -> Option<R> {
let pathname = yew::utils::window().location().pathname().unwrap();
R::recognize(&pathname)
}
/// Handle for the router's path event listener
pub struct RouteListener {
// this exists so listener is dropped when handle is dropped
#[allow(dead_code)]
listener: EventListener,
}
/// Adds a listener which is called when the current route is changed.
///
/// # Note
/// This expects that all three already have their expected separators (?, #, etc)
pub(crate) fn format_route_string(path: &str, query: &str, fragment: &str) -> String {
format!(
"{path}{query}{fragment}",
path = path,
query = query,
fragment = fragment
)
}
/// The callback receives `Option<R>` so it can handle the error case itself.
pub fn attach_route_listener<R>(callback: Callback<Option<R>>) -> RouteListener
where
R: Routable + 'static,
{
let listener = EventListener::new(&yew::utils::window(), "popstate", move |_| {
callback.emit(current_route())
});
fn get_state(history: &History) -> Value {
history.state().unwrap()
}
fn get_state_string(history: &History) -> Option<String> {
get_state(history).as_string()
RouteListener { listener }
}

View File

@ -1,234 +0,0 @@
//! Parses routes into enums or structs.
use crate::route::Route;
use std::fmt::Write;
/// Alias to Switch.
///
/// Eventually Switch will be renamed to Routable and this alias will be removed.
#[allow(bare_trait_objects)]
pub type Routable = Switch;
/// Derivable routing trait that allows instances of implementors to be constructed from Routes.
///
/// # Note
/// Don't try to implement this yourself, rely on the derive macro.
///
/// # Example
/// ```
/// use yew_router::{route::Route, Switch};
/// #[derive(Debug, Switch, PartialEq)]
/// enum TestEnum {
/// #[at = "/test/route"]
/// TestRoute,
/// #[at = "/capture/string/{path}"]
/// CaptureString { path: String },
/// #[at = "/capture/number/{num}"]
/// CaptureNumber { num: usize },
/// #[at = "/capture/unnamed/{doot}"]
/// CaptureUnnamed(String),
/// }
///
/// assert_eq!(
/// TestEnum::switch(Route::new_no_state("/test/route")),
/// Some(TestEnum::TestRoute)
/// );
/// assert_eq!(
/// TestEnum::switch(Route::new_no_state("/capture/string/lorem")),
/// Some(TestEnum::CaptureString {
/// path: "lorem".to_string()
/// })
/// );
/// assert_eq!(
/// TestEnum::switch(Route::new_no_state("/capture/number/22")),
/// Some(TestEnum::CaptureNumber { num: 22 })
/// );
/// assert_eq!(
/// TestEnum::switch(Route::new_no_state("/capture/unnamed/lorem")),
/// Some(TestEnum::CaptureUnnamed("lorem".to_string()))
/// );
/// ```
pub trait Switch: Sized {
/// Based on a route, possibly produce an itself.
fn switch<STATE>(route: Route<STATE>) -> Option<Self> {
Self::from_route_part(route.route, Some(route.state)).0
}
/// Get self from a part of the state
fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>);
/// Build part of a route from itself.
fn build_route_section<STATE>(self, route: &mut String) -> Option<STATE>;
/// Called when the key (the named capture group) can't be located. Instead of failing outright,
/// a default item can be provided instead.
///
/// Its primary motivation for existing is to allow implementing Switch for Option.
/// This doesn't make sense at the moment because this only works for the individual key section
/// - any surrounding literals are pretty much guaranteed to make the parse step fail.
/// because of this, this functionality might be removed in favor of using a nested Switch enum,
/// or multiple variants.
fn key_not_available() -> Option<Self> {
None
}
}
/// Wrapper that requires that an implementor of Switch must start with a `/`.
///
/// This is needed for any non-derived type provided by yew-router to be used by itself.
///
/// This is because route strings will almost always start with `/`, so in order to get a std type
/// with the `rest` attribute, without a specified leading `/`, this wrapper is needed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct LeadingSlash<T>(pub T);
impl<U: Switch> Switch for LeadingSlash<U> {
fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>) {
if let Some(part) = part.strip_prefix('/') {
let (inner, state) = U::from_route_part(part.to_owned(), state);
(inner.map(LeadingSlash), state)
} else {
(None, None)
}
}
fn build_route_section<T>(self, route: &mut String) -> Option<T> {
write!(route, "/").ok()?;
self.0.build_route_section(route)
}
}
/// Successfully match even when the captured section can't be found.
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Permissive<U>(pub Option<U>);
impl<U: Switch> Switch for Permissive<U> {
/// Option is very permissive in what is allowed.
fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>) {
let (inner, inner_state) = U::from_route_part(part, state);
if inner.is_some() {
(Some(Permissive(inner)), inner_state)
} else {
// The Some(None) here indicates that this will produce a None, if the wrapped value can't be parsed
(Some(Permissive(None)), None)
}
}
fn build_route_section<STATE>(self, route: &mut String) -> Option<STATE> {
if let Some(inner) = self.0 {
inner.build_route_section(route)
} else {
None
}
}
fn key_not_available() -> Option<Self> {
Some(Permissive(None))
}
}
// TODO the AllowMissing shim doesn't appear to offer much over Permissive.
// Documentation should improve (need examples - to show the difference) or it should be removed.
/// Allows a section to match, providing a None value,
/// if its contents are entirely missing, or starts with a '/'.
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct AllowMissing<U: std::fmt::Debug>(pub Option<U>);
impl<U: Switch + std::fmt::Debug> Switch for AllowMissing<U> {
fn from_route_part<STATE>(part: String, state: Option<STATE>) -> (Option<Self>, Option<STATE>) {
let route = part.clone();
let (inner, inner_state) = U::from_route_part(part, state);
if inner.is_some() {
(Some(AllowMissing(inner)), inner_state)
} else if route.is_empty()
|| route.starts_with('/')
|| route.starts_with('?')
|| route.starts_with('&')
|| route.starts_with('#')
{
(Some(AllowMissing(None)), inner_state)
} else {
(None, None)
}
}
fn build_route_section<STATE>(self, route: &mut String) -> Option<STATE> {
if let AllowMissing(Some(inner)) = self {
inner.build_route_section(route)
} else {
None
}
}
}
/// Builds a route from a switch.
fn build_route_from_switch<SW: Switch, STATE: Default>(switch: SW) -> Route<STATE> {
// URLs are recommended to not be over 255 characters,
// although browsers frequently support up to about 2000.
// Routes, being a subset of URLs should probably be smaller than 255 characters for the vast
// majority of circumstances, preventing reallocation under most conditions.
let mut buf = String::with_capacity(255);
let state: STATE = switch.build_route_section(&mut buf).unwrap_or_default();
buf.shrink_to_fit();
Route { route: buf, state }
}
impl<SW: Switch, STATE: Default> From<SW> for Route<STATE> {
fn from(switch: SW) -> Self {
build_route_from_switch(switch)
}
}
impl<T: std::str::FromStr + std::fmt::Display> Switch for T {
fn from_route_part<U>(part: String, state: Option<U>) -> (Option<Self>, Option<U>) {
(::std::str::FromStr::from_str(&part).ok(), state)
}
fn build_route_section<U>(self, route: &mut String) -> Option<U> {
write!(route, "{}", self).expect("Writing to string should never fail.");
None
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn isize_build_route() {
let mut route = "/".to_string();
let mut _state: Option<String> = None;
_state = _state.or_else(|| (-432isize).build_route_section(&mut route));
assert_eq!(route, "/-432".to_string());
}
#[test]
fn can_get_string_from_empty_str() {
let (s, _state) = String::from_route_part::<()>("".to_string(), Some(()));
assert_eq!(s, Some("".to_string()))
}
#[test]
fn uuid_from_route() {
let x = uuid::Uuid::switch::<()>(Route {
route: "5dc48134-35b5-4b8c-aa93-767bf00ae1d8".to_string(),
state: (),
});
assert!(x.is_some())
}
#[test]
fn uuid_to_route() {
use std::str::FromStr;
let id =
uuid::Uuid::from_str("5dc48134-35b5-4b8c-aa93-767bf00ae1d8").expect("should parse");
let mut buf = String::new();
id.build_route_section::<()>(&mut buf);
assert_eq!(buf, "5dc48134-35b5-4b8c-aa93-767bf00ae1d8".to_string())
}
#[test]
fn can_get_option_string_from_empty_str() {
let (s, _state): (Option<Permissive<String>>, Option<()>) =
Permissive::from_route_part("".to_string(), Some(()));
assert_eq!(s, Some(Permissive(Some("".to_string()))))
}
}

View File

@ -0,0 +1,32 @@
use wasm_bindgen::JsCast;
fn strip_slash(path: String) -> String {
if path != "/" {
path.strip_suffix("/")
.map(|it| it.to_string())
.unwrap_or(path)
} else {
path
}
}
pub fn base_url() -> Option<String> {
match yew::utils::document().query_selector("base[href]") {
Ok(Some(base)) => {
let base = base.unchecked_into::<web_sys::HtmlBaseElement>().href();
let url = web_sys::Url::new(&base).unwrap();
let base = url.pathname();
let base = strip_slash(base);
Some(base)
}
_ => None,
}
}
pub fn build_path_with_base(to: &str) -> String {
let to = format!("{}{}", base_url().as_deref().unwrap_or(""), to);
strip_slash(to)
}

View File

@ -1,119 +0,0 @@
use yew_router::{route::Route, switch::Permissive, Switch};
#[derive(Clone, Debug, Eq, PartialEq, Switch)]
pub enum InnerRoute {
#[at = "/left"]
Left,
#[to = "/right"]
Right,
}
#[derive(Clone, Debug, Eq, PartialEq, Switch)]
#[at = "/single/{number}"]
pub struct Single {
number: u32,
}
#[derive(Clone, Debug, Eq, PartialEq, Switch)]
#[at = "/othersingle/{number}"]
pub struct OtherSingle(u32);
#[derive(Clone, Debug, Eq, PartialEq, Switch)]
#[at = "{*:path}#{route}"]
pub struct FragmentAdapter<W: Switch> {
path: String,
route: W,
}
#[derive(Clone, Debug, PartialEq, Switch)]
pub enum AppRoute {
#[at = "/some/route"]
SomeRoute,
#[to = "/some/{thing}/{other}"]
// If you have a variant with named fields, the field names should appear in the matcher string.
Something { thing: String, other: String },
#[at = "/another/{}"] // Tuple-enums don't need names in the capture groups.
Another(String),
#[at = "/doot/{}/{something}"]
// You can still puts names in the capture groups to improve readability.
Yeet(String, String),
#[to = "/inner"]
#[rest] // same as /inner{*}
Nested(InnerRoute),
#[rest] // Rest delegates the remaining input to the next attribute
Single(Single),
#[rest]
OtherSingle(OtherSingle),
/// Because this is permissive, the inner item doesn't have to match.
#[at = "/option/{}"]
Optional(Permissive<String>),
/// Because this is permissive, a corresponding capture group doesn't need to exist
#[at = "/missing/capture"]
MissingCapture(Permissive<String>),
}
#[test]
fn switch() {
let route = Route::new_no_state("/some/route");
assert_eq!(AppRoute::switch(route), Some(AppRoute::SomeRoute));
let route = Route::new_no_state("/some/thing/other");
assert_eq!(
AppRoute::switch(route),
Some(AppRoute::Something {
thing: "thing".to_owned(),
other: "other".to_owned()
})
);
let route = Route::new_no_state("/another/other");
assert_eq!(
AppRoute::switch(route),
Some(AppRoute::Another("other".to_owned()))
);
let route = Route::new_no_state("/inner/left");
assert_eq!(
AppRoute::switch(route),
Some(AppRoute::Nested(InnerRoute::Left))
);
let route = Route::new_no_state("/yeet");
assert_eq!(AppRoute::switch(route), None);
let route = Route::new_no_state("/single/32");
assert_eq!(
AppRoute::switch(route),
Some(AppRoute::Single(Single { number: 32 }))
);
let route = Route::new_no_state("/othersingle/472");
assert_eq!(
AppRoute::switch(route),
Some(AppRoute::OtherSingle(OtherSingle(472)))
);
let route = Route::new_no_state("/option/test");
assert_eq!(
AppRoute::switch(route),
Some(AppRoute::Optional(Permissive(Some("test".to_owned()))))
);
}
#[test]
fn build_route() {
let mut buf = String::new();
AppRoute::Another("yeet".to_string()).build_route_section::<()>(&mut buf);
assert_eq!(buf, "/another/yeet");
let mut buf = String::new();
AppRoute::Something {
thing: "yeet".to_string(),
other: "yote".to_string(),
}
.build_route_section::<()>(&mut buf);
assert_eq!(buf, "/some/yeet/yote");
let mut buf = String::new();
OtherSingle(23).build_route_section::<()>(&mut buf);
assert_eq!(buf, "/othersingle/23");
}

View File

@ -0,0 +1,66 @@
use serde::Serialize;
use std::collections::HashMap;
use wasm_bindgen_test::wasm_bindgen_test as test;
use yew::utils::*;
use yew_router::parse_query;
use yew_router::prelude::*;
use yew_router::utils::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[derive(Debug, Clone, Copy, PartialEq, Routable)]
enum Routes {
#[at("/")]
Home,
#[at("/no")]
No,
#[at("/404")]
NotFound,
}
#[test]
fn test_base_url() {
assert_eq!(base_url(), None);
document()
.head()
.unwrap()
.set_inner_html(r#"<base href="/base/">"#);
assert_eq!(base_url(), Some("/base".to_string()));
document()
.head()
.unwrap()
.set_inner_html(r#"<base href="/base">"#);
assert_eq!(base_url(), Some("/base".to_string()));
}
#[derive(Serialize, Clone)]
struct QueryParams {
foo: String,
bar: u32,
}
#[test]
fn test_get_query_params() {
assert_eq!(
parse_query::<HashMap<String, String>>().unwrap(),
HashMap::new()
);
let query = QueryParams {
foo: "test".to_string(),
bar: 69,
};
yew_router::push_route_with_query(Routes::Home, query.clone()).unwrap();
let params: HashMap<String, String> = parse_query().unwrap();
assert_eq!(params, {
let mut map = HashMap::new();
map.insert("foo".to_string(), "test".to_string());
map.insert("bar".to_string(), "69".to_string());
map
});
}

View File

@ -0,0 +1,93 @@
use serde::{Deserialize, Serialize};
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
use yew::prelude::*;
use yew_functional::function_component;
use yew_router::prelude::*;
mod utils;
use utils::*;
wasm_bindgen_test_configure!(run_in_browser);
#[derive(Serialize, Deserialize)]
struct Query {
foo: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Routable)]
enum Routes {
#[at("/")]
Home,
#[at("/no/:id")]
No { id: u32 },
#[at("/404")]
NotFound,
}
#[derive(Properties, PartialEq, Clone)]
struct NoProps {
id: u32,
}
#[function_component(No)]
fn no(props: &NoProps) -> Html {
let route = props.id.to_string();
html! {
<>
<div id="result-params">{ route }</div>
<div id="result-query">{ yew_router::parse_query::<Query>().unwrap().foo }</div>
</>
}
}
#[function_component(Comp)]
fn component() -> Html {
let switch = Router::render(|routes| {
let onclick = Callback::from(|_| {
yew_router::push_route_with_query(
Routes::No { id: 2 },
Query {
foo: "bar".to_string(),
},
)
.unwrap();
});
match routes {
Routes::Home => html! {
<>
<div id="result">{"Home"}</div>
<a onclick=onclick>{"click me"}</a>
</>
},
Routes::No { id } => html! { <No id=id /> },
Routes::NotFound => html! { <div id="result">{"404"}</div> },
}
});
html! {
<Router<Routes> render=switch>
</Router<Routes>>
}
}
// all the tests are in place because document state isn't being reset between tests
// different routes at the time of execution are set and it causes weird behavior (tests
// failing randomly)
// this test tests
// - routing
// - parameters in the path
// - query parameters
// - 404 redirects
#[test]
fn router_works() {
let app = App::<Comp>::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
assert_eq!("Home", obtain_result_by_id("result"));
click("a");
assert_eq!("2", obtain_result_by_id("result-params"));
assert_eq!("bar", obtain_result_by_id("result-query"));
}

View File

@ -0,0 +1,18 @@
use wasm_bindgen::JsCast;
pub fn obtain_result_by_id(id: &str) -> String {
yew::utils::document()
.get_element_by_id(id)
.expect("No result found. Most likely, the application crashed and burned")
.inner_html()
}
pub fn click(selector: &str) {
yew::utils::document()
.query_selector(selector)
.unwrap()
.unwrap()
.dyn_into::<web_sys::HtmlElement>()
.unwrap()
.click();
}