1#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
2#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
3
4extern crate proc_macro;
5
6use layout::Layout;
7use nest::{
8 Nest,
9 NestId,
10};
11use proc_macro::TokenStream;
12use proc_macro2::TokenStream as TokenStream2;
13use quote::{
14 __private::Span,
15 ToTokens,
16 format_ident,
17 quote,
18};
19use redirect::Redirect;
20use route::{
21 Route,
22 RouteType,
23};
24use segment::RouteSegment;
25use syn::{
26 Ident,
27 Token,
28 Type,
29 parse::ParseStream,
30 parse_macro_input,
31};
32
33use crate::{
34 layout::LayoutId,
35 route_tree::ParseRouteTree,
36};
37
38mod hash;
39mod layout;
40mod nest;
41mod query;
42mod redirect;
43mod route;
44mod route_tree;
45mod segment;
46
47#[doc(alias = "route")]
48#[proc_macro_derive(
49 Routable,
50 attributes(route, nest, end_nest, layout, end_layout, redirect, child)
51)]
52pub fn routable(input: TokenStream) -> TokenStream {
53 let routes_enum = parse_macro_input!(input as syn::ItemEnum);
54
55 let route_enum = match RouteEnum::parse(routes_enum) {
56 Ok(route_enum) => route_enum,
57 Err(err) => return err.to_compile_error().into(),
58 };
59
60 let error_type = route_enum.error_type();
61 let parse_impl = route_enum.parse_impl();
62 let display_impl = route_enum.impl_display();
63 let routable_impl = route_enum.routable_impl();
64
65 (quote! {
66 const _: () = {
67 #error_type
68
69 #display_impl
70
71 #routable_impl
72
73 #parse_impl
74 };
75 })
76 .into()
77}
78
79struct RouteEnum {
80 name: Ident,
81 endpoints: Vec<RouteEndpoint>,
82 nests: Vec<Nest>,
83 layouts: Vec<Layout>,
84 site_map: Vec<SiteMapSegment>,
85}
86
87impl RouteEnum {
88 fn parse(data: syn::ItemEnum) -> syn::Result<Self> {
89 let name = &data.ident;
90
91 let mut site_map = Vec::new();
92 let mut site_map_stack: Vec<Vec<SiteMapSegment>> = Vec::new();
93
94 let mut endpoints = Vec::new();
95
96 let mut layouts: Vec<Layout> = Vec::new();
97 let mut layout_stack = Vec::new();
98
99 let mut nests = Vec::new();
100 let mut nest_stack = Vec::new();
101
102 for variant in &data.variants {
103 let mut excluded = Vec::new();
104 for attr in &variant.attrs {
106 if attr.path().is_ident("nest") {
107 let mut children_routes = Vec::new();
108 {
109 let mut level = 0;
111 'o: for variant in &data.variants {
112 children_routes.push(variant.fields.clone());
113 for attr in &variant.attrs {
114 if attr.path().is_ident("nest") {
115 level += 1;
116 } else if attr.path().is_ident("end_nest") {
117 level -= 1;
118 if level < 0 {
119 break 'o;
120 }
121 }
122 }
123 }
124 }
125
126 let nest_index = nests.len();
127
128 let parser = |input: ParseStream| {
129 Nest::parse(
130 input,
131 children_routes
132 .iter()
133 .filter_map(|f: &syn::Fields| match f {
134 syn::Fields::Named(fields) => Some(fields.clone()),
135 _ => None,
136 })
137 .collect(),
138 nest_index,
139 )
140 };
141 let nest = attr.parse_args_with(parser)?;
142
143 let segments: Vec<_> = nest
145 .segments
146 .iter()
147 .map(|seg| {
148 let segment_type = seg.into();
149 SiteMapSegment {
150 segment_type,
151 children: Vec::new(),
152 }
153 })
154 .collect();
155 if !segments.is_empty() {
156 site_map_stack.push(segments);
157 }
158
159 nests.push(nest);
160 nest_stack.push(NestId(nest_index));
161 } else if attr.path().is_ident("end_nest") {
162 nest_stack.pop();
163 if let Some(segment) = site_map_stack.pop() {
165 let children = site_map_stack
166 .last_mut()
167 .map(|seg| &mut seg.last_mut().unwrap().children)
168 .unwrap_or(&mut site_map);
169
170 let mut iter = segment.into_iter().rev();
172 let mut current = iter.next().unwrap();
173 for mut segment in iter {
174 segment.children.push(current);
175 current = segment;
176 }
177
178 children.push(current);
179 }
180 } else if attr.path().is_ident("layout") {
181 let parser = |input: ParseStream| {
182 let bang: Option<Token![!]> = input.parse().ok();
183 let exclude = bang.is_some();
184 Ok((exclude, Layout::parse(input, nest_stack.clone())?))
185 };
186 let (exclude, layout): (bool, Layout) = attr.parse_args_with(parser)?;
187
188 if exclude {
189 let Some(layout_index) = layouts.iter().position(|l| l.comp == layout.comp)
190 else {
191 return Err(syn::Error::new(
192 Span::call_site(),
193 "Attempted to exclude a layout that does not exist",
194 ));
195 };
196 excluded.push(LayoutId(layout_index));
197 } else {
198 let layout_index = layouts.len();
199 layouts.push(layout);
200 layout_stack.push(LayoutId(layout_index));
201 }
202 } else if attr.path().is_ident("end_layout") {
203 layout_stack.pop();
204 } else if attr.path().is_ident("redirect") {
205 let parser = |input: ParseStream| {
206 Redirect::parse(input, nest_stack.clone(), endpoints.len())
207 };
208 let redirect = attr.parse_args_with(parser)?;
209 endpoints.push(RouteEndpoint::Redirect(redirect));
210 }
211 }
212
213 let active_nests = nest_stack.clone();
214 let mut active_layouts = layout_stack.clone();
215 active_layouts.retain(|&id| !excluded.contains(&id));
216
217 let route = Route::parse(active_nests, active_layouts, variant.clone())?;
218
219 let mut segment = SiteMapSegment::new(&route.segments);
221 if let RouteType::Child(child) = &route.ty {
222 let new_segment = SiteMapSegment {
223 segment_type: SegmentType::Child(child.ty.clone()),
224 children: Vec::new(),
225 };
226 match &mut segment {
227 Some(segment) => {
228 fn set_last_child_to(
229 segment: &mut SiteMapSegment,
230 new_segment: SiteMapSegment,
231 ) {
232 if let Some(last) = segment.children.last_mut() {
233 set_last_child_to(last, new_segment);
234 } else {
235 segment.children = vec![new_segment];
236 }
237 }
238 set_last_child_to(segment, new_segment);
239 }
240 None => {
241 segment = Some(new_segment);
242 }
243 }
244 }
245
246 if let Some(segment) = segment {
247 let parent = site_map_stack.last_mut();
248 let children = match parent {
249 Some(parent) => &mut parent.last_mut().unwrap().children,
250 None => &mut site_map,
251 };
252 children.push(segment);
253 }
254
255 endpoints.push(RouteEndpoint::Route(route));
256 }
257
258 while let Some(segment) = site_map_stack.pop() {
260 let children = site_map_stack
261 .last_mut()
262 .map(|seg| &mut seg.last_mut().unwrap().children)
263 .unwrap_or(&mut site_map);
264
265 let mut iter = segment.into_iter().rev();
267 let mut current = iter.next().unwrap();
268 for mut segment in iter {
269 segment.children.push(current);
270 current = segment;
271 }
272
273 children.push(current);
274 }
275
276 let myself = Self {
277 name: name.clone(),
278 endpoints,
279 nests,
280 layouts,
281 site_map,
282 };
283
284 Ok(myself)
285 }
286
287 fn impl_display(&self) -> TokenStream2 {
288 let mut display_match = Vec::new();
289
290 for route in &self.endpoints {
291 if let RouteEndpoint::Route(route) = route {
292 display_match.push(route.display_match(&self.nests));
293 }
294 }
295
296 let name = &self.name;
297
298 quote! {
299 impl std::fmt::Display for #name {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 #[allow(unused)]
302 match self {
303 #(#display_match)*
304 }
305 Ok(())
306 }
307 }
308 }
309 }
310
311 fn parse_impl(&self) -> TokenStream2 {
312 let tree = ParseRouteTree::new(&self.endpoints, &self.nests);
313 let name = &self.name;
314
315 let error_name = format_ident!("{}MatchError", self.name);
316 let tokens = tree.roots.iter().map(|&id| {
317 let route = tree.get(id).unwrap();
318 route.to_tokens(&self.nests, &tree, self.name.clone(), error_name.clone())
319 });
320
321 quote! {
322 impl<'a> core::convert::TryFrom<&'a str> for #name {
323 type Error = <Self as std::str::FromStr>::Err;
324
325 fn try_from(s: &'a str) -> ::std::result::Result<Self, Self::Error> {
326 s.parse()
327 }
328 }
329
330 impl std::str::FromStr for #name {
331 type Err = freya_router::routable::RouteParseError<#error_name>;
332
333 fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
334 let route = s;
335 let (route, hash) = route.split_once('#').unwrap_or((route, ""));
336 let (route, query) = route.split_once('?').unwrap_or((route, ""));
337 let route = route.strip_suffix('/').unwrap_or(route);
340 let query = freya_router::exports::urlencoding::decode(query).unwrap_or(query.into());
341 let hash = freya_router::exports::urlencoding::decode(hash).unwrap_or(hash.into());
342 let mut segments = route.split('/').map(|s| freya_router::exports::urlencoding::decode(s).unwrap_or(s.into()));
343 if s.starts_with('/') {
345 let _ = segments.next();
346 } else {
347 return Err(freya_router::routable::RouteParseError {
349 attempted_routes: Vec::new(),
350 });
351 }
352 let mut errors = Vec::new();
353
354 #(#tokens)*
355
356 Err(freya_router::routable::RouteParseError {
357 attempted_routes: errors,
358 })
359 }
360 }
361 }
362 }
363
364 fn error_name(&self) -> Ident {
365 Ident::new(&(self.name.to_string() + "MatchError"), Span::call_site())
366 }
367
368 fn error_type(&self) -> TokenStream2 {
369 let match_error_name = self.error_name();
370
371 let mut type_defs = Vec::new();
372 let mut error_variants = Vec::new();
373 let mut display_match = Vec::new();
374
375 for endpoint in &self.endpoints {
376 match endpoint {
377 RouteEndpoint::Route(route) => {
378 let route_name = &route.route_name;
379
380 let error_name = route.error_ident();
381 let route_str = &route.route;
382 let comment = format!(
383 " An error that can occur when trying to parse the route [`{}::{}`] ('{}').",
384 self.name, route_name, route_str
385 );
386
387 error_variants.push(quote! {
388 #[doc = #comment]
389 #route_name(#error_name)
390 });
391 display_match.push(quote! { Self::#route_name(err) => write!(f, "Route '{}' ('{}') did not match:\n{}", stringify!(#route_name), #route_str, err)? });
392 type_defs.push(route.error_type());
393 }
394 RouteEndpoint::Redirect(redirect) => {
395 let error_variant = redirect.error_variant();
396 let error_name = redirect.error_ident();
397 let route_str = &redirect.route;
398 let comment = format!(
399 " An error that can occur when trying to parse the redirect '{}'.",
400 route_str.value()
401 );
402
403 error_variants.push(quote! {
404 #[doc = #comment]
405 #error_variant(#error_name)
406 });
407 display_match.push(quote! { Self::#error_variant(err) => write!(f, "Redirect '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
408 type_defs.push(redirect.error_type());
409 }
410 }
411 }
412
413 for nest in &self.nests {
414 let error_variant = nest.error_variant();
415 let error_name = nest.error_ident();
416 let route_str = &nest.route;
417 let comment = format!(
418 " An error that can occur when trying to parse the nested segment {error_name} ('{route_str}').",
419 );
420
421 error_variants.push(quote! {
422 #[doc = #comment]
423 #error_variant(#error_name)
424 });
425 display_match.push(quote! { Self::#error_variant(err) => write!(f, "Nest '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
426 type_defs.push(nest.error_type());
427 }
428
429 let comment = format!(
430 " An error that can occur when trying to parse the route enum [`{}`].",
431 self.name
432 );
433
434 quote! {
435 #(#type_defs)*
436
437 #[doc = #comment]
438 #[allow(non_camel_case_types)]
439 #[allow(clippy::derive_partial_eq_without_eq)]
440 pub enum #match_error_name {
441 #(#error_variants),*
442 }
443
444 impl std::fmt::Debug for #match_error_name {
445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446 write!(f, "{}({})", stringify!(#match_error_name), self)
447 }
448 }
449
450 impl std::fmt::Display for #match_error_name {
451 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452 match self {
453 #(#display_match),*
454 }
455 Ok(())
456 }
457 }
458 }
459 }
460
461 fn routable_impl(&self) -> TokenStream2 {
462 let name = &self.name;
463 let site_map = &self.site_map;
464
465 let mut matches = Vec::new();
466
467 for route in &self.endpoints {
469 if let RouteEndpoint::Route(route) = route {
470 matches.push(route.routable_match(&self.layouts, &self.nests));
471 }
472 }
473
474 quote! {
475 impl freya_router::routable::Routable for #name where Self: Clone {
476 const SITE_MAP: &'static [freya_router::routable::SiteMapSegment] = &[
477 #(#site_map,)*
478 ];
479
480 fn render(&self, level: usize) -> freya::prelude::Element {
481 let myself = self.clone();
482 match (level, myself) {
483 #(#matches)*
484 _ => unreachable!()
485 }
486 }
487 }
488 }
489 }
490}
491
492enum RouteEndpoint {
493 Route(Route),
494 Redirect(Redirect),
495}
496
497struct SiteMapSegment {
498 pub segment_type: SegmentType,
499 pub children: Vec<SiteMapSegment>,
500}
501
502impl SiteMapSegment {
503 fn new(segments: &[RouteSegment]) -> Option<Self> {
504 let mut current = None;
505 for segment in segments.iter().rev() {
507 let segment_type = segment.into();
508 let mut segment = SiteMapSegment {
509 segment_type,
510 children: Vec::new(),
511 };
512 if let Some(current) = current.take() {
514 segment.children.push(current)
515 }
516 current = Some(segment);
517 }
518 current
519 }
520}
521
522impl ToTokens for SiteMapSegment {
523 fn to_tokens(&self, tokens: &mut TokenStream2) {
524 let segment_type = &self.segment_type;
525 let children = if let SegmentType::Child(ty) = &self.segment_type {
526 quote! { #ty::SITE_MAP }
527 } else {
528 let children = self
529 .children
530 .iter()
531 .map(|child| child.to_token_stream())
532 .collect::<Vec<_>>();
533 quote! {
534 &[
535 #(#children,)*
536 ]
537 }
538 };
539
540 tokens.extend(quote! {
541 freya_router::routable::SiteMapSegment {
542 segment_type: #segment_type,
543 children: #children,
544 }
545 });
546 }
547}
548
549enum SegmentType {
550 Static(String),
551 Dynamic(String),
552 CatchAll(String),
553 Child(Type),
554}
555
556impl ToTokens for SegmentType {
557 fn to_tokens(&self, tokens: &mut TokenStream2) {
558 match self {
559 SegmentType::Static(s) => {
560 tokens.extend(quote! { freya_router::routable::SegmentType::Static(#s) })
561 }
562 SegmentType::Dynamic(s) => {
563 tokens.extend(quote! { freya_router::routable::SegmentType::Dynamic(#s) })
564 }
565 SegmentType::CatchAll(s) => {
566 tokens.extend(quote! { freya_router::routable::SegmentType::CatchAll(#s) })
567 }
568 SegmentType::Child(_) => {
569 tokens.extend(quote! { freya_router::routable::SegmentType::Child })
570 }
571 }
572 }
573}
574
575impl<'a> From<&'a RouteSegment> for SegmentType {
576 fn from(value: &'a RouteSegment) -> Self {
577 match value {
578 RouteSegment::Static(s) => SegmentType::Static(s.to_string()),
579 RouteSegment::Dynamic(s, _) => SegmentType::Dynamic(s.to_string()),
580 RouteSegment::CatchAll(s, _) => SegmentType::CatchAll(s.to_string()),
581 }
582 }
583}