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 get: Option<BoxedHttpService<B>>, 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 fallback: Fallback<B>,
100
101 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 if let Some(svc) = get {
362 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 assert_sync::<Arc<MethodRouter<Body>>>();
414 }
415}