1#![warn(missing_docs)]
4#![deny(missing_debug_implementations)]
5
6extern crate self as sycamore_router;
8
9mod router;
10
11use std::str::FromStr;
12
13pub use router::*;
14pub use sycamore_router_macro::Route;
15
16pub trait Route: Sized + Default {
21 fn match_route(&self, segments: &[&str]) -> Self;
26
27 fn match_path(&self, path: &str) -> Self {
29 let segments = path
30 .split('/')
31 .filter(|s| !s.is_empty())
32 .collect::<Vec<_>>();
33 self.match_route(&segments)
34 }
35}
36
37#[derive(Clone, Debug)]
39pub enum Segment {
40 Param(String),
42 DynParam,
44 DynSegments,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum Capture<'a> {
51 DynParam(&'a str),
53 DynSegments(Vec<&'a str>),
55}
56
57impl<'a> Capture<'a> {
58 pub fn as_dyn_param(&self) -> Option<&'a str> {
60 if let Self::DynParam(v) = self {
61 Some(*v)
62 } else {
63 None
64 }
65 }
66
67 pub fn as_dyn_segments(&self) -> Option<&[&'a str]> {
69 if let Self::DynSegments(v) = self {
70 Some(v)
71 } else {
72 None
73 }
74 }
75}
76
77#[derive(Clone, Debug)]
79pub struct RoutePath {
80 segments: Vec<Segment>,
81}
82
83impl RoutePath {
84 pub fn new(segments: Vec<Segment>) -> Self {
86 Self { segments }
87 }
88
89 pub fn match_path<'a>(&self, path: &[&'a str]) -> Option<Vec<Capture<'a>>> {
92 let mut paths = path.to_vec();
93 if let Some(last) = paths.last_mut() {
94 *last = last.split('?').next().unwrap().split('#').next().unwrap();
96 }
97 let mut paths = paths.iter();
98 let mut segments = self.segments.iter();
99 let mut captures = Vec::new();
100
101 while let Some(segment) = segments.next() {
102 match segment {
103 Segment::Param(param) => {
104 if paths.next() != Some(¶m.as_str()) {
105 return None;
106 }
107 }
108 Segment::DynParam => {
109 if let Some(p) = paths.next() {
110 captures.push(Capture::DynParam(p));
111 } else {
112 return None;
113 }
114 }
115 Segment::DynSegments => {
116 if let Some(next_segment) = segments.next() {
117 match next_segment {
119 Segment::Param(next_param) => {
120 let mut capture = Vec::new();
121 for next_path in &mut paths {
122 if next_path == next_param {
123 captures.push(Capture::DynSegments(capture));
124 break;
125 } else {
126 capture.push(next_path);
127 }
128 }
129 }
130 _ => unreachable!("segment following DynSegments cannot be dynamic"),
131 }
132 } else {
133 let mut capture = Vec::new();
135 for next_path in &mut paths {
136 capture.push(*next_path);
137 }
138 captures.push(Capture::DynSegments(capture));
139 }
140 }
141 }
142 }
143
144 if paths.next().is_some() {
145 return None; }
147
148 Some(captures)
149 }
150}
151
152pub trait TryFromParam: Sized {
156 #[must_use]
159 fn try_from_param(param: &str) -> Option<Self>;
160}
161
162impl<T> TryFromParam for T
163where
164 T: FromStr,
165{
166 fn try_from_param(param: &str) -> Option<Self> {
167 param.parse().ok()
168 }
169}
170
171pub trait TryFromSegments: Sized {
173 #[must_use]
176 fn try_from_segments(segments: &[&str]) -> Option<Self>;
177}
178
179impl<T> TryFromSegments for Vec<T>
180where
181 T: TryFromParam,
182{
183 fn try_from_segments(segments: &[&str]) -> Option<Self> {
184 let mut tmp = Vec::with_capacity(segments.len());
185 for segment in segments {
186 let value = T::try_from_param(segment)?;
187 tmp.push(value);
188 }
189 Some(tmp)
190 }
191}
192
193impl<T: Route> TryFromSegments for T {
194 fn try_from_segments(segments: &[&str]) -> Option<Self> {
195 Some(Self::match_route(&Self::default(), segments))
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use Segment::*;
204
205 use super::*;
206
207 #[track_caller]
208 fn check(path: &str, route: RoutePath, expected: Option<Vec<Capture>>) {
209 let path = path
210 .split('/')
211 .filter(|s| !s.is_empty())
212 .collect::<Vec<_>>();
213 assert_eq!(route.match_path(&path), expected);
214 }
215
216 #[test]
217 fn index_path() {
218 check("/", RoutePath::new(Vec::new()), Some(Vec::new()));
219 }
220
221 #[test]
222 fn static_path_single_segment() {
223 check(
224 "/path",
225 RoutePath::new(vec![Param("path".to_string())]),
226 Some(Vec::new()),
227 );
228 }
229
230 #[test]
231 fn static_path_multiple_segments() {
232 check(
233 "/my/static/path",
234 RoutePath::new(vec![
235 Param("my".to_string()),
236 Param("static".to_string()),
237 Param("path".to_string()),
238 ]),
239 Some(Vec::new()),
240 );
241 }
242
243 #[test]
244 fn do_not_match_if_leftover_segments() {
245 check("/path", RoutePath::new(vec![]), None);
246 check(
247 "/my/static/path",
248 RoutePath::new(vec![Param("my".to_string()), Param("static".to_string())]),
249 None,
250 );
251 }
252
253 #[test]
254 fn dyn_param_single_segment() {
255 check(
256 "/abcdef",
257 RoutePath::new(vec![DynParam]),
258 Some(vec![Capture::DynParam("abcdef")]),
259 );
260 }
261
262 #[test]
263 fn dyn_param_with_leading_segment() {
264 check(
265 "/id/abcdef",
266 RoutePath::new(vec![Param("id".to_string()), DynParam]),
267 Some(vec![Capture::DynParam("abcdef")]),
268 );
269 }
270
271 #[test]
272 fn dyn_param_with_leading_and_trailing_segment() {
273 check(
274 "/id/abcdef/account",
275 RoutePath::new(vec![
276 Param("id".to_string()),
277 DynParam,
278 Param("account".to_string()),
279 ]),
280 Some(vec![Capture::DynParam("abcdef")]),
281 );
282 }
283
284 #[test]
285 fn dyn_param_final_missing_root() {
286 check("/", RoutePath::new(vec![DynParam]), None);
287 }
288
289 #[test]
290 fn dyn_param_final_missing() {
291 check(
292 "/id",
293 RoutePath::new(vec![Param("id".to_string()), DynParam]),
294 None,
295 );
296 }
297
298 #[test]
299 fn multiple_dyn_params() {
300 check(
301 "/a/b",
302 RoutePath::new(vec![DynParam, DynParam]),
303 Some(vec![Capture::DynParam("a"), Capture::DynParam("b")]),
304 );
305 }
306
307 #[test]
308 fn dyn_segments_at_root() {
309 check(
310 "/a/b/c",
311 RoutePath::new(vec![DynSegments]),
312 Some(vec![Capture::DynSegments(vec!["a", "b", "c"])]),
313 );
314 }
315
316 #[test]
317 fn dyn_segments_final() {
318 check(
319 "/id/a/b/c",
320 RoutePath::new(vec![Param("id".to_string()), DynSegments]),
321 Some(vec![Capture::DynSegments(vec!["a", "b", "c"])]),
322 );
323 }
324
325 #[test]
326 fn dyn_segments_capture_lazy() {
327 check(
328 "/id/a/b/c/end",
329 RoutePath::new(vec![
330 Param("id".to_string()),
331 DynSegments,
332 Param("end".to_string()),
333 ]),
334 Some(vec![Capture::DynSegments(vec!["a", "b", "c"])]),
335 );
336 }
337
338 #[test]
339 fn dyn_segments_can_capture_zero_segments() {
340 check(
341 "/",
342 RoutePath::new(vec![DynSegments]),
343 Some(vec![Capture::DynSegments(Vec::new())]),
344 );
345 }
346
347 #[test]
348 fn multiple_dyn_segments() {
349 check(
350 "/a/b/c/param/e/f/g",
351 RoutePath::new(vec![DynSegments, Param("param".to_string()), DynSegments]),
352 Some(vec![
353 Capture::DynSegments(vec!["a", "b", "c"]),
354 Capture::DynSegments(vec!["e", "f", "g"]),
355 ]),
356 );
357 }
358
359 #[test]
360 fn ignore_query_params_static() {
361 check(
362 "/a/b?foo=bar",
363 RoutePath::new(vec![Param("a".to_string()), Param("b".to_string())]),
364 Some(Vec::new()),
365 );
366 }
367
368 #[test]
369 fn ingnore_query_params_dyn() {
370 check(
371 "/a/b/c?foo=bar",
372 RoutePath::new(vec![DynSegments]),
373 Some(vec![Capture::DynSegments(vec!["a", "b", "c"])]),
374 );
375 }
376
377 #[test]
378 fn ignore_hash_static() {
379 check(
380 "/a/b#foo",
381 RoutePath::new(vec![Param("a".to_string()), Param("b".to_string())]),
382 Some(Vec::new()),
383 );
384 }
385
386 #[test]
387 fn ingnore_hash_dyn() {
388 check(
389 "/a/b/c#foo",
390 RoutePath::new(vec![DynSegments]),
391 Some(vec![Capture::DynSegments(vec!["a", "b", "c"])]),
392 );
393 }
394
395 mod integration {
396 use crate::*;
397
398 #[test]
399 fn simple_router() {
400 #[derive(Debug, PartialEq, Eq, Route)]
401 enum Routes {
402 #[to("/")]
403 Home,
404 #[to("/about")]
405 About,
406 #[not_found]
407 NotFound,
408 }
409
410 assert_eq!(Routes::match_route(&Routes::default(), &[]), Routes::Home);
411 assert_eq!(
412 Routes::match_route(&Routes::default(), &["about"]),
413 Routes::About
414 );
415 assert_eq!(
416 Routes::match_route(&Routes::default(), &["404"]),
417 Routes::NotFound
418 );
419 assert_eq!(
420 Routes::match_route(&Routes::default(), &["about", "something"]),
421 Routes::NotFound
422 );
423 }
424
425 #[test]
426 fn router_dyn_params() {
427 #[derive(Debug, PartialEq, Eq, Route)]
428 enum Routes {
429 #[to("/account/<id>")]
430 Account { id: u32 },
431 #[not_found]
432 NotFound,
433 }
434
435 assert_eq!(
436 Routes::match_route(&Routes::default(), &["account", "123"]),
437 Routes::Account { id: 123 }
438 );
439 assert_eq!(
440 Routes::match_route(&Routes::default(), &["account", "-1"]),
441 Routes::NotFound
442 );
443 assert_eq!(
444 Routes::match_route(&Routes::default(), &["account", "abc"]),
445 Routes::NotFound
446 );
447 assert_eq!(
448 Routes::match_route(&Routes::default(), &["account"]),
449 Routes::NotFound
450 );
451 }
452
453 #[test]
454 fn router_multiple_dyn_params() {
455 #[derive(Debug, PartialEq, Eq, Route)]
456 enum Routes {
457 #[to("/hello/<name>/<age>")]
458 Hello { name: String, age: u32 },
459 #[not_found]
460 NotFound,
461 }
462
463 assert_eq!(
464 Routes::match_route(&Routes::default(), &["hello", "Bob", "21"]),
465 Routes::Hello {
466 name: "Bob".to_string(),
467 age: 21
468 }
469 );
470 assert_eq!(
471 Routes::match_route(&Routes::default(), &["hello", "21", "Bob"]),
472 Routes::NotFound
473 );
474 assert_eq!(
475 Routes::match_route(&Routes::default(), &["hello"]),
476 Routes::NotFound
477 );
478 }
479
480 #[test]
481 fn router_multiple_dyn_segments() {
482 #[derive(Debug, PartialEq, Eq, Route)]
483 enum Routes {
484 #[to("/path/<path..>")]
485 Path { path: Vec<String> },
486 #[to("/numbers/<numbers..>")]
487 Numbers { numbers: Vec<u32> },
488 #[not_found]
489 NotFound,
490 }
491
492 assert_eq!(
493 Routes::match_route(&Routes::default(), &["path", "a", "b", "c"]),
494 Routes::Path {
495 path: vec!["a".to_string(), "b".to_string(), "c".to_string()]
496 }
497 );
498 assert_eq!(
499 Routes::match_route(&Routes::default(), &["numbers", "1", "2", "3"]),
500 Routes::Numbers {
501 numbers: vec![1, 2, 3]
502 }
503 );
504 assert_eq!(
505 Routes::match_route(&Routes::default(), &["path"]),
506 Routes::Path { path: Vec::new() }
507 );
508 }
509
510 #[test]
511 fn router_multiple_dyn_segments_match_lazy() {
512 #[derive(Debug, PartialEq, Eq, Route)]
513 enum Routes {
514 #[to("/path/<path..>/end")]
515 Path { path: Vec<u32> },
516 #[not_found]
517 NotFound,
518 }
519
520 assert_eq!(
521 Routes::match_route(&Routes::default(), &["path", "1", "2", "end"]),
522 Routes::Path { path: vec![1, 2] }
523 );
524 assert_eq!(
525 Routes::match_route(&Routes::default(), &["path", "end"]),
526 Routes::Path { path: Vec::new() }
527 );
528 assert_eq!(
529 Routes::match_route(&Routes::default(), &["path", "1", "end", "2"]),
530 Routes::NotFound
531 );
532 }
533
534 #[test]
535 fn router_dyn_param_before_dyn_segment() {
536 #[derive(Debug, PartialEq, Eq, Route)]
537 enum Routes {
538 #[to("/<param>/<segments..>")]
539 Path {
540 param: String,
541 segments: Vec<String>,
542 },
543 #[not_found]
544 NotFound,
545 }
546
547 assert_eq!(
548 Routes::match_route(&Routes::default(), &["path", "1", "2"]),
549 Routes::Path {
550 param: "path".to_string(),
551 segments: vec!["1".to_string(), "2".to_string()]
552 }
553 );
554 }
555
556 #[test]
557 fn nested_router() {
558 #[derive(Debug, PartialEq, Eq, Route)]
559 enum Nested {
560 #[to("/nested")]
561 Nested,
562 #[not_found]
563 NotFound,
564 }
565
566 #[derive(Debug, PartialEq, Eq, Route)]
567 enum Routes {
568 #[to("/")]
569 Home,
570 #[to("/route/<_..>")]
571 Route(Nested),
572 #[not_found]
573 NotFound,
574 }
575
576 assert_eq!(Routes::match_route(&Routes::default(), &[]), Routes::Home);
577 assert_eq!(
578 Routes::match_route(&Routes::default(), &["route", "nested"]),
579 Routes::Route(Nested::Nested)
580 );
581 assert_eq!(
582 Routes::match_route(&Routes::default(), &["route", "404"]),
583 Routes::Route(Nested::NotFound)
584 );
585 assert_eq!(
586 Routes::match_route(&Routes::default(), &["404"]),
587 Routes::NotFound
588 );
589 }
590 }
591}