add static nesting
This commit is contained in:
1 parent
adfc10461e
commit
a73873a571
5 files changed
+218
-125
No files matched your search
@@ -64,11 +64,13 @@ fn Route6(cx: Scope, extra: Vec<String>) -> Element {
|
|||||||
enum Route {
|
enum Route {
|
||||||
#[route("/(dynamic)" Route1)]
|
#[route("/(dynamic)" Route1)]
|
||||||
Route1 { dynamic: String },
|
Route1 { dynamic: String },
|
||||||
#[route("/hello_world" Route2)]
|
#[nest("/hello_world")]
|
||||||
Route2 {},
|
#[route("/" Route2)]
|
||||||
// #[redirect("/(dynamic)/hello_world")]
|
Route2 {},
|
||||||
#[route("/hello_world/(dynamic)" Route3)]
|
// #[redirect("/(dynamic)/hello_world")]
|
||||||
Route3 { dynamic: u32 },
|
#[route("/(dynamic)" Route3)]
|
||||||
|
Route3 { dynamic: u32 },
|
||||||
|
#[end_nest]
|
||||||
#[route("/(number1)/(number2)" Route4)]
|
#[route("/(number1)/(number2)" Route4)]
|
||||||
Route4 { number1: u32, number2: u32 },
|
Route4 { number1: u32, number2: u32 },
|
||||||
#[route("/?(query)" Route5)]
|
#[route("/?(query)" Route5)]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use nest::Nest;
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{__private::Span, format_ident, quote, ToTokens};
|
use quote::{__private::Span, format_ident, quote, ToTokens};
|
||||||
use route::Route;
|
use route::Route;
|
||||||
@@ -14,7 +15,7 @@ mod route;
|
|||||||
mod route_tree;
|
mod route_tree;
|
||||||
mod segment;
|
mod segment;
|
||||||
|
|
||||||
#[proc_macro_derive(Routable, attributes(route))]
|
#[proc_macro_derive(Routable, attributes(route, nest, end_nest))]
|
||||||
pub fn derive_routable(input: TokenStream) -> TokenStream {
|
pub fn derive_routable(input: TokenStream) -> TokenStream {
|
||||||
let routes_enum = parse_macro_input!(input as syn::DeriveInput);
|
let routes_enum = parse_macro_input!(input as syn::DeriveInput);
|
||||||
|
|
||||||
@@ -54,15 +55,23 @@ impl RouteEnum {
|
|||||||
if let syn::Data::Enum(data) = input.data {
|
if let syn::Data::Enum(data) = input.data {
|
||||||
let mut routes = Vec::new();
|
let mut routes = Vec::new();
|
||||||
|
|
||||||
let mut current_base_route = String::new();
|
let mut current_base_route = Vec::new();
|
||||||
|
|
||||||
for variant in data.variants {
|
for variant in data.variants {
|
||||||
// Apply the any nesting attributes in order
|
// Apply the any nesting attributes in order
|
||||||
for attr in &variant.attrs {
|
for attr in &variant.attrs {
|
||||||
if attr.path.is_ident("nest") {}
|
if attr.path.is_ident("nest") {
|
||||||
|
let nest: Nest = attr.parse_args()?;
|
||||||
|
match nest {
|
||||||
|
Nest::Static(s) => current_base_route.push(s),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
} else if attr.path.is_ident("end_nest") {
|
||||||
|
current_base_route.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let route = Route::parse(current_base_route.clone(), variant)?;
|
let route = Route::parse(current_base_route.join("/"), variant)?;
|
||||||
routes.push(route);
|
routes.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,80 @@
|
|||||||
struct Nest {}
|
use quote::format_ident;
|
||||||
|
use syn::{parse::Parse, Ident, LitStr, Variant};
|
||||||
|
|
||||||
|
use crate::segment::RouteSegment;
|
||||||
|
|
||||||
|
pub enum Nest {
|
||||||
|
Static(String),
|
||||||
|
Layout(Layout),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Nest {
|
||||||
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
// First parse the route
|
||||||
|
let route: LitStr = input.parse()?;
|
||||||
|
|
||||||
|
if route.value().contains('(') {
|
||||||
|
// Then parse the layout name
|
||||||
|
let _ = input.parse::<syn::Token![,]>();
|
||||||
|
let layout_name: Ident = input.parse()?;
|
||||||
|
|
||||||
|
// Then parse the component name
|
||||||
|
let _ = input.parse::<syn::Token![,]>();
|
||||||
|
let comp: Variant = input.parse()?;
|
||||||
|
|
||||||
|
// Then parse the props name
|
||||||
|
let _ = input.parse::<syn::Token![,]>();
|
||||||
|
let props_name: Ident = input
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|_| format_ident!("{}Props", comp.ident.to_string()));
|
||||||
|
|
||||||
|
Ok(Self::Layout(Layout {
|
||||||
|
route: route.value(),
|
||||||
|
route_segments: Vec::new(),
|
||||||
|
layout_name,
|
||||||
|
comp,
|
||||||
|
props_name,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(Self::Static(route.value()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Layout {
|
||||||
|
pub route: String,
|
||||||
|
pub route_segments: Vec<RouteSegment>,
|
||||||
|
pub layout_name: Ident,
|
||||||
|
pub comp: Variant,
|
||||||
|
pub props_name: Ident,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Clone, Debug, PartialEq, Routable)]
|
||||||
|
// enum Route {
|
||||||
|
// // Each Variant is a route with a linked component, dynamic segments are defined with the syntax: (name) and the type is inferred from the field type. The type must implement FromStr
|
||||||
|
// #[route("/(dynamic)" Component1)]
|
||||||
|
// Route1 { dynamic: usize },
|
||||||
|
// // You can nest routes which makes all routes in the block relative to a parent route. Nested routes are flattened into the parent enum
|
||||||
|
// // Nest accepts a optional layout component. The layout component that wraps all children and renders them where the Outlet component is found. It can accept parameters from the nested route, just like a normal route
|
||||||
|
// #[nest("/(dynamic)" root_dynamic_segment Component { dynamic: String })]
|
||||||
|
// // If the component is not specified, the component is assumed to be at the path of the route (in this case /pages/hello_world.rs or /pages/hello_world/index.rs)
|
||||||
|
// #[route("/")]
|
||||||
|
// // You can opt out of a parent Layout
|
||||||
|
// #[layout(!Component)]
|
||||||
|
// Route2 {
|
||||||
|
// // implicitly adds
|
||||||
|
// // root_dynamic_segment: ComponentProps,
|
||||||
|
// },
|
||||||
|
// #[end_nest]
|
||||||
|
// // Queries are defined with the syntax: ?(name) and the type is inferred from the field type. The type must implement From<&str> (not FromStr because the query parsing must be infallible). The query part of the url is not included in the route path for file based routing. (in this case /pages/takes_query.rs or /pages/takes_query/index.rs)
|
||||||
|
// #[route("/takes_query?(dynamic)")]
|
||||||
|
// Route3 { dynamic: u32 },
|
||||||
|
// // Redirects are defined with the redirect attribute
|
||||||
|
// #[redirect("/old_hello_world/(dynamic)")]
|
||||||
|
// #[route("/hello_world/(dynamic)")]
|
||||||
|
// Route4 { dynamic: u32 },
|
||||||
|
// // members that can be parsed from all trailing segments are defined with the syntax: (...name) and the type is inferred from the field type. The type must implement FromSegments.
|
||||||
|
// // Because this route is defined after Route3, it will only be matched if Route3 does not match and it will act as a fallback
|
||||||
|
// #[route("/(...number2)")]
|
||||||
|
// Route5 { number1: u32, number2: u32 },
|
||||||
|
// }
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
use quote::{__private::Span, format_ident, quote, ToTokens};
|
use quote::{format_ident, quote, ToTokens};
|
||||||
use syn::parse::Parse;
|
use syn::parse::Parse;
|
||||||
use syn::parse::ParseStream;
|
use syn::parse::ParseStream;
|
||||||
use syn::{Ident, LitStr, Variant};
|
use syn::{Ident, LitStr};
|
||||||
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
|
||||||
use crate::query::QuerySegment;
|
use crate::query::QuerySegment;
|
||||||
|
use crate::segment::parse_route_segments;
|
||||||
use crate::segment::RouteSegment;
|
use crate::segment::RouteSegment;
|
||||||
|
|
||||||
struct RouteArgs {
|
struct RouteArgs {
|
||||||
@@ -237,114 +238,3 @@ impl ToTokens for Route {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_route_segments(
|
|
||||||
varient: &Variant,
|
|
||||||
route: &str,
|
|
||||||
) -> syn::Result<(Vec<RouteSegment>, Option<QuerySegment>)> {
|
|
||||||
let mut route_segments = Vec::new();
|
|
||||||
|
|
||||||
let (route_string, query) = match route.rsplit_once('?') {
|
|
||||||
Some((route, query)) => (route, Some(query)),
|
|
||||||
None => (route, None),
|
|
||||||
};
|
|
||||||
let mut iterator = route_string.split('/');
|
|
||||||
|
|
||||||
// skip the first empty segment
|
|
||||||
let first = iterator.next();
|
|
||||||
if first != Some("") {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
varient,
|
|
||||||
format!(
|
|
||||||
"Routes should start with /. Error found in the route '{}'",
|
|
||||||
route
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(segment) = iterator.next() {
|
|
||||||
if segment.starts_with('(') && segment.ends_with(')') {
|
|
||||||
let spread = segment.starts_with("(...");
|
|
||||||
|
|
||||||
let ident = if spread {
|
|
||||||
segment[4..segment.len() - 1].to_string()
|
|
||||||
} else {
|
|
||||||
segment[1..segment.len() - 1].to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let field = varient.fields.iter().find(|field| match field.ident {
|
|
||||||
Some(ref field_ident) => *field_ident == ident,
|
|
||||||
None => false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let ty = if let Some(field) = field {
|
|
||||||
field.ty.clone()
|
|
||||||
} else {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
varient,
|
|
||||||
format!(
|
|
||||||
"Could not find a field with the name '{}' in the variant '{}'",
|
|
||||||
ident, varient.ident
|
|
||||||
),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
if spread {
|
|
||||||
route_segments.push(RouteSegment::CatchAll(
|
|
||||||
Ident::new(&ident, Span::call_site()),
|
|
||||||
ty,
|
|
||||||
));
|
|
||||||
|
|
||||||
if iterator.next().is_some() {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
route,
|
|
||||||
"Catch-all route segments must be the last segment in a route. The route segments after the catch-all segment will never be matched.",
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
route_segments.push(RouteSegment::Dynamic(
|
|
||||||
Ident::new(&ident, Span::call_site()),
|
|
||||||
ty,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
route_segments.push(RouteSegment::Static(segment.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the route has a query string
|
|
||||||
let parsed_query = match query {
|
|
||||||
Some(query) => {
|
|
||||||
if query.starts_with('(') && query.ends_with(')') {
|
|
||||||
let query_ident = Ident::new(&query[1..query.len() - 1], Span::call_site());
|
|
||||||
let field = varient.fields.iter().find(|field| match field.ident {
|
|
||||||
Some(ref field_ident) => field_ident == &query_ident,
|
|
||||||
None => false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let ty = if let Some(field) = field {
|
|
||||||
field.ty.clone()
|
|
||||||
} else {
|
|
||||||
return Err(syn::Error::new_spanned(
|
|
||||||
varient,
|
|
||||||
format!(
|
|
||||||
"Could not find a field with the name '{}' in the variant '{}'",
|
|
||||||
query_ident, varient.ident
|
|
||||||
),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(QuerySegment {
|
|
||||||
ident: query_ident,
|
|
||||||
ty,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((route_segments, parsed_query))
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
use syn::{Ident, Type};
|
use syn::{Ident, Type, Variant};
|
||||||
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
|
|
||||||
|
use crate::query::QuerySegment;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RouteSegment {
|
pub enum RouteSegment {
|
||||||
@@ -119,3 +121,114 @@ impl RouteSegment {
|
|||||||
pub fn static_segment_idx(idx: usize) -> Ident {
|
pub fn static_segment_idx(idx: usize) -> Ident {
|
||||||
format_ident!("StaticSegment{}ParseError", idx)
|
format_ident!("StaticSegment{}ParseError", idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_route_segments(
|
||||||
|
varient: &Variant,
|
||||||
|
route: &str,
|
||||||
|
) -> syn::Result<(Vec<RouteSegment>, Option<QuerySegment>)> {
|
||||||
|
let mut route_segments = Vec::new();
|
||||||
|
|
||||||
|
let (route_string, query) = match route.rsplit_once('?') {
|
||||||
|
Some((route, query)) => (route, Some(query)),
|
||||||
|
None => (route, None),
|
||||||
|
};
|
||||||
|
let mut iterator = route_string.split('/');
|
||||||
|
|
||||||
|
// skip the first empty segment
|
||||||
|
let first = iterator.next();
|
||||||
|
if first != Some("") {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
varient,
|
||||||
|
format!(
|
||||||
|
"Routes should start with /. Error found in the route '{}'",
|
||||||
|
route
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(segment) = iterator.next() {
|
||||||
|
if segment.starts_with('(') && segment.ends_with(')') {
|
||||||
|
let spread = segment.starts_with("(...");
|
||||||
|
|
||||||
|
let ident = if spread {
|
||||||
|
segment[4..segment.len() - 1].to_string()
|
||||||
|
} else {
|
||||||
|
segment[1..segment.len() - 1].to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let field = varient.fields.iter().find(|field| match field.ident {
|
||||||
|
Some(ref field_ident) => *field_ident == ident,
|
||||||
|
None => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let ty = if let Some(field) = field {
|
||||||
|
field.ty.clone()
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
varient,
|
||||||
|
format!(
|
||||||
|
"Could not find a field with the name '{}' in the variant '{}'",
|
||||||
|
ident, varient.ident
|
||||||
|
),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if spread {
|
||||||
|
route_segments.push(RouteSegment::CatchAll(
|
||||||
|
Ident::new(&ident, Span::call_site()),
|
||||||
|
ty,
|
||||||
|
));
|
||||||
|
|
||||||
|
if iterator.next().is_some() {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
route,
|
||||||
|
"Catch-all route segments must be the last segment in a route. The route segments after the catch-all segment will never be matched.",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
route_segments.push(RouteSegment::Dynamic(
|
||||||
|
Ident::new(&ident, Span::call_site()),
|
||||||
|
ty,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
route_segments.push(RouteSegment::Static(segment.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the route has a query string
|
||||||
|
let parsed_query = match query {
|
||||||
|
Some(query) => {
|
||||||
|
if query.starts_with('(') && query.ends_with(')') {
|
||||||
|
let query_ident = Ident::new(&query[1..query.len() - 1], Span::call_site());
|
||||||
|
let field = varient.fields.iter().find(|field| match field.ident {
|
||||||
|
Some(ref field_ident) => field_ident == &query_ident,
|
||||||
|
None => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let ty = if let Some(field) = field {
|
||||||
|
field.ty.clone()
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
varient,
|
||||||
|
format!(
|
||||||
|
"Could not find a field with the name '{}' in the variant '{}'",
|
||||||
|
query_ident, varient.ident
|
||||||
|
),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(QuerySegment {
|
||||||
|
ident: query_ident,
|
||||||
|
ty,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((route_segments, parsed_query))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user