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