Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upstream yew-router project #1095

Merged
merged 3 commits into from Apr 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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())
}