Просмотр исходного кода

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

Automatically derive standard query parsing in the router macro
Jonathan Kelley 1 год назад
Родитель
Сommit
8f70509bc3

+ 21 - 7
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 <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 {
         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::<Route>{} }

+ 1 - 1
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")

+ 78 - 2
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<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
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
                 }
             }

+ 37 - 5
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,

+ 36 - 1
packages/router/src/routable.rs

@@ -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.