1use crate::{ApiErrorResponse, HttpRequestContext};
2use axum::extract::{FromRequest, Json, Request};
3use platform_core::AppError;
4use platform_core::error::ErrorDetail;
5use serde::de::DeserializeOwned;
6
7#[derive(Debug, Clone, Copy, Default)]
8pub struct JsonBody<T>(pub T);
9
10impl<S, T> FromRequest<S> for JsonBody<T>
11where
12 S: Send + Sync,
13 T: DeserializeOwned,
14{
15 type Rejection = ApiErrorResponse;
16
17 async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
18 let context = request.extensions().get::<HttpRequestContext>().cloned();
19
20 Json::<T>::from_request(request, state)
21 .await
22 .map(|Json(value)| Self(value))
23 .map_err(|rejection| {
24 let error = AppError::validation(
25 "Request validation failed",
26 vec![ErrorDetail {
27 field: None,
28 reason: json_rejection_reason(&rejection).to_owned(),
29 }],
30 );
31
32 match context {
33 Some(ctx) => ApiErrorResponse::with_context(error, &ctx),
34 None => error.into(),
35 }
36 })
37 }
38}
39
40fn json_rejection_reason(rejection: &axum::extract::rejection::JsonRejection) -> &'static str {
41 match rejection {
42 axum::extract::rejection::JsonRejection::JsonDataError(_) => {
43 "Request body contains invalid JSON data"
44 }
45 axum::extract::rejection::JsonRejection::JsonSyntaxError(_) => {
46 "Request body contains malformed JSON"
47 }
48 axum::extract::rejection::JsonRejection::MissingJsonContentType(_) => {
49 "Request body must be JSON"
50 }
51 axum::extract::rejection::JsonRejection::BytesRejection(_) => {
52 "Request body could not be read"
53 }
54 _ => "Request body is invalid",
55 }
56}