Merge pull request #1680 from ealmloff/derive-query-parsing

Automatically derive standard query parsing in the router macro
This commit is contained in:
Jonathan Kelley 2024-01-05 12:16:37 -08:00 committed by GitHub
commit 8f70509bc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 174 additions and 17 deletions

View file

@ -14,29 +14,35 @@ use dioxus_router::prelude::*;
#[derive(Routable, Clone)] #[derive(Routable, Clone)]
#[rustfmt::skip] #[rustfmt::skip]
enum Route { enum Route {
// segments that start with ?: are query segments // segments that start with ?:.. are query segments that capture the entire query
#[route("/blog?:query_params")] #[route("/blog?:..query_params")]
BlogPost { BlogPost {
// You must include query segments in child variants // You must include query segments in child variants
query_params: BlogQuerySegments, query_params: ManualBlogQuerySegments,
},
// segments that follow the ?:field&:other_field syntax are query segments that follow the standard url query syntax
#[route("/autoblog?:name&:surname")]
AutomaticBlogPost {
name: String,
surname: String,
}, },
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
struct BlogQuerySegments { struct ManualBlogQuerySegments {
name: String, name: String,
surname: String, surname: String,
} }
/// The display impl needs to display the query in a way that can be parsed: /// The display impl needs to display the query in a way that can be parsed:
impl Display for BlogQuerySegments { impl Display for ManualBlogQuerySegments {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "name={}&surname={}", self.name, self.surname) write!(f, "name={}&surname={}", self.name, self.surname)
} }
} }
/// The query segment is anything that implements <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html>. You can implement that trait for a struct if you want to parse multiple query parameters. /// The query segment is anything that implements <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html>. You can implement that trait for a struct if you want to parse multiple query parameters.
impl FromQuery for BlogQuerySegments { impl FromQuery for ManualBlogQuerySegments {
fn from_query(query: &str) -> Self { fn from_query(query: &str) -> Self {
let mut name = None; let mut name = None;
let mut surname = None; let mut surname = None;
@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments {
} }
#[component] #[component]
fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element { fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element {
render! { render! {
div{"This is your blogpost with a query segment:"} div{"This is your blogpost with a query segment:"}
div{format!("{:?}", query_params)} div{format!("{:?}", query_params)}
} }
} }
#[component]
fn AutomaticBlogPost(cx: Scope, name: String, surname: String) -> Element {
render! {
div{"This is your blogpost with a query segment:"}
div{format!("name={}&surname={}", name, surname)}
}
}
#[component] #[component]
fn App(cx: Scope) -> Element { fn App(cx: Scope) -> Element {
render! { Router::<Route>{} } render! { Router::<Route>{} }

View file

@ -36,7 +36,7 @@ mod segment;
/// 1. Static Segments: "/static" /// 1. Static Segments: "/static"
/// 2. Dynamic Segments: "/:dynamic" (where dynamic has a type that is FromStr in all child Variants) /// 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) /// 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: /// Routes are matched:
/// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:..route") /// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:..route")

View file

@ -4,12 +4,62 @@ use syn::{Ident, Type};
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
#[derive(Debug)] #[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 ident: Ident,
pub ty: Type, pub ty: Type,
} }
impl QuerySegment { impl FullQuerySegment {
pub fn parse(&self) -> TokenStream2 { pub fn parse(&self) -> TokenStream2 {
let ident = &self.ident; let ident = &self.ident;
let ty = &self.ty; 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)?;
}
}
}

View file

@ -282,7 +282,7 @@ impl Route {
} }
} }
if let Some(query) = &self.query { if let Some(query) = &self.query {
if &query.ident == name { if query.contains_ident(name) {
from_route = true from_route = true
} }
} }

View file

@ -3,7 +3,7 @@ use syn::{Ident, Type};
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
use crate::query::QuerySegment; use crate::query::{FullQuerySegment, QueryArgument, QuerySegment};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum RouteSegment { pub enum RouteSegment {
@ -201,7 +201,7 @@ pub fn parse_route_segments<'a>(
// check if the route has a query string // check if the route has a query string
let parsed_query = match query { let parsed_query = match query {
Some(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 query_ident = Ident::new(query, Span::call_site());
let field = fields.find(|(name, _)| *name == &query_ident); 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, ident: query_ident,
ty, ty,
}) }))
} else { } 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, None => None,

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. /// 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. /// Something that can be created from a route segment.
pub trait FromRouteSegment: Sized { pub trait FromRouteSegment: Sized {
/// The error that can occur when parsing a route segment. /// The error that can occur when parsing a route segment.