pavex/request/body/
json.rsuse http::HeaderMap;
use serde::Deserialize;
use crate::blueprint::constructor::{Constructor, RegisteredConstructor};
use crate::blueprint::Blueprint;
use crate::f;
use crate::request::RequestHead;
use super::{
buffered_body::BufferedBody,
errors::{
ExtractJsonBodyError, JsonContentTypeMismatch, JsonDeserializationError,
MissingJsonContentType,
},
};
#[doc(alias = "Json")]
#[derive(Debug)]
pub struct JsonBody<T>(pub T);
impl<T> JsonBody<T> {
pub fn extract<'head, 'body>(
request_head: &'head RequestHead,
buffered_body: &'body BufferedBody,
) -> Result<Self, ExtractJsonBodyError>
where
T: Deserialize<'body>,
{
check_json_content_type(&request_head.headers)?;
let mut deserializer = serde_json::Deserializer::from_slice(buffered_body.bytes.as_ref());
let body = serde_path_to_error::deserialize(&mut deserializer)
.map_err(|e| JsonDeserializationError { source: e })?;
Ok(JsonBody(body))
}
}
impl JsonBody<()> {
pub fn register(bp: &mut Blueprint) -> RegisteredConstructor {
Self::default_constructor().register(bp)
}
pub fn default_constructor() -> Constructor {
Constructor::request_scoped(f!(super::JsonBody::extract))
.error_handler(f!(super::errors::ExtractJsonBodyError::into_response))
}
}
fn check_json_content_type(headers: &HeaderMap) -> Result<(), ExtractJsonBodyError> {
let Some(content_type) = headers.get(http::header::CONTENT_TYPE) else {
return Err(MissingJsonContentType.into());
};
let Ok(content_type) = content_type.to_str() else {
return Err(MissingJsonContentType.into());
};
let Ok(mime) = content_type.parse::<mime::Mime>() else {
return Err(JsonContentTypeMismatch {
actual: content_type.to_string(),
}
.into());
};
let is_json_content_type = mime.type_() == "application"
&& (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json"));
if !is_json_content_type {
return Err(JsonContentTypeMismatch {
actual: content_type.to_string(),
}
.into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::request::body::JsonBody;
#[test]
fn missing_content_type() {
let headers = http::HeaderMap::new();
let err = super::check_json_content_type(&headers).unwrap_err();
insta::assert_snapshot!(err, @"The `Content-Type` header is missing. This endpoint expects requests with a `Content-Type` header set to `application/json`, or another `application/*+json` MIME type");
insta::assert_debug_snapshot!(err, @r###"
MissingContentType(
MissingJsonContentType,
)
"###);
}
#[test]
fn content_type_is_not_valid_mime() {
let mut headers = http::HeaderMap::new();
headers.insert(http::header::CONTENT_TYPE, "hello world".parse().unwrap());
let err = super::check_json_content_type(&headers).unwrap_err();
insta::assert_snapshot!(err, @"The `Content-Type` header was set to `hello world`. This endpoint expects requests with a `Content-Type` header set to `application/json`, or another `application/*+json` MIME type");
insta::assert_debug_snapshot!(err, @r###"
ContentTypeMismatch(
JsonContentTypeMismatch {
actual: "hello world",
},
)
"###);
}
#[test]
fn content_type_is_not_json() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/xml".parse().unwrap(),
);
let err = super::check_json_content_type(&headers).unwrap_err();
insta::assert_snapshot!(err, @"The `Content-Type` header was set to `application/xml`. This endpoint expects requests with a `Content-Type` header set to `application/json`, or another `application/*+json` MIME type");
insta::assert_debug_snapshot!(err, @r###"
ContentTypeMismatch(
JsonContentTypeMismatch {
actual: "application/xml",
},
)
"###);
}
#[test]
fn content_type_is_json() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
let outcome = super::check_json_content_type(&headers);
assert!(outcome.is_ok());
}
#[test]
fn content_type_has_json_suffix() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/hal+json".parse().unwrap(),
);
let outcome = super::check_json_content_type(&headers);
assert!(outcome.is_ok());
}
#[test]
fn json_content_type_with_charset() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/json; charset=utf-8".parse().unwrap(),
);
let outcome = super::check_json_content_type(&headers);
assert!(outcome.is_ok());
}
#[test]
fn missing_json_field() {
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct BodySchema {
name: String,
surname: String,
age: u8,
}
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/json; charset=utf-8".parse().unwrap(),
);
let request_head = crate::request::RequestHead {
headers,
method: http::Method::GET,
target: "/".parse().unwrap(),
version: http::Version::HTTP_11,
};
let body = serde_json::json!({
"name": "John Doe",
"age": 43,
});
let buffered_body = crate::request::body::BufferedBody {
bytes: serde_json::to_vec(&body).unwrap().into(),
};
let outcome: Result<JsonBody<BodySchema>, _> =
JsonBody::extract(&request_head, &buffered_body);
let err = outcome.unwrap_err();
insta::assert_snapshot!(err, @r###"
Failed to deserialize the body as a JSON document.
missing field `surname` at line 1 column 28
"###);
insta::assert_debug_snapshot!(err, @r###"
DeserializationError(
JsonDeserializationError {
source: Error {
path: Path {
segments: [],
},
original: Error("missing field `surname`", line: 1, column: 28),
},
},
)
"###);
}
}