1use std::collections::HashMap;
26
27use axum::extract::{FromRequest, Request};
28use axum::response::{IntoResponse, Response};
29
30pub struct Validated<T>(T);
40
41impl<T> Validated<T> {
42 pub(crate) const fn new(value: T) -> Self {
44 Self(value)
45 }
46
47 #[must_use]
49 pub fn into_inner(self) -> T {
50 self.0
51 }
52}
53
54impl<T> std::ops::Deref for Validated<T> {
55 type Target = T;
56 fn deref(&self) -> &T {
57 &self.0
58 }
59}
60
61impl<T> AsRef<T> for Validated<T> {
62 fn as_ref(&self) -> &T {
63 &self.0
64 }
65}
66
67pub trait ValidateExt: validator::Validate + Sized {
75 fn validate(self) -> crate::AutumnResult<Validated<Self>> {
82 if let Err(errors) = validator::Validate::validate(&self) {
83 return Err(validation_errors_to_autumn_error(&errors));
84 }
85 Ok(Validated::new(self))
86 }
87}
88
89impl<T: validator::Validate> ValidateExt for T {}
90
91pub struct Valid<T>(pub T);
112
113impl<S, T, Inner> FromRequest<S> for Valid<Inner>
114where
115 S: Send + Sync,
116 Inner: FromRequest<S> + AsValidatable<Inner = T>,
117 Inner::Rejection: IntoResponse,
118 T: validator::Validate,
119{
120 type Rejection = Response;
121
122 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
123 let inner = Inner::from_request(req, state)
124 .await
125 .map_err(IntoResponse::into_response)?;
126
127 let value = inner.as_validatable();
128 if let Err(errors) = validator::Validate::validate(value) {
129 return Err(
130 crate::AutumnError::validation(validation_errors_to_map(&errors)).into_response(),
131 );
132 }
133
134 Ok(Self(inner))
135 }
136}
137
138pub trait AsValidatable {
141 type Inner;
143 fn as_validatable(&self) -> &Self::Inner;
145}
146
147impl<T> AsValidatable for axum::Json<T> {
148 type Inner = T;
149 fn as_validatable(&self) -> &T {
150 &self.0
151 }
152}
153
154impl<T> AsValidatable for crate::extract::Json<T> {
155 type Inner = T;
156 fn as_validatable(&self) -> &T {
157 &self.0
158 }
159}
160
161impl<T> AsValidatable for axum::extract::Form<T> {
162 type Inner = T;
163 fn as_validatable(&self) -> &T {
164 &self.0
165 }
166}
167
168impl<T> AsValidatable for crate::extract::Form<T> {
169 type Inner = T;
170 fn as_validatable(&self) -> &T {
171 &self.0
172 }
173}
174
175impl<T> AsValidatable for axum::extract::Query<T> {
176 type Inner = T;
177 fn as_validatable(&self) -> &T {
178 &self.0
179 }
180}
181
182impl<T> AsValidatable for crate::extract::Query<T> {
183 type Inner = T;
184 fn as_validatable(&self) -> &T {
185 &self.0
186 }
187}
188
189pub(crate) fn validation_errors_to_map(
191 errors: &validator::ValidationErrors,
192) -> HashMap<String, Vec<String>> {
193 errors
194 .field_errors()
195 .into_iter()
196 .map(|(field, errs)| {
197 let messages = errs
198 .iter()
199 .map(|e| {
200 e.message.as_ref().map_or_else(
201 || format!("validation failed: {}", e.code),
202 ToString::to_string,
203 )
204 })
205 .collect();
206 (field.to_string(), messages)
207 })
208 .collect()
209}
210
211fn validation_errors_to_autumn_error(errors: &validator::ValidationErrors) -> crate::AutumnError {
217 crate::AutumnError::validation(validation_errors_to_map(errors))
218}
219
220#[doc(hidden)]
237pub struct MaybeValidate<'a, T>(pub &'a T);
238
239#[doc(hidden)]
242pub trait MaybeValidateViaValidator {
243 fn autumn_maybe_validate(&self) -> crate::AutumnResult<()>;
248}
249
250impl<T: validator::Validate> MaybeValidateViaValidator for MaybeValidate<'_, T> {
251 fn autumn_maybe_validate(&self) -> crate::AutumnResult<()> {
252 match validator::Validate::validate(self.0) {
253 Ok(()) => Ok(()),
254 Err(errors) => Err(validation_errors_to_autumn_error(&errors)),
255 }
256 }
257}
258
259#[doc(hidden)]
262pub trait MaybeValidateFallback {
263 fn autumn_maybe_validate(&self) -> crate::AutumnResult<()>;
268}
269
270impl<T> MaybeValidateFallback for &MaybeValidate<'_, T> {
271 fn autumn_maybe_validate(&self) -> crate::AutumnResult<()> {
272 Ok(())
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn validated_deref() {
282 let v = Validated::new(42);
283 assert_eq!(*v, 42);
284 }
285
286 #[test]
287 fn validated_into_inner() {
288 let v = Validated::new("hello".to_string());
289 let s = v.into_inner();
290 assert_eq!(s, "hello");
291 }
292
293 #[test]
294 fn validated_as_ref() {
295 let v = Validated::new(vec![1, 2, 3]);
296 let r: &Vec<i32> = v.as_ref();
297 assert_eq!(r.len(), 3);
298 }
299
300 #[test]
301 fn validation_errors_to_map_basic() {
302 #[derive(validator::Validate)]
303 struct TestForm {
304 #[validate(length(min = 5))]
305 name: String,
306 }
307
308 let form = TestForm {
309 name: "ab".to_string(),
310 };
311 let errors = validator::Validate::validate(&form).unwrap_err();
312 let map = validation_errors_to_map(&errors);
313
314 assert!(map.contains_key("name"));
315 assert_eq!(map["name"].len(), 1);
316 assert_eq!(map["name"][0], "validation failed: length");
317 }
318
319 #[test]
320 fn update_patch_field_validates_via_derive() {
321 use crate::hooks::Patch;
326
327 #[derive(validator::Validate)]
328 struct UpdatePost {
329 #[validate(length(min = 1))]
330 title: Patch<String>,
331 }
332
333 let bad = UpdatePost {
335 title: Patch::Set(String::new()),
336 };
337 let errors = validator::Validate::validate(&bad).unwrap_err();
338 let map = validation_errors_to_map(&errors);
339 assert!(map.contains_key("title"));
340 assert_eq!(map["title"][0], "validation failed: length");
341
342 let unchanged = UpdatePost {
344 title: Patch::Unchanged,
345 };
346 assert!(validator::Validate::validate(&unchanged).is_ok());
347
348 let good = UpdatePost {
350 title: Patch::Set("hello".into()),
351 };
352 assert!(validator::Validate::validate(&good).is_ok());
353 }
354
355 #[test]
356 fn validate_ext_ok() {
357 #[derive(validator::Validate)]
358 struct GoodInput {
359 #[validate(length(min = 1))]
360 value: String,
361 }
362
363 let input = GoodInput {
364 value: "hello".into(),
365 };
366 let validated = input.validate();
367 assert!(validated.is_ok());
368 assert_eq!(validated.unwrap().value, "hello");
369 }
370
371 #[test]
372 fn validate_ext_err() {
373 #[derive(validator::Validate)]
374 struct BadInput {
375 #[validate(length(min = 5))]
376 value: String,
377 }
378
379 let input = BadInput { value: "hi".into() };
380 let result = input.validate();
381 assert!(result.is_err());
382 }
383
384 #[test]
385 fn validation_errors_convert_to_autumn_error() {
386 #[derive(validator::Validate)]
387 struct Form {
388 #[validate(email)]
389 email: String,
390 }
391
392 let form = Form {
393 email: "not-an-email".into(),
394 };
395 let errors = validator::Validate::validate(&form).unwrap_err();
396 let autumn_err = validation_errors_to_autumn_error(&errors);
397 assert_eq!(
398 autumn_err.status(),
399 axum::http::StatusCode::UNPROCESSABLE_ENTITY
400 );
401 }
402
403 #[test]
404 fn validation_errors_to_map_fallback_message() {
405 let mut errors = validator::ValidationErrors::new();
406 let error = validator::ValidationError::new("custom_code");
408 errors.add("my_field", error);
409
410 let map = validation_errors_to_map(&errors);
411
412 assert!(map.contains_key("my_field"));
413 assert_eq!(map["my_field"][0], "validation failed: custom_code");
414 }
415
416 #[tokio::test]
417 async fn valid_extractor_ok() {
418 use axum::body::Body;
419
420 #[derive(serde::Deserialize, validator::Validate)]
421 struct TestInput {
422 #[validate(length(min = 1))]
423 name: String,
424 }
425
426 let req = Request::builder()
427 .method("POST")
428 .header("content-type", "application/json")
429 .body(Body::from(r#"{"name": "Alice"}"#))
430 .unwrap();
431
432 let state = ();
433 let result = Valid::<axum::Json<TestInput>>::from_request(req, &state).await;
434
435 assert!(result.is_ok());
436 assert_eq!(result.unwrap().0.0.name, "Alice");
437 }
438
439 #[test]
440 #[allow(clippy::needless_borrow, unused_imports)]
443 fn maybe_validate_runs_for_validate_types() {
444 use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
447
448 #[derive(validator::Validate)]
449 struct HasRules {
450 #[validate(length(min = 5))]
451 name: String,
452 }
453
454 let bad = HasRules {
455 name: "ab".to_string(),
456 };
457 let err = (&MaybeValidate(&bad))
458 .autumn_maybe_validate()
459 .expect_err("short name must fail validation");
460 assert_eq!(err.status(), axum::http::StatusCode::UNPROCESSABLE_ENTITY);
461
462 let good = HasRules {
463 name: "alice".to_string(),
464 };
465 assert!(
466 (&MaybeValidate(&good)).autumn_maybe_validate().is_ok(),
467 "valid input must pass"
468 );
469 }
470
471 #[test]
472 #[allow(clippy::needless_borrow, unused_imports)]
473 fn maybe_validate_is_noop_for_non_validate_types() {
474 use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
477
478 struct NoRules {
479 _name: String,
480 }
481
482 let value = NoRules {
483 _name: "anything".to_string(),
484 };
485 assert!(
486 (&MaybeValidate(&value)).autumn_maybe_validate().is_ok(),
487 "types without validation rules must be accepted unchanged"
488 );
489 }
490
491 #[tokio::test]
492 async fn valid_extractor_err() {
493 use axum::body::Body;
494
495 #[derive(serde::Deserialize, validator::Validate)]
496 struct TestInput {
497 #[validate(length(min = 5))]
498 name: String,
499 }
500
501 let req = Request::builder()
502 .method("POST")
503 .header("content-type", "application/json")
504 .body(Body::from(r#"{"name": "Bob"}"#)) .unwrap();
506
507 let state = ();
508 let result = Valid::<axum::Json<TestInput>>::from_request(req, &state).await;
509
510 match result {
511 Ok(_) => panic!("Expected validation error"),
512 Err(response) => {
513 assert_eq!(
514 response.status(),
515 axum::http::StatusCode::UNPROCESSABLE_ENTITY
516 );
517 }
518 }
519 }
520}