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(r#"{"name":"abc"}"#, Some("application/json; charset=utf-8"))
135 .await
136 .unwrap();
137 assert_eq!(input.name, "abc");
138 }
139
140 #[tokio::test]
141 async fn vendor_plus_json_is_accepted() {
142 let input = extract(r#"{"name":"abc"}"#, Some("application/vnd.api+json"))
143 .await
144 .unwrap();
145 assert_eq!(input.name, "abc");
146 }
147
148 #[tokio::test]
149 async fn serializes_as_response() {
150 #[derive(Serialize)]
151 struct Out {
152 id: u32,
153 }
154 let res = ApiJson(Out { id: 7 }).into_response();
155 assert_eq!(res.status(), StatusCode::OK);
156 let bytes = axum::body::to_bytes(res.into_body(), usize::MAX)
157 .await
158 .unwrap();
159 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
160 assert_eq!(v["id"], 7);
161 }
162}