mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-23 04:33:06 +00:00
Merge pull request #1680 from ealmloff/derive-query-parsing
Automatically derive standard query parsing in the router macro
This commit is contained in:
commit
8f70509bc3
6 changed files with 174 additions and 17 deletions
|
@ -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>{} }
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue