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