diff --git a/examples/query_segments_demo/src/main.rs b/examples/query_segments_demo/src/main.rs index aafcc0f43..3303aeecd 100644 --- a/examples/query_segments_demo/src/main.rs +++ b/examples/query_segments_demo/src/main.rs @@ -14,29 +14,35 @@ use dioxus_router::prelude::*; #[derive(Routable, Clone)] #[rustfmt::skip] enum Route { - // segments that start with ?: are query segments - #[route("/blog?:query_params")] + // segments that start with ?:.. are query segments that capture the entire query + #[route("/blog?:..query_params")] BlogPost { // 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)] -struct BlogQuerySegments { +struct ManualBlogQuerySegments { name: String, surname: String, } /// 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 { write!(f, "name={}&surname={}", self.name, self.surname) } } /// The query segment is anything that implements . 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 { let mut name = None; let mut surname = None; @@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments { } #[component] -fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element { +fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element { render! { div{"This is your blogpost with a query segment:"} 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] fn App(cx: Scope) -> Element { render! { Router::{} } diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index 8844c6a35..024570ae1 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -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") diff --git a/packages/router-macro/src/query.rs b/packages/router-macro/src/query.rs index c97d3be29..40af2b39a 100644 --- a/packages/router-macro/src/query.rs +++ b/packages/router-macro/src/query.rs @@ -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), +} + +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)?; + } + } +} diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index 26665e575..4724a4d7b 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -282,7 +282,7 @@ impl Route { } } if let Some(query) = &self.query { - if &query.ident == name { + if query.contains_ident(name) { from_route = true } } diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index 7e4e40662..21ebe5b60 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -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, diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index b1bb6d915..92277fd71 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -24,7 +24,7 @@ impl Display for RouteParseError { } } -/// 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 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; +} + +impl FromQueryArgument for T +where + ::Err: Display, +{ + type Err = ::Err; + + fn from_query_argument(argument: &str) -> Result { + 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.