Skip to main content

rust_web_server/router/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use std::collections::HashMap;
5
6use crate::request::Request;
7use crate::response::Response;
8use crate::server::ConnectionInfo;
9
10/// Named path-segment values extracted from a matched route pattern.
11///
12/// Given the pattern `/users/:id/posts/:post_id` matched against
13/// `/users/42/posts/7`, `params.get("id")` returns `Some("42")` and
14/// `params.get("post_id")` returns `Some("7")`.
15///
16/// Wildcard segments (`*name`) capture everything after the prefix:
17/// `/files/*path` matched against `/files/a/b/c` gives `path = "a/b/c"`.
18pub struct PathParams {
19    params: HashMap<String, String>,
20}
21
22impl PathParams {
23    fn new() -> Self {
24        PathParams { params: HashMap::new() }
25    }
26
27    /// Returns the value for the named parameter, or `None` if absent.
28    pub fn get(&self, name: &str) -> Option<&str> {
29        self.params.get(name).map(String::as_str)
30    }
31
32    fn insert(&mut self, key: String, value: String) {
33        self.params.insert(key, value);
34    }
35}
36
37enum Segment {
38    Literal(String),
39    Param(String),
40    Wildcard(String),
41}
42
43type HandlerFn =
44    Box<dyn Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static>;
45
46struct Route {
47    method: String,
48    segments: Vec<Segment>,
49    handler: HandlerFn,
50}
51
52/// A path-based HTTP router with named parameter extraction.
53///
54/// Register routes with [`Router::get`], [`Router::post`], etc. Each handler
55/// receives the parsed [`PathParams`] alongside the raw [`Request`] and
56/// [`ConnectionInfo`]. Call [`Router::handle`] from inside a [`Controller`]
57/// or an [`Application::execute`] implementation.
58///
59/// # Example
60///
61/// ```rust,no_run
62/// use rust_web_server::router::{Router, PathParams};
63/// use rust_web_server::request::Request;
64/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
65/// use rust_web_server::range::Range;
66/// use rust_web_server::mime_type::MimeType;
67/// use rust_web_server::server::ConnectionInfo;
68/// use rust_web_server::core::New;
69///
70/// let router = Router::new()
71///     .get("/hello", |_req, _params, _conn| {
72///         let mut r = Response::new();
73///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
74///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
75///         r.content_range_list = vec![Range::get_content_range(b"hello".to_vec(), MimeType::TEXT_PLAIN.to_string())];
76///         r
77///     })
78///     .get("/users/:id", |_req, params, _conn| {
79///         let id = params.get("id").unwrap_or("unknown");
80///         let mut r = Response::new();
81///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
82///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
83///         r.content_range_list = vec![Range::get_content_range(
84///             format!("user {}", id).into_bytes(),
85///             MimeType::TEXT_PLAIN.to_string(),
86///         )];
87///         r
88///     });
89/// ```
90pub struct Router {
91    routes: Vec<Route>,
92}
93
94impl Router {
95    pub fn new() -> Self {
96        Router { routes: Vec::new() }
97    }
98
99    /// Register a `GET` handler for `pattern`.
100    pub fn get<F>(self, pattern: &str, handler: F) -> Self
101    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
102        self.add("GET", pattern, handler)
103    }
104
105    /// Register a `POST` handler for `pattern`.
106    pub fn post<F>(self, pattern: &str, handler: F) -> Self
107    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
108        self.add("POST", pattern, handler)
109    }
110
111    /// Register a `PUT` handler for `pattern`.
112    pub fn put<F>(self, pattern: &str, handler: F) -> Self
113    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
114        self.add("PUT", pattern, handler)
115    }
116
117    /// Register a `PATCH` handler for `pattern`.
118    pub fn patch<F>(self, pattern: &str, handler: F) -> Self
119    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
120        self.add("PATCH", pattern, handler)
121    }
122
123    /// Register a `DELETE` handler for `pattern`.
124    pub fn delete<F>(self, pattern: &str, handler: F) -> Self
125    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
126        self.add("DELETE", pattern, handler)
127    }
128
129    fn add<F>(mut self, method: &str, pattern: &str, handler: F) -> Self
130    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
131        self.routes.push(Route {
132            method: method.to_string(),
133            segments: Self::parse_pattern(pattern),
134            handler: Box::new(handler),
135        });
136        self
137    }
138
139    fn parse_pattern(pattern: &str) -> Vec<Segment> {
140        if pattern == "/" {
141            return vec![];
142        }
143        pattern
144            .split('/')
145            .filter(|s| !s.is_empty())
146            .map(|seg| {
147                if let Some(name) = seg.strip_prefix(':') {
148                    Segment::Param(name.to_string())
149                } else if let Some(name) = seg.strip_prefix('*') {
150                    Segment::Wildcard(name.to_string())
151                } else {
152                    Segment::Literal(seg.to_string())
153                }
154            })
155            .collect()
156    }
157
158    /// Try to match `request` against registered routes in registration order.
159    ///
160    /// Returns `Some(response)` on the first match, `None` if no route matches.
161    /// The query string is stripped before matching; only the path is used.
162    pub fn handle(&self, request: &Request, connection: &ConnectionInfo) -> Option<Response> {
163        let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
164        let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
165
166        for route in &self.routes {
167            if route.method != request.method {
168                continue;
169            }
170            if let Some(params) = Self::try_match(&route.segments, &path_segs) {
171                return Some((route.handler)(request, &params, connection));
172            }
173        }
174        None
175    }
176
177    fn try_match(pattern: &[Segment], path: &[&str]) -> Option<PathParams> {
178        let mut params = PathParams::new();
179        let mut pi = 0;
180
181        for (si, seg) in pattern.iter().enumerate() {
182            match seg {
183                Segment::Literal(lit) => {
184                    if pi >= path.len() || path[pi] != lit.as_str() {
185                        return None;
186                    }
187                    pi += 1;
188                }
189                Segment::Param(name) => {
190                    if pi >= path.len() {
191                        return None;
192                    }
193                    params.insert(name.clone(), path[pi].to_string());
194                    pi += 1;
195                }
196                Segment::Wildcard(name) => {
197                    if si != pattern.len() - 1 {
198                        return None; // wildcard must be the last segment
199                    }
200                    params.insert(name.clone(), path[pi..].join("/"));
201                    pi = path.len();
202                }
203            }
204        }
205
206        if pi == path.len() { Some(params) } else { None }
207    }
208}