dhttp/services/
router.rs

1use std::collections::HashMap;
2
3use crate::core::{HttpServiceRaw, HttpService, HttpResult, HttpRead};
4use crate::reqres::{HttpRequest, StatusCode};
5
6/// Router is a service that nests other services on chosen routes
7///
8/// For example:
9/// ```
10/// # use dhttp::services::{DefaultService, Router};
11/// # use dhttp::reqres::HttpMethod;
12/// let mut router = Router::new();
13/// router.add("/hello", DefaultService);
14/// ```
15/// This will show the hello message on this route, and fire a 404 on others.
16///
17/// Routes can be of two types:
18/// - exact (does not end with `/`)
19/// - nested (ends with `/`)
20///
21/// Exact route is a hashmap match, nested route matches anything under chosen route.
22///
23/// Nested route example:
24/// ```
25/// # use dhttp::services::{Router, FilesService};
26/// # let mut router = Router::new();
27/// router.add("/files/", FilesService::new("files"));
28/// ```
29/// This code will host all files in the "files" dir, under route `/files/`.
30/// Note that it also matches just `/files`, and that nested routes strip their prefix -
31/// so `/files/something` becomes `/something` in the `route` argument. Original route is still
32/// accessible via `req.route`
33///
34/// Nested routes are implemented with a linear search, consider something more optimized
35/// if you have thousands of them
36///
37/// # Errors
38/// When a route cannot be matched, [`Router`] fires a `StatusCode(404)`
39#[derive(Default)]
40pub struct Router {
41    /// Exact routes
42    exact: HashMap<String, Box<dyn HttpServiceRaw>>,
43    /// Nested routes
44    nested: Vec<(String, Box<dyn HttpServiceRaw>)>,
45}
46
47impl Router {
48    /// Creates an empty `Router`
49    pub fn new() -> Router {
50        Router::default()
51    }
52
53    /// Adds a new route
54    pub fn add(&mut self, route: &str, service: impl HttpServiceRaw) -> &mut Self {
55        let mut route = route.to_string();
56        if route.ends_with("/") {
57            route.pop();
58            self.nested.push((route, Box::new(service)));
59        } else {
60            self.exact.insert(route, Box::new(service));
61        }
62        self
63    }
64
65    fn find<'a, 'b>(&'a self, route: &'b str) -> Option<(&'b str, &'a dyn HttpServiceRaw)> {
66        // remove url params part
67        let mut route_withoutparams = route;
68        if let Some(params_index) = route.find('?') {
69            route_withoutparams = &route[..params_index];
70        }
71        if let Some(service) = self.exact.get(route_withoutparams) {
72            return Some((route, &**service));
73        }
74
75        for (r, service) in &self.nested {
76            // compare prefix...
77            if let Some(route) = route.strip_prefix(r) {
78                // if nothing left, it matched fully...
79                if route.is_empty() {
80                    return Some(("/", &**service));
81                // if leftover starts with /, then it matched a subsegment...
82                } else if route.starts_with("/") {
83                    return Some((route, &**service));
84                }
85                // otherwise, it didn't match anything (think of /files vs /files123)
86            }
87        }
88
89        None
90    }
91}
92
93impl HttpService for Router {
94    async fn request(&self, route: &str, req: &HttpRequest, body: &mut dyn HttpRead) -> HttpResult {
95        match self.find(route) {
96            Some((route, service)) => service.request_raw(route, req, body).await,
97            None => Err(StatusCode::NOT_FOUND.into()),
98        }
99    }
100
101    fn filter(&self, route: &str, req: &HttpRequest) -> HttpResult<()> {
102        match self.find(route) {
103            Some((route, service)) => service.filter_raw(route, req),
104            None => Err(StatusCode::NOT_FOUND.into()),
105        }
106    }
107}