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(
85 body: &str,
86 content_type: Option<&str>,
87 ) -> Result<Input, (StatusCode, ApiError)> {
88 let mut builder = Request::builder().method("POST").uri("/");
89 if let Some(value) = content_type {
90 builder = builder.header(CONTENT_TYPE, value);
91 }
92 let req = builder.body(Body::from(body.to_owned())).unwrap();
93 ApiJson::<Input>::from_request(req, &())
94 .await
95 .map(|ApiJson(v)| v)
96 .map_err(|(status, Json(err))| (status, err))
97 }
98
99 #[tokio::test]
100 async fn valid_body_extracts() {
101 let input = extract(r#"{"name":"abc"}"#, Some("application/json"))
102 .await
103 .unwrap();
104 assert_eq!(input.name, "abc");
105 }
106
107 #[tokio::test]
108 async fn malformed_json_is_invalid_json() {
109 let (status, err) = extract("{not json", Some("application/json"))
110 .await
111 .unwrap_err();
112 assert_eq!(status, StatusCode::BAD_REQUEST);
113 assert_eq!(err.code, "INVALID_JSON");
114 }
115
116 #[tokio::test]
117 async fn wrong_shape_is_invalid_body() {
118 let (status, err) = extract(r#"{"name":123}"#, Some("application/json"))
119 .await
120 .unwrap_err();
121 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
122 assert_eq!(err.code, "INVALID_BODY");
123 }
124
125 #[tokio::test]
126 async fn missing_content_type_is_unsupported_media_type() {
127 let (status, err) = extract(r#"{"name":"abc"}"#, None).await.unwrap_err();
128 assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
129 assert_eq!(err.code, "UNSUPPORTED_MEDIA_TYPE");
130 }
131
132 #[tokio::test]
133 async fn json_with_charset_is_accepted() {
134 let input = extract(
135 r#"{"name":"abc"}"#,
136 Some("application/json; charset=utf-8"),
137 )
138 .await
139 .unwrap();
140 assert_eq!(input.name, "abc");
141 }
142
143 #[tokio::test]
144 async fn vendor_plus_json_is_accepted() {
145 let input = extract(r#"{"name":"abc"}"#, Some("application/vnd.api+json"))
146 .await
147 .unwrap();
148 assert_eq!(input.name, "abc");
149 }
150
151 #[tokio::test]
152 async fn serializes_as_response() {
153 #[derive(Serialize)]
154 struct Out {
155 id: u32,
156 }
157 let res = ApiJson(Out { id: 7 }).into_response();
158 assert_eq!(res.status(), StatusCode::OK);
159 let bytes = axum::body::to_bytes(res.into_body(), usize::MAX)
160 .await
161 .unwrap();
162 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
163 assert_eq!(v["id"], 7);
164 }
165}