Skip to main content

millipede_core/
router.rs

1//! Label- and method-based request routing.
2
3use std::sync::Arc;
4
5use futures_util::future::BoxFuture;
6
7use crate::errors::CrawlError;
8use crate::handler::{Middleware, RequestHandler};
9use crate::request::{Method, Request};
10
11/// Provides request metadata used to select a route.
12pub trait HasRequest {
13    /// Returns the request associated with this context.
14    fn request(&self) -> &Request;
15}
16
17/// Restricts a route to selected HTTP methods.
18#[derive(Debug, Clone)]
19#[non_exhaustive]
20pub enum MethodFilter {
21    /// Matches every HTTP method.
22    Any,
23    /// Matches only the listed HTTP methods.
24    Only(Vec<Method>),
25}
26
27impl MethodFilter {
28    /// Returns whether this filter accepts `method`.
29    pub fn matches(&self, method: &Method) -> bool {
30        match self {
31            Self::Any => true,
32            Self::Only(methods) => methods.contains(method),
33        }
34    }
35
36    fn shadows(&self, other: &Self) -> bool {
37        match (self, other) {
38            (Self::Any, _) => true,
39            (Self::Only(_), Self::Any) => false,
40            (Self::Only(earlier), Self::Only(later)) => {
41                later.iter().all(|method| earlier.contains(method))
42            }
43        }
44    }
45}
46
47/// Routes request contexts by label and HTTP method.
48pub struct Router<C> {
49    routes: Vec<Route<C>>,
50    default: Option<Arc<dyn RequestHandler<C>>>,
51    middleware: Vec<Arc<dyn Middleware<C>>>,
52}
53
54/// A registered route. A `None` label is a wildcard that matches every request label.
55struct Route<C> {
56    label: Option<String>,
57    methods: MethodFilter,
58    handler: Arc<dyn RequestHandler<C>>,
59}
60
61impl<C: HasRequest + Send + 'static> Router<C> {
62    /// Creates an empty router.
63    // Router intentionally has no `Default` implementation because the inherent `default`
64    // builder method would shadow `Default::default()` and produce a confusing arity error.
65    #[allow(clippy::new_without_default)]
66    pub fn new() -> Self {
67        Self {
68            routes: Vec::new(),
69            default: None,
70            middleware: Vec::new(),
71        }
72    }
73
74    /// Adds a route that matches `label` with any HTTP method.
75    pub fn route<H: RequestHandler<C>>(self, label: impl Into<String>, handler: H) -> Self {
76        self.push_route(Some(label.into()), MethodFilter::Any, handler)
77    }
78
79    /// Adds a route that matches `label` with one HTTP method.
80    pub fn route_method<H: RequestHandler<C>>(
81        self,
82        label: impl Into<String>,
83        method: Method,
84        handler: H,
85    ) -> Self {
86        self.route_methods(label, [method], handler)
87    }
88
89    /// Adds a route that matches `label` with any of the supplied HTTP methods.
90    pub fn route_methods<H, I>(self, label: impl Into<String>, methods: I, handler: H) -> Self
91    where
92        H: RequestHandler<C>,
93        I: IntoIterator<Item = Method>,
94    {
95        self.push_route(
96            Some(label.into()),
97            MethodFilter::Only(methods.into_iter().collect()),
98            handler,
99        )
100    }
101
102    /// Sets the fallback handler used when no registered route matches.
103    pub fn default<H: RequestHandler<C>>(mut self, handler: H) -> Self {
104        self.default = Some(Arc::new(handler));
105        self
106    }
107
108    /// Appends middleware that runs in registration order before a matched handler.
109    pub fn middleware<M: Middleware<C>>(mut self, middleware: M) -> Self {
110        self.middleware.push(Arc::new(middleware));
111        self
112    }
113
114    fn push_route<H: RequestHandler<C>>(
115        mut self,
116        label: Option<String>,
117        methods: MethodFilter,
118        handler: H,
119    ) -> Self {
120        if self.routes.iter().any(|route| {
121            (route.label.is_none() || route.label == label) && route.methods.shadows(&methods)
122        }) {
123            tracing::warn!(label = ?label, "route is unreachable because an earlier route shadows it");
124        }
125        self.routes.push(Route {
126            label,
127            methods,
128            handler: Arc::new(handler),
129        });
130        self
131    }
132}
133
134impl<C: HasRequest + Send + 'static> RequestHandler<C> for Router<C> {
135    fn handle(&self, ctx: C) -> BoxFuture<'static, Result<(), CrawlError>> {
136        let request = ctx.request();
137        let handler = self
138            .routes
139            .iter()
140            .find(|route| {
141                (route.label.is_none() || route.label.as_deref() == request.label.as_deref())
142                    && route.methods.matches(&request.method)
143            })
144            .map(|route| Arc::clone(&route.handler))
145            .or_else(|| self.default.as_ref().map(Arc::clone));
146
147        let Some(handler) = handler else {
148            let error = CrawlError::MissingRoute {
149                label: request.label.clone(),
150                method: request.method.clone(),
151            };
152            return Box::pin(async move { Err(error) });
153        };
154        let middleware = self.middleware.clone();
155
156        Box::pin(async move {
157            let mut ctx = ctx;
158            for middleware in middleware {
159                ctx = middleware.run(ctx).await?;
160            }
161            handler.handle(ctx).await
162        })
163    }
164}