freya_router_macro/
segment.rs

1use proc_macro2::{
2    Span,
3    TokenStream as TokenStream2,
4};
5use quote::{
6    ToTokens,
7    format_ident,
8    quote,
9};
10use syn::{
11    Ident,
12    Type,
13};
14
15use crate::{
16    hash::HashFragment,
17    query::QuerySegment,
18};
19
20#[derive(Debug, Clone)]
21pub enum RouteSegment {
22    Static(String),
23    Dynamic(Ident, Type),
24    CatchAll(Ident, Type),
25}
26
27impl RouteSegment {
28    pub fn name(&self) -> Option<Ident> {
29        match self {
30            Self::Static(_) => None,
31            Self::Dynamic(ident, _) => Some(ident.clone()),
32            Self::CatchAll(ident, _) => Some(ident.clone()),
33        }
34    }
35
36    pub fn write_segment(&self) -> TokenStream2 {
37        match self {
38            Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; },
39            Self::Dynamic(ident, _) => quote! {
40                {
41                    let as_string = #ident.to_string();
42                    write!(f, "/{}", freya_router::exports::urlencoding::encode(&as_string))?;
43                }
44            },
45            Self::CatchAll(ident, _) => quote! { #ident.display_route_segments(f)?; },
46        }
47    }
48
49    pub fn error_name(&self, idx: usize) -> Ident {
50        match self {
51            Self::Static(_) => static_segment_idx(idx),
52            Self::Dynamic(ident, _) => format_ident!("{}ParseError", ident),
53            Self::CatchAll(ident, _) => format_ident!("{}ParseError", ident),
54        }
55    }
56
57    pub fn missing_error_name(&self) -> Option<Ident> {
58        match self {
59            Self::Dynamic(ident, _) => Some(format_ident!("{}MissingError", ident)),
60            _ => None,
61        }
62    }
63
64    pub fn try_parse(
65        &self,
66        idx: usize,
67        error_enum_name: &Ident,
68        error_enum_variant: &Ident,
69        inner_parse_enum: &Ident,
70        parse_children: TokenStream2,
71    ) -> TokenStream2 {
72        let error_name = self.error_name(idx);
73        match self {
74            Self::Static(segment) => {
75                quote! {
76                    {
77                        let mut segments = segments.clone();
78                        let segment = segments.next();
79                        let segment = segment.as_deref();
80                        if let Some(#segment) = segment {
81                            #parse_children
82                        } else {
83                            errors.push(#error_enum_name::#error_enum_variant(#inner_parse_enum::#error_name(segment.map(|s|s.to_string()).unwrap_or_default())));
84                        }
85                    }
86                }
87            }
88            Self::Dynamic(name, ty) => {
89                let missing_error_name = self.missing_error_name().unwrap();
90                quote! {
91                    {
92                        let mut segments = segments.clone();
93                        let segment = segments.next();
94                        let parsed = if let Some(segment) = segment.as_deref() {
95                            <#ty as freya_router::routable::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_variant(#inner_parse_enum::#error_name(err)))
96                        } else {
97                            Err(#error_enum_name::#error_enum_variant(#inner_parse_enum::#missing_error_name))
98                        };
99                        match parsed {
100                            Ok(#name) => {
101                                #parse_children
102                            }
103                            Err(err) => {
104                                errors.push(err);
105                            }
106                        }
107                    }
108                }
109            }
110            Self::CatchAll(name, ty) => {
111                quote! {
112                    {
113                        let parsed = {
114                            let remaining_segments: Vec<_> = segments.collect();
115                            let mut new_segments: Vec<&str> = Vec::new();
116                            for segment in &remaining_segments {
117                                new_segments.push(&*segment);
118                            }
119                            <#ty as freya_router::routable::FromRouteSegments>::from_route_segments(&new_segments).map_err(|err| #error_enum_name::#error_enum_variant(#inner_parse_enum::#error_name(err)))
120                        };
121                        match parsed {
122                            Ok(#name) => {
123                                #parse_children
124                            }
125                            Err(err) => {
126                                errors.push(err);
127                            }
128                        }
129                    }
130                }
131            }
132        }
133    }
134}
135
136pub fn static_segment_idx(idx: usize) -> Ident {
137    format_ident!("StaticSegment{}ParseError", idx)
138}
139
140pub fn parse_route_segments<'a>(
141    route_span: Span,
142    fields: impl Iterator<Item = (&'a Ident, &'a Type)> + Clone,
143    route: &str,
144) -> syn::Result<(
145    Vec<RouteSegment>,
146    Option<QuerySegment>,
147    Option<HashFragment>,
148)> {
149    let mut route_segments = Vec::new();
150
151    let (route_string, hash) = match route.rsplit_once('#') {
152        Some((route, hash)) => (
153            route,
154            Some(HashFragment::parse_from_str(
155                route_span,
156                fields.clone(),
157                hash,
158            )?),
159        ),
160        None => (route, None),
161    };
162
163    let (route_string, query) = match route_string.rsplit_once('?') {
164        Some((route, query)) => (
165            route,
166            Some(QuerySegment::parse_from_str(
167                route_span,
168                fields.clone(),
169                query,
170            )?),
171        ),
172        None => (route_string, None),
173    };
174    let mut iterator = route_string.split('/');
175
176    // skip the first empty segment
177    let first = iterator.next();
178    if first != Some("") {
179        return Err(syn::Error::new(
180            route_span,
181            format!("Routes should start with /. Error found in the route '{route}'",),
182        ));
183    }
184
185    while let Some(segment) = iterator.next() {
186        if let Some(segment) = segment.strip_prefix(':') {
187            let spread = segment.starts_with("..");
188
189            let ident = if spread {
190                segment[2..].to_string()
191            } else {
192                segment.to_string()
193            };
194
195            let field = fields.clone().find(|(name, _)| **name == ident);
196
197            let ty = if let Some(field) = field {
198                field.1.clone()
199            } else {
200                return Err(syn::Error::new(
201                    route_span,
202                    format!("Could not find a field with the name '{ident}'"),
203                ));
204            };
205            if spread {
206                route_segments.push(RouteSegment::CatchAll(
207                    Ident::new(&ident, Span::call_site()),
208                    ty,
209                ));
210
211                if iterator.next().is_some() {
212                    return Err(syn::Error::new(
213                        route_span,
214                        "Catch-all route segments must be the last segment in a route. The route segments after the catch-all segment will never be matched.",
215                    ));
216                } else {
217                    break;
218                }
219            } else {
220                route_segments.push(RouteSegment::Dynamic(
221                    Ident::new(&ident, Span::call_site()),
222                    ty,
223                ));
224            }
225        } else {
226            route_segments.push(RouteSegment::Static(segment.to_string()));
227        }
228    }
229
230    Ok((route_segments, query, hash))
231}
232
233pub(crate) fn create_error_type(
234    route: &str,
235    error_name: Ident,
236    segments: &[RouteSegment],
237    child_type: Option<&Type>,
238) -> TokenStream2 {
239    let mut error_variants = Vec::new();
240    let mut display_match = Vec::new();
241
242    for (i, segment) in segments.iter().enumerate() {
243        let error_name = segment.error_name(i);
244        match segment {
245            RouteSegment::Static(index) => {
246                let comment = format!(
247                    " An error that can occur when trying to parse the static segment '/{index}'.",
248                );
249                error_variants.push(quote! {
250                    #[doc = #comment]
251                    #error_name(String)
252                });
253                display_match.push(quote! { Self::#error_name(found) => write!(f, "Static segment '{}' did not match instead found '{}'", #index, found)? });
254            }
255            RouteSegment::Dynamic(ident, ty) => {
256                let missing_error = segment.missing_error_name().unwrap();
257                let comment = format!(
258                    " An error that can occur when trying to parse the dynamic segment '/:{ident}'.",
259                );
260                error_variants.push(quote! {
261                    #[doc = #comment]
262                    #error_name(<#ty as freya_router::routable::FromRouteSegment>::Err)
263                });
264                display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
265                error_variants.push(quote! {
266                    #[doc = #comment]
267                    #missing_error
268                });
269                display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
270            }
271            RouteSegment::CatchAll(ident, ty) => {
272                let comment = format!(
273                    " An error that can occur when trying to parse the catch-all segment '/:..{ident}'.",
274                );
275                error_variants.push(quote! {
276                    #[doc = #comment]
277                    #error_name(<#ty as freya_router::routable::FromRouteSegments>::Err)
278                });
279                display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
280            }
281        }
282    }
283
284    let child_type_variant = child_type
285        .map(|child_type| {
286            let comment = format!(
287                " An error that can occur when trying to parse the child route [`{}`].",
288                child_type.to_token_stream()
289            );
290            quote! {
291                #[doc = #comment]
292                ChildRoute(<#child_type as std::str::FromStr>::Err)
293            }
294        })
295        .into_iter();
296
297    let child_type_error = child_type
298        .map(|_| {
299            quote! {
300                Self::ChildRoute(error) => {
301                    write!(f, "{}", error)?
302                }
303            }
304        })
305        .into_iter();
306
307    let comment =
308        format!(" An error that can occur when trying to parse the route variant `{route}`.",);
309
310    quote! {
311        #[doc = #comment]
312        #[allow(non_camel_case_types)]
313        #[allow(clippy::derive_partial_eq_without_eq)]
314        pub enum #error_name {
315            #[doc = " An error that can occur when extra segments are provided after the route."]
316            ExtraSegments(String),
317            #(#child_type_variant,)*
318            #(#error_variants,)*
319        }
320
321        impl std::fmt::Debug for #error_name {
322            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323                write!(f, "{}({})", stringify!(#error_name), self)
324            }
325        }
326
327        impl std::fmt::Display for #error_name {
328            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329                match self {
330                    Self::ExtraSegments(segments) => {
331                        write!(f, "Found additional trailing segments: {}", segments)?
332                    },
333                    #(#child_type_error,)*
334                    #(#display_match,)*
335                }
336                Ok(())
337            }
338        }
339    }
340}