mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-02-17 06:08:26 +00:00
create redirects
This commit is contained in:
parent
87794b5039
commit
58b74c1155
9 changed files with 307 additions and 419 deletions
|
@ -4,6 +4,7 @@ use layout::Layout;
|
|||
use nest::{Nest, NestId};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{__private::Span, format_ident, quote, ToTokens};
|
||||
use redirect::Redirect;
|
||||
use route::Route;
|
||||
use segment::RouteSegment;
|
||||
use syn::{parse::ParseStream, parse_macro_input, Ident, Token};
|
||||
|
@ -13,14 +14,17 @@ use proc_macro2::TokenStream as TokenStream2;
|
|||
use crate::{layout::LayoutId, route_tree::RouteTree};
|
||||
|
||||
mod layout;
|
||||
mod macro2;
|
||||
mod nest;
|
||||
mod query;
|
||||
mod redirect;
|
||||
mod route;
|
||||
mod route_tree;
|
||||
mod segment;
|
||||
|
||||
#[proc_macro_derive(Routable)]
|
||||
#[proc_macro_derive(
|
||||
Routable,
|
||||
attributes(route, nest, end_nest, layout, end_layout, redirect)
|
||||
)]
|
||||
pub fn routable(input: TokenStream) -> TokenStream {
|
||||
let routes_enum = parse_macro_input!(input as syn::ItemEnum);
|
||||
|
||||
|
@ -69,6 +73,7 @@ pub fn routable(input: TokenStream) -> TokenStream {
|
|||
struct RouteEnum {
|
||||
vis: syn::Visibility,
|
||||
name: Ident,
|
||||
redirects: Vec<Redirect>,
|
||||
routes: Vec<Route>,
|
||||
nests: Vec<Nest>,
|
||||
layouts: Vec<Layout>,
|
||||
|
@ -85,6 +90,8 @@ impl RouteEnum {
|
|||
|
||||
let mut routes = Vec::new();
|
||||
|
||||
let mut redirects = Vec::new();
|
||||
|
||||
let mut layouts: Vec<Layout> = Vec::new();
|
||||
let mut layout_stack = Vec::new();
|
||||
|
||||
|
@ -197,6 +204,16 @@ impl RouteEnum {
|
|||
}
|
||||
} else if attr.path.is_ident("end_layout") {
|
||||
layout_stack.pop();
|
||||
} else if attr.path.is_ident("redirect") {
|
||||
let parser = |input: ParseStream| {
|
||||
Redirect::parse(
|
||||
input,
|
||||
nest_stack.iter().rev().cloned().collect(),
|
||||
redirects.len(),
|
||||
)
|
||||
};
|
||||
let redirect = attr.parse_args_with(parser)?;
|
||||
redirects.push(redirect);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,6 +242,7 @@ impl RouteEnum {
|
|||
vis: vis.clone(),
|
||||
name: name.clone(),
|
||||
routes,
|
||||
redirects,
|
||||
nests,
|
||||
layouts,
|
||||
site_map,
|
||||
|
@ -256,7 +274,7 @@ impl RouteEnum {
|
|||
}
|
||||
|
||||
fn parse_impl(&self) -> TokenStream2 {
|
||||
let tree = RouteTree::new(&self.routes, &self.nests);
|
||||
let tree = RouteTree::new(&self.routes, &self.nests, &self.redirects);
|
||||
let name = &self.name;
|
||||
|
||||
let error_name = format_ident!("{}MatchError", self.name);
|
||||
|
@ -325,6 +343,16 @@ impl RouteEnum {
|
|||
type_defs.push(nest.error_type());
|
||||
}
|
||||
|
||||
for redirect in &self.redirects {
|
||||
let error_variant = redirect.error_variant();
|
||||
let error_name = redirect.error_ident();
|
||||
let route_str = &redirect.route;
|
||||
|
||||
error_variants.push(quote! { #error_variant(#error_name) });
|
||||
display_match.push(quote! { Self::#error_variant(err) => write!(f, "Redirect '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
|
||||
type_defs.push(redirect.error_type());
|
||||
}
|
||||
|
||||
quote! {
|
||||
#(#type_defs)*
|
||||
|
||||
|
|
|
@ -1,286 +0,0 @@
|
|||
use syn::{braced, parenthesized, parse::Parse, Expr, Ident, LitStr, Path, Token};
|
||||
|
||||
#[test]
|
||||
fn parses() {
|
||||
use quote::quote;
|
||||
|
||||
let tokens = quote! {
|
||||
// The name of the enum
|
||||
Route,
|
||||
// All nests that have dynamic segments must have a name used to generate the enum
|
||||
route(User, "user" / user_id: usize) {
|
||||
route(Product, "product" / product_id: usize / dynamic: usize ) {
|
||||
// Render creates a new route (that will be included in the enum) and is rendered with the given component
|
||||
// The component uses the struct of the parent route as a prop (in this case, Product)
|
||||
render(Other)
|
||||
}
|
||||
|
||||
// You can nest routes inside a layout to wrap them in a component that accepts the struct of the parent route as a prop (in this case, User)
|
||||
layout(UserFrame) {
|
||||
route(Route1Props, "hello_world" / dynamic: usize ) {
|
||||
// (Accepts Route1Props as a prop)
|
||||
render(Route1)
|
||||
}
|
||||
|
||||
// You can opt out of the layout by using !layout
|
||||
!layout(UserFrame) {
|
||||
route(Route2Props, "hello_world" / dynamic: usize ) {
|
||||
// (Accepts Route2Props as a prop)
|
||||
render(Route2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route(Route3Props, "hello_world" / dynamic: usize ) {
|
||||
// (Accepts Route3Props as a prop)
|
||||
render(Route3)
|
||||
}
|
||||
|
||||
route(RedirectData, dynamic: usize / extra: String) {
|
||||
// Redirects accept a function that receives the struct of the parent route and returns the new route
|
||||
redirect(|data: RedirectData| todo!() )
|
||||
}
|
||||
};
|
||||
|
||||
let _ = syn::parse2::<RouteTree>(tokens).unwrap();
|
||||
}
|
||||
|
||||
struct RouteTree {
|
||||
name: Ident,
|
||||
roots: Vec<RouteSegment>,
|
||||
}
|
||||
|
||||
impl Parse for RouteTree {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let name = input.parse()?;
|
||||
let _ = input.parse::<Token![,]>();
|
||||
|
||||
let mut roots = Vec::new();
|
||||
while !input.is_empty() {
|
||||
roots.push(input.parse()?);
|
||||
}
|
||||
Ok(Self { name, roots })
|
||||
}
|
||||
}
|
||||
|
||||
enum RouteSegment {
|
||||
Route(Route),
|
||||
Layout(Layout),
|
||||
Render(Render),
|
||||
Redirect(Redirect),
|
||||
}
|
||||
|
||||
impl Parse for RouteSegment {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let lookahead = input.lookahead1();
|
||||
if lookahead.peek(syn::Token![!]) {
|
||||
input.parse::<Token![!]>()?;
|
||||
let ident: Ident = input.parse()?;
|
||||
if ident == "layout" {
|
||||
let mut layout: Layout = input.parse()?;
|
||||
layout.opt_out = true;
|
||||
Ok(RouteSegment::Layout(layout))
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
} else if lookahead.peek(syn::Ident) {
|
||||
let ident: Ident = input.parse()?;
|
||||
if ident == "route" {
|
||||
let route = input.parse()?;
|
||||
Ok(RouteSegment::Route(route))
|
||||
} else if ident == "layout" {
|
||||
let layout = input.parse()?;
|
||||
Ok(RouteSegment::Layout(layout))
|
||||
} else if ident == "render" {
|
||||
let render = input.parse()?;
|
||||
Ok(RouteSegment::Render(render))
|
||||
} else if ident == "redirect" {
|
||||
let redirect = input.parse()?;
|
||||
Ok(RouteSegment::Redirect(redirect))
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Render {
|
||||
component: Path,
|
||||
}
|
||||
|
||||
impl Parse for Render {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let inner;
|
||||
parenthesized!(inner in input);
|
||||
let component = inner.parse()?;
|
||||
|
||||
Ok(Self { component })
|
||||
}
|
||||
}
|
||||
|
||||
struct Layout {
|
||||
opt_out: bool,
|
||||
component: Path,
|
||||
children: Vec<RouteSegment>,
|
||||
}
|
||||
|
||||
impl Parse for Layout {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let inner;
|
||||
parenthesized!(inner in input);
|
||||
let component = inner.parse()?;
|
||||
|
||||
let content;
|
||||
braced!(content in input);
|
||||
let mut children = Vec::new();
|
||||
while !content.is_empty() {
|
||||
children.push(content.parse()?);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
opt_out: false,
|
||||
component,
|
||||
children,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Route {
|
||||
name: Ident,
|
||||
path: RoutePath,
|
||||
children: Vec<RouteSegment>,
|
||||
}
|
||||
|
||||
impl Parse for Route {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let inner;
|
||||
parenthesized!(inner in input);
|
||||
let name = inner.parse()?;
|
||||
inner.parse::<Token![,]>()?;
|
||||
let path = inner.parse()?;
|
||||
|
||||
let content;
|
||||
braced!(content in input);
|
||||
let mut children = Vec::new();
|
||||
while !content.is_empty() {
|
||||
children.push(content.parse()?);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
path,
|
||||
children,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct RoutePath {
|
||||
segments: Vec<RoutePathSegment>,
|
||||
query: Option<QuerySegment>,
|
||||
}
|
||||
|
||||
impl Parse for RoutePath {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
// parse all segments first
|
||||
let mut segments = Vec::new();
|
||||
// remove any leading slash
|
||||
if input.peek(syn::Token![/]) {
|
||||
input.parse::<syn::Token![/]>()?;
|
||||
}
|
||||
|
||||
while !input.is_empty() {
|
||||
let peak = input.lookahead1();
|
||||
// check if the next segment is a query
|
||||
if peak.peek(syn::Token![?]) {
|
||||
break;
|
||||
} else if peak.peek(syn::Token![/]) {
|
||||
input.parse::<syn::Token![/]>()?;
|
||||
} else if peak.peek(syn::Ident) || peak.peek(syn::Token![...]) || peak.peek(syn::LitStr)
|
||||
{
|
||||
// parse the segment
|
||||
segments.push(input.parse()?);
|
||||
} else {
|
||||
return Err(peak.error());
|
||||
}
|
||||
}
|
||||
// then parse the query
|
||||
let query = if input.peek(syn::Token![?]) {
|
||||
Some(input.parse()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Self { segments, query })
|
||||
}
|
||||
}
|
||||
|
||||
enum RoutePathSegment {
|
||||
Static(String),
|
||||
Dynamic(Ident, Path),
|
||||
CatchAll(Ident, Path),
|
||||
}
|
||||
|
||||
impl Parse for RoutePathSegment {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let lookahead = input.lookahead1();
|
||||
if lookahead.peek(Token![...]) {
|
||||
input.parse::<Token![...]>()?;
|
||||
let name: Ident = input.parse()?;
|
||||
input.parse::<Token![:]>()?;
|
||||
let type_: Path = input.parse()?;
|
||||
|
||||
// parse the /
|
||||
let _ = input.parse::<Token![/]>();
|
||||
|
||||
Ok(RoutePathSegment::CatchAll(name, type_))
|
||||
} else if lookahead.peek(LitStr) {
|
||||
let lit: LitStr = input.parse()?;
|
||||
|
||||
// parse the /
|
||||
let _ = input.parse::<Token![/]>();
|
||||
|
||||
Ok(RoutePathSegment::Static(lit.value()))
|
||||
} else if lookahead.peek(Ident) {
|
||||
let ident: Ident = input.parse()?;
|
||||
input.parse::<Token![:]>()?;
|
||||
let type_: Path = input.parse()?;
|
||||
|
||||
// parse the /
|
||||
let _ = input.parse::<Token![/]>();
|
||||
|
||||
Ok(RoutePathSegment::Dynamic(ident, type_))
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QuerySegment {
|
||||
name: Ident,
|
||||
type_: Path,
|
||||
}
|
||||
|
||||
impl Parse for QuerySegment {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
input.parse::<syn::Token![?]>()?;
|
||||
let name = input.parse()?;
|
||||
input.parse::<syn::Token![:]>()?;
|
||||
let type_ = input.parse()?;
|
||||
Ok(Self { name, type_ })
|
||||
}
|
||||
}
|
||||
|
||||
struct Redirect {
|
||||
function: Expr,
|
||||
}
|
||||
|
||||
impl Parse for Redirect {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let inner;
|
||||
parenthesized!(inner in input);
|
||||
let function = inner.parse()?;
|
||||
Ok(Self { function })
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
|
|||
use quote::{format_ident, quote};
|
||||
use syn::{Ident, LitStr};
|
||||
|
||||
use crate::segment::{parse_route_segments, RouteSegment};
|
||||
use crate::segment::{create_error_type, parse_route_segments, RouteSegment};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NestId(pub usize);
|
||||
|
@ -25,7 +25,10 @@ impl Nest {
|
|||
|
||||
let route_segments = parse_route_segments(
|
||||
route.span(),
|
||||
children_routes.iter().flat_map(|f| f.named.iter()),
|
||||
children_routes
|
||||
.iter()
|
||||
.flat_map(|f| f.named.iter())
|
||||
.map(|f| (f.ident.as_ref().unwrap(), &f.ty)),
|
||||
&route.value(),
|
||||
)?
|
||||
.0;
|
||||
|
@ -79,42 +82,6 @@ impl Nest {
|
|||
pub fn error_type(&self) -> TokenStream {
|
||||
let error_name = self.error_ident();
|
||||
|
||||
let mut error_variants = Vec::new();
|
||||
let mut display_match = Vec::new();
|
||||
|
||||
for (i, segment) in self.segments.iter().enumerate() {
|
||||
let error_name = segment.error_name(i);
|
||||
match segment {
|
||||
RouteSegment::Static(index) => {
|
||||
error_variants.push(quote! { #error_name });
|
||||
display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? });
|
||||
}
|
||||
RouteSegment::Dynamic(ident, ty) => {
|
||||
let missing_error = segment.missing_error_name().unwrap();
|
||||
error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) });
|
||||
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
||||
error_variants.push(quote! { #missing_error });
|
||||
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum #error_name {
|
||||
#(#error_variants,)*
|
||||
}
|
||||
|
||||
impl std::fmt::Display for #error_name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#(#display_match,)*
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
create_error_type(error_name, &self.segments)
|
||||
}
|
||||
}
|
||||
|
|
91
packages/router-macro/src/redirect.rs
Normal file
91
packages/router-macro/src/redirect.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use syn::LitStr;
|
||||
|
||||
use crate::{
|
||||
nest::NestId,
|
||||
query::QuerySegment,
|
||||
segment::{create_error_type, parse_route_segments, RouteSegment},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Redirect {
|
||||
pub route: LitStr,
|
||||
pub nests: Vec<NestId>,
|
||||
pub segments: Vec<RouteSegment>,
|
||||
pub query: Option<QuerySegment>,
|
||||
pub function: syn::ExprClosure,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl Redirect {
|
||||
pub fn error_ident(&self) -> Ident {
|
||||
format_ident!("Redirect{}ParseError", self.index)
|
||||
}
|
||||
|
||||
pub fn error_variant(&self) -> Ident {
|
||||
format_ident!("Redirect{}", self.index)
|
||||
}
|
||||
|
||||
pub fn error_type(&self) -> TokenStream {
|
||||
let error_name = self.error_ident();
|
||||
|
||||
create_error_type(error_name, &self.segments)
|
||||
}
|
||||
|
||||
pub fn parse_query(&self) -> TokenStream {
|
||||
match &self.query {
|
||||
Some(query) => query.parse(),
|
||||
None => quote! {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(
|
||||
input: syn::parse::ParseStream,
|
||||
active_nests: Vec<NestId>,
|
||||
index: usize,
|
||||
) -> syn::Result<Self> {
|
||||
let path = input.parse::<syn::LitStr>()?;
|
||||
|
||||
let _ = input.parse::<syn::Token![,]>();
|
||||
let function = input.parse::<syn::ExprClosure>()?;
|
||||
|
||||
let mut closure_arguments = Vec::new();
|
||||
for arg in function.inputs.iter() {
|
||||
match arg {
|
||||
syn::Pat::Type(pat) => match &*pat.pat {
|
||||
syn::Pat::Ident(ident) => {
|
||||
closure_arguments.push((ident.ident.clone(), (*pat.ty).clone()));
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
arg,
|
||||
"Expected closure argument to be a typed pattern",
|
||||
))
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
arg,
|
||||
"Expected closure argument to be a typed pattern",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (segments, query) = parse_route_segments(
|
||||
path.span(),
|
||||
closure_arguments.iter().map(|(name, ty)| (name, ty)),
|
||||
&path.value(),
|
||||
)?;
|
||||
|
||||
Ok(Redirect {
|
||||
route: path,
|
||||
nests: active_nests,
|
||||
segments,
|
||||
query,
|
||||
function,
|
||||
index,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ use crate::layout::LayoutId;
|
|||
use crate::nest::Nest;
|
||||
use crate::nest::NestId;
|
||||
use crate::query::QuerySegment;
|
||||
use crate::segment::create_error_type;
|
||||
use crate::segment::parse_route_segments;
|
||||
use crate::segment::RouteSegment;
|
||||
|
||||
|
@ -90,8 +91,14 @@ impl Route {
|
|||
}
|
||||
};
|
||||
|
||||
let (route_segments, query) =
|
||||
parse_route_segments(variant.ident.span(), named_fields.named.iter(), &route)?;
|
||||
let (route_segments, query) = parse_route_segments(
|
||||
variant.ident.span(),
|
||||
named_fields
|
||||
.named
|
||||
.iter()
|
||||
.map(|f| (f.ident.as_ref().unwrap(), &f.ty)),
|
||||
&route,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
comp_name,
|
||||
|
@ -217,50 +224,7 @@ impl Route {
|
|||
pub fn error_type(&self) -> TokenStream2 {
|
||||
let error_name = self.error_ident();
|
||||
|
||||
let mut error_variants = Vec::new();
|
||||
let mut display_match = Vec::new();
|
||||
|
||||
for (i, segment) in self.segments.iter().enumerate() {
|
||||
let error_name = segment.error_name(i);
|
||||
match segment {
|
||||
RouteSegment::Static(index) => {
|
||||
error_variants.push(quote! { #error_name });
|
||||
display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? });
|
||||
}
|
||||
RouteSegment::Dynamic(ident, ty) => {
|
||||
let missing_error = segment.missing_error_name().unwrap();
|
||||
error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) });
|
||||
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
||||
error_variants.push(quote! { #missing_error });
|
||||
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
|
||||
}
|
||||
RouteSegment::CatchAll(ident, ty) => {
|
||||
error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegments>::Err) });
|
||||
display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum #error_name {
|
||||
ExtraSegments(String),
|
||||
#(#error_variants,)*
|
||||
}
|
||||
|
||||
impl std::fmt::Display for #error_name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ExtraSegments(segments) => {
|
||||
write!(f, "Found additional trailing segments: {}", segments)?
|
||||
}
|
||||
#(#display_match,)*
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
create_error_type(error_name, &self.segments)
|
||||
}
|
||||
|
||||
pub fn parse_query(&self) -> TokenStream2 {
|
||||
|
|
|
@ -4,13 +4,14 @@ use slab::Slab;
|
|||
use syn::Ident;
|
||||
|
||||
use crate::{
|
||||
nest::Nest,
|
||||
nest::{Nest, NestId},
|
||||
redirect::Redirect,
|
||||
route::Route,
|
||||
segment::{static_segment_idx, RouteSegment},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RouteTree<'a> {
|
||||
pub(crate) struct RouteTree<'a> {
|
||||
pub roots: Vec<usize>,
|
||||
entries: Slab<RouteTreeSegmentData<'a>>,
|
||||
}
|
||||
|
@ -47,6 +48,13 @@ impl<'a> RouteTree<'a> {
|
|||
_ => 1,
|
||||
}
|
||||
}
|
||||
RouteTreeSegmentData::Redirect(redirect) => {
|
||||
// Routes that end in a catch all segment should be checked last
|
||||
match redirect.segments.last() {
|
||||
Some(RouteSegment::CatchAll(..)) => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -89,10 +97,15 @@ impl<'a> RouteTree<'a> {
|
|||
.expect("Cannot get children of non static or nest segment")
|
||||
}
|
||||
|
||||
pub fn new(routes: &'a [Route], nests: &'a [Nest]) -> Self {
|
||||
pub(crate) fn new(routes: &'a [Route], nests: &'a [Nest], redirects: &'a [Redirect]) -> Self {
|
||||
let routes = routes
|
||||
.iter()
|
||||
.map(|route| RouteIter::new(route, nests))
|
||||
.map(|route| PathIter::new_route(route, nests))
|
||||
.chain(
|
||||
redirects
|
||||
.iter()
|
||||
.map(|redirect| PathIter::new_redirect(redirect, nests)),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut myself = Self::default();
|
||||
|
@ -102,7 +115,7 @@ impl<'a> RouteTree<'a> {
|
|||
myself
|
||||
}
|
||||
|
||||
pub fn construct(&mut self, routes: Vec<RouteIter<'a>>) -> Vec<usize> {
|
||||
pub fn construct(&mut self, routes: Vec<PathIter<'a>>) -> Vec<usize> {
|
||||
let mut segments = Vec::new();
|
||||
|
||||
// Add all routes to the tree
|
||||
|
@ -127,7 +140,7 @@ impl<'a> RouteTree<'a> {
|
|||
for &seg_id in segments.iter() {
|
||||
let seg = self.get(seg_id).unwrap();
|
||||
if let RouteTreeSegmentData::Static { segment: s, .. } = seg {
|
||||
if s == segment {
|
||||
if *s == segment {
|
||||
// If it does, just update the current route
|
||||
current_route = Some(seg_id);
|
||||
continue 'o;
|
||||
|
@ -223,9 +236,7 @@ impl<'a> RouteTree<'a> {
|
|||
}
|
||||
// If there is no static segment, add the route to the current_route
|
||||
None => {
|
||||
let id = self
|
||||
.entries
|
||||
.insert(RouteTreeSegmentData::Route(route.route));
|
||||
let id = self.entries.insert(route.final_segment);
|
||||
let current_children_mut = current_route
|
||||
.map(|id| self.children_mut(id))
|
||||
.unwrap_or_else(|| &mut segments);
|
||||
|
@ -246,7 +257,7 @@ pub struct StaticErrorVariant {
|
|||
|
||||
// First deduplicate the routes by the static part of the route
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RouteTreeSegmentData<'a> {
|
||||
pub(crate) enum RouteTreeSegmentData<'a> {
|
||||
Static {
|
||||
segment: &'a str,
|
||||
error_variant: StaticErrorVariant,
|
||||
|
@ -258,6 +269,7 @@ pub enum RouteTreeSegmentData<'a> {
|
|||
children: Vec<usize>,
|
||||
},
|
||||
Route(&'a Route),
|
||||
Redirect(&'a Redirect),
|
||||
}
|
||||
|
||||
impl<'a> RouteTreeSegmentData<'a> {
|
||||
|
@ -362,6 +374,52 @@ impl<'a> RouteTreeSegmentData<'a> {
|
|||
&varient_parse_error,
|
||||
)
|
||||
}
|
||||
Self::Redirect(redirect) => {
|
||||
// At this point, we have matched all static segments, so we can just check if the remaining segments match the route
|
||||
let varient_parse_error = redirect.error_ident();
|
||||
let enum_varient = &redirect.error_variant();
|
||||
|
||||
let route_segments = redirect
|
||||
.segments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_)));
|
||||
|
||||
let parse_query = redirect.parse_query();
|
||||
|
||||
let insure_not_trailing = redirect
|
||||
.segments
|
||||
.last()
|
||||
.map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _)))
|
||||
.unwrap_or(true);
|
||||
|
||||
let redirect_function = &redirect.function;
|
||||
let args = redirect_function.inputs.iter().map(|pat| match pat {
|
||||
syn::Pat::Type(ident) => {
|
||||
let name = &ident.pat;
|
||||
quote! {#name}
|
||||
}
|
||||
_ => panic!("Expected closure argument to be a typed pattern"),
|
||||
});
|
||||
let return_redirect = quote! {
|
||||
(#redirect_function)(#(#args,)*)
|
||||
};
|
||||
|
||||
print_route_segment(
|
||||
route_segments.peekable(),
|
||||
return_constructed(
|
||||
insure_not_trailing,
|
||||
return_redirect,
|
||||
&error_enum_name,
|
||||
enum_varient,
|
||||
&varient_parse_error,
|
||||
parse_query,
|
||||
),
|
||||
&error_enum_name,
|
||||
enum_varient,
|
||||
&varient_parse_error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -435,18 +493,39 @@ fn return_constructed(
|
|||
}
|
||||
}
|
||||
|
||||
pub struct RouteIter<'a> {
|
||||
route: &'a Route,
|
||||
nests: &'a [Nest],
|
||||
pub struct PathIter<'a> {
|
||||
final_segment: RouteTreeSegmentData<'a>,
|
||||
active_nests: &'a [NestId],
|
||||
all_nests: &'a [Nest],
|
||||
segments: &'a [RouteSegment],
|
||||
error_ident: Ident,
|
||||
error_variant: Ident,
|
||||
nest_index: usize,
|
||||
static_segment_index: usize,
|
||||
}
|
||||
|
||||
impl<'a> RouteIter<'a> {
|
||||
fn new(route: &'a Route, nests: &'a [Nest]) -> Self {
|
||||
impl<'a> PathIter<'a> {
|
||||
fn new_route(route: &'a Route, nests: &'a [Nest]) -> Self {
|
||||
Self {
|
||||
route,
|
||||
nests,
|
||||
final_segment: RouteTreeSegmentData::Route(route),
|
||||
active_nests: &*route.nests,
|
||||
segments: &*route.segments,
|
||||
error_ident: route.error_ident(),
|
||||
error_variant: route.route_name.clone(),
|
||||
all_nests: nests,
|
||||
nest_index: 0,
|
||||
static_segment_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_redirect(redirect: &'a Redirect, nests: &'a [Nest]) -> Self {
|
||||
Self {
|
||||
final_segment: RouteTreeSegmentData::Redirect(redirect),
|
||||
active_nests: &*redirect.nests,
|
||||
segments: &*redirect.segments,
|
||||
error_ident: redirect.error_ident(),
|
||||
error_variant: redirect.error_variant(),
|
||||
all_nests: nests,
|
||||
nest_index: 0,
|
||||
static_segment_index: 0,
|
||||
}
|
||||
|
@ -454,15 +533,15 @@ impl<'a> RouteIter<'a> {
|
|||
|
||||
fn next_nest(&mut self) -> Option<&'a Nest> {
|
||||
let idx = self.nest_index;
|
||||
let nest_index = self.route.nests.get(idx)?;
|
||||
let nest = &self.nests[nest_index.0];
|
||||
let nest_index = self.active_nests.get(idx)?;
|
||||
let nest = &self.all_nests[nest_index.0];
|
||||
self.nest_index += 1;
|
||||
Some(nest)
|
||||
}
|
||||
|
||||
fn next_static_segment(&mut self) -> Option<(usize, &'a str)> {
|
||||
let idx = self.static_segment_index;
|
||||
let segment = self.route.segments.get(idx)?;
|
||||
let segment = self.segments.get(idx)?;
|
||||
match segment {
|
||||
RouteSegment::Static(segment) => {
|
||||
self.static_segment_index += 1;
|
||||
|
@ -474,8 +553,8 @@ impl<'a> RouteIter<'a> {
|
|||
|
||||
fn error_variant(&self) -> StaticErrorVariant {
|
||||
StaticErrorVariant {
|
||||
varient_parse_error: self.route.error_ident(),
|
||||
enum_varient: self.route.route_name.clone(),
|
||||
varient_parse_error: self.error_ident.clone(),
|
||||
enum_varient: self.error_variant.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ pub fn static_segment_idx(idx: usize) -> Ident {
|
|||
|
||||
pub fn parse_route_segments<'a>(
|
||||
route_span: Span,
|
||||
mut fields: impl Iterator<Item = &'a syn::Field>,
|
||||
mut fields: impl Iterator<Item = (&'a Ident, &'a Type)>,
|
||||
route: &str,
|
||||
) -> syn::Result<(Vec<RouteSegment>, Option<QuerySegment>)> {
|
||||
let mut route_segments = Vec::new();
|
||||
|
@ -157,13 +157,10 @@ pub fn parse_route_segments<'a>(
|
|||
segment.to_string()
|
||||
};
|
||||
|
||||
let field = fields.find(|field| match field.ident {
|
||||
Some(ref field_ident) => *field_ident == ident,
|
||||
None => false,
|
||||
});
|
||||
let field = fields.find(|(name, _)| **name == ident);
|
||||
|
||||
let ty = if let Some(field) = field {
|
||||
field.ty.clone()
|
||||
field.1.clone()
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
route_span,
|
||||
|
@ -200,13 +197,10 @@ pub fn parse_route_segments<'a>(
|
|||
Some(query) => {
|
||||
if let Some(query) = query.strip_prefix(':') {
|
||||
let query_ident = Ident::new(query, Span::call_site());
|
||||
let field = fields.find(|field| match field.ident {
|
||||
Some(ref field_ident) => field_ident == &query_ident,
|
||||
None => false,
|
||||
});
|
||||
let field = fields.find(|(name, _)| *name == &query_ident);
|
||||
|
||||
let ty = if let Some(field) = field {
|
||||
field.ty.clone()
|
||||
let ty = if let Some((_, ty)) = field {
|
||||
ty.clone()
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
route_span,
|
||||
|
@ -227,3 +221,52 @@ pub fn parse_route_segments<'a>(
|
|||
|
||||
Ok((route_segments, parsed_query))
|
||||
}
|
||||
|
||||
pub(crate) fn create_error_type(error_name: Ident, segments: &[RouteSegment]) -> TokenStream2 {
|
||||
let mut error_variants = Vec::new();
|
||||
let mut display_match = Vec::new();
|
||||
|
||||
for (i, segment) in segments.iter().enumerate() {
|
||||
let error_name = segment.error_name(i);
|
||||
match segment {
|
||||
RouteSegment::Static(index) => {
|
||||
error_variants.push(quote! { #error_name });
|
||||
display_match.push(quote! { Self::#error_name => write!(f, "Static segment '{}' did not match", #index)? });
|
||||
}
|
||||
RouteSegment::Dynamic(ident, ty) => {
|
||||
let missing_error = segment.missing_error_name().unwrap();
|
||||
error_variants.push(
|
||||
quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) },
|
||||
);
|
||||
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
||||
error_variants.push(quote! { #missing_error });
|
||||
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
|
||||
}
|
||||
RouteSegment::CatchAll(ident, ty) => {
|
||||
error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegments>::Err) });
|
||||
display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum #error_name {
|
||||
ExtraSegments(String),
|
||||
#(#error_variants,)*
|
||||
}
|
||||
|
||||
impl std::fmt::Display for #error_name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ExtraSegments(segments) => {
|
||||
write!(f, "Found additional trailing segments: {}", segments)?
|
||||
}
|
||||
#(#display_match,)*
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,13 +45,13 @@ fn UserFrame(cx: Scope, user_id: usize) -> Element {
|
|||
}
|
||||
|
||||
#[inline_props]
|
||||
fn Route1(cx: Scope, user_id: usize, dynamic: usize, extra: String) -> Element {
|
||||
fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: String) -> Element {
|
||||
render! {
|
||||
pre {
|
||||
"Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\textra:{extra}\n}}"
|
||||
"Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query},\n\textra:{extra}\n}}"
|
||||
}
|
||||
Link {
|
||||
target: Route::Route1 { user_id: *user_id, dynamic: *dynamic, extra: extra.clone() + "." },
|
||||
target: Route::Route1 { user_id: *user_id, dynamic: *dynamic, query: String::new(), extra: extra.clone() + "." },
|
||||
"Route1 with extra+\".\""
|
||||
}
|
||||
p { "Footer" }
|
||||
|
@ -131,12 +131,13 @@ enum Route {
|
|||
// Everything inside the nest has the added parameter `user_id: String`
|
||||
// UserFrame is a layout component that will receive the `user_id: String` parameter
|
||||
#[layout(UserFrame)]
|
||||
// Route1 is a non-layout component that will receive the `user_id: String` and `dynamic: String` parameters
|
||||
#[route("/:dynamic", Route1)]
|
||||
// Route1 is a non-layout component that will receive the `user_id: String` and `dynamic: String` parameters
|
||||
#[route("/:dynamic?:query", Route1)]
|
||||
Route1 {
|
||||
// The type is taken from the first instance of the dynamic parameter
|
||||
user_id: usize,
|
||||
dynamic: usize,
|
||||
query: String,
|
||||
extra: String,
|
||||
},
|
||||
// Route2 is a non-layout component that will receive the `user_id: String` parameter
|
||||
|
@ -146,6 +147,7 @@ enum Route {
|
|||
Route2 { user_id: usize },
|
||||
#[end_layout]
|
||||
#[end_nest]
|
||||
#[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
|
||||
#[route("/:dynamic", Route3)]
|
||||
Route3 { dynamic: String },
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use super::{
|
|||
HistoryProvider,
|
||||
};
|
||||
|
||||
fn update_scroll<R: Serialize + DeserializeOwned>(window: &Window, history: &History) {
|
||||
fn update_scroll<R: Serialize + DeserializeOwned + Routable>(window: &Window, history: &History) {
|
||||
if let Some(WebHistoryState { state, .. }) = get_current::<WebHistoryState<R>>(history) {
|
||||
let scroll = ScrollPosition::of_window(window);
|
||||
let state = WebHistoryState { state, scroll };
|
||||
|
@ -42,7 +42,7 @@ struct WebHistoryState<R> {
|
|||
/// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
|
||||
///
|
||||
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
||||
pub struct WebHistory<R: Serialize + DeserializeOwned> {
|
||||
pub struct WebHistory<R: Serialize + DeserializeOwned + Routable> {
|
||||
do_scroll_restoration: bool,
|
||||
history: History,
|
||||
listener_navigation: Option<EventListener>,
|
||||
|
@ -102,10 +102,10 @@ impl<R: Serialize + DeserializeOwned + Routable> WebHistory<R> {
|
|||
phantom: Default::default(),
|
||||
};
|
||||
|
||||
let state = myself.current_route();
|
||||
|
||||
let state = myself.create_state(state);
|
||||
let _ = replace_state_with_url(&myself.history, &state, None);
|
||||
let current_route = myself.current_route();
|
||||
let current_url = current_route.to_string();
|
||||
let state = myself.create_state(current_route);
|
||||
let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url));
|
||||
|
||||
myself
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue