micro_web/router/
mod.rs

1pub mod filter;
2
3use crate::{handler_fn, FnTrait, PathParams};
4use crate::handler::RequestHandler;
5
6use crate::handler::handler_decorator::HandlerDecorator;
7use crate::handler::handler_decorator_factory::{
8    HandlerDecoratorFactory, HandlerDecoratorFactoryComposer, HandlerDecoratorFactoryExt, IdentityHandlerDecoratorFactory,
9};
10use filter::{AllFilter, Filter};
11use std::collections::HashMap;
12use tracing::error;
13use crate::extract::FromRequest;
14use crate::responder::Responder;
15
16type RouterFilter = dyn Filter + Send + Sync + 'static;
17type InnerRouter<T> = matchit::Router<T>;
18
19/// Main router structure that handles HTTP request routing
20pub struct Router {
21    inner_router: InnerRouter<Vec<RouterItem>>,
22}
23
24/// A router item containing a filter and handler
25pub struct RouterItem {
26    filter: Box<RouterFilter>,
27    handler: Box<dyn RequestHandler>,
28}
29
30/// Result of matching a route, containing matched items and path parameters
31pub struct RouteResult<'router, 'req> {
32    router_items: &'router [RouterItem],
33    params: PathParams<'router, 'req>,
34}
35
36impl Router {
37    /// Creates a new router builder with default wrappers
38    pub fn builder() -> RouterBuilder<IdentityHandlerDecoratorFactory> {
39        RouterBuilder::new()
40    }
41
42    /// Matches a path against the router's routes
43    ///
44    /// Returns a `RouteResult` containing matched handlers and path parameters
45    ///
46    /// # Arguments
47    /// * `path` - The path to match against
48    pub fn at<'router, 'req>(&'router self, path: &'req str) -> RouteResult<'router, 'req> {
49        self.inner_router
50            .at(path)
51            .map(|matched| RouteResult { router_items: matched.value.as_slice(), params: matched.params.into() })
52            .map_err(|e| error!("match '{}' error: {}", path, e))
53            .unwrap_or(RouteResult::empty())
54    }
55}
56
57impl RouterItem {
58    /// Gets the filter for this router item
59    pub fn filter(&self) -> &RouterFilter {
60        self.filter.as_ref()
61    }
62
63    /// Gets the request handler for this router item
64    pub fn handler(&self) -> &dyn RequestHandler {
65        self.handler.as_ref()
66    }
67}
68
69impl<'router, 'req> RouteResult<'router, 'req> {
70    fn empty() -> Self {
71        Self { router_items: &[], params: PathParams::empty() }
72    }
73
74    /// Returns true if no routes were matched
75    #[inline]
76    pub fn is_empty(&self) -> bool {
77        self.router_items.is_empty()
78    }
79
80    /// Gets the path parameters from the matched route
81    pub fn params(&self) -> &PathParams<'router, 'req> {
82        &self.params
83    }
84
85    /// Gets the matched router items
86    pub fn router_items(&self) -> &'router [RouterItem] {
87        self.router_items
88    }
89}
90
91pub struct RouterBuilder<DF> {
92    data: HashMap<String, Vec<RouterItemBuilder>>,
93    decorator_factory: DF,
94}
95
96impl RouterBuilder<IdentityHandlerDecoratorFactory> {
97    fn new() -> Self {
98        Self { data: HashMap::new(), decorator_factory: IdentityHandlerDecoratorFactory }
99    }
100}
101impl<DF> RouterBuilder<DF> {
102    pub fn route(mut self, route: impl Into<String>, item_builder: RouterItemBuilder) -> Self {
103        let vec = self.data.entry(route.into()).or_default();
104        vec.push(item_builder);
105        self
106    }
107
108    pub fn with_global_decorator<DF2>(self, factory: DF2) -> RouterBuilder<HandlerDecoratorFactoryComposer<DF, DF2>>
109    where
110        DF: HandlerDecoratorFactory,
111        DF2: HandlerDecoratorFactory,
112    {
113        RouterBuilder { data: self.data, decorator_factory: self.decorator_factory.and_then(factory) }
114    }
115
116    /// Builds the router from the accumulated routes and wrappers
117    pub fn build(self) -> Router
118    where
119        DF: HandlerDecoratorFactory,
120    {
121        let mut inner_router = InnerRouter::new();
122
123        for (path, items) in self.data.into_iter() {
124            let router_items = items
125                .into_iter()
126                .map(|item_builder| item_builder.build())
127                .map(|item| {
128                    let decorator = self.decorator_factory.create_decorator();
129                    let handler = decorator.decorate(item.handler);
130                    RouterItem { handler: Box::new(handler), ..item }
131                })
132                .collect::<Vec<_>>();
133
134            inner_router.insert(path, router_items).unwrap();
135        }
136
137        Router { inner_router }
138    }
139}
140
141macro_rules! inner_method_router_filter {
142    ($method:ident, $method_name:ident) => {
143        #[inline]
144        pub fn $method<H: RequestHandler + 'static>(handler: H) -> RouterItemBuilder {
145            let mut filters = filter::all_filter();
146            filters.and(filter::$method_name());
147            RouterItemBuilder { filters, handler: Box::new(handler) }
148        }
149    };
150}
151
152macro_rules! method_router_filter {
153    ($method:ident, $inner_method:ident) => {
154        pub fn $method<F, Args>(f: F) -> RouterItemBuilder
155        where
156            for<'r> F: FnTrait<Args> + 'r,
157            for<'r> Args: FromRequest + 'r,
158            for<'r> F: FnTrait<Args::Output<'r>>,
159            for<'r> <F as FnTrait<Args::Output<'r>>>::Output: Responder,
160        {
161            let handler = handler_fn(f);
162            $inner_method(handler)
163        }
164    };
165}
166
167inner_method_router_filter!(inner_get, get_method);
168inner_method_router_filter!(inner_post, post_method);
169inner_method_router_filter!(inner_put, put_method);
170inner_method_router_filter!(inner_delete, delete_method);
171inner_method_router_filter!(inner_head, head_method);
172inner_method_router_filter!(inner_options, options_method);
173inner_method_router_filter!(inner_connect, connect_method);
174inner_method_router_filter!(inner_patch, patch_method);
175inner_method_router_filter!(inner_trace, trace_method);
176
177method_router_filter!(get, inner_get);
178method_router_filter!(post, inner_post);
179method_router_filter!(put, inner_put);
180method_router_filter!(delete, inner_delete);
181method_router_filter!(head, inner_head);
182method_router_filter!(options, inner_options);
183method_router_filter!(connect, inner_connect);
184method_router_filter!(patch, inner_patch);
185method_router_filter!(trace, inner_trace);
186
187pub struct RouterItemBuilder {
188    filters: AllFilter,
189    handler: Box<dyn RequestHandler>,
190}
191
192impl RouterItemBuilder {
193    pub fn with<F: Filter + Send + Sync + 'static>(mut self, filter: F) -> Self {
194        self.filters.and(filter);
195        self
196    }
197
198    fn build(self) -> RouterItem {
199        // todo: we can remove indirect when filters has only one filter
200        RouterItem { filter: Box::new(self.filters), handler: self.handler }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::filter::header;
207    use super::{Router, get, post};
208    use crate::{PathParams, RequestContext};
209    use http::{HeaderValue, Method, Request};
210    use micro_http::protocol::RequestHeader;
211
212    async fn simple_get_1(_method: &Method) -> String {
213        "hello world".into()
214    }
215
216    async fn simple_get_2(_method: &Method) -> String {
217        "hello world".into()
218    }
219
220    fn router() -> Router {
221        Router::builder()
222            .route("/", get(simple_get_1))
223            .route(
224                "/",
225                post(simple_get_1).with(header(
226                    http::header::CONTENT_TYPE,
227                    HeaderValue::from_str(mime::APPLICATION_WWW_FORM_URLENCODED.as_ref()).unwrap(),
228                )),
229            )
230            .route("/", post(simple_get_1))
231            .route("/2", get(simple_get_2))
232            .build()
233    }
234
235    #[test]
236    fn test_route_get() {
237        let router = router();
238        let route_result = router.at("/");
239
240        assert_eq!(route_result.params.len(), 0);
241
242        let items = route_result.router_items;
243        assert_eq!(items.len(), 3);
244
245        let header: RequestHeader = Request::builder().method(Method::GET).body(()).unwrap().into_parts().0.into();
246        let params = PathParams::empty();
247        let req_ctx = RequestContext::new(&header, &params);
248
249        assert!(items[0].filter.matches(&req_ctx));
250        assert!(!items[1].filter.matches(&req_ctx));
251        assert!(!items[2].filter.matches(&req_ctx));
252    }
253
254    #[test]
255    fn test_route_post() {
256        let router = router();
257        let route_result = router.at("/");
258
259        assert_eq!(route_result.params.len(), 0);
260
261        let items = route_result.router_items;
262        assert_eq!(items.len(), 3);
263
264        let header: RequestHeader = Request::builder().method(Method::POST).body(()).unwrap().into_parts().0.into();
265        let params = PathParams::empty();
266        let req_ctx = RequestContext::new(&header, &params);
267
268        assert!(!items[0].filter.matches(&req_ctx));
269        assert!(!items[1].filter.matches(&req_ctx));
270        assert!(items[2].filter.matches(&req_ctx));
271    }
272
273    #[test]
274    fn test_route_post_with_content_type() {
275        let router = router();
276        let route_result = router.at("/");
277
278        assert_eq!(route_result.params.len(), 0);
279
280        let items = route_result.router_items;
281        assert_eq!(items.len(), 3);
282
283        let header: RequestHeader = Request::builder()
284            .method(Method::POST)
285            .header(http::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
286            .body(())
287            .unwrap()
288            .into_parts()
289            .0
290            .into();
291        let params = PathParams::empty();
292        let req_ctx = RequestContext::new(&header, &params);
293
294        assert!(!items[0].filter.matches(&req_ctx));
295        assert!(items[1].filter.matches(&req_ctx));
296        assert!(items[2].filter.matches(&req_ctx));
297    }
298}