axum_api_kit/
validated.rs1use axum::{
2 extract::{FromRequest, Request},
3 http::StatusCode,
4 Json,
5};
6use serde::de::DeserializeOwned;
7use validator::Validate;
8
9use crate::error::json_rejection_to_api_error;
10use crate::ApiError;
11
12#[derive(Debug, Clone)]
48pub struct ValidatedJson<T>(pub T);
49
50impl<T, S> FromRequest<S> for ValidatedJson<T>
51where
52 T: DeserializeOwned + Validate,
53 S: Send + Sync,
54{
55 type Rejection = (StatusCode, Json<ApiError>);
56
57 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
58 let Json(value) = Json::<T>::from_request(req, state)
59 .await
60 .map_err(json_rejection_to_api_error)?;
61
62 value.validate().map_err(|errors| {
63 (
64 StatusCode::UNPROCESSABLE_ENTITY,
65 Json(ApiError::from(errors)),
66 )
67 })?;
68
69 Ok(ValidatedJson(value))
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use axum::{body::Body, http::header::CONTENT_TYPE, http::Request};
77 use serde::Deserialize;
78
79 #[derive(Debug, Deserialize, Validate)]
80 struct Input {
81 #[validate(length(min = 2))]
82 name: String,
83 }
84
85 async fn extract(body: &str, with_content_type: bool) -> Result<Input, (StatusCode, ApiError)> {
86 let mut builder = Request::builder().method("POST").uri("/");
87 if with_content_type {
88 builder = builder.header(CONTENT_TYPE, "application/json");
89 }
90 let req = builder.body(Body::from(body.to_owned())).unwrap();
91 ValidatedJson::<Input>::from_request(req, &())
92 .await
93 .map(|ValidatedJson(v)| v)
94 .map_err(|(status, Json(err))| (status, err))
95 }
96
97 #[tokio::test]
98 async fn valid_body_extracts() {
99 let input = extract(r#"{"name":"abcd"}"#, true).await.unwrap();
100 assert_eq!(input.name, "abcd");
101 }
102
103 #[tokio::test]
104 async fn malformed_json_is_invalid_json() {
105 let (status, err) = extract("{not json", true).await.unwrap_err();
106 assert_eq!(status, StatusCode::BAD_REQUEST);
107 assert_eq!(err.code, "INVALID_JSON");
108 }
109
110 #[tokio::test]
111 async fn wrong_shape_is_invalid_body() {
112 let (status, err) = extract(r#"{"name":123}"#, true).await.unwrap_err();
113 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
114 assert_eq!(err.code, "INVALID_BODY");
115 }
116
117 #[tokio::test]
118 async fn missing_content_type_is_unsupported_media_type() {
119 let (status, err) = extract(r#"{"name":"abcd"}"#, false).await.unwrap_err();
120 assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
121 assert_eq!(err.code, "UNSUPPORTED_MEDIA_TYPE");
122 }
123
124 #[tokio::test]
125 async fn validation_failure_is_validation_error_with_fields() {
126 let (status, err) = extract(r#"{"name":"a"}"#, true).await.unwrap_err();
127 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
128 assert_eq!(err.code, "VALIDATION_ERROR");
129 let v = serde_json::to_value(&err).unwrap();
130 assert!(v["details"]["fields"]["name"].is_array());
131 }
132}