derive query parsing in the router macro

This commit is contained in:
Evan Almloff committed 2023-12-01 15:31:45 -06:00
1 parent c8a9a7b1d5
commit 8a44fe8d03
6 files changed
+175 -17

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ mod segment;
/// 1. Static Segments: "/static"
/// 2. Dynamic Segments: "/:dynamic" (where dynamic has a type that is FromStr in all child Variants)
/// 3. Catch all Segments: "/:..segments" (where segments has a type that is FromSegments in all child Variants)
/// 4. Query Segments: "/?:query" (where query has a type that is FromQuery in all child Variants)
/// 4. Query Segments: "/?:..query" (where query has a type that is FromQuery in all child Variants) or "/?:query&:other_query" (where query and other_query has a type that is FromQueryArgument in all child Variants)
///
/// Routes are matched:
/// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:..route")
+78 -2
View File
@@ -4,12 +4,62 @@ use syn::{Ident, Type};
use proc_macro2::TokenStream as TokenStream2;
#[derive(Debug)]
pub struct QuerySegment {
pub enum QuerySegment {
Single(FullQuerySegment),
Segments(Vec<QueryArgument>),
}
impl QuerySegment {
pub fn contains_ident(&self, ident: &Ident) -> bool {
match self {
QuerySegment::Single(segment) => segment.ident == *ident,
QuerySegment::Segments(segments) => {
segments.iter().any(|segment| segment.ident == *ident)
}
}
}
pub fn parse(&self) -> TokenStream2 {
match self {
QuerySegment::Single(segment) => segment.parse(),
QuerySegment::Segments(segments) => {
let mut tokens = TokenStream2::new();
tokens.extend(quote! { let split_query: std::collections::HashMap<&str, &str> = query.split('&').filter_map(|s| s.split_once('=')).collect(); });
for segment in segments {
tokens.extend(segment.parse());
}
tokens
}
}
}
pub fn write(&self) -> TokenStream2 {
match self {
QuerySegment::Single(segment) => segment.write(),
QuerySegment::Segments(segments) => {
let mut tokens = TokenStream2::new();
tokens.extend(quote! { write!(f, "?")?; });
let mut segments_iter = segments.iter();
if let Some(first_segment) = segments_iter.next() {
tokens.extend(first_segment.write());
}
for segment in segments_iter {
tokens.extend(quote! { write!(f, "&")?; });
tokens.extend(segment.write());
}
tokens
}
}
}
}
#[derive(Debug)]
pub struct FullQuerySegment {
pub ident: Ident,
pub ty: Type,
}
impl QuerySegment {
impl FullQuerySegment {
pub fn parse(&self) -> TokenStream2 {
let ident = &self.ident;
let ty = &self.ty;
@@ -25,3 +75,29 @@ impl QuerySegment {
}
}
}
#[derive(Debug)]
pub struct QueryArgument {
pub ident: Ident,
pub ty: Type,
}
impl QueryArgument {
pub fn parse(&self) -> TokenStream2 {
let ident = &self.ident;
let ty = &self.ty;
quote! {
let #ident = match split_query.get(stringify!(#ident)) {
Some(query_argument) => <#ty as dioxus_router::routable::FromQueryArgument>::from_query_argument(query_argument).unwrap_or_default(),
None => <#ty as Default>::default(),
};
}
}
pub fn write(&self) -> TokenStream2 {
let ident = &self.ident;
quote! {
write!(f, "{}={}", stringify!(#ident), #ident)?;
}
}
}
+1 -1
View File
@@ -282,7 +282,7 @@ impl Route {
}
}
if let Some(query) = &self.query {
if &query.ident == name {
if query.contains_ident(name) {
from_route = true
}
}
+37 -5
View File
@@ -3,7 +3,7 @@ use syn::{Ident, Type};
use proc_macro2::{Span, TokenStream as TokenStream2};
use crate::query::QuerySegment;
use crate::query::{FullQuerySegment, QueryArgument, QuerySegment};
#[derive(Debug, Clone)]
pub enum RouteSegment {
@@ -201,7 +201,7 @@ pub fn parse_route_segments<'a>(
// check if the route has a query string
let parsed_query = match query {
Some(query) => {
if let Some(query) = query.strip_prefix(':') {
if let Some(query) = query.strip_prefix(":..") {
let query_ident = Ident::new(query, Span::call_site());
let field = fields.find(|(name, _)| *name == &query_ident);
@@ -214,12 +214,44 @@ pub fn parse_route_segments<'a>(
));
};
Some(QuerySegment {
Some(QuerySegment::Single(FullQuerySegment {
ident: query_ident,
ty,
})
}))
} else {
None
let mut query_arguments = Vec::new();
for segment in query.split('&') {
if segment.is_empty() {
return Err(syn::Error::new(
route_span,
"Query segments should be non-empty",
));
}
if let Some(query_argument) = segment.strip_prefix(':') {
let query_ident = Ident::new(query_argument, Span::call_site());
let field = fields.find(|(name, _)| *name == &query_ident);
let ty = if let Some((_, ty)) = field {
ty.clone()
} else {
return Err(syn::Error::new(
route_span,
format!("Could not find a field with the name '{}'", query_ident),
));
};
query_arguments.push(QueryArgument {
ident: query_ident,
ty,
});
} else {
return Err(syn::Error::new(
route_span,
"Query segments should be a : followed by the name of the query argument",
));
}
}
Some(QuerySegment::Segments(query_arguments))
}
}
None => None,
+36 -1
View File
@@ -24,7 +24,7 @@ impl<E: Display> Display for RouteParseError<E> {
}
}
/// Something that can be created from a query string.
/// Something that can be created from an entire query string.
///
/// This trait needs to be implemented if you want to turn a query string into a struct.
///
@@ -40,6 +40,41 @@ impl<T: for<'a> From<&'a str>> FromQuery for T {
}
}
/// Something that can be created from a query argument.
///
/// This trait must be implemented for every type used within a query string in the router macro.
pub trait FromQueryArgument: Default {
/// The error that can occur when parsing a query argument.
type Err;
/// Create an instance of `Self` from a query string.
fn from_query_argument(argument: &str) -> Result<Self, Self::Err>;
}
impl<T: Default + FromStr> FromQueryArgument for T
where
<T as FromStr>::Err: Display,
{
type Err = <T as FromStr>::Err;
fn from_query_argument(argument: &str) -> Result<Self, Self::Err> {
let result = match urlencoding::decode(argument) {
Ok(argument) => T::from_str(&argument),
Err(err) => {
tracing::error!("Failed to decode url encoding: {}", err);
T::from_str(argument)
}
};
match result {
Ok(result) => Ok(result),
Err(err) => {
tracing::error!("Failed to parse query argument: {}", err);
Err(err)
}
}
}
}
/// Something that can be created from a route segment.
pub trait FromRouteSegment: Sized {
/// The error that can occur when parsing a route segment.