this/core/validation/
extractor.rs1use super::config::EntityValidationConfig;
7use axum::{
8 Json,
9 extract::{FromRequest, Request},
10 http::StatusCode,
11 response::{IntoResponse, Response},
12};
13use serde_json::{Value, json};
14
15pub trait ValidatableEntity {
19 fn validation_config(operation: &str) -> EntityValidationConfig;
21}
22
23pub struct Validated<T>(pub Value, std::marker::PhantomData<T>);
35
36impl<T> Validated<T> {
37 pub fn new(payload: Value) -> Self {
39 Self(payload, std::marker::PhantomData)
40 }
41
42 pub fn into_inner(self) -> Value {
44 self.0
45 }
46}
47
48impl<T> std::ops::Deref for Validated<T> {
50 type Target = Value;
51
52 fn deref(&self) -> &Self::Target {
53 &self.0
54 }
55}
56
57impl<S, T> FromRequest<S> for Validated<T>
58where
59 S: Send + Sync,
60 T: ValidatableEntity + Send + Sync,
61{
62 type Rejection = Response;
63
64 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
65 let method = req.method().clone();
67
68 let Json(payload): Json<Value> = match Json::from_request(req, state).await {
70 Ok(json) => json,
71 Err(e) => {
72 return Err((
73 StatusCode::BAD_REQUEST,
74 Json(json!({
75 "error": "Invalid JSON",
76 "details": e.to_string()
77 })),
78 )
79 .into_response());
80 }
81 };
82
83 let operation = match method.as_str() {
85 "POST" => "create",
86 "PUT" | "PATCH" => "update",
87 _ => "create", };
89
90 let config = T::validation_config(operation);
92
93 match config.validate_and_filter(payload) {
95 Ok(validated_payload) => Ok(Validated::new(validated_payload)),
96 Err(errors) => Err((
97 StatusCode::UNPROCESSABLE_ENTITY,
98 Json(json!({
99 "error": "Validation failed",
100 "errors": errors
101 })),
102 )
103 .into_response()),
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use axum::body::Body;
112 use axum::extract::FromRequest;
113 use axum::http::Request;
114 use serde_json::json;
115
116 struct TestEntity;
120
121 impl ValidatableEntity for TestEntity {
122 fn validation_config(operation: &str) -> EntityValidationConfig {
123 let mut config = EntityValidationConfig::new("test_entity");
124 if operation == "create" {
125 config.add_validator("name", |field, value| {
126 if value.is_null() {
127 Err(format!("{} is required", field))
128 } else {
129 Ok(())
130 }
131 });
132 config.add_validator("name", |field, value| {
133 if let Some(s) = value.as_str()
134 && s.len() < 2
135 {
136 return Err(format!("{} too short", field));
137 }
138 Ok(())
139 });
140 config.add_filter("name", |_field, value| {
141 if let Some(s) = value.as_str() {
142 Ok(Value::String(s.trim().to_string()))
143 } else {
144 Ok(value)
145 }
146 });
147 }
148 config
149 }
150 }
151
152 fn json_request(method: &str, body: Value) -> Request<Body> {
154 Request::builder()
155 .method(method)
156 .header("content-type", "application/json")
157 .body(Body::from(serde_json::to_vec(&body).unwrap()))
158 .unwrap()
159 }
160
161 #[test]
164 fn test_validated_new_and_into_inner() {
165 let val = json!({"name": "test"});
166 let validated = Validated::<TestEntity>::new(val.clone());
167 assert_eq!(validated.into_inner(), val);
168 }
169
170 #[test]
171 fn test_validated_deref() {
172 let val = json!({"key": 42});
173 let validated = Validated::<TestEntity>::new(val);
174 assert_eq!(validated["key"], 42);
176 assert!(validated.is_object());
177 }
178
179 #[tokio::test]
182 async fn test_from_request_post_valid_payload() {
183 let req = json_request("POST", json!({"name": " Alice "}));
184 let result = Validated::<TestEntity>::from_request(req, &()).await;
185 assert!(result.is_ok());
186 let validated = result.unwrap();
187 assert_eq!(validated.0["name"], "Alice");
189 }
190
191 #[tokio::test]
192 async fn test_from_request_post_validation_failure() {
193 let req = json_request("POST", json!({"name": null}));
195 let result = Validated::<TestEntity>::from_request(req, &()).await;
196 assert!(result.is_err());
197 match result {
198 Err(response) => assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY),
199 Ok(_) => panic!("expected error"),
200 }
201 }
202
203 #[tokio::test]
204 async fn test_from_request_post_too_short_after_trim() {
205 let req = json_request("POST", json!({"name": " a "}));
207 let result = Validated::<TestEntity>::from_request(req, &()).await;
208 assert!(result.is_err());
209 match result {
210 Err(response) => assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY),
211 Ok(_) => panic!("expected error"),
212 }
213 }
214
215 #[tokio::test]
216 async fn test_from_request_put_uses_update_operation() {
217 let req = json_request("PUT", json!({"name": null}));
219 let result = Validated::<TestEntity>::from_request(req, &()).await;
220 assert!(result.is_ok());
221 }
222
223 #[tokio::test]
224 async fn test_from_request_patch_uses_update_operation() {
225 let req = json_request("PATCH", json!({"name": null}));
226 let result = Validated::<TestEntity>::from_request(req, &()).await;
227 assert!(result.is_ok());
228 }
229
230 #[tokio::test]
231 async fn test_from_request_get_defaults_to_create_operation() {
232 let req = json_request("GET", json!({"name": null}));
234 let result = Validated::<TestEntity>::from_request(req, &()).await;
235 assert!(result.is_err());
236 }
237
238 #[tokio::test]
239 async fn test_from_request_invalid_json_returns_400() {
240 let req = Request::builder()
241 .method("POST")
242 .header("content-type", "application/json")
243 .body(Body::from("not valid json {{{"))
244 .unwrap();
245 let result = Validated::<TestEntity>::from_request(req, &()).await;
246 match result {
247 Err(response) => assert_eq!(response.status(), StatusCode::BAD_REQUEST),
248 Ok(_) => panic!("expected error"),
249 }
250 }
251
252 #[tokio::test]
253 async fn test_from_request_missing_content_type_returns_400() {
254 let req = Request::builder()
255 .method("POST")
256 .body(Body::from(r#"{"name": "test"}"#))
257 .unwrap();
258 let result = Validated::<TestEntity>::from_request(req, &()).await;
259 match result {
260 Err(response) => assert_eq!(response.status(), StatusCode::BAD_REQUEST),
261 Ok(_) => panic!("expected error"),
262 }
263 }
264}