create redirects

This commit is contained in:
Evan Almloff 2023-05-31 12:11:11 -05:00
parent 87794b5039
commit 58b74c1155
9 changed files with 307 additions and 419 deletions

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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(),
}
}
}

View file

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

View file

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

View file

@ -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(&current_url));
myself
}