Skip to content

Commit

Permalink
Upstream yew-router project (#1095)
Browse files Browse the repository at this point in the history
* Add yew-router

* Add router examples

* Add CODEOWNERS
  • Loading branch information
jstarry committed Apr 23, 2020
1 parent ce020d6 commit 9f286d1
Show file tree
Hide file tree
Showing 66 changed files with 6,897 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
@@ -0,0 +1,5 @@
# Reference: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners

yew-router/ @hgzimmerman
yew-router-macro/ @hgzimmerman
yew-router-route-parser/ @hgzimmerman
9 changes: 9 additions & 0 deletions Cargo.toml
Expand Up @@ -4,6 +4,15 @@ members = [
"yew-functional",
"yew-macro",

# Router
"yew-router",
"yew-router-macro",
"yew-router-route-parser",
"yew-router/examples/guide",
"yew-router/examples/minimal",
"yew-router/examples/router_component",
"yew-router/examples/switch",

# Examples
"examples/counter",
"examples/crm",
Expand Down
20 changes: 20 additions & 0 deletions yew-router-macro/Cargo.toml
@@ -0,0 +1,20 @@
[package]
name = "yew-router-macro"
version = "0.11.0"
authors = ["Henry Zimmerman <zimhen7@gmail.com>"]
edition = "2018"
license = "MIT/Apache-2.0"
description = "Contains macros used with yew-router"
repository = "https://github.com/yewstack/yew_router"

[lib]
proc-macro = true

[dependencies]
syn = "1.0.2"
quote = "1.0.1"
yew-router-route-parser = { path = "../yew-router-route-parser" }
proc-macro2 = "1.0.1"

[dev-dependencies]
yew-router = { path = "../yew-router" } # This should probably be removed, it makes the deploy process much more annoying.
102 changes: 102 additions & 0 deletions yew-router-macro/src/lib.rs
@@ -0,0 +1,102 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

mod switch;

/// Implements the `Switch` trait based on attributes present on the struct or enum variants.
///
/// If deriving an enum, each variant should have a `#[to = ""]` attribute,
/// and if deriving a struct, the struct itself should have a `#[to = ""]` attribute.
///
/// 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, `#[to = "/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 {
/// #[to = "/some/simple/route"]
/// SomeSimpleRoute,
/// #[to = "/capture/{}"]
/// Capture(String),
/// #[to = "/named/capture/{name}"]
/// NamedCapture { name: String },
/// #[to = "/convert/{id}"]
/// Convert { id: usize },
/// #[rest] // shorthand for #[to="{*}"]
/// Inner(InnerRoute),
/// }
///
/// #[derive(Switch, Clone)]
/// #[to = "/inner/route/{first}/{second}"]
/// struct InnerRoute {
/// first: String,
/// second: String,
/// }
/// ```
/// Check out the examples directory in the repository to see some more usages of the routing syntax.
#[proc_macro_derive(Switch, attributes(to, 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 rest(_: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
}

#[proc_macro_attribute]
pub fn end(_: TokenStream, _: TokenStream) -> TokenStream {
TokenStream::new()
}
180 changes: 180 additions & 0 deletions yew-router-macro/src/switch.rs
@@ -0,0 +1,180 @@
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())
}

0 comments on commit 9f286d1

Please sign in to comment.