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