1use axum::{
102 extract::{FromRequest, Request},
103 response::{IntoResponse, Response},
104 Json,
105};
106use domainstack::ValidationError;
107use std::marker::PhantomData;
108
109pub struct DomainJson<T, Dto = ()> {
110 pub domain: T,
111 _dto: PhantomData<Dto>,
112}
113
114impl<T, Dto> DomainJson<T, Dto> {
115 pub fn new(domain: T) -> Self {
116 Self {
117 domain,
118 _dto: PhantomData,
119 }
120 }
121}
122
123pub struct ErrorResponse(pub error_envelope::Error);
124
125impl From<error_envelope::Error> for ErrorResponse {
126 fn from(err: error_envelope::Error) -> Self {
127 ErrorResponse(err)
128 }
129}
130
131impl From<ValidationError> for ErrorResponse {
132 fn from(err: ValidationError) -> Self {
133 use domainstack_envelope::IntoEnvelopeError;
134 ErrorResponse(err.into_envelope_error())
135 }
136}
137
138#[axum::async_trait]
139impl<T, Dto, S> FromRequest<S> for DomainJson<T, Dto>
140where
141 Dto: serde::de::DeserializeOwned,
142 T: TryFrom<Dto, Error = ValidationError>,
143 S: Send + Sync,
144{
145 type Rejection = ErrorResponse;
146
147 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
148 let Json(dto) = Json::<Dto>::from_request(req, state).await.map_err(|e| {
149 ErrorResponse(error_envelope::Error::bad_request(format!(
150 "Invalid JSON: {}",
151 e
152 )))
153 })?;
154
155 let domain = domainstack_http::into_domain(dto).map_err(ErrorResponse)?;
156
157 Ok(DomainJson::new(domain))
158 }
159}
160
161impl IntoResponse for ErrorResponse {
162 fn into_response(self) -> Response {
163 let status = axum::http::StatusCode::from_u16(self.0.status)
164 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
165
166 let body = serde_json::to_string(&self.0).unwrap_or_else(|_| {
167 r#"{"code":"INTERNAL","message":"Serialization failed"}"#.to_string()
168 });
169
170 (status, body).into_response()
171 }
172}
173
174pub struct ValidatedJson<Dto>(pub Dto);
175
176#[axum::async_trait]
177impl<Dto, S> FromRequest<S> for ValidatedJson<Dto>
178where
179 Dto: serde::de::DeserializeOwned + domainstack::Validate,
180 S: Send + Sync,
181{
182 type Rejection = ErrorResponse;
183
184 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
185 let Json(dto) = Json::<Dto>::from_request(req, state).await.map_err(|e| {
186 ErrorResponse(error_envelope::Error::bad_request(format!(
187 "Invalid JSON: {}",
188 e
189 )))
190 })?;
191
192 dto.validate().map(|_| ValidatedJson(dto)).map_err(|e| {
193 use domainstack_envelope::IntoEnvelopeError;
194 ErrorResponse(e.into_envelope_error())
195 })
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use axum::{routing::post, Router};
203 use domainstack::{prelude::*, Validate};
204
205 #[derive(Debug, Clone, serde::Deserialize)]
208 struct UserDto {
209 name: String,
210 age: u8,
211 }
212
213 #[derive(Debug, serde::Serialize)]
214 struct User {
215 name: String,
216 age: u8,
217 }
218
219 impl TryFrom<UserDto> for User {
220 type Error = ValidationError;
221
222 fn try_from(dto: UserDto) -> Result<Self, Self::Error> {
223 let mut err = ValidationError::new();
224
225 let name_rule = rules::min_len(2).and(rules::max_len(50));
226 if let Err(e) = validate("name", dto.name.as_str(), &name_rule) {
227 err.extend(e);
228 }
229
230 let age_rule = rules::range(18, 120);
231 if let Err(e) = validate("age", &dto.age, &age_rule) {
232 err.extend(e);
233 }
234
235 if !err.is_empty() {
236 return Err(err);
237 }
238
239 Ok(Self {
240 name: dto.name,
241 age: dto.age,
242 })
243 }
244 }
245
246 async fn create_user(DomainJson { domain: user, .. }: DomainJson<User, UserDto>) -> Json<User> {
247 Json(user)
248 }
249
250 type UserJson = DomainJson<User, UserDto>;
251
252 async fn create_user_with_alias(UserJson { domain: user, .. }: UserJson) -> Json<User> {
253 Json(user)
254 }
255
256 async fn create_user_result_style(
257 UserJson { domain: user, .. }: UserJson,
258 ) -> Result<Json<User>, ErrorResponse> {
259 Ok(Json(user))
260 }
261
262 #[tokio::test]
263 async fn test_domain_json_valid() {
264 let app = Router::new().route("/", post(create_user));
265
266 let server = axum_test::TestServer::new(app).unwrap();
267
268 let response = server
269 .post("/")
270 .json(&serde_json::json!({"name": "Alice", "age": 30}))
271 .await;
272
273 response.assert_status_ok();
274 }
275
276 #[tokio::test]
277 async fn test_domain_json_invalid() {
278 let app = Router::new().route("/", post(create_user));
279
280 let server = axum_test::TestServer::new(app).unwrap();
281
282 let response = server
283 .post("/")
284 .json(&serde_json::json!({"name": "A", "age": 200}))
285 .await;
286
287 response.assert_status_bad_request();
288
289 let body: serde_json::Value = response.json();
290
291 assert!(body["details"].is_object());
292 assert_eq!(
293 body["message"].as_str().unwrap(),
294 "Validation failed with 2 errors"
295 );
296
297 let details = body["details"].as_object().unwrap();
298 let fields = details["fields"].as_object().unwrap();
299
300 assert!(fields.contains_key("name"));
301 assert!(fields.contains_key("age"));
302 }
303
304 #[tokio::test]
305 async fn test_domain_json_malformed_json() {
306 let app = Router::new().route("/", post(create_user));
307
308 let server = axum_test::TestServer::new(app).unwrap();
309
310 let response = server.post("/").text("{invalid json").await;
311
312 response.assert_status_bad_request();
313 }
314
315 #[tokio::test]
316 async fn test_domain_json_missing_fields() {
317 let app = Router::new().route("/", post(create_user));
318
319 let server = axum_test::TestServer::new(app).unwrap();
320
321 let response = server
322 .post("/")
323 .json(&serde_json::json!({"name": "Alice"}))
324 .await;
325
326 response.assert_status_bad_request();
327 }
328
329 #[tokio::test]
330 async fn test_type_alias_pattern() {
331 let app = Router::new().route("/", post(create_user_with_alias));
332
333 let server = axum_test::TestServer::new(app).unwrap();
334
335 let response = server
336 .post("/")
337 .json(&serde_json::json!({"name": "Alice", "age": 30}))
338 .await;
339
340 response.assert_status_ok();
341 }
342
343 #[tokio::test]
344 async fn test_result_style_handler() {
345 let app = Router::new().route("/", post(create_user_result_style));
346
347 let server = axum_test::TestServer::new(app).unwrap();
348
349 let response = server
350 .post("/")
351 .json(&serde_json::json!({"name": "Alice", "age": 30}))
352 .await;
353
354 response.assert_status_ok();
355 }
356
357 #[derive(Debug, Clone, Validate, serde::Deserialize, serde::Serialize)]
359 struct ValidatedUserDto {
360 #[validate(length(min = 2, max = 50))]
361 name: String,
362
363 #[validate(range(min = 18, max = 120))]
364 age: u8,
365 }
366
367 async fn accept_validated_dto(
368 ValidatedJson(dto): ValidatedJson<ValidatedUserDto>,
369 ) -> Json<ValidatedUserDto> {
370 Json(dto)
371 }
372
373 #[tokio::test]
374 async fn test_validated_json_valid() {
375 let app = Router::new().route("/", post(accept_validated_dto));
376
377 let server = axum_test::TestServer::new(app).unwrap();
378
379 let response = server
380 .post("/")
381 .json(&serde_json::json!({"name": "Alice", "age": 30}))
382 .await;
383
384 response.assert_status_ok();
385 let body: ValidatedUserDto = response.json();
386 assert_eq!(body.name, "Alice");
387 assert_eq!(body.age, 30);
388 }
389
390 #[tokio::test]
391 async fn test_validated_json_invalid() {
392 let app = Router::new().route("/", post(accept_validated_dto));
393
394 let server = axum_test::TestServer::new(app).unwrap();
395
396 let response = server
397 .post("/")
398 .json(&serde_json::json!({"name": "A", "age": 200}))
399 .await;
400
401 response.assert_status_bad_request();
402
403 let body: serde_json::Value = response.json();
404 assert_eq!(
405 body["message"].as_str().unwrap(),
406 "Validation failed with 2 errors"
407 );
408
409 let details = body["details"].as_object().unwrap();
410 let fields = details["fields"].as_object().unwrap();
411
412 assert!(fields.contains_key("name"));
413 assert!(fields.contains_key("age"));
414 }
415
416 #[tokio::test]
417 async fn test_validated_json_malformed_json() {
418 let app = Router::new().route("/", post(accept_validated_dto));
419
420 let server = axum_test::TestServer::new(app).unwrap();
421
422 let response = server.post("/").text("{invalid json").await;
423
424 response.assert_status_bad_request();
425 }
426
427 #[tokio::test]
428 async fn test_error_response_into_response() {
429 let err = ErrorResponse(error_envelope::Error::bad_request("Test error"));
430 let response = err.into_response();
431 assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
432 }
433
434 #[tokio::test]
435 async fn test_error_response_custom_status() {
436 let mut err = error_envelope::Error::bad_request("Test");
437 err.status = 422;
438 let response = ErrorResponse(err).into_response();
439 assert_eq!(
440 response.status(),
441 axum::http::StatusCode::UNPROCESSABLE_ENTITY
442 );
443 }
444}