Skip to main content

typeway_server/
typed.rs

1//! Type-level endpoint wrappers for compile-time enforcement.
2//!
3//! These wrappers sit in the API type and enforce constraints at compile time:
4//!
5//! - [`Validated<V, E>`] — validates request bodies before the handler runs
6//! - [`Versioned<V, E>`] — API version routing
7//! - [`ContentType<C, E>`] — enforces request content type
8//! - [`RateLimited<E>`] — declares rate limiting on an endpoint
9//!
10//! See also [`Protected<Auth, E>`](crate::auth::Protected) for authentication.
11
12use std::marker::PhantomData;
13
14use typeway_core::effects::{Effect, Requires};
15use typeway_core::ApiSpec;
16
17use crate::handler_for::BindableEndpoint;
18
19// ===========================================================================
20// Validated<V, E> — request body validation
21// ===========================================================================
22
23/// Trait for request body validators.
24///
25/// Implement this to define validation rules for a request body type.
26/// The validator runs after JSON deserialization but before the handler.
27///
28/// # Example
29///
30/// ```ignore
31/// struct CreateUserValidator;
32///
33/// impl Validate<CreateUser> for CreateUserValidator {
34///     fn validate(body: &CreateUser) -> Result<(), String> {
35///         if body.username.is_empty() { return Err("username required".into()); }
36///         if body.password.len() < 8 { return Err("password too short".into()); }
37///         Ok(())
38///     }
39/// }
40///
41/// type API = (
42///     Validated<CreateUserValidator, PostEndpoint<UsersPath, CreateUser, User>>,
43/// );
44/// ```
45pub trait Validate<T>: Send + Sync + 'static {
46    /// Validate the deserialized body. Returns `Err(message)` on failure.
47    fn validate(body: &T) -> Result<(), String>;
48}
49
50/// An endpoint with compile-time validated request bodies.
51///
52/// `V` implements `Validate<Req>` where `Req` is the endpoint's body type.
53/// The framework validates the body after deserialization and returns 422
54/// if validation fails, before the handler is called.
55pub struct Validated<V, E> {
56    _marker: PhantomData<(V, E)>,
57}
58
59impl<V: Send + Sync + 'static, E: ApiSpec> ApiSpec for Validated<V, E> {}
60
61// Validated<V, E> delegates AllProvided to the inner endpoint E.
62impl<V: Send + Sync + 'static, E, Provided, Idx> typeway_core::effects::AllProvided<Provided, Idx>
63    for Validated<V, E>
64where
65    E: typeway_core::effects::AllProvided<Provided, Idx>,
66{
67}
68
69impl<V: Send + Sync + 'static, E: BindableEndpoint> BindableEndpoint for Validated<V, E> {
70    fn method() -> http::Method {
71        E::method()
72    }
73    fn pattern() -> String {
74        E::pattern()
75    }
76    fn match_fn() -> crate::router::MatchFn {
77        E::match_fn()
78    }
79}
80
81// ===========================================================================
82// Versioned<V, E> — API versioning
83// ===========================================================================
84
85/// A version marker type. Use unit structs for each version.
86///
87/// ```ignore
88/// struct V1;
89/// struct V2;
90/// ```
91pub trait ApiVersion: Send + Sync + 'static {
92    /// The version prefix string (e.g., "v1", "v2").
93    const PREFIX: &'static str;
94}
95
96/// An endpoint scoped to a specific API version.
97///
98/// The version prefix is prepended to the path at routing time.
99///
100/// ```ignore
101/// struct V1;
102/// impl ApiVersion for V1 { const PREFIX: &'static str = "v1"; }
103///
104/// type API = (
105///     Versioned<V1, GetEndpoint<UsersPath, Vec<User>>>,  // /v1/users
106///     Versioned<V2, GetEndpoint<UsersPath, Vec<User>>>,  // /v2/users
107/// );
108/// ```
109pub struct Versioned<V, E> {
110    _marker: PhantomData<(V, E)>,
111}
112
113impl<V: ApiVersion, E: ApiSpec> ApiSpec for Versioned<V, E> {}
114impl<V: ApiVersion, E: BindableEndpoint> BindableEndpoint for Versioned<V, E> {
115    fn method() -> http::Method {
116        E::method()
117    }
118    fn pattern() -> String {
119        format!("/{}{}", V::PREFIX, E::pattern())
120    }
121    fn match_fn() -> crate::router::MatchFn {
122        let prefix = V::PREFIX;
123        let inner_match = E::match_fn();
124        Box::new(move |segments: &[&str]| {
125            if segments.first() == Some(&prefix) {
126                inner_match(&segments[1..])
127            } else {
128                false
129            }
130        })
131    }
132}
133
134// ===========================================================================
135// ContentType<C, E> — content type enforcement
136// ===========================================================================
137
138/// A content type marker.
139pub trait ContentTypeMarker: Send + Sync + 'static {
140    /// The expected Content-Type header value.
141    const CONTENT_TYPE: &'static str;
142}
143
144/// Built-in JSON content type marker.
145pub struct JsonContent;
146impl ContentTypeMarker for JsonContent {
147    const CONTENT_TYPE: &'static str = "application/json";
148}
149
150/// Built-in form content type marker.
151pub struct FormContent;
152impl ContentTypeMarker for FormContent {
153    const CONTENT_TYPE: &'static str = "application/x-www-form-urlencoded";
154}
155
156/// An endpoint that enforces a specific request Content-Type.
157///
158/// Requests without the correct Content-Type header are rejected with
159/// 415 Unsupported Media Type before the handler is called.
160///
161/// ```ignore
162/// type API = (
163///     ContentType<JsonContent, PostEndpoint<UsersPath, CreateUser, User>>,
164/// );
165/// ```
166pub struct ContentType<C, E> {
167    _marker: PhantomData<(C, E)>,
168}
169
170impl<C: ContentTypeMarker, E: ApiSpec> ApiSpec for ContentType<C, E> {}
171impl<C: ContentTypeMarker, E: BindableEndpoint> BindableEndpoint for ContentType<C, E> {
172    fn method() -> http::Method {
173        E::method()
174    }
175    fn pattern() -> String {
176        E::pattern()
177    }
178    fn match_fn() -> crate::router::MatchFn {
179        E::match_fn()
180    }
181}
182
183// ===========================================================================
184// RateLimited<E> — rate limiting declaration
185// ===========================================================================
186
187/// Rate limiting configuration.
188pub trait RateLimit: Send + Sync + 'static {
189    /// Maximum requests per window.
190    const MAX_REQUESTS: u32;
191    /// Window duration in seconds.
192    const WINDOW_SECS: u64;
193}
194
195/// An endpoint with declared rate limits.
196///
197/// This is a type-level declaration — the actual enforcement is done by
198/// the framework's rate limiting middleware, which reads the limits from
199/// the type at startup.
200///
201/// ```ignore
202/// struct StandardRate;
203/// impl RateLimit for StandardRate {
204///     const MAX_REQUESTS: u32 = 100;
205///     const WINDOW_SECS: u64 = 60;
206/// }
207///
208/// type API = (
209///     RateLimited<StandardRate, PostEndpoint<LoginPath, LoginReq, LoginRes>>,
210/// );
211/// ```
212pub struct RateLimited<R, E> {
213    _marker: PhantomData<(R, E)>,
214}
215
216impl<R: RateLimit, E: ApiSpec> ApiSpec for RateLimited<R, E> {}
217impl<R: RateLimit, E: BindableEndpoint> BindableEndpoint for RateLimited<R, E> {
218    fn method() -> http::Method {
219        E::method()
220    }
221    fn pattern() -> String {
222        E::pattern()
223    }
224    fn match_fn() -> crate::router::MatchFn {
225        E::match_fn()
226    }
227}
228
229// ===========================================================================
230// Requires<E, T> — middleware effect requirement (BindableEndpoint delegation)
231// ===========================================================================
232
233/// `Requires<E, T>` delegates all endpoint metadata to the inner type `T`.
234///
235/// This allows `bind!()` to work with `Requires`-wrapped endpoints.
236/// The `Requires` wrapper is purely a compile-time marker — at runtime,
237/// routing behaves identically to the unwrapped endpoint.
238impl<E: Effect, T: BindableEndpoint> BindableEndpoint for Requires<E, T> {
239    fn method() -> http::Method {
240        T::method()
241    }
242    fn pattern() -> String {
243        T::pattern()
244    }
245    fn match_fn() -> crate::router::MatchFn {
246        T::match_fn()
247    }
248}