Skip to main content

ullun/
router.rs

1//! Router implementation using matchit
2
3use crate::error::{Error, Result};
4use crate::handler::DynHandler;
5use crate::openapi::{RouteInfo, RouteKind};
6use crate::request::Params;
7
8/// A registered handler together with the pattern it was registered under
9#[derive(Clone)]
10struct Entry {
11    handler: DynHandler,
12    pattern: std::sync::Arc<str>,
13}
14
15/// HTTP method routing
16#[derive(Default)]
17pub struct Router {
18    get: matchit::Router<Entry>,
19    post: matchit::Router<Entry>,
20    put: matchit::Router<Entry>,
21    delete: matchit::Router<Entry>,
22    patch: matchit::Router<Entry>,
23    registered: Vec<RouteInfo>,
24}
25
26impl Router {
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    pub fn insert(&mut self, method: &str, path: &str, handler: DynHandler) -> Result<()> {
32        self.insert_as(RouteKind::Http, method, path, handler)
33    }
34
35    pub(crate) fn insert_as(
36        &mut self,
37        kind: RouteKind,
38        method: &str,
39        path: &str,
40        handler: DynHandler,
41    ) -> Result<()> {
42        let method = method.to_uppercase();
43        let router = match method.as_str() {
44            "GET" => &mut self.get,
45            "POST" => &mut self.post,
46            "PUT" => &mut self.put,
47            "DELETE" => &mut self.delete,
48            "PATCH" => &mut self.patch,
49            _ => {
50                return Err(Error::bad_request(format!(
51                    "Unsupported method: {}",
52                    method
53                )))
54            }
55        };
56
57        router
58            .insert(
59                path,
60                Entry {
61                    handler,
62                    pattern: path.into(),
63                },
64            )
65            .map_err(|e| Error::internal(format!("Failed to insert route: {}", e)))?;
66
67        self.registered.push(RouteInfo {
68            method,
69            path: path.to_string(),
70            kind,
71        });
72        Ok(())
73    }
74
75    /// Methods with a route for `path`, in a fixed order, including the
76    /// implicit `HEAD` (next to `GET`) and `OPTIONS`. Empty when the path is unknown.
77    pub fn allowed_methods(&self, path: &str) -> Vec<&'static str> {
78        let tables: [(&'static str, &matchit::Router<Entry>); 5] = [
79            ("GET", &self.get),
80            ("POST", &self.post),
81            ("PUT", &self.put),
82            ("DELETE", &self.delete),
83            ("PATCH", &self.patch),
84        ];
85        let mut allowed = Vec::new();
86        for (name, table) in tables {
87            if table.at(path).is_ok() {
88                allowed.push(name);
89                if name == "GET" {
90                    allowed.push("HEAD");
91                }
92            }
93        }
94        if !allowed.is_empty() {
95            allowed.push("OPTIONS");
96        }
97        allowed
98    }
99
100    pub(crate) fn registered(&self) -> &[RouteInfo] {
101        &self.registered
102    }
103
104    pub fn match_route(&self, method: &str, path: &str) -> Result<(DynHandler, Params)> {
105        self.match_route_with_pattern(method, path)
106            .map(|(handler, params, _)| (handler, params))
107    }
108
109    /// Like [`Router::match_route`], also returning the pattern the route was
110    /// registered under (`/users/{id}`), a low-cardinality name for the route
111    pub fn match_route_with_pattern(
112        &self,
113        method: &str,
114        path: &str,
115    ) -> Result<(DynHandler, Params, std::sync::Arc<str>)> {
116        let router = match method.to_uppercase().as_str() {
117            // HEAD is answered by the GET handler, the body is dropped afterwards
118            "GET" | "HEAD" => &self.get,
119            "POST" => &self.post,
120            "PUT" => &self.put,
121            "DELETE" => &self.delete,
122            "PATCH" => &self.patch,
123            _ => return Err(Error::not_found("Route not found")),
124        };
125
126        let matched = router
127            .at(path)
128            .map_err(|_| Error::not_found("Route not found"))?;
129
130        let mut params = Params::new();
131        for (key, value) in matched.params.iter() {
132            params.insert(key.to_string(), value.to_string());
133        }
134
135        Ok((
136            matched.value.handler.clone(),
137            params,
138            matched.value.pattern.clone(),
139        ))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::handler::Handler;
147    use crate::request::Request;
148    use crate::response::Response;
149    use std::sync::Arc;
150
151    struct TestHandler;
152
153    #[async_trait::async_trait]
154    impl Handler for TestHandler {
155        async fn call(&self, _req: Request) -> Result<Response> {
156            Ok(Response::text("test"))
157        }
158    }
159
160    #[test]
161    fn test_router_insert_and_match() {
162        let mut router = Router::new();
163        let handler = Arc::new(TestHandler) as DynHandler;
164
165        router
166            .insert("GET", "/hello/{name}", handler.clone())
167            .unwrap();
168
169        let (_, params) = router.match_route("GET", "/hello/world").unwrap();
170        assert_eq!(params.get("name").unwrap(), "world");
171    }
172
173    #[test]
174    fn allowed_methods_lists_what_the_path_supports() {
175        let mut router = Router::new();
176        let handler = Arc::new(TestHandler) as DynHandler;
177        router
178            .insert("GET", "/users/{id}", handler.clone())
179            .unwrap();
180        router
181            .insert("DELETE", "/users/{id}", handler.clone())
182            .unwrap();
183        router.insert("POST", "/users", handler.clone()).unwrap();
184
185        assert_eq!(
186            router.allowed_methods("/users/7"),
187            ["GET", "HEAD", "DELETE", "OPTIONS"]
188        );
189        assert_eq!(router.allowed_methods("/users"), ["POST", "OPTIONS"]);
190        assert!(router.allowed_methods("/nothing").is_empty());
191        // HEAD resolves like GET
192        assert!(router.match_route("HEAD", "/users/7").is_ok());
193        assert!(router.match_route("HEAD", "/users").is_err());
194    }
195
196    #[test]
197    fn test_router_not_found() {
198        let router = Router::new();
199        assert!(router.match_route("GET", "/not-found").is_err());
200    }
201}