Skip to main content

typeway_server/
typed_bind.rs

1//! Bind functions for typed endpoint wrappers.
2//!
3//! These create `BoundHandler`s that add runtime enforcement (validation,
4//! content-type checking) on top of the type-level declarations.
5
6use serde::de::DeserializeOwned;
7
8use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
9use crate::handler_for::{BindableEndpoint, BoundHandler};
10use crate::response::IntoResponse;
11use crate::typed::*;
12
13// ===========================================================================
14// bind_validated — validates body before handler
15// ===========================================================================
16
17/// Bind a handler to a `Validated<V, E>` endpoint.
18///
19/// Deserializes the request body, runs the validator, and only calls the
20/// handler if validation passes. Returns 422 on validation failure.
21pub fn bind_validated<V, E, H, Args>(handler: H) -> BoundHandler<Validated<V, E>>
22where
23    V: Validate<E::Req>,
24    E: BindableEndpoint + HasReqType,
25    E::Req: DeserializeOwned + Send + 'static,
26    H: Handler<Args>,
27    Args: 'static,
28{
29    let method = E::method();
30    let pattern = E::pattern();
31    let match_fn = E::match_fn();
32
33    // Wrap the handler with validation.
34    let inner = into_boxed_handler(handler);
35    let boxed: BoxedHandler = std::sync::Arc::new(move |parts, body| {
36        // Try to deserialize and validate the body.
37        let validation_result: Result<(), String> = serde_json::from_slice::<E::Req>(&body)
38            .map_err(|e| format!("invalid request body: {e}"))
39            .and_then(|parsed| V::validate(&parsed));
40
41        match validation_result {
42            Ok(()) => inner(parts, body),
43            Err(msg) => {
44                let error = crate::error::JsonError::unprocessable(msg);
45                Box::pin(async move { error.into_response() })
46            }
47        }
48    });
49
50    BoundHandler::new(method, pattern, match_fn, boxed)
51}
52
53/// Helper trait to extract the Req type from an endpoint.
54pub trait HasReqType {
55    type Req;
56}
57
58impl<M: typeway_core::HttpMethod, P: typeway_core::PathSpec, Req, Res, Q, Err> HasReqType
59    for typeway_core::Endpoint<M, P, Req, Res, Q, Err>
60{
61    type Req = Req;
62}
63
64// ===========================================================================
65// bind_content_type — checks Content-Type before handler
66// ===========================================================================
67
68/// Bind a handler to a `ContentType<C, E>` endpoint.
69///
70/// Checks the Content-Type header before calling the handler.
71/// Returns 415 Unsupported Media Type if the header doesn't match.
72pub fn bind_content_type<C, E, H, Args>(handler: H) -> BoundHandler<ContentType<C, E>>
73where
74    C: ContentTypeMarker,
75    E: BindableEndpoint,
76    H: Handler<Args>,
77    Args: 'static,
78{
79    let method = E::method();
80    let pattern = E::pattern();
81    let match_fn = E::match_fn();
82
83    let inner = into_boxed_handler(handler);
84    let expected = C::CONTENT_TYPE;
85    let boxed: BoxedHandler =
86        std::sync::Arc::new(move |parts: http::request::Parts, body: bytes::Bytes| {
87            let ct = parts
88                .headers
89                .get(http::header::CONTENT_TYPE)
90                .and_then(|v| v.to_str().ok())
91                .unwrap_or("");
92
93            if !ct.starts_with(expected) {
94                let error = crate::error::JsonError::new(
95                    http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
96                    format!("expected Content-Type: {expected}, got: {ct}"),
97                );
98                return Box::pin(async move { error.into_response() });
99            }
100
101            inner(parts, body)
102        });
103
104    BoundHandler::new(method, pattern, match_fn, boxed)
105}
106
107// ===========================================================================
108// Convenience macros
109// ===========================================================================
110
111/// Bind a handler with body validation.
112#[macro_export]
113macro_rules! bind_validated {
114    ($handler:expr) => {
115        $crate::typed_bind::bind_validated::<_, _, _, _>($handler)
116    };
117}
118
119/// Bind a handler with content-type enforcement.
120#[macro_export]
121macro_rules! bind_content_type {
122    ($handler:expr) => {
123        $crate::typed_bind::bind_content_type::<_, _, _, _>($handler)
124    };
125}