1use std::collections::HashMap;
29
30use axum::extract::{FromRequest, Request};
31use axum::response::{IntoResponse, Response};
32
33pub struct Validated<T>(T);
43
44impl<T> Validated<T> {
45 pub(crate) const fn new(value: T) -> Self {
47 Self(value)
48 }
49
50 #[must_use]
52 pub fn into_inner(self) -> T {
53 self.0
54 }
55}
56
57impl<T> std::ops::Deref for Validated<T> {
58 type Target = T;
59 fn deref(&self) -> &T {
60 &self.0
61 }
62}
63
64impl<T> AsRef<T> for Validated<T> {
65 fn as_ref(&self) -> &T {
66 &self.0
67 }
68}
69
70pub trait ValidateExt: validator::Validate + Sized {
99 fn validate(self) -> crate::AutumnResult<Validated<Self>> {
106 if let Err(errors) = validator::Validate::validate(&self) {
107 return Err(validation_errors_to_autumn_error(&errors));
108 }
109 Ok(Validated::new(self))
110 }
111}
112
113impl<T: validator::Validate> ValidateExt for T {}
114
115pub struct Valid<T>(pub T);
136
137impl<S, T, Inner> FromRequest<S> for Valid<Inner>
138where
139 S: Send + Sync,
140 Inner: FromRequest<S> + AsValidatable<Inner = T>,
141 Inner::Rejection: IntoResponse,
142 T: validator::Validate,
143{
144 type Rejection = Response;
145
146 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
147 let inner = Inner::from_request(req, state)
148 .await
149 .map_err(IntoResponse::into_response)?;
150
151 let value = inner.as_validatable();
152 if let Err(errors) = validator::Validate::validate(value) {
153 return Err(
154 crate::AutumnError::validation(validation_errors_to_map(&errors)).into_response(),
155 );
156 }
157
158 Ok(Self(inner))
159 }
160}
161
162pub trait AsValidatable {
165 type Inner;
167 fn as_validatable(&self) -> &Self::Inner;
169}
170
171impl<T> AsValidatable for axum::Json<T> {
172 type Inner = T;
173 fn as_validatable(&self) -> &T {
174 &self.0
175 }
176}
177
178impl<T> AsValidatable for crate::extract::Json<T> {
179 type Inner = T;
180 fn as_validatable(&self) -> &T {
181 &self.0
182 }
183}
184
185impl<T> AsValidatable for axum::extract::Form<T> {
186 type Inner = T;
187 fn as_validatable(&self) -> &T {
188 &self.0
189 }
190}
191
192impl<T> AsValidatable for crate::extract::Form<T> {
193 type Inner = T;
194 fn as_validatable(&self) -> &T {
195 &self.0
196 }
197}
198
199impl<T> AsValidatable for axum::extract::Query<T> {
200 type Inner = T;
201 fn as_validatable(&self) -> &T {
202 &self.0
203 }
204}
205
206impl<T> AsValidatable for crate::extract::Query<T> {
207 type Inner = T;
208 fn as_validatable(&self) -> &T {
209 &self.0
210 }
211}
212
213pub(crate) fn validation_errors_to_map(
215 errors: &validator::ValidationErrors,
216) -> HashMap<String, Vec<String>> {
217 errors
218 .field_errors()
219 .into_iter()
220 .map(|(field, errs)| {
221 let messages = errs
222 .iter()
223 .map(|e| {
224 e.message.as_ref().map_or_else(
225 || format!("validation failed: {}", e.code),
226 ToString::to_string,
227 )
228 })
229 .collect();
230 (field.to_string(), messages)
231 })
232 .collect()
233}
234
235fn validation_errors_to_autumn_error(errors: &validator::ValidationErrors) -> crate::AutumnError {
241 crate::AutumnError::validation(validation_errors_to_map(errors))
242}
243
244#[doc(hidden)]
261pub struct MaybeValidate<'a, T>(pub &'a T);
262
263#[doc(hidden)]
266pub trait MaybeValidateViaValidator {
267 fn autumn_maybe_validate(&self) -> crate::AutumnResult<()>;
272}
273
274impl<T: validator::Validate> MaybeValidateViaValidator for MaybeValidate<'_, T> {
275 fn autumn_maybe_validate(&self) -> crate::AutumnResult<()> {
276 match validator::Validate::validate(self.0) {
277 Ok(()) => Ok(()),
278 Err(errors) => Err(validation_errors_to_autumn_error(&errors)),
279 }
280 }
281}
282
283#[doc(hidden)]
286pub trait MaybeValidateFallback {
287 fn autumn_maybe_validate(&self) -> crate::AutumnResult<()>;
292}
293
294impl<T> MaybeValidateFallback for &MaybeValidate<'_, T> {
295 fn autumn_maybe_validate(&self) -> crate::AutumnResult<()> {
296 Ok(())
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn validated_deref() {
306 let v = Validated::new(42);
307 assert_eq!(*v, 42);
308 }
309
310 #[test]
311 fn validated_into_inner() {
312 let v = Validated::new("hello".to_string());
313 let s = v.into_inner();
314 assert_eq!(s, "hello");
315 }
316
317 #[test]
318 fn validated_as_ref() {
319 let v = Validated::new(vec![1, 2, 3]);
320 let r: &Vec<i32> = v.as_ref();
321 assert_eq!(r.len(), 3);
322 }
323
324 #[test]
325 fn validation_errors_to_map_basic() {
326 #[derive(validator::Validate)]
327 struct TestForm {
328 #[validate(length(min = 5))]
329 name: String,
330 }
331
332 let form = TestForm {
333 name: "ab".to_string(),
334 };
335 let errors = validator::Validate::validate(&form).unwrap_err();
336 let map = validation_errors_to_map(&errors);
337
338 assert!(map.contains_key("name"));
339 assert_eq!(map["name"].len(), 1);
340 assert_eq!(map["name"][0], "validation failed: length");
341 }
342
343 #[test]
344 fn update_patch_field_validates_via_derive() {
345 use crate::hooks::Patch;
350
351 #[derive(validator::Validate)]
352 struct UpdatePost {
353 #[validate(length(min = 1))]
354 title: Patch<String>,
355 }
356
357 let bad = UpdatePost {
359 title: Patch::Set(String::new()),
360 };
361 let errors = validator::Validate::validate(&bad).unwrap_err();
362 let map = validation_errors_to_map(&errors);
363 assert!(map.contains_key("title"));
364 assert_eq!(map["title"][0], "validation failed: length");
365
366 let unchanged = UpdatePost {
368 title: Patch::Unchanged,
369 };
370 assert!(validator::Validate::validate(&unchanged).is_ok());
371
372 let good = UpdatePost {
374 title: Patch::Set("hello".into()),
375 };
376 assert!(validator::Validate::validate(&good).is_ok());
377 }
378
379 #[test]
380 fn validate_ext_ok() {
381 #[derive(validator::Validate)]
382 struct GoodInput {
383 #[validate(length(min = 1))]
384 value: String,
385 }
386
387 let input = GoodInput {
388 value: "hello".into(),
389 };
390 let validated = input.validate();
391 assert!(validated.is_ok());
392 assert_eq!(validated.unwrap().value, "hello");
393 }
394
395 #[test]
396 fn validate_ext_err() {
397 #[derive(validator::Validate)]
398 struct BadInput {
399 #[validate(length(min = 5))]
400 value: String,
401 }
402
403 let input = BadInput { value: "hi".into() };
404 let result = input.validate();
405 assert!(result.is_err());
406 }
407
408 #[test]
409 fn validation_errors_convert_to_autumn_error() {
410 #[derive(validator::Validate)]
411 struct Form {
412 #[validate(email)]
413 email: String,
414 }
415
416 let form = Form {
417 email: "not-an-email".into(),
418 };
419 let errors = validator::Validate::validate(&form).unwrap_err();
420 let autumn_err = validation_errors_to_autumn_error(&errors);
421 assert_eq!(
422 autumn_err.status(),
423 axum::http::StatusCode::UNPROCESSABLE_ENTITY
424 );
425 }
426
427 #[test]
428 fn validate_ext_failure_exposes_field_details() {
429 #[derive(validator::Validate)]
431 struct Form {
432 #[validate(email(message = "Must be a valid email address"))]
433 email: String,
434 }
435
436 let Err(err) = Form {
437 email: "not-an-email".into(),
438 }
439 .validate() else {
440 panic!("invalid email is rejected");
441 };
442
443 let details = err.details().expect("validation error carries details");
444 assert_eq!(
445 details["email"],
446 ["Must be a valid email address".to_owned()]
447 );
448 assert_eq!(err.code(), "autumn.validation_failed");
449 assert_eq!(
450 err.to_string(),
451 "Validation failed: email: Must be a valid email address"
452 );
453 }
454
455 #[test]
456 fn validation_errors_to_map_fallback_message() {
457 let mut errors = validator::ValidationErrors::new();
458 let error = validator::ValidationError::new("custom_code");
460 errors.add("my_field", error);
461
462 let map = validation_errors_to_map(&errors);
463
464 assert!(map.contains_key("my_field"));
465 assert_eq!(map["my_field"][0], "validation failed: custom_code");
466 }
467
468 #[tokio::test]
469 async fn valid_extractor_ok() {
470 use axum::body::Body;
471
472 #[derive(serde::Deserialize, validator::Validate)]
473 struct TestInput {
474 #[validate(length(min = 1))]
475 name: String,
476 }
477
478 let req = Request::builder()
479 .method("POST")
480 .header("content-type", "application/json")
481 .body(Body::from(r#"{"name": "Alice"}"#))
482 .unwrap();
483
484 let state = ();
485 let result = Valid::<axum::Json<TestInput>>::from_request(req, &state).await;
486
487 assert!(result.is_ok());
488 assert_eq!(result.unwrap().0.0.name, "Alice");
489 }
490
491 #[test]
492 #[allow(clippy::needless_borrow, unused_imports)]
495 fn maybe_validate_runs_for_validate_types() {
496 use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
499
500 #[derive(validator::Validate)]
501 struct HasRules {
502 #[validate(length(min = 5))]
503 name: String,
504 }
505
506 let bad = HasRules {
507 name: "ab".to_string(),
508 };
509 let err = (&MaybeValidate(&bad))
510 .autumn_maybe_validate()
511 .expect_err("short name must fail validation");
512 assert_eq!(err.status(), axum::http::StatusCode::UNPROCESSABLE_ENTITY);
513
514 let good = HasRules {
515 name: "alice".to_string(),
516 };
517 assert!(
518 (&MaybeValidate(&good)).autumn_maybe_validate().is_ok(),
519 "valid input must pass"
520 );
521 }
522
523 #[test]
524 #[allow(clippy::needless_borrow, unused_imports)]
525 fn maybe_validate_survives_an_extra_borrow() {
526 use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
533
534 #[derive(validator::Validate)]
535 struct HasRules {
536 #[validate(length(min = 5))]
537 name: String,
538 }
539
540 let owned = HasRules {
541 name: "ab".to_string(),
542 };
543 let payload: &HasRules = &owned;
544 assert!(
545 (&MaybeValidate(payload)).autumn_maybe_validate().is_err(),
546 "the emitted form must reach the validating branch"
547 );
548 assert!(
549 (&MaybeValidate(&payload)).autumn_maybe_validate().is_err(),
550 "a wrapped `&&New*` must not fall through to the no-op arm"
551 );
552 }
553
554 #[test]
555 #[allow(clippy::needless_borrow, unused_imports)]
556 fn maybe_validate_is_noop_for_non_validate_types() {
557 use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
560
561 struct NoRules {
562 _name: String,
563 }
564
565 let value = NoRules {
566 _name: "anything".to_string(),
567 };
568 assert!(
569 (&MaybeValidate(&value)).autumn_maybe_validate().is_ok(),
570 "types without validation rules must be accepted unchanged"
571 );
572 }
573
574 #[tokio::test]
575 async fn valid_extractor_err() {
576 use axum::body::Body;
577
578 #[derive(serde::Deserialize, validator::Validate)]
579 struct TestInput {
580 #[validate(length(min = 5))]
581 name: String,
582 }
583
584 let req = Request::builder()
585 .method("POST")
586 .header("content-type", "application/json")
587 .body(Body::from(r#"{"name": "Bob"}"#)) .unwrap();
589
590 let state = ();
591 let result = Valid::<axum::Json<TestInput>>::from_request(req, &state).await;
592
593 match result {
594 Ok(_) => panic!("Expected validation error"),
595 Err(response) => {
596 assert_eq!(
597 response.status(),
598 axum::http::StatusCode::UNPROCESSABLE_ENTITY
599 );
600 }
601 }
602 }
603}