1use axum::{
2 extract::{FromRequest, Request},
3 http::StatusCode,
4 response::{IntoResponse, Response},
5 Json,
6};
7use serde::{de::DeserializeOwned, Serialize};
8
9use crate::error::json_rejection_to_api_error;
10use crate::ApiError;
11
12#[derive(Debug, Clone)]
50pub struct ApiJson<T>(pub T);
51
52impl<T, S> FromRequest<S> for ApiJson<T>
53where
54 T: DeserializeOwned,
55 S: Send + Sync,
56{
57 type Rejection = (StatusCode, Json<ApiError>);
58
59 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
60 let Json(value) = Json::<T>::from_request(req, state)
61 .await
62 .map_err(json_rejection_to_api_error)?;
63 Ok(ApiJson(value))
64 }
65}
66
67impl<T: Serialize> IntoResponse for ApiJson<T> {
68 fn into_response(self) -> Response {
69 Json(self.0).into_response()
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)]
80 struct Input {
81 name: String,
82 }
83
84 async fn extract(body: &str, with_content_type: bool) -> Result<Input, (StatusCode, ApiError)> {
85 let mut builder = Request::builder().method("POST").uri("/");
86 if with_content_type {
87 builder = builder.header(CONTENT_TYPE, "application/json");
88 }
89 let req = builder.body(Body::from(body.to_owned())).unwrap();
90 ApiJson::<Input>::from_request(req, &())
91 .await
92 .map(|ApiJson(v)| v)
93 .map_err(|(status, Json(err))| (status, err))
94 }
95
96 #[tokio::test]
97 async fn valid_body_extracts() {
98 let input = extract(r#"{"name":"abc"}"#, true).await.unwrap();
99 assert_eq!(input.name, "abc");
100 }
101
102 #[tokio::test]
103 async fn malformed_json_is_invalid_json() {
104 let (status, err) = extract("{not json", true).await.unwrap_err();
105 assert_eq!(status, StatusCode::BAD_REQUEST);
106 assert_eq!(err.code, "INVALID_JSON");
107 }
108
109 #[tokio::test]
110 async fn wrong_shape_is_invalid_body() {
111 let (status, err) = extract(r#"{"name":123}"#, true).await.unwrap_err();
112 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
113 assert_eq!(err.code, "INVALID_BODY");
114 }
115
116 #[tokio::test]
117 async fn missing_content_type_is_unsupported_media_type() {
118 let (status, err) = extract(r#"{"name":"abc"}"#, false).await.unwrap_err();
119 assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
120 assert_eq!(err.code, "UNSUPPORTED_MEDIA_TYPE");
121 }
122
123 #[tokio::test]
124 async fn serializes_as_response() {
125 #[derive(Serialize)]
126 struct Out {
127 id: u32,
128 }
129 let res = ApiJson(Out { id: 7 }).into_response();
130 assert_eq!(res.status(), StatusCode::OK);
131 let bytes = axum::body::to_bytes(res.into_body(), usize::MAX)
132 .await
133 .unwrap();
134 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
135 assert_eq!(v["id"], 7);
136 }
137}