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 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}