1use axum::{
20 async_trait,
21 body::Bytes,
22 extract::{FromRequest, Request},
23 http::{header::CONTENT_TYPE, StatusCode},
24 response::{IntoResponse, Response},
25 Json,
26};
27use serde::de::DeserializeOwned;
28
29use crate::http::ApiResponse;
30use crate::violation::Violation;
31
32pub struct JsonOrForm<T>(pub T);
35
36pub struct StrictJsonOrForm<T>(pub T);
43
44#[async_trait]
45impl<S, T> FromRequest<S> for JsonOrForm<T>
46where
47 S: Send + Sync,
48 T: DeserializeOwned + 'static,
49{
50 type Rejection = Response;
51
52 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
53 decode_body(req, state, false).await.map(Self)
54 }
55}
56
57#[async_trait]
58impl<S, T> FromRequest<S> for StrictJsonOrForm<T>
59where
60 S: Send + Sync,
61 T: DeserializeOwned + 'static,
62{
63 type Rejection = Response;
64
65 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
66 decode_body(req, state, true).await.map(Self)
67 }
68}
69
70const ECHOED_KEYS: &[&str] = &[
72 "id",
73 "metadata",
74 "created_at",
75 "updated_at",
76 "deleted_at",
77 "createdAt",
78 "updatedAt",
79 "deletedAt",
80];
81
82async fn decode_body<S, T>(req: Request, state: &S, strict: bool) -> Result<T, Response>
83where
84 S: Send + Sync,
85 T: DeserializeOwned,
86{
87 let is_form = req
88 .headers()
89 .get(CONTENT_TYPE)
90 .and_then(|v| v.to_str().ok())
91 .map(|ct| ct.starts_with("application/x-www-form-urlencoded"))
92 .unwrap_or(false);
93 let bytes = Bytes::from_request(req, state).await.map_err(|rejection| {
94 let status = rejection.status();
95 refusal(
96 status,
97 rejection.body_text(),
98 Violation::new("", "unreadable_body", "The request body could not be read"),
99 )
100 })?;
101
102 if is_form {
103 return serde_urlencoded::from_bytes(&bytes).map_err(|e| {
104 let message = e.to_string();
105 refusal(
106 StatusCode::UNPROCESSABLE_ENTITY,
107 format!("invalid form body: {message}"),
108 Violation::new("", "invalid_value", message),
109 )
110 });
111 }
112 decode_json(&bytes, strict)
113}
114
115pub(crate) struct Refusal {
117 status: StatusCode,
118 error: String,
119 violations: Vec<Violation>,
120}
121
122impl Refusal {
123 fn new(status: StatusCode, error: String, violations: Vec<Violation>) -> Self {
124 Self {
125 status,
126 error,
127 violations,
128 }
129 }
130
131 pub(crate) fn at_item(mut self, index: usize) -> Self {
133 for v in &mut self.violations {
134 v.path = crate::violation::batch_item_path(index, &v.path);
135 }
136 self.error = format!("item {index}: {}", self.error);
137 self
138 }
139
140 pub(crate) fn into_response(self) -> Response {
141 (
142 self.status,
143 Json(ApiResponse::<()>::rejected(self.error, self.violations)),
144 )
145 .into_response()
146 }
147}
148
149#[allow(clippy::result_large_err)]
153pub(crate) fn decode_json<T: DeserializeOwned>(bytes: &[u8], strict: bool) -> Result<T, Response> {
154 decode_json_or_refusal(bytes, strict).map_err(Refusal::into_response)
155}
156
157pub(crate) fn decode_json_or_refusal<T: DeserializeOwned>(
159 bytes: &[u8],
160 strict: bool,
161) -> Result<T, Refusal> {
162 let mut ignored: Vec<String> = Vec::new();
163 let mut de = serde_json::Deserializer::from_slice(bytes);
164 let decoded: Result<T, serde_path_to_error::Error<serde_json::Error>> = if strict {
165 let mut record = |path: serde_ignored::Path<'_>| {
166 if let Some(path) = ignored_key(&path) {
167 ignored.push(path);
168 }
169 };
170 let tracked = serde_ignored::Deserializer::new(&mut de, &mut record);
171 serde_path_to_error::deserialize(tracked)
172 } else {
173 serde_path_to_error::deserialize(&mut de)
174 };
175 let value = match decoded {
176 Ok(value) => value,
177 Err(e) => {
178 let path = path_segments(e.path());
179 return Err(json_refusal(path, e.into_inner()));
180 }
181 };
182 if let Err(e) = de.end() {
184 return Err(json_refusal(Vec::new(), e));
185 }
186 if !ignored.is_empty() {
187 ignored.sort();
188 ignored.dedup();
189 let violations = ignored
190 .iter()
191 .map(|path| {
192 let key = path.rsplit('/').next().unwrap_or(path);
193 Violation::new(
194 path.clone(),
195 "unknown_field",
196 format!("unknown_field: `{key}` is not a field this write accepts"),
197 )
198 })
199 .collect::<Vec<_>>();
200 let sentence = violations
201 .iter()
202 .map(|v| v.message.as_str())
203 .collect::<Vec<_>>()
204 .join("; ");
205 return Err(Refusal::new(
206 StatusCode::UNPROCESSABLE_ENTITY,
207 format!("validation failed: {sentence}"),
208 violations,
209 ));
210 }
211 Ok(value)
212}
213
214fn ignored_key(path: &serde_ignored::Path<'_>) -> Option<String> {
216 let mut segments = Vec::new();
217 collect_segments(path, &mut segments);
218 let echoed = segments.len() <= 2
219 && segments
220 .last()
221 .is_some_and(|(key, is_key)| *is_key && ECHOED_KEYS.contains(&key.as_str()))
222 && (segments.len() == 1 || !segments[0].1);
223 if echoed || segments.is_empty() {
224 return None;
225 }
226 Some(
227 segments
228 .into_iter()
229 .map(|(s, _)| s)
230 .collect::<Vec<_>>()
231 .join("/"),
232 )
233}
234
235fn collect_segments(path: &serde_ignored::Path<'_>, out: &mut Vec<(String, bool)>) {
237 use serde_ignored::Path;
238 match path {
239 Path::Root => {}
240 Path::Seq { parent, index } => {
241 collect_segments(parent, out);
242 out.push((index.to_string(), false));
243 }
244 Path::Map { parent, key } => {
245 collect_segments(parent, out);
246 out.push((key.clone(), true));
247 }
248 Path::Some { parent }
249 | Path::NewtypeStruct { parent }
250 | Path::NewtypeVariant { parent } => collect_segments(parent, out),
251 }
252}
253
254fn path_segments(path: &serde_path_to_error::Path) -> Vec<String> {
256 path.iter()
257 .filter_map(|segment| match segment {
258 serde_path_to_error::Segment::Seq { index } => Some(index.to_string()),
259 serde_path_to_error::Segment::Map { key } => Some(key.clone()),
260 serde_path_to_error::Segment::Enum { variant } => Some(variant.clone()),
261 serde_path_to_error::Segment::Unknown => None,
262 })
263 .collect()
264}
265
266fn json_refusal(mut path: Vec<String>, inner: serde_json::Error) -> Refusal {
268 use serde_json::error::Category;
269 let message = without_position(&inner.to_string());
270 match inner.classify() {
271 Category::Syntax | Category::Eof | Category::Io => Refusal::new(
272 StatusCode::BAD_REQUEST,
273 format!("malformed JSON body: {message}"),
274 vec![Violation::new(
275 "",
276 "malformed_body",
277 format!("The request body is not valid JSON: {message}"),
278 )],
279 ),
280 Category::Data => {
281 let (code, text) = match missing_field(&message) {
282 Some(field) => {
283 path.push(field.to_string());
284 ("required", format!("{field} is required"))
285 }
286 None => ("invalid_value", message.clone()),
287 };
288 let path = path.join("/");
289 let sentence = if path.is_empty() {
290 text.clone()
291 } else {
292 format!("{path}: {text}")
293 };
294 Refusal::new(
295 StatusCode::UNPROCESSABLE_ENTITY,
296 format!("validation failed: {sentence}"),
297 vec![Violation::new(path, code, text)],
298 )
299 }
300 }
301}
302
303fn missing_field(message: &str) -> Option<&str> {
305 message.strip_prefix("missing field `")?.split('`').next()
306}
307
308fn without_position(message: &str) -> String {
311 match message.rfind(" at line ") {
312 Some(at) => message[..at].to_string(),
313 None => message.to_string(),
314 }
315}
316
317fn refusal(status: StatusCode, error: String, violation: Violation) -> Response {
318 (
319 status,
320 Json(ApiResponse::<()>::rejected(error, vec![violation])),
321 )
322 .into_response()
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use serde::Deserialize;
329
330 #[derive(Debug, Deserialize)]
331 #[serde(rename_all = "camelCase")]
332 #[allow(dead_code)]
333 struct Line {
334 amount: i64,
335 #[serde(alias = "unit_price", default)]
336 unit_price: Option<i64>,
337 }
338
339 #[derive(Debug, Deserialize)]
340 #[allow(dead_code)]
341 struct Doc {
342 name: String,
343 #[serde(default)]
344 lines: Vec<Line>,
345 }
346
347 async fn body_of(r: Response) -> (StatusCode, serde_json::Value) {
348 let status = r.status();
349 let bytes = axum::body::to_bytes(r.into_body(), usize::MAX)
350 .await
351 .unwrap();
352 (status, serde_json::from_slice(&bytes).unwrap())
353 }
354
355 #[tokio::test]
356 async fn malformed_json_is_a_400_envelope() {
357 let (status, body) = body_of(decode_json::<Doc>(b"{\"name\": ", false).unwrap_err()).await;
358 assert_eq!(status, StatusCode::BAD_REQUEST);
359 assert_eq!(body["success"], false);
360 assert_eq!(body["violations"][0]["code"], "malformed_body");
361 }
362
363 #[tokio::test]
364 async fn a_wrong_value_names_its_field() {
365 let raw = br#"{"name":"a","lines":[{"amount":1},{"amount":"x"}]}"#;
366 let (status, body) = body_of(decode_json::<Doc>(raw, false).unwrap_err()).await;
367 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
368 assert_eq!(body["violations"][0]["path"], "lines/1/amount");
369 assert_eq!(body["violations"][0]["code"], "invalid_value");
370 }
371
372 #[tokio::test]
373 async fn a_missing_field_is_required_on_that_field() {
374 let (status, body) =
375 body_of(decode_json::<Doc>(br#"{"lines":[]}"#, false).unwrap_err()).await;
376 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
377 assert_eq!(body["violations"][0]["path"], "name");
378 assert_eq!(body["violations"][0]["code"], "required");
379 }
380
381 #[tokio::test]
382 async fn strict_refuses_a_key_the_type_does_not_take() {
383 let raw = br#"{"name":"a","colour":"red","lines":[{"amount":1,"discount":2}]}"#;
384 assert!(
385 decode_json::<Doc>(raw, false).is_ok(),
386 "lenient decoding ignores the key"
387 );
388 let (status, body) = body_of(decode_json::<Doc>(raw, true).unwrap_err()).await;
389 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
390 let paths: Vec<&str> = body["violations"]
391 .as_array()
392 .unwrap()
393 .iter()
394 .map(|v| v["path"].as_str().unwrap())
395 .collect();
396 assert_eq!(paths, vec!["colour", "lines/0/discount"]);
397 assert_eq!(body["violations"][0]["code"], "unknown_field");
398 }
399
400 #[test]
401 fn strict_tolerates_what_a_response_echoes_and_both_casings() {
402 let raw = br#"{"id":"x","metadata":{},"createdAt":"t","name":"a","lines":[{"amount":1,"unitPrice":2},{"amount":1,"unit_price":3}]}"#;
403 assert!(decode_json::<Doc>(raw, true).is_ok());
404 assert!(decode_json::<Vec<Doc>>(br#"[{"id":"x","name":"a"}]"#, true).is_ok());
406 let err = decode_json::<Vec<Doc>>(br#"[{"name":"a"},{"name":"b","bogus":1}]"#, true);
407 assert!(err.is_err());
408 }
409}