Skip to main content

typeway_server/
typed_response.rs

1//! Typed response wrapper for compile-time return type enforcement.
2//!
3//! [`Strict<E>`] wraps an endpoint to enforce that the handler returns
4//! the exact type declared in the API spec. Without `Strict`, handlers
5//! can return any `impl IntoResponse` regardless of the declared `Res`.
6//!
7//! # Example
8//!
9//! ```ignore
10//! type API = (
11//!     // Handler MUST return Json<Vec<User>> — compiler enforced
12//!     Strict<GetEndpoint<UsersPath, Json<Vec<User>>>>,
13//!
14//!     // Handler can return anything implementing IntoResponse
15//!     GetEndpoint<TagsPath, TagsResponse>,
16//! );
17//!
18//! // This compiles — return type matches Res:
19//! async fn list_users() -> Json<Vec<User>> { Json(vec![]) }
20//!
21//! // This would NOT compile — String ≠ Json<Vec<User>>:
22//! // async fn list_users() -> String { "nope".into() }
23//! ```
24
25use std::future::Future;
26use std::marker::PhantomData;
27use std::pin::Pin;
28
29use typeway_core::ApiSpec;
30
31use crate::body::BoxBody;
32use crate::extract::{FromRequest, FromRequestParts};
33use crate::handler::BoxedHandler;
34use crate::handler_for::{BindableEndpoint, BoundHandler};
35use crate::response::IntoResponse;
36
37/// An endpoint that enforces the handler's return type matches `Res`.
38///
39/// `BindableEndpoint` is NOT implemented — `bind!()` cannot be used.
40/// Use `bind_strict!()` instead, which checks the return type.
41pub struct Strict<E> {
42    _marker: PhantomData<E>,
43}
44
45impl<E: ApiSpec> ApiSpec for Strict<E> {}
46
47// NOTE: BindableEndpoint intentionally NOT implemented for Strict.
48// This forces users to use bind_strict!() which checks the return type.
49
50/// Trait to extract the Res type from an endpoint.
51pub trait HasResType {
52    type Res;
53}
54
55impl<M: typeway_core::HttpMethod, P: typeway_core::PathSpec, Req, Res, Q, Err> HasResType
56    for typeway_core::Endpoint<M, P, Req, Res, Q, Err>
57{
58    type Res = Res;
59}
60
61/// Trait to extract the Err type from an endpoint.
62pub trait HasErrType {
63    type Err;
64}
65
66impl<M: typeway_core::HttpMethod, P: typeway_core::PathSpec, Req, Res, Q, Err> HasErrType
67    for typeway_core::Endpoint<M, P, Req, Res, Q, Err>
68{
69    type Err = Err;
70}
71
72/// A handler that returns exactly type `Res`.
73pub trait StrictHandler<Res, Args>: Clone + Send + Sync + 'static {
74    fn call(
75        self,
76        parts: http::request::Parts,
77        body: bytes::Bytes,
78    ) -> Pin<Box<dyn Future<Output = http::Response<BoxBody>> + Send>>;
79}
80
81// Arity 0: fn() -> Res
82impl<F, Fut, Res> StrictHandler<Res, ()> for F
83where
84    F: FnOnce() -> Fut + Clone + Send + Sync + 'static,
85    Fut: Future<Output = Res> + Send,
86    Res: IntoResponse,
87{
88    fn call(
89        self,
90        _parts: http::request::Parts,
91        _body: bytes::Bytes,
92    ) -> Pin<Box<dyn Future<Output = http::Response<BoxBody>> + Send>> {
93        Box::pin(async move { self().await.into_response() })
94    }
95}
96
97// Generate impls for fn(extractors...) -> Res
98macro_rules! impl_strict_handler {
99    ([$($T:ident),+], [$($t:ident),+]) => {
100        #[allow(non_snake_case)]
101        impl<F, Fut, Res, $($T,)+> StrictHandler<Res, ($($T,)+)> for F
102        where
103            F: FnOnce($($T,)+) -> Fut + Clone + Send + Sync + 'static,
104            Fut: Future<Output = Res> + Send,
105            Res: IntoResponse,
106            $($T: FromRequestParts + 'static,)+
107        {
108            fn call(
109                self,
110                parts: http::request::Parts,
111                _body: bytes::Bytes,
112            ) -> Pin<Box<dyn Future<Output = http::Response<BoxBody>> + Send>> {
113                Box::pin(async move {
114                    $(
115                        let $t = match $T::from_request_parts(&parts) {
116                            Ok(v) => v,
117                            Err(e) => return e.into_response(),
118                        };
119                    )+
120                    self($($t,)+).await.into_response()
121                })
122            }
123        }
124    };
125}
126
127impl_strict_handler!([T1], [t1]);
128impl_strict_handler!([T1, T2], [t1, t2]);
129impl_strict_handler!([T1, T2, T3], [t1, t2, t3]);
130impl_strict_handler!([T1, T2, T3, T4], [t1, t2, t3, t4]);
131impl_strict_handler!([T1, T2, T3, T4, T5], [t1, t2, t3, t4, t5]);
132impl_strict_handler!([T1, T2, T3, T4, T5, T6], [t1, t2, t3, t4, t5, t6]);
133
134/// Marker for strict handlers with a body extractor.
135pub struct StrictWithBody<Parts, Body>(PhantomData<(Parts, Body)>);
136
137impl<F, Fut, Res, B> StrictHandler<Res, StrictWithBody<(), B>> for F
138where
139    F: FnOnce(B) -> Fut + Clone + Send + Sync + 'static,
140    Fut: Future<Output = Res> + Send,
141    Res: IntoResponse,
142    B: FromRequest + 'static,
143{
144    fn call(
145        self,
146        parts: http::request::Parts,
147        body: bytes::Bytes,
148    ) -> Pin<Box<dyn Future<Output = http::Response<BoxBody>> + Send>> {
149        Box::pin(async move {
150            let b = match B::from_request(&parts, body).await {
151                Ok(v) => v,
152                Err(e) => return e.into_response(),
153            };
154            self(b).await.into_response()
155        })
156    }
157}
158
159macro_rules! impl_strict_handler_body_marker {
160    ([$($T:ident),+], [$($t:ident),+]) => {
161        #[allow(non_snake_case)]
162        impl<F, Fut, Res, $($T,)+ B> StrictHandler<Res, StrictWithBody<($($T,)+), B>> for F
163        where
164            F: FnOnce($($T,)+ B) -> Fut + Clone + Send + Sync + 'static,
165            Fut: Future<Output = Res> + Send,
166            Res: IntoResponse,
167            $($T: FromRequestParts + 'static,)+
168            B: FromRequest + 'static,
169        {
170            fn call(
171                self,
172                parts: http::request::Parts,
173                body: bytes::Bytes,
174            ) -> Pin<Box<dyn Future<Output = http::Response<BoxBody>> + Send>> {
175                Box::pin(async move {
176                    $(
177                        let $t = match $T::from_request_parts(&parts) {
178                            Ok(v) => v,
179                            Err(e) => return e.into_response(),
180                        };
181                    )+
182                    let b = match B::from_request(&parts, body).await {
183                        Ok(v) => v,
184                        Err(e) => return e.into_response(),
185                    };
186                    self($($t,)+ b).await.into_response()
187                })
188            }
189        }
190    };
191}
192
193impl_strict_handler_body_marker!([T1], [t1]);
194impl_strict_handler_body_marker!([T1, T2], [t1, t2]);
195impl_strict_handler_body_marker!([T1, T2, T3], [t1, t2, t3]);
196impl_strict_handler_body_marker!([T1, T2, T3, T4], [t1, t2, t3, t4]);
197impl_strict_handler_body_marker!([T1, T2, T3, T4, T5], [t1, t2, t3, t4, t5]);
198
199// ---------------------------------------------------------------------------
200// bind_strict
201// ---------------------------------------------------------------------------
202
203/// Trait to extract binding info from a Strict endpoint.
204pub trait StrictEndpoint {
205    type Inner: BindableEndpoint + HasResType;
206}
207
208impl<E: BindableEndpoint + HasResType> StrictEndpoint for Strict<E> {
209    type Inner = E;
210}
211
212/// Bind a handler to a `Strict<E>` endpoint.
213///
214/// The handler's return type must exactly match `E::Res`.
215pub fn bind_strict<S, H, Args>(handler: H) -> BoundHandler<S>
216where
217    S: StrictEndpoint,
218    H: StrictHandler<<S::Inner as HasResType>::Res, Args>,
219    Args: 'static,
220{
221    let method = S::Inner::method();
222    let pattern = S::Inner::pattern();
223    let match_fn = S::Inner::match_fn();
224
225    let boxed: BoxedHandler = std::sync::Arc::new(move |parts, body| {
226        let h = handler.clone();
227        h.call(parts, body)
228    });
229
230    BoundHandler::new(method, pattern, match_fn, boxed)
231}
232
233/// Convenience macro for binding strict handlers.
234#[macro_export]
235macro_rules! bind_strict {
236    ($handler:expr) => {
237        $crate::typed_response::bind_strict::<_, _, _>($handler)
238    };
239}