Skip to main content

webx_host/
router.rs

1//! Matchit-based router with zero-alloc route matching.
2//!
3//! Uses `matchit::Router` (the same library as Axum) for efficient
4//! radix-tree based route matching with zero heap allocations per request.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8use webx_core::error::Result;
9use webx_core::http::IHttpContext;
10use webx_core::routing::{HttpMethod, IEndpoint, IRouter};
11
12type RouteValue = (Arc<dyn IEndpoint>, String);
13
14/// Per-method matchit router.
15struct MethodRouter {
16    inner: matchit::Router<RouteValue>,
17}
18
19impl MethodRouter {
20    fn new() -> Self {
21        Self {
22            inner: matchit::Router::new(),
23        }
24    }
25
26    fn insert(&mut self, path: &str, endpoint: Arc<dyn IEndpoint>) {
27        let value = (endpoint, path.to_string());
28        if let Err(e) = self.inner.insert(path, value) {
29            tracing::warn!("[Router] Failed to register route '{}': {}", path, e);
30        }
31    }
32
33    fn at<'a>(&'a self, path: &'a str) -> Option<matchit::Match<'a, 'a, &'a RouteValue>> {
34        self.inner.at(path).ok()
35    }
36}
37
38/// Matchit-based router.
39pub struct Router {
40    get: MethodRouter,
41    post: MethodRouter,
42    put: MethodRouter,
43    delete: MethodRouter,
44    patch: MethodRouter,
45    head: MethodRouter,
46    options: MethodRouter,
47}
48
49impl Router {
50    pub fn new() -> Self {
51        Self {
52            get: MethodRouter::new(),
53            post: MethodRouter::new(),
54            put: MethodRouter::new(),
55            delete: MethodRouter::new(),
56            patch: MethodRouter::new(),
57            head: MethodRouter::new(),
58            options: MethodRouter::new(),
59        }
60    }
61
62    fn method_router_mut(&mut self, m: HttpMethod) -> &mut MethodRouter {
63        match m {
64            HttpMethod::Get => &mut self.get,
65            HttpMethod::Post => &mut self.post,
66            HttpMethod::Put => &mut self.put,
67            HttpMethod::Delete => &mut self.delete,
68            HttpMethod::Patch => &mut self.patch,
69            HttpMethod::Head => &mut self.head,
70            HttpMethod::Options => &mut self.options,
71        }
72    }
73
74    fn method_router(&self, m: HttpMethod) -> &MethodRouter {
75        match m {
76            HttpMethod::Get => &self.get,
77            HttpMethod::Post => &self.post,
78            HttpMethod::Put => &self.put,
79            HttpMethod::Delete => &self.delete,
80            HttpMethod::Patch => &self.patch,
81            HttpMethod::Head => &self.head,
82            HttpMethod::Options => &self.options,
83        }
84    }
85
86    /// Check if a path is registered for any HTTP method.
87    pub fn path_exists(&self, path: &str) -> bool {
88        self.get.at(path).is_some()
89            || self.post.at(path).is_some()
90            || self.put.at(path).is_some()
91            || self.delete.at(path).is_some()
92            || self.patch.at(path).is_some()
93            || self.head.at(path).is_some()
94            || self.options.at(path).is_some()
95    }
96
97    /// The methods registered for `path`, for `Allow` headers and OPTIONS.
98    ///
99    /// `HEAD` is included when `GET` is registered because the router serves
100    /// HEAD through the GET route (RFC 9110 §9.3.2). The list is empty when the
101    /// path is unknown.
102    pub fn allowed_methods(&self, path: &str) -> Vec<&'static str> {
103        let mut methods = Vec::new();
104        if self.get.at(path).is_some() {
105            methods.push("GET");
106            methods.push("HEAD");
107        }
108        if self.post.at(path).is_some() {
109            methods.push("POST");
110        }
111        if self.put.at(path).is_some() {
112            methods.push("PUT");
113        }
114        if self.patch.at(path).is_some() {
115            methods.push("PATCH");
116        }
117        if self.delete.at(path).is_some() {
118            methods.push("DELETE");
119        }
120        if self.head.at(path).is_some() && !methods.contains(&"HEAD") {
121            methods.push("HEAD");
122        }
123        if self.options.at(path).is_some() {
124            methods.push("OPTIONS");
125        }
126        methods
127    }
128}
129
130#[async_trait::async_trait]
131impl IRouter for Router {
132    fn register(&mut self, method: HttpMethod, path: &str, endpoint: Arc<dyn IEndpoint>) {
133        self.method_router_mut(method).insert(path, endpoint);
134    }
135
136    async fn match_route(
137        &self,
138        ctx: &mut dyn IHttpContext,
139    ) -> Result<Option<(Arc<dyn IEndpoint>, HashMap<String, String>, String)>> {
140        let path = ctx.request().path();
141        let method_str = ctx.request().method();
142        let method = match HttpMethod::from_str(method_str) {
143            Some(m) => m,
144            None => return Ok(None),
145        };
146
147        if let Some(matched) = self.method_router(method).at(path) {
148            return Ok(Some(collect_match(matched)));
149        }
150
151        // HEAD falls back to the GET route for the same path (RFC 9110 §9.3.2:
152        // HEAD is identical to GET without a body). The host suppresses the
153        // body when it writes the response, so the headers — including
154        // `Content-Length`, `ETag` and `Accept-Ranges` — are exactly those of
155        // the equivalent GET.
156        if method == HttpMethod::Head {
157            if let Some(matched) = self.get.at(path) {
158                return Ok(Some(collect_match(matched)));
159            }
160        }
161
162        Ok(None)
163    }
164}
165
166fn collect_match(
167    matched: matchit::Match<'_, '_, &RouteValue>,
168) -> (Arc<dyn IEndpoint>, HashMap<String, String>, String) {
169    let mut params = HashMap::new();
170    for (key, value) in matched.params.iter() {
171        params.insert(key.to_string(), value.to_string());
172    }
173    let (endpoint, pattern) = matched.value;
174    (Arc::clone(endpoint), params, pattern.clone())
175}
176
177impl Default for Router {
178    fn default() -> Self {
179        Self::new()
180    }
181}