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 lookup_key: TriggerLookupKey,
26 based_route: Cow<'static, str>,
28 raw_route: Cow<'static, str>,
30 parsed_based_route: ParsedRoute,
33}
34
35#[derive(Clone, Debug, Hash, PartialEq, Eq)]
38pub enum TriggerLookupKey {
39 Component(String),
41 Trigger(String),
43}
44
45impl std::fmt::Display for TriggerLookupKey {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 Self::Component(id) => f.write_str(id),
49 Self::Trigger(id) => f.write_str(id),
50 }
51 }
52}
53
54#[derive(Debug)] pub struct DuplicateRoute {
57 route: String,
59 pub replaced_id: String,
61 pub effective_id: String,
63}
64
65impl Router {
66 pub fn build<'a>(
71 base: &str,
72 trigger_routes: impl IntoIterator<Item = (&'a TriggerLookupKey, &'a HttpTriggerRouteConfig)>,
73 mut duplicate_routes: Option<&mut Vec<DuplicateRoute>>,
74 ) -> Result<Self> {
75 struct RoutingEntry<'a> {
77 based_route: String,
78 raw_route: &'a str,
79 lookup_key: &'a TriggerLookupKey,
80 }
81
82 let mut routes: IndexMap<&str, RoutingEntry> = IndexMap::new();
83
84 let routes_iter = trigger_routes
86 .into_iter()
87 .filter_map(|(lookup_key, route)| {
88 match route {
89 HttpTriggerRouteConfig::Route(raw_route) => {
90 let based_route = sanitize_with_base(base, raw_route);
91 Some(Ok(RoutingEntry { based_route, raw_route, lookup_key }))
92 }
93 HttpTriggerRouteConfig::Private(endpoint) => if endpoint.private {
94 None
95 } else {
96 Some(Err(anyhow!("route must be a string pattern or '{{ private = true }}': '{lookup_key}' has {{ private = false }}")))
97 }
98 }
99 });
100
101 for re in routes_iter {
103 let re = re?;
104 if let Some(replaced) = routes.insert(re.raw_route, re) {
105 if let Some(duplicate_routes) = &mut duplicate_routes {
106 let effective_id = routes
107 .get(replaced.raw_route)
108 .unwrap() .lookup_key
110 .to_string();
111 duplicate_routes.push(DuplicateRoute {
112 route: replaced.based_route,
113 replaced_id: replaced.lookup_key.to_string(),
114 effective_id,
115 });
116 }
117 }
118 }
119
120 let mut rf = routefinder::Router::new();
123
124 for re in routes.into_values() {
125 let (rfroute, parsed) = Self::parse_route(&re.based_route).map_err(|e| {
126 anyhow!(
127 "Error parsing route {} associated with component {}: {e}",
128 re.based_route,
129 re.lookup_key,
130 )
131 })?;
132
133 let handler = RouteHandler {
134 lookup_key: re.lookup_key.clone(),
135 based_route: re.based_route.into(),
136 raw_route: re.raw_route.to_string().into(),
137 parsed_based_route: parsed,
138 };
139
140 rf.add(rfroute, handler).map_err(|e| anyhow!("{e}"))?;
141 }
142
143 let router = Self {
144 router: std::sync::Arc::new(rf),
145 };
146
147 Ok(router)
148 }
149
150 fn parse_route(based_route: &str) -> Result<(routefinder::RouteSpec, ParsedRoute), String> {
151 if let Some(wild_suffixed) = based_route.strip_suffix("/...") {
152 let rs = format!("{wild_suffixed}/*").try_into()?;
153 let parsed = ParsedRoute::trailing_wildcard(wild_suffixed);
154 Ok((rs, parsed))
155 } else if let Some(wild_suffixed) = based_route.strip_suffix("/*") {
156 let rs = based_route.try_into()?;
157 let parsed = ParsedRoute::trailing_wildcard(wild_suffixed);
158 Ok((rs, parsed))
159 } else {
160 let rs = based_route.try_into()?;
161 let parsed = ParsedRoute::exact(based_route);
162 Ok((rs, parsed))
163 }
164 }
165
166 pub fn routes(
168 &self,
169 ) -> impl Iterator<Item = (&(impl fmt::Display + fmt::Debug), &TriggerLookupKey)> {
170 self.router
171 .iter()
172 .map(|(_spec, handler)| (&handler.parsed_based_route, &handler.lookup_key))
173 }
174
175 pub fn contains_reserved_route(&self) -> bool {
178 self.router
179 .iter()
180 .any(|(_spec, handler)| handler.based_route.starts_with(crate::WELL_KNOWN_PREFIX))
181 }
182
183 pub fn route<'path, 'router: 'path>(
190 &'router self,
191 path: &'path str,
192 ) -> Result<RouteMatch<'router, 'path>> {
193 let best_match = self
194 .router
195 .best_match(path)
196 .ok_or_else(|| anyhow!("Cannot match route for path {path}"))?;
197
198 let route_handler = best_match.handler();
199 let captures = best_match.captures();
200
201 Ok(RouteMatch {
202 inner: RouteMatchKind::Real {
203 route_handler,
204 captures,
205 path,
206 },
207 })
208 }
209}
210
211impl DuplicateRoute {
212 pub fn route(&self) -> &str {
214 if self.route.is_empty() {
215 "/"
216 } else {
217 &self.route
218 }
219 }
220}
221
222#[derive(Clone, Debug)]
223enum ParsedRoute {
224 Exact(String),
225 TrailingWildcard(String),
226}
227
228impl ParsedRoute {
229 fn exact(route: impl Into<String>) -> Self {
230 Self::Exact(route.into())
231 }
232
233 fn trailing_wildcard(route: impl Into<String>) -> Self {
234 Self::TrailingWildcard(route.into())
235 }
236}
237
238impl fmt::Display for ParsedRoute {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 match &self {
241 ParsedRoute::Exact(path) => write!(f, "{path}"),
242 ParsedRoute::TrailingWildcard(pattern) => write!(f, "{pattern} (wildcard)"),
243 }
244 }
245}
246
247pub struct RouteMatch<'router, 'path> {
249 inner: RouteMatchKind<'router, 'path>,
250}
251
252impl RouteMatch<'_, '_> {
253 pub fn synthetic(component_id: String, path: String) -> Self {
256 Self {
257 inner: RouteMatchKind::Synthetic {
258 route_handler: RouteHandler {
259 lookup_key: TriggerLookupKey::Component(component_id),
260 based_route: "/...".into(),
261 raw_route: "/...".into(),
262 parsed_based_route: ParsedRoute::TrailingWildcard(String::new()),
263 },
264 trailing_wildcard: path,
265 },
266 }
267 }
268
269 pub fn lookup_key(&self) -> &TriggerLookupKey {
271 &self.inner.route_handler().lookup_key
272 }
273
274 pub fn based_route(&self) -> &str {
276 &self.inner.route_handler().based_route
277 }
278
279 pub fn based_route_or_prefix(&self) -> &str {
281 self.inner
282 .route_handler()
283 .based_route
284 .strip_suffix("/...")
285 .unwrap_or(&self.inner.route_handler().based_route)
286 }
287
288 pub fn raw_route(&self) -> &str {
290 &self.inner.route_handler().raw_route
291 }
292
293 pub fn raw_route_or_prefix(&self) -> &str {
295 self.inner
296 .route_handler()
297 .raw_route
298 .strip_suffix("/...")
299 .unwrap_or(&self.inner.route_handler().raw_route)
300 }
301
302 pub fn named_wildcards(&self) -> HashMap<&str, &str> {
304 self.inner.named_wildcards()
305 }
306
307 pub fn trailing_wildcard(&self) -> Cow<'_, str> {
309 self.inner.trailing_wildcard()
310 }
311}
312
313enum RouteMatchKind<'router, 'path> {
317 Synthetic {
319 route_handler: RouteHandler,
321 trailing_wildcard: String,
323 },
324 Real {
326 route_handler: &'router RouteHandler,
328 captures: routefinder::Captures<'router, 'path>,
330 path: &'path str,
332 },
333}
334
335impl RouteMatchKind<'_, '_> {
336 fn route_handler(&self) -> &RouteHandler {
338 match self {
339 RouteMatchKind::Synthetic { route_handler, .. } => route_handler,
340 RouteMatchKind::Real { route_handler, .. } => route_handler,
341 }
342 }
343
344 pub fn named_wildcards(&self) -> HashMap<&str, &str> {
346 let Self::Real { captures, .. } = &self else {
347 return HashMap::new();
348 };
349 captures.iter().collect()
350 }
351
352 pub fn trailing_wildcard(&self) -> Cow<'_, str> {
354 let (captures, path) = match self {
355 Self::Synthetic {
357 trailing_wildcard, ..
358 } => return trailing_wildcard.into(),
359 Self::Real { captures, path, .. } => (captures, path),
360 };
361
362 captures
363 .wildcard()
364 .map(|s|
365 match (s.is_empty(), path.ends_with('/')) {
368 (true, false) => s.into(),
370 (true, true) => "/".into(),
372 (false, false) => format!("/{s}").into(),
374 (false, true) => format!("/{s}/").into(),
376 })
377 .unwrap_or_default()
378 }
379}
380
381fn sanitize_with_base<S: Into<String>>(base: S, path: S) -> String {
383 let path = absolutize(path);
384
385 format!("{}{}", sanitize(base.into()), sanitize(path))
386}
387
388fn absolutize<S: Into<String>>(s: S) -> String {
389 let s = s.into();
390 if s.starts_with('/') {
391 s
392 } else {
393 format!("/{s}")
394 }
395}
396
397fn sanitize<S: Into<String>>(s: S) -> String {
399 let s = s.into();
400 match s.strip_suffix('/') {
404 Some(s) => s.into(),
405 None => s,
406 }
407}
408
409#[derive(Clone, Debug, Deserialize, Serialize)]
411#[serde(untagged)]
412pub enum HttpTriggerRouteConfig {
413 Route(String),
415 Private(HttpPrivateEndpoint),
417}
418
419#[derive(Clone, Debug, Default, Deserialize, Serialize)]
421#[serde(deny_unknown_fields)]
422pub struct HttpPrivateEndpoint {
423 pub private: bool,
425}
426
427impl Default for HttpTriggerRouteConfig {
428 fn default() -> Self {
429 Self::Route(Default::default())
430 }
431}
432
433impl<T: Into<String>> From<T> for HttpTriggerRouteConfig {
434 fn from(value: T) -> Self {
435 Self::Route(value.into())
436 }
437}
438
439#[cfg(test)]
440mod route_tests {
441 use super::*;
442
443 fn component_key(value: &str) -> TriggerLookupKey {
444 TriggerLookupKey::Component(value.to_string())
445 }
446
447 fn trigger_key(value: &str) -> TriggerLookupKey {
448 TriggerLookupKey::Trigger(value.to_string())
449 }
450
451 impl TriggerLookupKey {
452 fn component_id(&self) -> &str {
453 match self {
454 TriggerLookupKey::Component(id) => id,
455 TriggerLookupKey::Trigger(_) => {
456 panic!("expected component ref but was trigger ref")
457 }
458 }
459 }
460 }
461
462 fn component_router<'a>(
464 base: &str,
465 components: impl IntoIterator<Item = (&'a str, &'a str)>,
466 duplicate_routes: Option<&mut Vec<DuplicateRoute>>,
467 ) -> anyhow::Result<Router> {
468 let owned_routes = components
469 .into_iter()
470 .map(|(cid, path)| (component_key(cid), HttpTriggerRouteConfig::from(path)))
471 .collect::<Vec<_>>();
472 let routes = owned_routes.iter().map(|(k, v)| (k, v)); Router::build(base, routes, duplicate_routes)
475 }
476
477 impl RouteMatch<'_, '_> {
478 fn component_id(&self) -> &str {
479 self.lookup_key().component_id()
480 }
481 }
482
483 #[test]
484 fn test_router_exact() -> Result<()> {
485 let r = component_router("/", [("foo", "/foo"), ("foobar", "/foo/bar")], None)?;
486
487 assert_eq!(r.route("/foo")?.component_id(), "foo");
488 assert_eq!(r.route("/foo/bar")?.component_id(), "foobar");
489 Ok(())
490 }
491
492 #[test]
493 fn router_returns_trigger_or_component() -> Result<()> {
494 let r = Router::build(
495 "/",
496 [
497 (&component_key("compy"), &"/foo".into()),
498 (&trigger_key("triggy"), &"/foo/bar".into()),
499 ],
500 None,
501 )?;
502
503 assert!(
504 matches!(r.route("/foo")?.lookup_key(), TriggerLookupKey::Component(c) if c == "compy")
505 );
506 assert!(
507 matches!(r.route("/foo/bar")?.lookup_key(), TriggerLookupKey::Trigger(t) if t == "triggy")
508 );
509 Ok(())
510 }
511
512 #[test]
513 fn test_router_respects_base() -> Result<()> {
514 let r = component_router("/base", [("foo", "/foo"), ("foobar", "/foo/bar")], None)?;
515
516 assert_eq!(r.route("/base/foo")?.component_id(), "foo");
517 assert_eq!(r.route("/base/foo/bar")?.component_id(), "foobar");
518 Ok(())
519 }
520
521 #[test]
522 fn test_router_wildcard() -> Result<()> {
523 let r = component_router("/", [("all", "/...")], None)?;
524
525 assert_eq!(r.route("/foo/bar")?.component_id(), "all");
526 assert_eq!(r.route("/abc/")?.component_id(), "all");
527 assert_eq!(r.route("/")?.component_id(), "all");
528 assert_eq!(
529 r.route("/this/should/be/captured?abc=def")?.component_id(),
530 "all"
531 );
532 Ok(())
533 }
534
535 #[test]
536 fn wildcard_routes_use_custom_display() {
537 let routes = component_router("/", vec![("comp", "/whee/...")], None).unwrap();
538
539 let (route, rh) = routes.routes().next().unwrap();
540
541 assert_eq!("comp", rh.component_id());
542 assert_eq!("/whee (wildcard)", format!("{route}"));
543 }
544
545 #[test]
546 fn test_router_respects_longest_match() -> Result<()> {
547 let r = component_router(
548 "/",
549 [
550 ("one_wildcard", "/one/..."),
551 ("onetwo_wildcard", "/one/two/..."),
552 ("onetwothree_wildcard", "/one/two/three/..."),
553 ],
554 None,
555 )?;
556
557 assert_eq!(
558 r.route("/one/two/three/four")?.component_id(),
559 "onetwothree_wildcard"
560 );
561
562 let r = component_router(
564 "/",
565 [
566 ("onetwothree_wildcard", "/one/two/three/..."),
567 ("onetwo_wildcard", "/one/two/..."),
568 ("one_wildcard", "/one/..."),
569 ],
570 None,
571 )?;
572
573 assert_eq!(
574 r.route("/one/two/three/four")?.component_id(),
575 "onetwothree_wildcard"
576 );
577 Ok(())
578 }
579
580 #[test]
581 fn test_router_exact_beats_wildcard() -> Result<()> {
582 let r = component_router("/", [("one_exact", "/one"), ("wildcard", "/...")], None)?;
583
584 assert_eq!(r.route("/one")?.component_id(), "one_exact");
585
586 Ok(())
587 }
588
589 #[test]
590 fn sensible_routes_are_reachable() {
591 let mut duplicates = Vec::new();
592 let routes = component_router(
593 "/",
594 [
595 ("/", "/"),
596 ("/foo", "/foo"),
597 ("/bar", "/bar"),
598 ("/whee/...", "/whee/..."),
599 ],
600 Some(&mut duplicates),
601 )
602 .unwrap();
603
604 assert_eq!(4, routes.routes().count());
605 assert_eq!(0, duplicates.len());
606 }
607
608 #[test]
609 fn order_of_reachable_routes_is_preserved() {
610 let routes = component_router(
611 "/",
612 [
613 ("comp-/", "/"),
614 ("comp-/foo", "/foo"),
615 ("comp-/bar", "/bar"),
616 ("comp-/whee/...", "/whee/..."),
617 ],
618 None,
619 )
620 .unwrap();
621
622 assert_eq!("comp-/", routes.routes().next().unwrap().1.component_id());
623 assert_eq!(
624 "comp-/foo",
625 routes.routes().nth(1).unwrap().1.component_id()
626 );
627 assert_eq!(
628 "comp-/bar",
629 routes.routes().nth(2).unwrap().1.component_id()
630 );
631 assert_eq!(
632 "comp-/whee/...",
633 routes.routes().nth(3).unwrap().1.component_id()
634 );
635 }
636
637 #[test]
638 fn duplicate_routes_are_unreachable() {
639 let mut duplicates = Vec::new();
640 let routes = component_router(
641 "/",
642 [
643 ("comp-/", "/"),
644 ("comp-first /foo", "/foo"),
645 ("comp-second /foo", "/foo"),
646 ("comp-/whee/...", "/whee/..."),
647 ],
648 Some(&mut duplicates),
649 )
650 .unwrap();
651
652 assert_eq!(3, routes.routes().count());
653 assert_eq!(1, duplicates.len());
654 }
655
656 #[test]
657 fn duplicate_routes_last_one_wins() {
658 let mut duplicates = Vec::new();
659 let routes = component_router(
660 "/",
661 [
662 ("comp-/", "/"),
663 ("comp-first /foo", "/foo"),
664 ("comp-second /foo", "/foo"),
665 ("comp-/whee/...", "/whee/..."),
666 ],
667 Some(&mut duplicates),
668 )
669 .unwrap();
670
671 assert_eq!(
672 "comp-second /foo",
673 routes.routes().nth(1).unwrap().1.component_id()
674 );
675 assert_eq!("comp-first /foo", duplicates[0].replaced_id);
676 assert_eq!("comp-second /foo", duplicates[0].effective_id);
677 }
678
679 #[test]
680 fn duplicate_routes_reporting_is_faithful() {
681 let mut duplicates = Vec::new();
682 let _ = component_router(
683 "/",
684 [
685 ("comp-first /", "/"),
686 ("comp-second /", "/"),
687 ("comp-first /foo", "/foo"),
688 ("comp-second /foo", "/foo"),
689 ("comp-first /...", "/..."),
690 ("comp-second /...", "/..."),
691 ("comp-first /whee/...", "/whee/..."),
692 ("comp-second /whee/...", "/whee/..."),
693 ],
694 Some(&mut duplicates),
695 )
696 .unwrap();
697
698 assert_eq!("comp-first /", duplicates[0].replaced_id);
699 assert_eq!("/", duplicates[0].route());
700
701 assert_eq!("comp-first /foo", duplicates[1].replaced_id);
702 assert_eq!("/foo", duplicates[1].route());
703
704 assert_eq!("comp-first /...", duplicates[2].replaced_id);
705 assert_eq!("/...", duplicates[2].route());
706
707 assert_eq!("comp-first /whee/...", duplicates[3].replaced_id);
708 assert_eq!("/whee/...", duplicates[3].route());
709 }
710
711 #[test]
712 fn unroutable_routes_are_skipped() {
713 let routes = Router::build(
714 "/",
715 [
716 (&component_key("comp-/"), &"/".into()),
717 (&component_key("comp-/foo"), &"/foo".into()),
718 (
719 &component_key("comp-private"),
720 &HttpTriggerRouteConfig::Private(HttpPrivateEndpoint { private: true }),
721 ),
722 (&component_key("comp-/whee/..."), &"/whee/...".into()),
723 ],
724 None,
725 )
726 .unwrap();
727
728 assert_eq!(3, routes.routes().count());
729 assert!(!routes
730 .routes()
731 .any(|(_r, tcr)| tcr.component_id() == "comp-private"));
732 }
733
734 #[test]
735 fn unroutable_routes_have_to_be_unroutable_thats_just_common_sense() {
736 let e = Router::build(
737 "/",
738 vec![
739 (&component_key("comp-/"), &"/".into()),
740 (&component_key("comp-/foo"), &"/foo".into()),
741 (
742 &component_key("comp-bad component"),
743 &HttpTriggerRouteConfig::Private(HttpPrivateEndpoint { private: false }),
744 ),
745 (&component_key("comp-/whee/..."), &"/whee/...".into()),
746 ],
747 None,
748 )
749 .expect_err("should not have accepted a 'route = true'");
750
751 assert!(e.to_string().contains("comp-bad component"));
752 }
753
754 #[test]
755 fn trailing_wildcard_is_captured() {
756 let routes = component_router("/", [("comp", "/...")], None).unwrap();
757 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
758 assert_eq!("/1/2/3", m.trailing_wildcard());
759
760 let routes = component_router("/", [("comp", "/1/...")], None).unwrap();
761 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
762 assert_eq!("/2/3", m.trailing_wildcard());
763 }
764
765 #[test]
766 fn trailing_wildcard_respects_trailing_slash() {
767 let routes = component_router("/", [("comp", "/test/...")], None).unwrap();
771 let m = routes.route("/test").expect("/test should have matched");
772 assert_eq!("", m.trailing_wildcard());
773 let m = routes.route("/test/").expect("/test/ should have matched");
774 assert_eq!("/", m.trailing_wildcard());
775 let m = routes
776 .route("/test/hello")
777 .expect("/test/hello should have matched");
778 assert_eq!("/hello", m.trailing_wildcard());
779 let m = routes
780 .route("/test/hello/")
781 .expect("/test/hello/ should have matched");
782 assert_eq!("/hello/", m.trailing_wildcard());
783 }
784
785 #[test]
786 fn named_wildcard_is_captured() {
787 let routes = component_router("/", [("comp", "/1/:two/3")], None).unwrap();
788 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
789 assert_eq!("2", m.named_wildcards()["two"]);
790
791 let routes = component_router("/", [("comp", "/1/:two/...")], None).unwrap();
792 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
793 assert_eq!("2", m.named_wildcards()["two"]);
794 }
795
796 #[test]
797 fn reserved_routes_are_reserved() {
798 let routes = component_router("/", [("comp", "/.well-known/spin/...")], None).unwrap();
799 assert!(routes.contains_reserved_route());
800
801 let routes = component_router("/", [("comp", "/.well-known/spin/fie")], None).unwrap();
802 assert!(routes.contains_reserved_route());
803 }
804
805 #[test]
806 fn unreserved_routes_are_unreserved() {
807 let routes = component_router("/", [("comp", "/.well-known/spindle/...")], None).unwrap();
808 assert!(!routes.contains_reserved_route());
809
810 let routes = component_router("/", [("comp", "/.well-known/spi/...")], None).unwrap();
811 assert!(!routes.contains_reserved_route());
812
813 let routes = component_router("/", [("comp", "/.well-known/spin")], None).unwrap();
814 assert!(!routes.contains_reserved_route());
815 }
816}