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}