Skip to main content

cot/router/
method.rs

1//! Route to handlers based on HTTP methods.
2
3#[cfg(feature = "openapi")]
4pub mod openapi;
5
6use std::fmt::{Debug, Formatter};
7
8use cot_core::handler::{BoxRequestHandler, into_box_request_handler};
9
10use crate::error::MethodNotAllowed;
11use crate::request::Request;
12use crate::response::Response;
13use crate::{Method, RequestHandler};
14
15/// A router that routes requests based on the HTTP method.
16///
17/// This router allows you to register different handlers for different HTTP
18/// methods at the same path. When a request is received, the router will
19/// dispatch it to the handler registered for that HTTP method.
20///
21/// If no handler is registered for a particular method, the router will return
22/// a [405 Method Not Allowed] response. If no handler is registered for
23/// [`HEAD`] requests, the router will return the response generated by the
24/// handler for [`GET`] requests.
25///
26/// [405 Method Not Allowed]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
27/// [`HEAD`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/HEAD
28/// [`GET`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET
29///
30/// # Examples
31///
32/// ```
33/// use cot::html::Html;
34/// use cot::router::method::{MethodRouter, get};
35/// use cot::router::{Route, Router};
36/// use cot::test::TestRequestBuilder;
37///
38/// async fn get_handler() -> Html {
39///     Html::new("GET response")
40/// }
41///
42/// async fn post_handler() -> Html {
43///     Html::new("POST response")
44/// }
45///
46/// # #[tokio::main]
47/// # async fn main() -> cot::Result<()> {
48/// let router = Router::with_urls([Route::with_handler(
49///     "/",
50///     get(get_handler).post(post_handler),
51/// )]);
52///
53/// let request = TestRequestBuilder::get("/").router(router.clone()).build();
54/// assert_eq!(
55///     router
56///         .handle(request)
57///         .await?
58///         .into_body()
59///         .into_bytes()
60///         .await?,
61///     "GET response"
62/// );
63///
64/// let request = TestRequestBuilder::post("/").router(router.clone()).build();
65/// assert_eq!(
66///     router
67///         .handle(request)
68///         .await?
69///         .into_body()
70///         .into_bytes()
71///         .await?,
72///     "POST response"
73/// );
74/// # Ok(())
75/// # }
76/// ```
77#[derive(Debug)]
78#[must_use]
79pub struct MethodRouter {
80    inner: InnerMethodRouter<InnerHandler>,
81}
82
83macro_rules! define_method {
84    ($name:ident => $method:ident) => {
85        #[doc = concat!("Set a handler for the [`",
86                    stringify!($method),
87                    "`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/",
88                    stringify!($method),
89                    ") HTTP method.")]
90        /// # Examples
91        ///
92        /// ```
93        /// use cot::html::Html;
94        /// use cot::router::method::MethodRouter;
95        ///
96        /// async fn test_handler() -> Html {
97        ///     Html::new("test")
98        /// }
99        ///
100        /// # #[tokio::main]
101        /// # async fn main() -> cot::Result<()> {
102        #[doc = concat!(
103            "let method_router = MethodRouter::new().",
104            stringify!($name),
105            "(test_handler);"
106        )]
107        /// #
108        /// # let router = cot::router::Router::with_urls(
109        /// #     [cot::router::Route::with_handler("/", method_router)]
110        /// # );
111        /// #
112        #[doc = concat!(
113            "# let request = cot::test::TestRequestBuilder::with_method(\"/\", cot::Method::",
114            stringify!($method),
115            ")"
116        )]
117        /// #     .router(router.clone())
118        /// #     .build();
119        /// # assert_eq!(
120        /// #     router
121        /// #         .handle(request)
122        /// #         .await?
123        /// #         .into_body()
124        /// #         .into_bytes()
125        /// #         .await?,
126        /// #     "test"
127        /// # );
128        /// # Ok(())
129        /// # }
130        /// ```
131        pub fn $name<HandlerParams, H>(mut self, handler: H) -> Self
132        where
133            HandlerParams: 'static,
134            H: RequestHandler<HandlerParams> + Send + Sync + 'static,
135        {
136            self.inner.$name = Some(InnerHandler::new(handler));
137            self
138        }
139    };
140}
141
142impl Default for MethodRouter {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148impl MethodRouter {
149    /// Create a new [`MethodRouter`].
150    ///
151    /// You might consider using [`get`], [`post`], or one of the other
152    /// functions defined in [`cot::router::method`] which serve as convenient
153    /// constructors for a [`MethodRouter`] with a specific handler.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use cot::html::Html;
159    /// use cot::router::method::MethodRouter;
160    /// use cot::router::{Route, Router};
161    /// use cot::test::TestRequestBuilder;
162    ///
163    /// async fn test_handler() -> Html {
164    ///     Html::new("GET response")
165    /// }
166    ///
167    /// # #[tokio::main]
168    /// # async fn main() -> cot::Result<()> {
169    /// let method_router = MethodRouter::new().get(test_handler);
170    ///
171    /// let router = Router::with_urls([Route::with_handler("/", method_router)]);
172    ///
173    /// let request = TestRequestBuilder::get("/").router(router.clone()).build();
174    /// assert_eq!(
175    ///     router
176    ///         .handle(request)
177    ///         .await?
178    ///         .into_body()
179    ///         .into_bytes()
180    ///         .await?,
181    ///     "GET response"
182    /// );
183    /// # Ok(())
184    /// # }
185    /// ```
186    pub fn new() -> Self {
187        Self {
188            inner: InnerMethodRouter::new(),
189        }
190    }
191
192    define_method!(get => GET);
193    define_method!(head => HEAD);
194    define_method!(delete => DELETE);
195    define_method!(options => OPTIONS);
196    define_method!(patch => PATCH);
197    define_method!(post => POST);
198    define_method!(put => PUT);
199    define_method!(trace => TRACE);
200    define_method!(connect => CONNECT);
201
202    /// Set a fallback handler that gets called when no other handler matches.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use cot::StatusCode;
208    /// use cot::html::Html;
209    /// use cot::response::IntoResponse;
210    /// use cot::router::method::MethodRouter;
211    /// use cot::router::{Route, Router};
212    /// use cot::test::TestRequestBuilder;
213    ///
214    /// async fn fallback_handler() -> impl IntoResponse {
215    ///     Html::new("Method Not Allowed").with_status(StatusCode::METHOD_NOT_ALLOWED)
216    /// }
217    ///
218    /// # #[tokio::main]
219    /// # async fn main() -> cot::Result<()> {
220    /// let method_router = MethodRouter::new().fallback(fallback_handler);
221    ///
222    /// let router = Router::with_urls([Route::with_handler("/", method_router)]);
223    ///
224    /// let request = TestRequestBuilder::get("/").router(router.clone()).build();
225    /// assert_eq!(
226    ///     router
227    ///         .handle(request)
228    ///         .await?
229    ///         .into_body()
230    ///         .into_bytes()
231    ///         .await?,
232    ///     "Method Not Allowed"
233    /// );
234    /// # Ok(())
235    /// # }
236    /// ```
237    pub fn fallback<HandlerParams, H>(mut self, handler: H) -> Self
238    where
239        HandlerParams: 'static,
240        H: RequestHandler<HandlerParams> + Send + Sync + 'static,
241    {
242        self.inner.fallback = InnerHandler::new(handler);
243        self
244    }
245}
246
247impl RequestHandler for MethodRouter {
248    fn handle(&self, request: Request) -> impl Future<Output = cot::Result<Response>> + Send {
249        self.inner.handle(request)
250    }
251}
252
253#[derive(Debug)]
254#[must_use]
255struct InnerMethodRouter<T> {
256    pub(self) get: Option<T>,
257    pub(self) head: Option<T>,
258    pub(self) delete: Option<T>,
259    pub(self) options: Option<T>,
260    pub(self) patch: Option<T>,
261    pub(self) post: Option<T>,
262    pub(self) put: Option<T>,
263    pub(self) trace: Option<T>,
264    // CONNECT can't be used in OpenAPI, so it's always a base handler
265    pub(self) connect: Option<InnerHandler>,
266    pub(self) fallback: InnerHandler,
267}
268
269impl<T> InnerMethodRouter<T> {
270    pub(crate) fn new() -> Self {
271        Self {
272            get: None,
273            head: None,
274            delete: None,
275            options: None,
276            patch: None,
277            post: None,
278            put: None,
279            trace: None,
280            connect: None,
281            fallback: InnerHandler::new(default_fallback),
282        }
283    }
284}
285
286impl<T: RequestHandler + Send + Sync> RequestHandler for InnerMethodRouter<T> {
287    async fn handle(&self, request: Request) -> cot::Result<Response> {
288        macro_rules! handle_method {
289            ($name:ident => $method:ident) => {
290                if request.method() == Method::$method {
291                    if let Some(handler) = &self.$name {
292                        return handler.handle(request).await;
293                    }
294                }
295            };
296        }
297
298        handle_method!(get => GET);
299        handle_method!(head => HEAD);
300        handle_method!(delete => DELETE);
301        handle_method!(options => OPTIONS);
302        handle_method!(patch => PATCH);
303        handle_method!(post => POST);
304        handle_method!(put => PUT);
305        handle_method!(trace => TRACE);
306        handle_method!(connect => CONNECT);
307
308        if request.method() == Method::HEAD {
309            // handle HEAD requests by calling the GET handler
310            // if the HEAD handler is not set
311
312            if let Some(handler) = &self.get {
313                return handler.handle(request).await;
314            }
315        }
316
317        self.fallback.handle(request).await
318    }
319}
320
321struct InnerHandler(Box<dyn BoxRequestHandler + Send + Sync>);
322
323impl InnerHandler {
324    fn new<HandlerParams, H>(handler: H) -> Self
325    where
326        HandlerParams: 'static,
327        H: RequestHandler<HandlerParams> + Send + Sync + 'static,
328    {
329        Self(Box::new(into_box_request_handler(handler)))
330    }
331}
332
333impl Debug for InnerHandler {
334    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
335        f.debug_tuple("InnerHandler").finish_non_exhaustive()
336    }
337}
338
339impl RequestHandler for InnerHandler {
340    fn handle(&self, request: Request) -> impl Future<Output = cot::Result<Response>> + Send {
341        self.0.handle(request)
342    }
343}
344
345macro_rules! define_method_router {
346    ($name:ident => $method:ident) => {
347        #[doc = concat!(
348                    "Create a new [`MethodRouter`] with a [`",
349                    stringify!($method),
350                    "`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/",
351                    stringify!($method),
352                    ") handler."
353                )]
354        ///
355        #[doc = concat!(
356            "This is a shorthand to calling [`MethodRouter::new`] and then [`MethodRouter::",
357            stringify!($name),
358            "`]."
359        )]
360        /// # Examples
361        ///
362        /// ```
363        /// use cot::html::Html;
364        #[doc = concat!("use cot::router::method::", stringify!($name), ";")]
365        ///
366        /// async fn test_handler() -> cot::Result<Html> {
367        ///     Ok(Html::new("test"))
368        /// }
369        ///
370        /// # #[tokio::main]
371        /// # async fn main() -> cot::Result<()> {
372        #[doc = concat!("let method_router = ", stringify!($name), "(test_handler);")]
373        /// #
374        /// # let router = cot::router::Router::with_urls(
375        /// #     [cot::router::Route::with_handler("/", method_router)]
376        /// # );
377        /// #
378        #[doc = concat!(
379            "# let request = cot::test::TestRequestBuilder::with_method(\"/\", cot::Method::",
380            stringify!($method),
381            ")"
382        )]
383        /// #     .router(router.clone())
384        /// #     .build();
385        /// # assert_eq!(
386        /// #     router
387        /// #         .handle(request)
388        /// #         .await?
389        /// #         .into_body()
390        /// #         .into_bytes()
391        /// #         .await?,
392        /// #     "test"
393        /// # );
394        /// # Ok(())
395        /// # }
396        /// ```
397        pub fn $name<HandlerParams, H>(handler: H) -> MethodRouter
398        where
399            HandlerParams: 'static,
400            H: RequestHandler<HandlerParams> + Send + Sync + 'static,
401        {
402            MethodRouter::new().$name(handler)
403        }
404    };
405}
406
407define_method_router!(get => GET);
408define_method_router!(head => HEAD);
409define_method_router!(delete => DELETE);
410define_method_router!(options => OPTIONS);
411define_method_router!(patch => PATCH);
412define_method_router!(post => POST);
413define_method_router!(put => PUT);
414define_method_router!(trace => TRACE);
415define_method_router!(connect => CONNECT);
416
417async fn default_fallback(method: Method) -> crate::Error {
418    MethodNotAllowed::new(method).into()
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use crate::StatusCode;
425    use crate::html::Html;
426    use crate::test::TestRequestBuilder;
427
428    async fn test_handler(method: Method) -> Html {
429        Html::new(method.as_str())
430    }
431
432    #[test]
433    fn inner_handler_debug() {
434        let handler = InnerHandler::new(test_handler);
435
436        let debug_str = format!("{handler:?}");
437
438        assert_eq!(debug_str, "InnerHandler(..)");
439    }
440
441    #[cot::test]
442    async fn method_router_fallback() {
443        let router = MethodRouter::new();
444
445        let request = TestRequestBuilder::get("/").build();
446        let response = router.handle(request).await.unwrap_err();
447        let inner = response.inner();
448
449        assert_eq!(inner.status_code(), StatusCode::METHOD_NOT_ALLOWED);
450        assert!(inner.is::<MethodNotAllowed>());
451    }
452
453    #[cot::test]
454    async fn method_router_default_fallback() {
455        let router = MethodRouter::default();
456
457        let request = TestRequestBuilder::get("/").build();
458        let response = router.handle(request).await.unwrap_err();
459        let inner = response.inner();
460
461        assert_eq!(inner.status_code(), StatusCode::METHOD_NOT_ALLOWED);
462        assert!(inner.is::<MethodNotAllowed>());
463    }
464
465    #[cot::test]
466    async fn method_router_custom_fallback() {
467        let router = MethodRouter::new().fallback(test_handler);
468
469        let request = TestRequestBuilder::get("/").build();
470        let response = router.handle(request).await.unwrap();
471
472        assert_eq!(response.status(), StatusCode::OK);
473        assert_eq!(response.into_body().into_bytes().await.unwrap(), "GET");
474    }
475
476    #[cot::test]
477    async fn method_router_get() {
478        let router = get(test_handler);
479
480        let request = TestRequestBuilder::get("/").build();
481        let response = router.handle(request).await.unwrap();
482
483        assert_eq!(response.status(), StatusCode::OK);
484
485        // check other methods
486        let methods = [
487            Method::DELETE,
488            Method::OPTIONS,
489            Method::PATCH,
490            Method::POST,
491            Method::PUT,
492            Method::TRACE,
493            Method::CONNECT,
494        ];
495        for method in methods {
496            let request = TestRequestBuilder::with_method("/", method).build();
497            let response = router.handle(request).await.unwrap_err();
498            let inner = response.inner();
499
500            assert_eq!(inner.status_code(), StatusCode::METHOD_NOT_ALLOWED);
501            assert!(inner.is::<MethodNotAllowed>());
502        }
503    }
504
505    macro_rules! test_method_router {
506        ($test_name:ident, $constructor_name:ident, $method_name:ident) => {
507            #[cot::test]
508            async fn $test_name() {
509                let router = $constructor_name(test_handler);
510
511                let request = TestRequestBuilder::with_method("/", Method::$method_name).build();
512                let response = router.handle(request).await.unwrap();
513
514                assert_eq!(response.status(), StatusCode::OK);
515            }
516        };
517    }
518
519    test_method_router!(method_router_head, head, HEAD);
520    test_method_router!(method_router_delete, delete, DELETE);
521    test_method_router!(method_router_options, options, OPTIONS);
522    test_method_router!(method_router_patch, patch, PATCH);
523    test_method_router!(method_router_post, post, POST);
524    test_method_router!(method_router_put, put, PUT);
525    test_method_router!(method_router_trace, trace, TRACE);
526    test_method_router!(method_router_connect, connect, CONNECT);
527
528    #[cot::test]
529    async fn method_router_default_head() {
530        // verify that the default method router doesn't handle HEAD
531        let router = MethodRouter::new();
532
533        let request = TestRequestBuilder::with_method("/", Method::HEAD).build();
534        let response = router.handle(request).await.unwrap_err();
535        let inner = response.inner();
536
537        assert_eq!(inner.status_code(), StatusCode::METHOD_NOT_ALLOWED);
538        assert!(inner.is::<MethodNotAllowed>());
539
540        // check that if GET handler is defined, HEAD is routed to it
541        let router = get(test_handler);
542
543        let request = TestRequestBuilder::with_method("/", Method::HEAD).build();
544        let response = router.handle(request).await.unwrap();
545
546        assert_eq!(response.status(), StatusCode::OK);
547    }
548
549    #[cot::test]
550    async fn method_router_multiple() {
551        let router = MethodRouter::new()
552            .get(test_handler)
553            .head(test_handler)
554            .delete(test_handler)
555            .options(test_handler)
556            .patch(test_handler)
557            .post(test_handler)
558            .put(test_handler)
559            .trace(test_handler)
560            .connect(test_handler);
561
562        for (method, expected_string) in [
563            (Method::GET, "GET"),
564            (Method::HEAD, "HEAD"),
565            (Method::DELETE, "DELETE"),
566            (Method::OPTIONS, "OPTIONS"),
567            (Method::PATCH, "PATCH"),
568            (Method::POST, "POST"),
569            (Method::PUT, "PUT"),
570            (Method::TRACE, "TRACE"),
571            (Method::CONNECT, "CONNECT"),
572        ] {
573            let request = TestRequestBuilder::with_method("/", method).build();
574            let response = router.handle(request).await.unwrap();
575
576            assert_eq!(response.status(), StatusCode::OK);
577            assert_eq!(
578                response.into_body().into_bytes().await.unwrap(),
579                expected_string
580            );
581        }
582    }
583}