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