1#![deny(missing_docs)]
4
5use anyhow::{Result, anyhow};
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(&self) -> impl Iterator<Item = (&impl RouteInfo, &TriggerLookupKey)> {
168 self.router
169 .iter()
170 .map(|(_spec, handler)| (&handler.parsed_based_route, &handler.lookup_key))
171 }
172
173 pub fn contains_reserved_route(&self) -> bool {
176 self.router
177 .iter()
178 .any(|(_spec, handler)| handler.based_route.starts_with(crate::WELL_KNOWN_PREFIX))
179 }
180
181 pub fn route<'path, 'router: 'path>(
188 &'router self,
189 path: &'path str,
190 ) -> Result<RouteMatch<'router, 'path>> {
191 let best_match = self
192 .router
193 .best_match(path)
194 .ok_or_else(|| anyhow!("Cannot match route for path {path}"))?;
195
196 let route_handler = best_match.handler();
197 let captures = best_match.captures();
198
199 Ok(RouteMatch {
200 inner: RouteMatchKind::Real {
201 route_handler,
202 captures,
203 path,
204 },
205 })
206 }
207}
208
209impl DuplicateRoute {
210 pub fn route(&self) -> &str {
212 if self.route.is_empty() {
213 "/"
214 } else {
215 &self.route
216 }
217 }
218}
219
220pub trait RouteInfo: fmt::Display + fmt::Debug {
222 fn path(&self) -> &str;
224 fn is_wildcard(&self) -> bool;
226}
227
228#[derive(Clone, Debug)]
229enum ParsedRoute {
230 Exact(String),
231 TrailingWildcard(String),
232}
233
234impl ParsedRoute {
235 fn exact(route: impl Into<String>) -> Self {
236 Self::Exact(route.into())
237 }
238
239 fn trailing_wildcard(route: impl Into<String>) -> Self {
240 Self::TrailingWildcard(route.into())
241 }
242}
243
244impl RouteInfo for ParsedRoute {
245 fn path(&self) -> &str {
246 let p = match self {
247 ParsedRoute::Exact(path) => path,
248 ParsedRoute::TrailingWildcard(pattern) => pattern,
249 };
250 if p.is_empty() { "/" } else { p }
251 }
252
253 fn is_wildcard(&self) -> bool {
254 matches!(self, ParsedRoute::TrailingWildcard(_))
255 }
256}
257
258impl fmt::Display for ParsedRoute {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 match &self {
261 ParsedRoute::Exact(path) => write!(f, "{path}"),
262 ParsedRoute::TrailingWildcard(pattern) => write!(f, "{pattern} (wildcard)"),
263 }
264 }
265}
266
267pub struct RouteMatch<'router, 'path> {
269 inner: RouteMatchKind<'router, 'path>,
270}
271
272impl RouteMatch<'_, '_> {
273 pub fn synthetic(component_id: String, path: String) -> Self {
276 Self {
277 inner: RouteMatchKind::Synthetic {
278 route_handler: RouteHandler {
279 lookup_key: TriggerLookupKey::Component(component_id),
280 based_route: "/...".into(),
281 raw_route: "/...".into(),
282 parsed_based_route: ParsedRoute::TrailingWildcard(String::new()),
283 },
284 trailing_wildcard: path,
285 },
286 }
287 }
288
289 pub fn lookup_key(&self) -> &TriggerLookupKey {
291 &self.inner.route_handler().lookup_key
292 }
293
294 pub fn based_route(&self) -> &str {
296 &self.inner.route_handler().based_route
297 }
298
299 pub fn based_route_or_prefix(&self) -> &str {
301 self.inner
302 .route_handler()
303 .based_route
304 .strip_suffix("/...")
305 .unwrap_or(&self.inner.route_handler().based_route)
306 }
307
308 pub fn raw_route(&self) -> &str {
310 &self.inner.route_handler().raw_route
311 }
312
313 pub fn raw_route_or_prefix(&self) -> &str {
315 self.inner
316 .route_handler()
317 .raw_route
318 .strip_suffix("/...")
319 .unwrap_or(&self.inner.route_handler().raw_route)
320 }
321
322 pub fn named_wildcards(&self) -> HashMap<&str, &str> {
324 self.inner.named_wildcards()
325 }
326
327 pub fn trailing_wildcard(&self) -> Cow<'_, str> {
329 self.inner.trailing_wildcard()
330 }
331}
332
333enum RouteMatchKind<'router, 'path> {
337 Synthetic {
339 route_handler: RouteHandler,
341 trailing_wildcard: String,
343 },
344 Real {
346 route_handler: &'router RouteHandler,
348 captures: routefinder::Captures<'router, 'path>,
350 path: &'path str,
352 },
353}
354
355impl RouteMatchKind<'_, '_> {
356 fn route_handler(&self) -> &RouteHandler {
358 match self {
359 RouteMatchKind::Synthetic { route_handler, .. } => route_handler,
360 RouteMatchKind::Real { route_handler, .. } => route_handler,
361 }
362 }
363
364 pub fn named_wildcards(&self) -> HashMap<&str, &str> {
366 let Self::Real { captures, .. } = &self else {
367 return HashMap::new();
368 };
369 captures.iter().collect()
370 }
371
372 pub fn trailing_wildcard(&self) -> Cow<'_, str> {
374 let (captures, path) = match self {
375 Self::Synthetic {
377 trailing_wildcard, ..
378 } => return trailing_wildcard.into(),
379 Self::Real { captures, path, .. } => (captures, path),
380 };
381
382 captures
383 .wildcard()
384 .map(|s|
385 match (s.is_empty(), path.ends_with('/')) {
388 (true, false) => s.into(),
390 (true, true) => "/".into(),
392 (false, false) => format!("/{s}").into(),
394 (false, true) => format!("/{s}/").into(),
396 })
397 .unwrap_or_default()
398 }
399}
400
401fn sanitize_with_base<S: Into<String>>(base: S, path: S) -> String {
403 let path = absolutize(path);
404
405 format!("{}{}", sanitize(base.into()), sanitize(path))
406}
407
408fn absolutize<S: Into<String>>(s: S) -> String {
409 let s = s.into();
410 if s.starts_with('/') {
411 s
412 } else {
413 format!("/{s}")
414 }
415}
416
417fn sanitize<S: Into<String>>(s: S) -> String {
419 let s = s.into();
420 match s.strip_suffix('/') {
424 Some(s) => s.into(),
425 None => s,
426 }
427}
428
429#[derive(Clone, Debug, Deserialize, Serialize)]
431#[serde(untagged)]
432pub enum HttpTriggerRouteConfig {
433 Route(String),
435 Private(HttpPrivateEndpoint),
437}
438
439#[derive(Clone, Debug, Default, Deserialize, Serialize)]
441#[serde(deny_unknown_fields)]
442pub struct HttpPrivateEndpoint {
443 pub private: bool,
445}
446
447impl Default for HttpTriggerRouteConfig {
448 fn default() -> Self {
449 Self::Route(Default::default())
450 }
451}
452
453impl<T: Into<String>> From<T> for HttpTriggerRouteConfig {
454 fn from(value: T) -> Self {
455 Self::Route(value.into())
456 }
457}
458
459#[cfg(test)]
460mod route_tests {
461 use super::*;
462
463 fn component_key(value: &str) -> TriggerLookupKey {
464 TriggerLookupKey::Component(value.to_string())
465 }
466
467 fn trigger_key(value: &str) -> TriggerLookupKey {
468 TriggerLookupKey::Trigger(value.to_string())
469 }
470
471 impl TriggerLookupKey {
472 fn component_id(&self) -> &str {
473 match self {
474 TriggerLookupKey::Component(id) => id,
475 TriggerLookupKey::Trigger(_) => {
476 panic!("expected component ref but was trigger ref")
477 }
478 }
479 }
480 }
481
482 fn component_router<'a>(
484 base: &str,
485 components: impl IntoIterator<Item = (&'a str, &'a str)>,
486 duplicate_routes: Option<&mut Vec<DuplicateRoute>>,
487 ) -> anyhow::Result<Router> {
488 let owned_routes = components
489 .into_iter()
490 .map(|(cid, path)| (component_key(cid), HttpTriggerRouteConfig::from(path)))
491 .collect::<Vec<_>>();
492 let routes = owned_routes.iter().map(|(k, v)| (k, v)); Router::build(base, routes, duplicate_routes)
495 }
496
497 impl RouteMatch<'_, '_> {
498 fn component_id(&self) -> &str {
499 self.lookup_key().component_id()
500 }
501 }
502
503 #[test]
504 fn test_router_exact() -> Result<()> {
505 let r = component_router("/", [("foo", "/foo"), ("foobar", "/foo/bar")], None)?;
506
507 assert_eq!(r.route("/foo")?.component_id(), "foo");
508 assert_eq!(r.route("/foo/bar")?.component_id(), "foobar");
509 Ok(())
510 }
511
512 #[test]
513 fn router_returns_trigger_or_component() -> Result<()> {
514 let r = Router::build(
515 "/",
516 [
517 (&component_key("compy"), &"/foo".into()),
518 (&trigger_key("triggy"), &"/foo/bar".into()),
519 ],
520 None,
521 )?;
522
523 assert!(
524 matches!(r.route("/foo")?.lookup_key(), TriggerLookupKey::Component(c) if c == "compy")
525 );
526 assert!(
527 matches!(r.route("/foo/bar")?.lookup_key(), TriggerLookupKey::Trigger(t) if t == "triggy")
528 );
529 Ok(())
530 }
531
532 #[test]
533 fn test_router_respects_base() -> Result<()> {
534 let r = component_router("/base", [("foo", "/foo"), ("foobar", "/foo/bar")], None)?;
535
536 assert_eq!(r.route("/base/foo")?.component_id(), "foo");
537 assert_eq!(r.route("/base/foo/bar")?.component_id(), "foobar");
538 Ok(())
539 }
540
541 #[test]
542 fn test_router_wildcard() -> Result<()> {
543 let r = component_router("/", [("all", "/...")], None)?;
544
545 assert_eq!(r.route("/foo/bar")?.component_id(), "all");
546 assert_eq!(r.route("/abc/")?.component_id(), "all");
547 assert_eq!(r.route("/")?.component_id(), "all");
548 assert_eq!(
549 r.route("/this/should/be/captured?abc=def")?.component_id(),
550 "all"
551 );
552 Ok(())
553 }
554
555 #[test]
556 fn wildcard_routes_use_custom_display() {
557 let routes = component_router("/", vec![("comp", "/whee/...")], None).unwrap();
558
559 let (route, rh) = routes.routes().next().unwrap();
560
561 assert_eq!("comp", rh.component_id());
562 assert_eq!("/whee (wildcard)", format!("{route}"));
563 }
564
565 #[test]
566 fn test_router_respects_longest_match() -> Result<()> {
567 let r = component_router(
568 "/",
569 [
570 ("one_wildcard", "/one/..."),
571 ("onetwo_wildcard", "/one/two/..."),
572 ("onetwothree_wildcard", "/one/two/three/..."),
573 ],
574 None,
575 )?;
576
577 assert_eq!(
578 r.route("/one/two/three/four")?.component_id(),
579 "onetwothree_wildcard"
580 );
581
582 let r = component_router(
584 "/",
585 [
586 ("onetwothree_wildcard", "/one/two/three/..."),
587 ("onetwo_wildcard", "/one/two/..."),
588 ("one_wildcard", "/one/..."),
589 ],
590 None,
591 )?;
592
593 assert_eq!(
594 r.route("/one/two/three/four")?.component_id(),
595 "onetwothree_wildcard"
596 );
597 Ok(())
598 }
599
600 #[test]
601 fn test_router_exact_beats_wildcard() -> Result<()> {
602 let r = component_router("/", [("one_exact", "/one"), ("wildcard", "/...")], None)?;
603
604 assert_eq!(r.route("/one")?.component_id(), "one_exact");
605
606 Ok(())
607 }
608
609 #[test]
610 fn sensible_routes_are_reachable() {
611 let mut duplicates = Vec::new();
612 let routes = component_router(
613 "/",
614 [
615 ("/", "/"),
616 ("/foo", "/foo"),
617 ("/bar", "/bar"),
618 ("/whee/...", "/whee/..."),
619 ],
620 Some(&mut duplicates),
621 )
622 .unwrap();
623
624 assert_eq!(4, routes.routes().count());
625 assert_eq!(0, duplicates.len());
626 }
627
628 #[test]
629 fn order_of_reachable_routes_is_preserved() {
630 let routes = component_router(
631 "/",
632 [
633 ("comp-/", "/"),
634 ("comp-/foo", "/foo"),
635 ("comp-/bar", "/bar"),
636 ("comp-/whee/...", "/whee/..."),
637 ],
638 None,
639 )
640 .unwrap();
641
642 assert_eq!("comp-/", routes.routes().next().unwrap().1.component_id());
643 assert_eq!(
644 "comp-/foo",
645 routes.routes().nth(1).unwrap().1.component_id()
646 );
647 assert_eq!(
648 "comp-/bar",
649 routes.routes().nth(2).unwrap().1.component_id()
650 );
651 assert_eq!(
652 "comp-/whee/...",
653 routes.routes().nth(3).unwrap().1.component_id()
654 );
655 }
656
657 #[test]
658 fn duplicate_routes_are_unreachable() {
659 let mut duplicates = Vec::new();
660 let routes = component_router(
661 "/",
662 [
663 ("comp-/", "/"),
664 ("comp-first /foo", "/foo"),
665 ("comp-second /foo", "/foo"),
666 ("comp-/whee/...", "/whee/..."),
667 ],
668 Some(&mut duplicates),
669 )
670 .unwrap();
671
672 assert_eq!(3, routes.routes().count());
673 assert_eq!(1, duplicates.len());
674 }
675
676 #[test]
677 fn duplicate_routes_last_one_wins() {
678 let mut duplicates = Vec::new();
679 let routes = component_router(
680 "/",
681 [
682 ("comp-/", "/"),
683 ("comp-first /foo", "/foo"),
684 ("comp-second /foo", "/foo"),
685 ("comp-/whee/...", "/whee/..."),
686 ],
687 Some(&mut duplicates),
688 )
689 .unwrap();
690
691 assert_eq!(
692 "comp-second /foo",
693 routes.routes().nth(1).unwrap().1.component_id()
694 );
695 assert_eq!("comp-first /foo", duplicates[0].replaced_id);
696 assert_eq!("comp-second /foo", duplicates[0].effective_id);
697 }
698
699 #[test]
700 fn duplicate_routes_reporting_is_faithful() {
701 let mut duplicates = Vec::new();
702 let _ = component_router(
703 "/",
704 [
705 ("comp-first /", "/"),
706 ("comp-second /", "/"),
707 ("comp-first /foo", "/foo"),
708 ("comp-second /foo", "/foo"),
709 ("comp-first /...", "/..."),
710 ("comp-second /...", "/..."),
711 ("comp-first /whee/...", "/whee/..."),
712 ("comp-second /whee/...", "/whee/..."),
713 ],
714 Some(&mut duplicates),
715 )
716 .unwrap();
717
718 assert_eq!("comp-first /", duplicates[0].replaced_id);
719 assert_eq!("/", duplicates[0].route());
720
721 assert_eq!("comp-first /foo", duplicates[1].replaced_id);
722 assert_eq!("/foo", duplicates[1].route());
723
724 assert_eq!("comp-first /...", duplicates[2].replaced_id);
725 assert_eq!("/...", duplicates[2].route());
726
727 assert_eq!("comp-first /whee/...", duplicates[3].replaced_id);
728 assert_eq!("/whee/...", duplicates[3].route());
729 }
730
731 #[test]
732 fn unroutable_routes_are_skipped() {
733 let routes = Router::build(
734 "/",
735 [
736 (&component_key("comp-/"), &"/".into()),
737 (&component_key("comp-/foo"), &"/foo".into()),
738 (
739 &component_key("comp-private"),
740 &HttpTriggerRouteConfig::Private(HttpPrivateEndpoint { private: true }),
741 ),
742 (&component_key("comp-/whee/..."), &"/whee/...".into()),
743 ],
744 None,
745 )
746 .unwrap();
747
748 assert_eq!(3, routes.routes().count());
749 assert!(
750 !routes
751 .routes()
752 .any(|(_r, tcr)| tcr.component_id() == "comp-private")
753 );
754 }
755
756 #[test]
757 fn unroutable_routes_have_to_be_unroutable_thats_just_common_sense() {
758 let e = Router::build(
759 "/",
760 vec![
761 (&component_key("comp-/"), &"/".into()),
762 (&component_key("comp-/foo"), &"/foo".into()),
763 (
764 &component_key("comp-bad component"),
765 &HttpTriggerRouteConfig::Private(HttpPrivateEndpoint { private: false }),
766 ),
767 (&component_key("comp-/whee/..."), &"/whee/...".into()),
768 ],
769 None,
770 )
771 .expect_err("should not have accepted a 'route = true'");
772
773 assert!(e.to_string().contains("comp-bad component"));
774 }
775
776 #[test]
777 fn trailing_wildcard_is_captured() {
778 let routes = component_router("/", [("comp", "/...")], None).unwrap();
779 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
780 assert_eq!("/1/2/3", m.trailing_wildcard());
781
782 let routes = component_router("/", [("comp", "/1/...")], None).unwrap();
783 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
784 assert_eq!("/2/3", m.trailing_wildcard());
785 }
786
787 #[test]
788 fn trailing_wildcard_respects_trailing_slash() {
789 let routes = component_router("/", [("comp", "/test/...")], None).unwrap();
793 let m = routes.route("/test").expect("/test should have matched");
794 assert_eq!("", m.trailing_wildcard());
795 let m = routes.route("/test/").expect("/test/ should have matched");
796 assert_eq!("/", m.trailing_wildcard());
797 let m = routes
798 .route("/test/hello")
799 .expect("/test/hello should have matched");
800 assert_eq!("/hello", m.trailing_wildcard());
801 let m = routes
802 .route("/test/hello/")
803 .expect("/test/hello/ should have matched");
804 assert_eq!("/hello/", m.trailing_wildcard());
805 }
806
807 #[test]
808 fn named_wildcard_is_captured() {
809 let routes = component_router("/", [("comp", "/1/:two/3")], None).unwrap();
810 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
811 assert_eq!("2", m.named_wildcards()["two"]);
812
813 let routes = component_router("/", [("comp", "/1/:two/...")], None).unwrap();
814 let m = routes.route("/1/2/3").expect("/1/2/3 should have matched");
815 assert_eq!("2", m.named_wildcards()["two"]);
816 }
817
818 #[test]
819 fn reserved_routes_are_reserved() {
820 let routes = component_router("/", [("comp", "/.well-known/spin/...")], None).unwrap();
821 assert!(routes.contains_reserved_route());
822
823 let routes = component_router("/", [("comp", "/.well-known/spin/fie")], None).unwrap();
824 assert!(routes.contains_reserved_route());
825 }
826
827 #[test]
828 fn unreserved_routes_are_unreserved() {
829 let routes = component_router("/", [("comp", "/.well-known/spindle/...")], None).unwrap();
830 assert!(!routes.contains_reserved_route());
831
832 let routes = component_router("/", [("comp", "/.well-known/spi/...")], None).unwrap();
833 assert!(!routes.contains_reserved_route());
834
835 let routes = component_router("/", [("comp", "/.well-known/spin")], None).unwrap();
836 assert!(!routes.contains_reserved_route());
837 }
838}