rama_http/service/web/endpoint/extract/body/
json.rsuse super::BytesRejection;
use crate::dep::http_body_util::BodyExt;
use crate::service::web::extract::FromRequest;
use crate::utils::macros::{composite_http_rejection, define_http_rejection};
use crate::Request;
pub use crate::response::Json;
define_http_rejection! {
#[status = UNSUPPORTED_MEDIA_TYPE]
#[body = "Json requests must have `Content-Type: application/json`"]
pub struct InvalidJsonContentType;
}
define_http_rejection! {
#[status = BAD_REQUEST]
#[body = "Failed to deserialize json payload"]
pub struct FailedToDeserializeJson(Error);
}
composite_http_rejection! {
pub enum JsonRejection {
InvalidJsonContentType,
FailedToDeserializeJson,
BytesRejection,
}
}
impl<T> FromRequest for Json<T>
where
T: serde::de::DeserializeOwned + Send + Sync + 'static,
{
type Rejection = JsonRejection;
async fn from_request(req: Request) -> Result<Self, Self::Rejection> {
if !crate::service::web::extract::has_any_content_type(
req.headers(),
&[&mime::APPLICATION_JSON],
) {
return Err(InvalidJsonContentType.into());
}
let body = req.into_body();
match body.collect().await {
Ok(c) => {
let b = c.to_bytes();
match serde_json::from_slice(&b) {
Ok(s) => Ok(Self(s)),
Err(err) => Err(FailedToDeserializeJson::from_err(err).into()),
}
}
Err(err) => Err(BytesRejection::from_err(err).into()),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::service::web::WebService;
use crate::StatusCode;
use rama_core::{Context, Service};
#[tokio::test]
async fn test_json() {
#[derive(Debug, serde::Deserialize)]
struct Input {
name: String,
age: u8,
alive: Option<bool>,
}
let service = WebService::default().post("/", |Json(body): Json<Input>| async move {
assert_eq!(body.name, "glen");
assert_eq!(body.age, 42);
assert_eq!(body.alive, None);
});
let req = http::Request::builder()
.method(http::Method::POST)
.header(
http::header::CONTENT_TYPE,
"application/json; charset=utf-8",
)
.body(r#"{"name": "glen", "age": 42}"#.into())
.unwrap();
let resp = service.serve(Context::default(), req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_json_missing_content_type() {
#[derive(Debug, serde::Deserialize)]
struct Input {
_name: String,
_age: u8,
_alive: Option<bool>,
}
let service =
WebService::default().post("/", |Json(_): Json<Input>| async move { StatusCode::OK });
let req = http::Request::builder()
.method(http::Method::POST)
.header(http::header::CONTENT_TYPE, "text/plain")
.body(r#"{"name": "glen", "age": 42}"#.into())
.unwrap();
let resp = service.serve(Context::default(), req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
}
#[tokio::test]
async fn test_json_invalid_body_encoding() {
#[derive(Debug, serde::Deserialize)]
struct Input {
_name: String,
_age: u8,
_alive: Option<bool>,
}
let service =
WebService::default().post("/", |Json(_): Json<Input>| async move { StatusCode::OK });
let req = http::Request::builder()
.method(http::Method::POST)
.header(
http::header::CONTENT_TYPE,
"application/json; charset=utf-8",
)
.body(r#"deal with it, or not?!"#.into())
.unwrap();
let resp = service.serve(Context::default(), req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
}