Skip to main content

zon_router/
method_router.rs

1use bytes::{Bytes, BytesMut};
2use http::{header, HeaderValue, Method, StatusCode};
3use zon_core::{Body, HttpMiddleware, HttpService, IntoHttpService, Response};
4
5use crate::service::{BoxedHttpService, Fallback};
6
7#[inline]
8pub fn get<B, X>(
9    svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
10) -> MethodRouter<B>
11where
12    B: Send + 'static,
13{
14    MethodRouter::new().get(svc)
15}
16
17#[inline]
18pub fn head<B, X>(
19    svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
20) -> MethodRouter<B>
21where
22    B: Send + 'static,
23{
24    MethodRouter::new().head(svc)
25}
26
27#[inline]
28pub fn post<B, X>(
29    svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
30) -> MethodRouter<B>
31where
32    B: Send + 'static,
33{
34    MethodRouter::new().post(svc)
35}
36
37#[inline]
38pub fn put<B, X>(
39    svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
40) -> MethodRouter<B>
41where
42    B: Send + 'static,
43{
44    MethodRouter::new().put(svc)
45}
46
47#[inline]
48pub fn patch<B, X>(
49    svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
50) -> MethodRouter<B>
51where
52    B: Send + 'static,
53{
54    MethodRouter::new().patch(svc)
55}
56
57#[inline]
58pub fn delete<B, X>(
59    svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
60) -> MethodRouter<B>
61where
62    B: Send + 'static,
63{
64    MethodRouter::new().delete(svc)
65}
66
67#[inline]
68pub fn options<B, X>(
69    svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
70) -> MethodRouter<B>
71where
72    B: Send + 'static,
73{
74    MethodRouter::new().options(svc)
75}
76
77#[inline]
78pub fn trace<B, X>(
79    svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
80) -> MethodRouter<B>
81where
82    B: Send + 'static,
83{
84    MethodRouter::new().trace(svc)
85}
86
87pub struct MethodRouter<B> {
88    // Services for specific HTTP methods
89    get: Option<BoxedHttpService<B>>, // also handles head if no head service set
90    head: Option<BoxedHttpService<B>>,
91    post: Option<BoxedHttpService<B>>,
92    put: Option<BoxedHttpService<B>>,
93    patch: Option<BoxedHttpService<B>>,
94    delete: Option<BoxedHttpService<B>>,
95    options: Option<BoxedHttpService<B>>,
96    trace: Option<BoxedHttpService<B>>,
97
98    /// The service to call when no service for a given request's method is set
99    fallback: Fallback<B>,
100
101    /// The allow header's value to add to the fallback service's response.
102    allow_header: Option<Bytes>,
103}
104
105impl<B> MethodRouter<B>
106where
107    B: Send + 'static,
108{
109    #[inline]
110    pub fn new() -> Self {
111        Self {
112            get: None,
113            head: None,
114            post: None,
115            put: None,
116            patch: None,
117            delete: None,
118            options: None,
119            trace: None,
120            fallback: Fallback::Default(BoxedHttpService::new(|| async {
121                StatusCode::METHOD_NOT_ALLOWED
122            })),
123            allow_header: None,
124        }
125    }
126
127    /// Register a service that's called for `GET` requests.
128    ///
129    /// If you don't specify a separate service for `HEAD` requests, the same
130    /// service will be called for them, but any body set by it will be
131    /// stripped.
132    #[track_caller]
133    pub fn get<X>(
134        mut self,
135        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
136    ) -> Self {
137        register_service(
138            &mut self.get,
139            BoxedHttpService::new(svc),
140            "GET",
141            &mut self.allow_header,
142            "GET,HEAD",
143        );
144        self
145    }
146
147    /// Register a service that's called for `HEAD` requests.
148    #[track_caller]
149    pub fn head<X>(
150        mut self,
151        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
152    ) -> Self {
153        register_service(
154            &mut self.head,
155            BoxedHttpService::new(svc),
156            "HEAD",
157            &mut self.allow_header,
158            "HEAD",
159        );
160        self
161    }
162
163    /// Register a service that's called for `POST` requests.
164    #[track_caller]
165    pub fn post<X>(
166        mut self,
167        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
168    ) -> Self {
169        register_service(
170            &mut self.post,
171            BoxedHttpService::new(svc),
172            "POST",
173            &mut self.allow_header,
174            "POST",
175        );
176        self
177    }
178
179    /// Register a service that's called for `PUT` requests.
180    #[track_caller]
181    pub fn put<X>(
182        mut self,
183        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
184    ) -> Self {
185        register_service(
186            &mut self.put,
187            BoxedHttpService::new(svc),
188            "PUT",
189            &mut self.allow_header,
190            "PUT",
191        );
192        self
193    }
194
195    /// Register a service that's called for `PATCH` requests.
196    #[track_caller]
197    pub fn patch<X>(
198        mut self,
199        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
200    ) -> Self {
201        register_service(
202            &mut self.patch,
203            BoxedHttpService::new(svc),
204            "PATCH",
205            &mut self.allow_header,
206            "PATCH",
207        );
208        self
209    }
210
211    /// Register a service that's called for `DELETE` requests.
212    #[track_caller]
213    pub fn delete<X>(
214        mut self,
215        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
216    ) -> Self {
217        register_service(
218            &mut self.delete,
219            BoxedHttpService::new(svc),
220            "DELETE",
221            &mut self.allow_header,
222            "DELETE",
223        );
224        self
225    }
226
227    /// Register a service that's called for `OPTIONS` requests.
228    #[track_caller]
229    pub fn options<X>(
230        mut self,
231        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
232    ) -> Self {
233        register_service(
234            &mut self.options,
235            BoxedHttpService::new(svc),
236            "OPTIONS",
237            &mut self.allow_header,
238            "OPTIONS",
239        );
240        self
241    }
242
243    /// Register a service that's called for `TRACE` requests.
244    #[track_caller]
245    pub fn trace<X>(
246        mut self,
247        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
248    ) -> Self {
249        register_service(
250            &mut self.trace,
251            BoxedHttpService::new(svc),
252            "TRACE",
253            &mut self.allow_header,
254            "TRACE",
255        );
256        self
257    }
258
259    /// Register a service that's called for requests that don't match any of
260    /// the other services registered in this `MethodRouter`.
261    ///
262    /// This method is useful if you want to add a custom body to your
263    /// method-not-allowed responses. Note that an `Accept` header will still be
264    /// added on the response based on the other services registered, unless the
265    /// fallback service already set this header.
266    #[track_caller]
267    pub fn fallback<X>(
268        mut self,
269        svc: impl IntoHttpService<B, X, ResponseBody = Body, Service: Send + 'static>,
270    ) -> Self {
271        if self.fallback.is_custom() {
272            panic!("A fallback service was already set.");
273        }
274        self.fallback = Fallback::Custom(BoxedHttpService::new(svc));
275        self
276    }
277
278    pub fn middleware(
279        self,
280        mid: impl HttpMiddleware<BoxedHttpService<B>, Service: HttpService<B, ResponseBody = Body> + Send>
281            + Clone
282            + 'static,
283    ) -> Self {
284        let apply_middleware = |svc| BoxedHttpService::new(mid.clone().apply(svc));
285        Self {
286            get: self.get.map(apply_middleware),
287            head: self.head.map(apply_middleware),
288            post: self.post.map(apply_middleware),
289            put: self.put.map(apply_middleware),
290            patch: self.patch.map(apply_middleware),
291            delete: self.delete.map(apply_middleware),
292            options: self.options.map(apply_middleware),
293            trace: self.trace.map(apply_middleware),
294            fallback: self.fallback.middleware(mid),
295            allow_header: self.allow_header,
296        }
297    }
298}
299
300#[track_caller]
301fn register_service<B: Send + 'static>(
302    slot: &mut Option<BoxedHttpService<B>>,
303    svc: BoxedHttpService<B>,
304    method_name: &str,
305    allow_header: &mut Option<Bytes>,
306    allow: &str,
307) {
308    if slot.is_some() {
309        panic!("Overlapping service. Cannot add two services that both handle `{method_name}`");
310    }
311    *slot = Some(svc);
312
313    *allow_header = {
314        let capacity = allow_header.as_ref().map_or(0, |hdr| hdr.len() + 1) + allow.len();
315        let mut new_allow_hdr = BytesMut::with_capacity(capacity);
316        if let Some(old_allow_hdr) = allow_header {
317            new_allow_hdr.extend_from_slice(old_allow_hdr);
318            new_allow_hdr.extend_from_slice(b",");
319        }
320        new_allow_hdr.extend_from_slice(allow.as_bytes());
321        Some(new_allow_hdr.freeze())
322    };
323}
324
325impl<B> Default for MethodRouter<B>
326where
327    B: Send + 'static,
328{
329    fn default() -> Self {
330        Self::new()
331    }
332}
333
334impl<B> HttpService<B> for MethodRouter<B>
335where
336    B: Send + 'static,
337{
338    type ResponseBody = Body;
339
340    async fn call(&self, request: http::Request<B>) -> Response {
341        // Make sure nothing is forgotten via exhaustive pattern match
342        let Self { get, head, post, put, patch, delete, options, trace, fallback, allow_header } =
343            self;
344
345        let method = request.method();
346        macro_rules! call {
347            ($method:expr, $maybe_svc:expr) => {
348                if method == $method {
349                    if let Some(svc) = $maybe_svc {
350                        return svc.call(request).await;
351                    }
352                }
353            };
354        }
355
356        if method == Method::HEAD {
357            if let Some(svc) = head {
358                return svc.call(request).await;
359            }
360            // GET services implicitly also handle HEAD requests
361            if let Some(svc) = get {
362                // strip bodies of responses to HEAD requests if they're handled
363                // by the GET service
364                let mut res = svc.call(request).await;
365                *res.body_mut() = Body::empty();
366                return res;
367            }
368        }
369
370        call!(Method::GET, get);
371        call!(Method::POST, post);
372        call!(Method::PUT, put);
373        call!(Method::PATCH, patch);
374        call!(Method::DELETE, delete);
375        call!(Method::OPTIONS, options);
376        call!(Method::TRACE, trace);
377
378        let mut res = fallback.call(request).await;
379        maybe_add_allow_header(&mut res, allow_header);
380        res
381    }
382}
383
384fn maybe_add_allow_header(res: &mut http::Response<Body>, allow_header: &Option<Bytes>) {
385    if let Some(allow_header) = allow_header {
386        if let header::Entry::Vacant(v) = res.headers_mut().entry(header::ACCEPT) {
387            if let Ok(value) = HeaderValue::from_maybe_shared(allow_header.clone()) {
388                v.insert(value);
389            } else {
390                #[cfg(debug_assertions)]
391                panic!("Invalid allow header value. This should never happen.");
392            }
393        }
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use std::sync::Arc;
400
401    use zon_core::Body;
402
403    use super::MethodRouter;
404
405    #[test]
406    fn method_router_hyper_compatible() {
407        fn assert_sync<S: Sync>() {}
408
409        // Need to be able to clone zon services for hyper integration.
410        // Not realistic for MethodRouter directly, usual workaround is wrapping
411        // in an `Arc`. Needs MethodRouter to be `Send` in addition to `Sync`.
412        // This was previously not the case, this test ensures we don't regress.
413        assert_sync::<Arc<MethodRouter<Body>>>();
414    }
415}