Skip to main content

rama_http/service/web/endpoint/
mod.rs

1#![expect(
2    clippy::allow_attributes,
3    reason = "macro-generated `#[allow]` attributes whose underlying lints fire only for some expansions"
4)]
5
6use std::convert::Infallible;
7
8use rama_core::{
9    Service,
10    service::{BoxService, StaticOutput},
11};
12
13use crate::{Body, Request, Response, matcher::HttpMatcher};
14
15pub mod extract;
16pub mod response;
17
18use response::IntoResponse;
19
20#[derive(Debug, Clone)]
21pub(crate) struct Endpoint {
22    pub(crate) matcher: HttpMatcher<Body>,
23    pub(crate) service: BoxService<Request, Response, Infallible>,
24}
25
26/// utility trait to accept multiple types as an endpoint service for [`super::WebService`]
27pub trait IntoEndpointService<T>: private::Sealed<T, ()> {
28    type Service: Service<Request>;
29
30    /// convert the type into a [`rama_core::Service`].
31    fn into_endpoint_service(self) -> Self::Service;
32}
33
34pub trait IntoEndpointServiceWithState<T, State>: private::Sealed<T, State> {
35    type Service: Service<Request>;
36
37    /// convert the type into a [`rama_core::Service`] with state.
38    fn into_endpoint_service_with_state(self, state: State) -> Self::Service;
39}
40
41impl<S> IntoEndpointService<(S,)> for S
42where
43    S: Service<Request>,
44{
45    type Service = Self;
46
47    #[inline(always)]
48    fn into_endpoint_service(self) -> Self::Service {
49        self
50    }
51}
52
53impl<S, State> IntoEndpointServiceWithState<(S,), State> for S
54where
55    S: Service<Request>,
56{
57    type Service = Self;
58
59    fn into_endpoint_service_with_state(self, _state: State) -> Self::Service {
60        self
61    }
62}
63
64impl<O> IntoEndpointService<()> for Result<O, Infallible>
65where
66    O: Clone + Send + Sync + 'static,
67{
68    type Service = StaticOutput<O>;
69
70    fn into_endpoint_service(self) -> Self::Service {
71        StaticOutput::new(self.unwrap())
72    }
73}
74
75impl<O> IntoEndpointService<Response> for O
76where
77    O: IntoResponse + Clone + Send + Sync + 'static,
78{
79    type Service = StaticOutput<O>;
80
81    fn into_endpoint_service(self) -> Self::Service {
82        StaticOutput::new(self)
83    }
84}
85
86impl<O, State> IntoEndpointServiceWithState<(), State> for Result<O, Infallible>
87where
88    O: Clone + Send + Sync + 'static,
89{
90    type Service = StaticOutput<O>;
91
92    fn into_endpoint_service_with_state(self, _state: State) -> Self::Service {
93        self.into_endpoint_service()
94    }
95}
96
97impl<O, State> IntoEndpointServiceWithState<Response, State> for O
98where
99    O: IntoResponse + Clone + Send + Sync + 'static,
100{
101    type Service = StaticOutput<O>;
102
103    fn into_endpoint_service_with_state(self, _state: State) -> Self::Service {
104        self.into_endpoint_service()
105    }
106}
107
108mod service;
109#[doc(inline)]
110pub use service::EndpointServiceFn;
111
112/// Wrapper svc used for creating a endpoint service from a function.
113pub struct EndpointServiceFnWrapper<F, T, State> {
114    inner: F,
115    _marker: std::marker::PhantomData<fn(T)>,
116    state: State,
117}
118
119impl<F: std::fmt::Debug, T, State: std::fmt::Debug> std::fmt::Debug
120    for EndpointServiceFnWrapper<F, T, State>
121{
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        f.debug_struct("EndpointServiceFnWrapper")
124            .field("inner", &self.inner)
125            .field("state", &self.state)
126            .field(
127                "_marker",
128                &format_args!("{}", std::any::type_name::<fn(T)>()),
129            )
130            .finish()
131    }
132}
133
134impl<F, T, State> Clone for EndpointServiceFnWrapper<F, T, State>
135where
136    F: Clone,
137    State: Clone,
138{
139    fn clone(&self) -> Self {
140        Self {
141            inner: self.inner.clone(),
142            _marker: std::marker::PhantomData,
143            state: self.state.clone(),
144        }
145    }
146}
147
148impl<F, T, State> Service<Request> for EndpointServiceFnWrapper<F, T, State>
149where
150    F: EndpointServiceFn<T, State>,
151    T: Send + 'static,
152    State: Send + Sync + Clone + 'static,
153{
154    type Output = F::Output;
155    type Error = F::Error;
156
157    async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
158        self.inner.call(req, &self.state).await
159    }
160}
161
162impl<F, T> IntoEndpointService<(F, T)> for F
163where
164    F: EndpointServiceFn<T, ()>,
165    T: Send + 'static,
166{
167    type Service = EndpointServiceFnWrapper<F, T, ()>;
168
169    fn into_endpoint_service(self) -> Self::Service {
170        EndpointServiceFnWrapper {
171            inner: self,
172            _marker: std::marker::PhantomData,
173            state: (),
174        }
175    }
176}
177
178impl<F, T, State> IntoEndpointServiceWithState<(F, T), State> for F
179where
180    F: EndpointServiceFn<T, State>,
181    T: Send + 'static,
182    State: Send + Sync + Clone + 'static,
183{
184    type Service = EndpointServiceFnWrapper<F, T, State>;
185
186    fn into_endpoint_service_with_state(self, state: State) -> Self::Service {
187        EndpointServiceFnWrapper {
188            inner: self,
189            _marker: std::marker::PhantomData,
190            state,
191        }
192    }
193}
194
195mod private {
196    use super::*;
197
198    pub trait Sealed<T, State> {}
199
200    impl<S, State> Sealed<(S,), State> for S where S: Service<Request> {}
201
202    impl<O, State> Sealed<(), State> for Result<O, Infallible> {}
203
204    impl<O, State> Sealed<Response, State> for O {}
205
206    impl<F, T, State> Sealed<(F, T), State> for F where F: EndpointServiceFn<T, State> {}
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use crate::{Body, Method, Request, StatusCode, body::util::BodyExt};
213    use extract::*;
214    use rama_core::conversion::FromRef;
215
216    fn assert_into_endpoint_service<T, I>(_: I)
217    where
218        I: IntoEndpointService<T>,
219    {
220    }
221
222    #[test]
223    fn test_into_endpoint_service_static() {
224        assert_into_endpoint_service(StatusCode::OK);
225        assert_into_endpoint_service("hello");
226        assert_into_endpoint_service("hello".to_owned());
227    }
228
229    #[tokio::test]
230    async fn test_into_endpoint_service_impl() {
231        #[derive(Debug, Clone)]
232        struct OkService;
233
234        impl Service<Request> for OkService {
235            type Output = StatusCode;
236            type Error = Infallible;
237
238            async fn serve(&self, _req: Request) -> Result<Self::Output, Self::Error> {
239                Ok(StatusCode::OK)
240            }
241        }
242
243        let svc = OkService;
244        let resp = svc
245            .serve(
246                Request::builder()
247                    .uri("http://example.com")
248                    .body(Body::empty())
249                    .unwrap(),
250            )
251            .await
252            .unwrap();
253        assert_eq!(resp, StatusCode::OK);
254
255        assert_into_endpoint_service(svc)
256    }
257
258    #[test]
259    fn test_into_endpoint_service_fn_no_param() {
260        assert_into_endpoint_service(async || StatusCode::OK);
261        assert_into_endpoint_service(async || "hello");
262    }
263
264    #[tokio::test]
265    async fn test_service_fn_wrapper_no_param() {
266        let svc = async || StatusCode::OK;
267        let svc = svc.into_endpoint_service();
268
269        let res = svc
270            .serve(
271                Request::builder()
272                    .uri("http://example.com")
273                    .body(Body::empty())
274                    .unwrap(),
275            )
276            .await
277            .unwrap();
278        assert_eq!(res, StatusCode::OK);
279    }
280
281    #[tokio::test]
282    async fn test_service_fn_wrapper_single_param_request() {
283        let svc = async |req: Request| req.uri().to_string();
284        let svc = svc.into_endpoint_service();
285
286        let res = svc
287            .serve(
288                Request::builder()
289                    .uri("http://example.com")
290                    .body(Body::empty())
291                    .unwrap(),
292            )
293            .await
294            .unwrap();
295        // native Uri preserves the empty path (no forced trailing `/`)
296        assert_eq!(res, "http://example.com")
297    }
298
299    #[tokio::test]
300    async fn test_service_fn_wrapper_with_state() {
301        let state = "test_string".to_owned();
302        let svc = async |State(state): State<String>| state;
303        let svc = svc.into_endpoint_service_with_state(state.clone());
304
305        let res = svc
306            .serve(
307                Request::builder()
308                    .uri("http://example.com")
309                    .body(Body::empty())
310                    .unwrap(),
311            )
312            .await
313            .unwrap();
314        assert_eq!(res, "test_string");
315    }
316
317    #[tokio::test]
318    async fn test_service_fn_wrapper_with_derived_state() {
319        #[derive(Clone, Debug, Default, FromRef)]
320        #[allow(dead_code)]
321        struct GlobalState {
322            numbers: u8,
323            text: String,
324        }
325
326        let state = GlobalState {
327            text: "test_string".to_owned(),
328            ..Default::default()
329        };
330
331        let svc = async |State(state): State<GlobalState>| state.text;
332        let svc = svc.into_endpoint_service_with_state(state.clone());
333
334        let res = svc
335            .serve(
336                Request::builder()
337                    .uri("http://example.com")
338                    .body(Body::empty())
339                    .unwrap(),
340            )
341            .await
342            .unwrap();
343        assert_eq!(res, "test_string");
344    }
345
346    #[tokio::test]
347    async fn test_service_fn_wrapper_single_param_host() {
348        let svc = async |Host(host): Host| host.to_string();
349        let svc = svc.into_endpoint_service();
350
351        let res = svc
352            .serve(
353                Request::builder()
354                    .uri("http://example.com")
355                    .body(Body::empty())
356                    .unwrap(),
357            )
358            .await
359            .unwrap();
360        assert_eq!(res, "example.com")
361    }
362
363    #[tokio::test]
364    async fn test_service_fn_wrapper_multi_param_host() {
365        #[derive(Debug, Clone, serde::Deserialize)]
366        struct Params {
367            foo: String,
368        }
369
370        let svc = crate::service::web::WebService::default().with_get(
371            "/{foo}/bar",
372            async |Host(host): Host, Path(params): Path<Params>| {
373                format!("{} => {}", host, params.foo)
374            },
375        );
376        let svc = svc.into_endpoint_service();
377
378        let resp = svc
379            .serve(
380                Request::builder()
381                    .uri("http://example.com/42/bar")
382                    .body(Body::empty())
383                    .unwrap(),
384            )
385            .await
386            .unwrap();
387        assert_eq!(resp.status(), StatusCode::OK);
388        let body = resp.into_body().collect().await.unwrap().to_bytes();
389        assert_eq!(body, "example.com => 42")
390    }
391
392    #[test]
393    fn test_into_endpoint_service_fn_single_param() {
394        #[derive(Debug, Clone, serde::Deserialize)]
395        struct Params {
396            foo: String,
397        }
398
399        assert_into_endpoint_service(async |_path: Path<Params>| StatusCode::OK);
400        assert_into_endpoint_service(async |Path(params): Path<Params>| params.foo);
401        assert_into_endpoint_service(async |Query(query): Query<Params>| query.foo);
402        assert_into_endpoint_service(async |method: Method| method.to_string());
403        assert_into_endpoint_service(async |req: Request| req.uri().to_string());
404        assert_into_endpoint_service(async |_host: Host| StatusCode::OK);
405        assert_into_endpoint_service(async |Host(_host): Host| StatusCode::OK);
406    }
407}