1use std::sync::Arc;
47
48use rainier_http::coerce;
49use rainier_http::{FromRequest, Request};
50use rainier_support::{BoxedFuture, Error, Result};
51use serde::de::DeserializeOwned;
52use serde_json::Value;
53
54use crate::rule::Rule;
55use crate::validator::{RuleSet, ValidationErrors, Validator};
56
57#[async_trait::async_trait]
59pub trait FormRequest: DeserializeOwned + Send + Sync + 'static {
60 fn rules() -> RuleSet;
62
63 async fn authorize(request: &Request) -> bool {
72 let _ = request;
73 true
74 }
75
76 fn messages() -> Vec<(&'static str, &'static str)> {
78 Vec::new()
79 }
80
81 fn validation_data(request: &Request) -> Value {
86 request.all()
87 }
88
89 fn from_input(input: Value) -> Result<Self> {
95 coerce::from_value(&input).map_err(|e| {
96 Error::internal(format!(
99 "`{}` passed validation but could not be built from the input: {e}",
100 std::any::type_name::<Self>()
101 ))
102 })
103 }
104
105 async fn validate_request(request: &Request) -> Result<Self> {
110 if !Self::authorize(request).await {
111 return Err(Error::unauthorized("This action is unauthorized."));
112 }
113
114 let input = Self::validation_data(request);
115 let validator = Self::validator();
116
117 let validated = validator.validated_only(&input).map_err(Error::from)?;
118 Self::from_input(validated)
119 }
120
121 fn validator() -> Validator {
123 Validator::from_rules(Self::rules()).custom_messages(Self::messages())
124 }
125
126 fn check(request: &Request) -> std::result::Result<(), ValidationErrors> {
128 Self::validator().validate(&Self::validation_data(request))
129 }
130}
131
132#[derive(Debug, Clone, Copy, Default)]
140pub struct Validated<T>(pub T);
141
142impl<T> Validated<T> {
143 pub fn into_inner(self) -> T {
145 self.0
146 }
147}
148
149impl<T> std::ops::Deref for Validated<T> {
150 type Target = T;
151
152 fn deref(&self) -> &T {
153 &self.0
154 }
155}
156
157impl<T: FormRequest> FromRequest for Validated<T> {
158 fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
159 Box::pin(async move { T::validate_request(&request).await.map(Validated) })
160 }
161}
162
163pub fn field(
174 name: &'static str,
175 rules: impl IntoIterator<Item = Rule>,
176) -> (&'static str, Vec<Rule>) {
177 (name, rules.into_iter().collect())
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use rainier_http::Method;
184 use serde::Deserialize;
185 use serde_json::json;
186
187 #[derive(Debug, Deserialize, PartialEq)]
188 struct StorePost {
189 title: String,
190 body: String,
191 #[serde(default)]
192 published: Option<bool>,
193 }
194
195 #[async_trait::async_trait]
196 impl FormRequest for StorePost {
197 fn rules() -> RuleSet {
198 vec![
199 field("title", [Rule::Required, Rule::String, Rule::Between(3.0, 120.0)]),
200 field("body", [Rule::Required, Rule::String]),
201 field("published", [Rule::Boolean]),
202 ]
203 }
204 }
205
206 fn post(body: Value) -> Request {
207 Request::builder().method(Method::POST).json(&body).build()
208 }
209
210 #[tokio::test]
211 async fn a_valid_request_becomes_a_typed_payload() {
212 let request = post(json!({ "title": "Hello", "body": "World", "published": true }));
213 let payload = StorePost::validate_request(&request).await.unwrap();
214
215 assert_eq!(
216 payload,
217 StorePost { title: "Hello".into(), body: "World".into(), published: Some(true) }
218 );
219 }
220
221 #[tokio::test]
222 async fn an_invalid_request_is_a_422_with_the_failures() {
223 let request = post(json!({ "title": "Hi" }));
224 let err = StorePost::validate_request(&request).await.unwrap_err();
225
226 assert_eq!(err.status(), 422);
227 let details = err.details().unwrap();
228 assert!(details["title"][0].as_str().unwrap().contains("between"));
229 assert!(details["body"][0].as_str().unwrap().contains("required"));
230 }
231
232 #[tokio::test]
233 async fn undeclared_fields_never_reach_the_payload() {
234 #[derive(Debug, Deserialize)]
235 struct Partial {
236 title: String,
237 #[serde(default)]
238 is_admin: bool,
239 }
240
241 #[async_trait::async_trait]
242 impl FormRequest for Partial {
243 fn rules() -> RuleSet {
244 vec![field("title", [Rule::Required])]
245 }
246 }
247
248 let request = post(json!({ "title": "Hello", "is_admin": true }));
249 let payload = Partial::validate_request(&request).await.unwrap();
250
251 assert_eq!(payload.title, "Hello");
252 assert!(!payload.is_admin, "a field with no rule must not be mass-assignable");
253 }
254
255 #[tokio::test]
256 async fn authorisation_runs_before_validation() {
257 #[derive(Debug, Deserialize)]
258 struct Guarded {
259 #[allow(dead_code)]
260 title: String,
261 }
262
263 #[async_trait::async_trait]
264 impl FormRequest for Guarded {
265 fn rules() -> RuleSet {
266 vec![field("title", [Rule::Required])]
267 }
268 async fn authorize(request: &Request) -> bool {
269 request.bearer_token() == Some("secret")
270 }
271 }
272
273 let denied = Guarded::validate_request(&post(json!({}))).await.unwrap_err();
276 assert_eq!(denied.status(), 403);
277
278 let allowed = Request::builder()
279 .method(Method::POST)
280 .header("authorization", "Bearer secret")
281 .json(&json!({ "title": "ok" }))
282 .build();
283 assert!(Guarded::validate_request(&allowed).await.is_ok());
284 }
285
286 #[tokio::test]
287 async fn custom_messages_are_applied() {
288 #[derive(Debug, Deserialize)]
289 struct Custom {
290 #[allow(dead_code)]
291 email: String,
292 }
293
294 #[async_trait::async_trait]
295 impl FormRequest for Custom {
296 fn rules() -> RuleSet {
297 vec![field("email", [Rule::Required, Rule::Email])]
298 }
299 fn messages() -> Vec<(&'static str, &'static str)> {
300 vec![("email.required", "We really need your email.")]
301 }
302 }
303
304 let err = Custom::validate_request(&post(json!({}))).await.unwrap_err();
305 assert_eq!(err.details().unwrap()["email"][0], "We really need your email.");
306 }
307
308 #[tokio::test]
309 async fn validation_data_can_fold_in_route_parameters() {
310 #[derive(Debug, Deserialize)]
311 struct UpdatePost {
312 id: u64,
313 #[allow(dead_code)]
314 title: String,
315 }
316
317 #[async_trait::async_trait]
318 impl FormRequest for UpdatePost {
319 fn rules() -> RuleSet {
320 vec![field("id", [Rule::Required, Rule::Integer]), field("title", [Rule::Required])]
321 }
322 fn validation_data(request: &Request) -> Value {
323 let mut data = request.all();
324 if let (Some(object), Some(id)) =
325 (data.as_object_mut(), request.route_param("post"))
326 {
327 object.insert("id".into(), Value::String(id.to_string()));
328 }
329 data
330 }
331 }
332
333 let request = Request::builder()
334 .method(Method::POST)
335 .route_param("post", "42")
336 .json(&json!({ "title": "Edited" }))
337 .build();
338
339 let payload = UpdatePost::validate_request(&request).await.unwrap();
340 assert_eq!(payload.id, 42);
341 }
342
343 #[tokio::test]
344 async fn coercion_lets_a_form_body_fill_typed_fields() {
345 #[derive(Debug, Deserialize)]
346 struct Paged {
347 page: u32,
348 active: bool,
349 }
350
351 #[async_trait::async_trait]
352 impl FormRequest for Paged {
353 fn rules() -> RuleSet {
354 vec![
355 field("page", [Rule::Required, Rule::Integer]),
356 field("active", [Rule::Required, Rule::Boolean]),
357 ]
358 }
359 }
360
361 let request =
362 Request::builder().method(Method::POST).form(&[("page", "3"), ("active", "1")]).build();
363
364 let payload = Paged::validate_request(&request).await.unwrap();
365 assert_eq!(payload.page, 3);
366 assert!(payload.active);
367 }
368
369 #[tokio::test]
370 async fn the_extractor_wires_it_into_a_handler_signature() {
371 let request = Arc::new(post(json!({ "title": "Hello", "body": "World" })));
372 let Validated(payload) = Validated::<StorePost>::from_request(request).await.unwrap();
373 assert_eq!(payload.title, "Hello");
374 }
375
376 #[tokio::test]
377 async fn the_extractor_surfaces_the_422() {
378 let request = Arc::new(post(json!({})));
379 let err = Validated::<StorePost>::from_request(request).await.unwrap_err();
380 assert_eq!(err.status(), 422);
381 }
382
383 #[tokio::test]
384 async fn check_validates_without_building_the_payload() {
385 let errors = StorePost::check(&post(json!({ "title": "Hi" }))).unwrap_err();
386 assert!(errors.has("title"));
387 assert!(errors.has("body"));
388
389 assert!(StorePost::check(&post(json!({ "title": "Hello", "body": "x" }))).is_ok());
390 }
391
392 #[tokio::test]
393 async fn a_payload_that_disagrees_with_its_rules_is_an_internal_error() {
394 #[derive(Debug, Deserialize)]
398 struct Mismatched {
399 #[allow(dead_code)]
400 title: u64,
401 }
402
403 #[async_trait::async_trait]
404 impl FormRequest for Mismatched {
405 fn rules() -> RuleSet {
406 vec![field("title", [Rule::Required, Rule::String])]
407 }
408 }
409
410 let err = Mismatched::validate_request(&post(json!({ "title": "not a number" })))
411 .await
412 .unwrap_err();
413 assert_eq!(err.status(), 500);
414 assert!(err.message().contains("passed validation"), "{}", err.message());
415 }
416}