1#![deny(missing_docs)]
4
5use anyhow::{anyhow, Result};
6use indexmap::IndexMap;
7use std::{borrow::Cow, collections::HashMap, fmt};
8
9use crate::config::HttpTriggerRouteConfig;
10
11#[derive(Clone, Debug)]
13pub struct Router {
14 router: std::sync::Arc<routefinder::Router<RouteHandler>>,
17}
18
19#[derive(Clone, Debug)]
21struct RouteHandler {
22 component_id: String,
24 based_route: Cow<'static, str>,
26 raw_route: Cow<'static, str>,
28 parsed_based_route: ParsedRoute,
31}
32
33#[derive(Debug)] pub struct DuplicateRoute {
36 route: String,
38 pub replaced_id: String,
40 pub effective_id: String,
42}
43
44impl Router {
45 pub fn build<'a>(
50 base: &str,
51 component_routes: impl IntoIterator<Item = (&'a str, &'a HttpTriggerRouteConfig)>,
52 mut duplicate_routes: Option<&mut Vec<DuplicateRoute>>,
53 ) -> Result<Self> {
54 struct RoutingEntry<'a> {
56 based_route: String,
57 raw_route: &'a str,
58 component_id: &'a str,
59 }
60
61 let mut routes = IndexMap::new();
62
63 let routes_iter = component_routes
65 .into_iter()
66 .filter_map(|(component_id, route)| {
67 match route {
68 HttpTriggerRouteConfig::Route(raw_route) => {
69 let based_route = sanitize_with_base(base, raw_route);
70 Some(Ok(RoutingEntry { based_route, raw_route, component_id }))
71 }
72 HttpTriggerRouteConfig::Private(endpoint) => if endpoint.private {
73 None
74 } else {
75 Some(Err(anyhow!("route must be a string pattern or '{{ private = true }}': component '{component_id}' has {{ private = false }}")))
76 }
77 }
78 });
79
80 for re in routes_iter {
82 let re = re?;
83 if let Some(replaced) = routes.insert(re.raw_route, re) {
84 if let Some(duplicate_routes) = &mut duplicate_routes {
85 let effective_id = routes
86 .get(replaced.raw_route)
87 .unwrap() .component_id
89 .to_owned();
90 duplicate_routes.push(DuplicateRoute {
91 route: replaced.based_route,
92 replaced_id: replaced.component_id.to_owned(),
93 effective_id,
94 });
95 }
96 }
97 }
98
99 let mut rf = routefinder::Router::new();
102
103 for re in routes.into_values() {
104 let (rfroute, parsed) = Self::parse_route(&re.based_route).map_err(|e| {
105 anyhow!(
106 "Error parsing route {} associated with component {}: {e}",
107 re.based_route,
108 re.component_id
109 )
110 })?;
111
112 let handler = RouteHandler {
113 component_id: re.component_id.to_string(),
114 based_route: re.based_route.into(),
115 raw_route: re.raw_route.to_string().into(),
116 parsed_based_route: parsed,
117 };
118
119 rf.add(rfroute, handler).map_err(|e| anyhow!("{e}"))?;
120 }
121
122 let router = Self {
123 router: std::sync::Arc::new(rf),
124 };
125
126 Ok(router)
127 }
128
129 fn parse_route(based_route: &str) -> Result<(routefinder::RouteSpec, ParsedRoute), String> {
130 if let Some(wild_suffixed) = based_route.strip_suffix("/...") {
131 let rs = format!("{wild_suffixed}/*").try_into()?;
132 let parsed = ParsedRoute::trailing_wildcard(wild_suffixed);
133 Ok((rs, parsed))
134 } else if let Some(wild_suffixed) = based_route.strip_suffix("/*") {
135 let rs = based_route.try_into()?;
136 let parsed = ParsedRoute::trailing_wildcard(wild_suffixed);
137 Ok((rs, parsed))
138 } else {
139 let rs = based_route.try_into()?;
140 let parsed = ParsedRoute::exact(based_route);
141 Ok((rs, parsed))
142 }
143 }
144
145 pub fn routes(&self) -> impl Iterator<Item = (&(impl fmt::Display + fmt::Debug), &String)> {
147 self.router
148 .iter()
149 .map(|(_spec, handler)| (&handler.parsed_based_route, &handler.component_id))
150 }
151
152 pub fn contains_reserved_route(&self) -> bool {
155 self.router
156 .iter()
157 .any(|(_spec, handler)| handler.based_route.starts_with(crate::WELL_KNOWN_PREFIX))
158 }
159
160 pub fn route<'path, 'router: 'path>(
167 &'router self,
168 path: &'path str,
169 ) -> Result<RouteMatch<'router, 'path>> {
170 let best_match = self
171 .router
172 .best_match(path)
173 .ok_or_else(|| anyhow!("Cannot match route for path {path}"))?;
174
175 let route_handler = best_match.handler();
176 let captures = best_match.captures();
177
178 Ok(RouteMatch {
179 inner: RouteMatchKind::Real {
180 route_handler,
181 captures,
182 path,
183 },
184 })
185 }
186}
187
188impl DuplicateRoute {
189 pub fn route(&self) -> &str {
191 if self.route.is_empty() {
192 "/"
193 } else {
194 &self.route
195 }
196 }
197}
198
199#[derive(Clone, Debug)]
200enum ParsedRoute {
201 Exact(String),
202 TrailingWildcard(String),
203}
204
205impl ParsedRoute {
206 fn exact(route: impl Into<String>) -> Self {
207 Self::Exact(route.into())
208 }
209
210 fn trailing_wildcard(route: impl Into<String>) -> Self {
211 Self::TrailingWildcard(route.into())
212 }
213}
214
215impl fmt::Display for ParsedRoute {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 match &self {
218 ParsedRoute::Exact(path) => write!(f, "{}", path),
219 ParsedRoute::TrailingWildcard(pattern) => write!(f, "{} (wildcard)", pattern),
220 }
221 }
222}
223
224pub struct RouteMatch<'router, 'path> {
226 inner: RouteMatchKind<'router, 'path>,
227}
228
229impl RouteMatch<'_, '_> {
230 pub fn synthetic(component_id: String, path: String) -> Self {
233 Self {
234 inner: RouteMatchKind::Synthetic {
235 route_handler: RouteHandler {
236 component_id,
237 based_route: "/...".into(),
238 raw_route: "/...".into(),
239 parsed_based_route: ParsedRoute::TrailingWildcard(String::new()),
240 },
241 trailing_wildcard: path,
242 },
243 }
244 }
245
246 pub fn component_id(&self) -> &str {
248 &self.inner.route_handler().component_id
249 }
250
251 pub fn based_route(&self) -> &str {
253 &self.inner.route_handler().based_route
254 }
255
256 pub fn based_route_or_prefix(&self) -> &str {
258 self.inner
259 .route_handler()
260 .based_route
261 .strip_suffix("/...")
262 .unwrap_or(&self.inner.route_handler().based_route)
263 }
264
265 pub fn raw_route(&self) -> &str {
267 &self.inner.route_handler().raw_route
268 }
269
270 pub fn raw_route_or_prefix(&self) -> &str {
272 self.inner
273 .route_handler()
274 .raw_route
275 .strip_suffix("/...")
276 .unwrap_or(&self.inner.route_handler().raw_route)
277 }
278
279 pub fn named_wildcards(&self) -> HashMap<&str, &str> {
281 self.inner.named_wildcards()
282 }
283
284 pub fn trailing_wildcard(&self) -> Cow<'_, str> {
286 self.inner.trailing_wildcard()
287 }
288}
289
290enum RouteMatchKind<'router, 'path> {
294 Synthetic {
296 route_handler: RouteHandler,
298 trailing_wildcard: String,
300 },
301 Real {
303 route_handler: &'router RouteHandler,
305 captures: routefinder::Captures<'router, 'path>,
307 path: &'path str,
309 },
310}
311
312impl RouteMatchKind<'_, '_> {
313 fn route_handler(&self) -> &RouteHandler {
315 match self {
316 RouteMatchKind::Synthetic { route_handler, .. } => route_handler,
317 RouteMatchKind::Real { route_handler, .. } => route_handler,
318 }
319 }
320
321 pub fn named_wildcards(&self) -> HashMap<&str, &str> {
323 let Self::Real { captures, .. } = &self else {
324 return HashMap::new();
325 };
326 captures.iter().collect()
327 }
328
329 pub fn trailing_wildcard(&self) -> Cow<'_, str> {
331 let (captures, path) = match self {
332 Self::Synthetic {
334 trailing_wildcard, ..
335 } => return trailing_wildcard.into(),
336 Self::Real { captures, path, .. } => (captures, path),
337 };
338
339 captures
340 .wildcard()
341 .map(|s|
342 match (s.is_empty(), path.ends_with('/')) {
345 (true, false) => s.into(),
347 (true, true) => "/".into(),
349 (false, false) => format!("/{s}").into(),
351 (false, true) => format!("/{s}/").into(),
353 })
354 .unwrap_or_default()
355 }
356}
357
358fn sanitize_with_base<S: Into<String>>(base: S, path: S) -> String {
360 let path = absolutize(path);
361
362 format!("{}{}", sanitize(base.into()), sanitize(path))
363}
364
365fn absolutize<S: Into<String>>(s: S) -> String {
366 let s = s.into();
367 if s.starts_with('/') {
368 s
369 } else {
370 format!("/{s}")
371 }
372}
373
374fn sanitize<S: Into<String>>(s: S) -> String {
376 let s = s.into();
377 match s.strip_suffix('/') {
381 Some(s) => s.into(),
382 None => s,
383 }
384}
385
386#[cfg(test)]
387mod route_tests {
388 use crate::config::HttpPrivateEndpoint;
389
390 use super::*;
391
392 #[test]
393 fn test_router_exact() -> Result<()> {
394 let r = Router::build(
395 "/",
396 [("foo", &"/foo".into()), ("foobar", &"/foo/bar".into())],
397 None,
398 )?;
399
400 assert_eq!(r.route("/foo")?.component_id(), "foo");
401 assert_eq!(r.route("/foo/bar")?.component_id(), "foobar");
402 Ok(())
403 }
404
405 #[test]
406 fn test_router_respects_base() -> Result<()> {
407 let r = Router::build(
408 "/base",
409 [("foo", &"/foo".into()), ("foobar", &"/foo/bar".into())],
410 None,
411 )?;
412
413 assert_eq!(r.route("/base/foo")?.component_id(), "foo");
414 assert_eq!(r.route("/base/foo/bar")?.component_id(), "foobar");
415 Ok(())
416 }
417
418 #[test]
419 fn test_router_wildcard() -> Result<()> {
420 let r = Router::build("/", [("all", &"/...".into())], None)?;
421
422 assert_eq!(r.route("/foo/bar")?.component_id(), "all");
423 assert_eq!(r.route("/abc/")?.component_id(), "all");
424 assert_eq!(r.route("/")?.component_id(), "all");
425 assert_eq!(
426 r.route("/this/should/be/captured?abc=def")?.component_id(),
427 "all"
428 );
429 Ok(())
430 }
431
432 #[test]
433 fn wildcard_routes_use_custom_display() {
434 let routes = Router::build("/", vec![("comp", &"/whee/...".into())], None).unwrap();
435
436 let (route, component_id) = routes.routes().next().unwrap();
437
438 assert_eq!("comp", component_id);
439 assert_eq!("/whee (wildcard)", format!("{route}"));
440 }
441
442 #[test]
443 fn test_router_respects_longest_match() -> Result<()> {
444 let r = Router::build(
445 "/",
446 [
447 ("one_wildcard", &"/one/...".into()),
448 ("onetwo_wildcard", &"/one/two/...".into()),
449 ("onetwothree_wildcard", &"/one/two/three/...".into()),
450 ],
451 None,
452 )?;
453
454 assert_eq!(
455 r.route("/one/two/three/four")?.component_id(),
456 "onetwothree_wildcard"
457 );
458
459 let r = Router::build(
461 "/",
462 [
463 ("onetwothree_wildcard", &"/one/two/three/...".into()),
464 ("onetwo_wildcard", &"/one/two/...".into()),
465 ("one_wildcard", &"/one/...".into()),
466 ],
467 None,
468 )?;
469
470 assert_eq!(
471 r.route("/one/two/three/four")?.component_id(),
472 "onetwothree_wildcard"
473 );
474 Ok(())
475 }
476
477 #[test]
478 fn test_router_exact_beats_wildcard() -> Result<()> {
479 let r = Router::build(
480 "/",
481 [("one_exact", &"/one".into()), ("wildcard", &"/...".into())],
482 None,
483 )?;
484
485 assert_eq!(r.route("/one")?.component_id(), "one_exact");
486
487 Ok(())
488 }
489
490 #[test]
491 fn sensible_routes_are_reachable() {
492 let mut duplicates = Vec::new();
493 let routes = Router::build(
494 "/",
495 vec![
496 ("/", &"/".into()),
497 ("/foo", &"/foo".into()),
498 ("/bar", &"/bar".into()),
499 ("/whee/...", &"/whee/...".into()),
500 ],
501 Some(&mut duplicates),
502 )
503 .unwrap();
504
505 assert_eq!(4, routes.routes().count());
506 assert_eq!(0, duplicates.len());
507 }
508
509 #[test]
510 fn order_of_reachable_routes_is_preserved() {
511 let routes = Router::build(
512 "/",
513 vec![
514 ("comp-/", &"/".into()),
515 ("comp-/foo", &"/foo".into()),
516 ("comp-/bar", &"/bar".into()),
517 ("comp-/whee/...", &"/whee/...".into()),
518 ],
519 None,
520 )
521 .unwrap();
522
523 assert_eq!("comp-/", routes.routes().next().unwrap().1);
524 assert_eq!("comp-/foo", routes.routes().nth(1).unwrap().1);
525 assert_eq!("comp-/bar", routes.routes().nth(2).unwrap().1);
526 assert_eq!("comp-/whee/...", routes.routes().nth(3).unwrap().1);
527 }
528
529 #[test]
530 fn duplicate_routes_are_unreachable() {
531 let mut duplicates = Vec::new();
532 let routes = Router::build(
533 "/",
534 vec![
535 ("comp-/", &"/".into()),
536 ("comp-first /foo", &"/foo".into()),
537 ("comp-second /foo", &"/foo".into()),
538 ("comp-/whee/...", &"/whee/...".into()),
539 ],
540 Some(&mut duplicates),
541 )
542 .unwrap();
543
544 assert_eq!(3, routes.routes().count());
545 assert_eq!(1, duplicates.len());
546 }
547
548 #[test]
549 fn duplicate_routes_last_one_wins() {
550 let mut duplicates = Vec::new();
551 let routes = Router::build(
552 "/",
553 vec![
554 ("comp-/", &"/".into()),
555 ("comp-first /foo", &"/foo".into()),
556 ("comp-second /foo", &"/foo".into()),
557 ("comp-/whee/...", &"/whee/...".into()),
558 ],
559 Some(&mut duplicates),
560 )
561 .unwrap();
562
563 assert_eq!("comp-second /foo", routes.routes().nth(1).unwrap().1);
564 assert_eq!("comp-first /foo", duplicates[0].replaced_id);
565 assert_eq!("comp-second /foo", duplicates[0].effective_id);
566 }
567
568 #[test]
569 fn duplicate_routes_reporting_is_faithful() {
570 let mut duplicates = Vec::new();
571 let _ = Router::build(
572 "/",
573 vec![
574 ("comp-first /", &"/".into()),
575 ("comp-second /", &"/".into()),
576 ("comp-first /foo", &"/foo".into()),
577 ("comp-second /foo", &"/foo".into()),
578 ("comp-first /...", &"/...".into()),
579 ("comp-second /...", &"/...".into()),
580 ("comp-first /whee/...", &"/whee/...".into()),
581 ("comp-second /whee/...", &"/whee/...".into()),
582 ],
583 Some(&mut duplicates),
584 )
585 .unwrap();
586
587 assert_eq!("comp-first /", duplicates[0].replaced_id);
588 assert_eq!("/", duplicates[0].route());
589
590 assert_eq!("comp-first /foo", duplicates[1].replaced_id);
591 assert_eq!("/foo", duplicates[1].route());
592
593 assert_eq!("comp-first /...", duplicates[2].replaced_id);
594 assert_eq!("/...", duplicates[2].route());
595
596 assert_eq!("comp-first /whee/...", duplicates[3].replaced_id);
597 assert_eq!("/whee/...", duplicates[3].route());
598 }
599
600 #[test]
601 fn unroutable_routes_are_skipped() {
602 let routes = Router::build(
603 "/",
604 vec![
605 ("comp-/", &"/".into()),
606 ("comp-/foo", &"/foo".into()),
607 (
608 "comp-private",
609 &HttpTriggerRouteConfig::Private(HttpPrivateEndpoint { private: true }),
610 ),
611 ("comp-/whee/...", &"/whee/...".into()),
612 ],
613 None,
614 )
615 .unwrap();
616
617 assert_eq!(3, routes.routes().count());
618 assert!(!routes.routes().any(|(_r, c)| c == "comp-private"));
619 }
620
621 #[test]
622 fn unroutable_routes_have_to_be_unroutable_thats_just_common_sense() {
623 let e = Router::build(
624 "/",
625 vec![
626 ("comp-/", &"/".into()),
627 ("comp-/foo", &"/foo".into()),
628 (
629 "comp-bad component",
630 &HttpTriggerRouteConfig::Private(HttpPrivateEndpoint { private: false }),
631 ),
632 ("comp-/whee/...", &"/whee/...".into()),
633 ],
634 None,
635 )
636 .expect_err("should not have accepted a 'route = true'");
637
638 assert!(e.to_string().contains("comp-bad component"));
639 }
640
641 #[test]
642 fn trailing_wildcard_is_captured() {
643 let routes = Router::build("/", vec![("comp", &"/...".into())], None).unwrap();
644 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
645 assert_eq!("/1/2/3", m.trailing_wildcard());
646
647 let routes = Router::build("/", vec![("comp", &"/1/...".into())], None).unwrap();
648 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
649 assert_eq!("/2/3", m.trailing_wildcard());
650 }
651
652 #[test]
653 fn trailing_wildcard_respects_trailing_slash() {
654 let routes = Router::build("/", vec![("comp", &"/test/...".into())], None).unwrap();
658 let m = routes.route("/test").expect("/test should have matched");
659 assert_eq!("", m.trailing_wildcard());
660 let m = routes.route("/test/").expect("/test/ should have matched");
661 assert_eq!("/", m.trailing_wildcard());
662 let m = routes
663 .route("/test/hello")
664 .expect("/test/hello should have matched");
665 assert_eq!("/hello", m.trailing_wildcard());
666 let m = routes
667 .route("/test/hello/")
668 .expect("/test/hello/ should have matched");
669 assert_eq!("/hello/", m.trailing_wildcard());
670 }
671
672 #[test]
673 fn named_wildcard_is_captured() {
674 let routes = Router::build("/", vec![("comp", &"/1/:two/3".into())], None).unwrap();
675 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
676 assert_eq!("2", m.named_wildcards()["two"]);
677
678 let routes = Router::build("/", vec![("comp", &"/1/:two/...".into())], None).unwrap();
679 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
680 assert_eq!("2", m.named_wildcards()["two"]);
681 }
682
683 #[test]
684 fn reserved_routes_are_reserved() {
685 let routes =
686 Router::build("/", vec![("comp", &"/.well-known/spin/...".into())], None).unwrap();
687 assert!(routes.contains_reserved_route());
688
689 let routes =
690 Router::build("/", vec![("comp", &"/.well-known/spin/fie".into())], None).unwrap();
691 assert!(routes.contains_reserved_route());
692 }
693
694 #[test]
695 fn unreserved_routes_are_unreserved() {
696 let routes = Router::build(
697 "/",
698 vec![("comp", &"/.well-known/spindle/...".into())],
699 None,
700 )
701 .unwrap();
702 assert!(!routes.contains_reserved_route());
703
704 let routes =
705 Router::build("/", vec![("comp", &"/.well-known/spi/...".into())], None).unwrap();
706 assert!(!routes.contains_reserved_route());
707
708 let routes = Router::build("/", vec![("comp", &"/.well-known/spin".into())], None).unwrap();
709 assert!(!routes.contains_reserved_route());
710 }
711}