mirror of
https://github.com/tokio-rs/axum.git
synced 2024-12-28 23:38:20 +01:00
Common JSON wrapper type for response and request (#140)
This commit is contained in:
parent
3d45a97db9
commit
345163e98d
6 changed files with 162 additions and 141 deletions
|
@ -1,76 +0,0 @@
|
|||
use super::{has_content_type, rejection::*, take_body, FromRequest, RequestParts};
|
||||
use async_trait::async_trait;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Extractor that deserializes request bodies into some type.
|
||||
///
|
||||
/// `T` is expected to implement [`serde::Deserialize`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::prelude::*;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct CreateUser {
|
||||
/// email: String,
|
||||
/// password: String,
|
||||
/// }
|
||||
///
|
||||
/// async fn create_user(payload: extract::Json<CreateUser>) {
|
||||
/// let payload: CreateUser = payload.0;
|
||||
///
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = route("/users", post(create_user));
|
||||
/// # async {
|
||||
/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// If the query string cannot be parsed it will reject the request with a `400
|
||||
/// Bad Request` response.
|
||||
///
|
||||
/// The request is required to have a `Content-Type: application/json` header.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Json<T>(pub T);
|
||||
|
||||
#[async_trait]
|
||||
impl<T, B> FromRequest<B> for Json<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
B: http_body::Body + Send,
|
||||
B::Data: Send,
|
||||
B::Error: Into<tower::BoxError>,
|
||||
{
|
||||
type Rejection = JsonRejection;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
|
||||
use bytes::Buf;
|
||||
|
||||
if has_content_type(req, "application/json")? {
|
||||
let body = take_body(req)?;
|
||||
|
||||
let buf = hyper::body::aggregate(body)
|
||||
.await
|
||||
.map_err(InvalidJsonBody::from_err)?;
|
||||
|
||||
let value = serde_json::from_reader(buf.reader()).map_err(InvalidJsonBody::from_err)?;
|
||||
|
||||
Ok(Json(value))
|
||||
} else {
|
||||
Err(MissingJsonContentType.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Json<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
|
@ -257,7 +257,6 @@ pub mod rejection;
|
|||
mod content_length_limit;
|
||||
mod extension;
|
||||
mod form;
|
||||
mod json;
|
||||
mod path;
|
||||
mod query;
|
||||
mod raw_query;
|
||||
|
@ -274,7 +273,6 @@ pub use self::{
|
|||
extension::Extension,
|
||||
extractor_middleware::extractor_middleware,
|
||||
form::Form,
|
||||
json::Json,
|
||||
path::Path,
|
||||
query::Query,
|
||||
raw_query::RawQuery,
|
||||
|
@ -282,6 +280,8 @@ pub use self::{
|
|||
url_params::UrlParams,
|
||||
url_params_map::UrlParamsMap,
|
||||
};
|
||||
#[doc(no_inline)]
|
||||
pub use crate::Json;
|
||||
|
||||
#[cfg(feature = "multipart")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
|
||||
|
@ -568,7 +568,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn has_content_type<B>(
|
||||
pub(crate) fn has_content_type<B>(
|
||||
req: &RequestParts<B>,
|
||||
expected_content_type: &str,
|
||||
) -> Result<bool, HeadersAlreadyExtracted> {
|
||||
|
@ -591,6 +591,6 @@ fn has_content_type<B>(
|
|||
Ok(content_type.starts_with(expected_content_type))
|
||||
}
|
||||
|
||||
fn take_body<B>(req: &mut RequestParts<B>) -> Result<B, BodyAlreadyExtracted> {
|
||||
pub(crate) fn take_body<B>(req: &mut RequestParts<B>) -> Result<B, BodyAlreadyExtracted> {
|
||||
req.take_body().ok_or(BodyAlreadyExtracted)
|
||||
}
|
||||
|
|
150
src/json.rs
Normal file
150
src/json.rs
Normal file
|
@ -0,0 +1,150 @@
|
|||
use crate::{
|
||||
extract::{has_content_type, rejection::*, take_body, FromRequest, RequestParts},
|
||||
prelude::response::IntoResponse,
|
||||
Body,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use http::{
|
||||
header::{self, HeaderValue},
|
||||
StatusCode,
|
||||
};
|
||||
use hyper::Response;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
/// JSON Extractor/Response
|
||||
///
|
||||
/// When used as an extractor, it can deserialize request bodies into some type that
|
||||
/// implements [`serde::Serialize`]. If the request body cannot be parsed, or it does not contain
|
||||
/// the `Content-Type: application/json` header, it will reject the request and return a
|
||||
/// `400 Bad Request` response.
|
||||
///
|
||||
/// # Extractor example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::prelude::*;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct CreateUser {
|
||||
/// email: String,
|
||||
/// password: String,
|
||||
/// }
|
||||
///
|
||||
/// async fn create_user(extract::Json(payload): extract::Json<CreateUser>) {
|
||||
/// // payload is a `CreateUser`
|
||||
///
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = route("/users", post(create_user));
|
||||
/// # async {
|
||||
/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// When used as a response, it can serialize any type that implements [`serde::Serialize`] to `JSON`,
|
||||
/// and will automatically set `Content-Type: application/json` header.
|
||||
///
|
||||
/// # Response example
|
||||
///
|
||||
/// ```
|
||||
/// use axum::{
|
||||
/// prelude::*,
|
||||
/// extract::Path,
|
||||
/// Json,
|
||||
/// };
|
||||
/// use serde::Serialize;
|
||||
/// use uuid::Uuid;
|
||||
///
|
||||
/// #[derive(Serialize)]
|
||||
/// struct User {
|
||||
/// name: String,
|
||||
/// email: String,
|
||||
/// }
|
||||
///
|
||||
/// async fn get_user(Path(user_id) : Path<Uuid>) -> Json<User> {
|
||||
/// todo!()
|
||||
/// }
|
||||
///
|
||||
/// let app = route("/users/:id", get(get_user));
|
||||
/// # async {
|
||||
/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Json<T>(pub T);
|
||||
|
||||
#[async_trait]
|
||||
impl<T, B> FromRequest<B> for Json<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
B: http_body::Body + Send,
|
||||
B::Data: Send,
|
||||
B::Error: Into<tower::BoxError>,
|
||||
{
|
||||
type Rejection = JsonRejection;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
|
||||
use bytes::Buf;
|
||||
|
||||
if has_content_type(req, "application/json")? {
|
||||
let body = take_body(req)?;
|
||||
|
||||
let buf = hyper::body::aggregate(body)
|
||||
.await
|
||||
.map_err(InvalidJsonBody::from_err)?;
|
||||
|
||||
let value = serde_json::from_reader(buf.reader()).map_err(InvalidJsonBody::from_err)?;
|
||||
|
||||
Ok(Json(value))
|
||||
} else {
|
||||
Err(MissingJsonContentType.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Json<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for Json<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Json<T> {
|
||||
fn from(inner: T) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for Json<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn into_response(self) -> Response<Body> {
|
||||
let bytes = match serde_json::to_vec(&self.0) {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
return Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from(err.to_string()))
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
let mut res = Response::new(Body::from(bytes));
|
||||
res.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json"),
|
||||
);
|
||||
res
|
||||
}
|
||||
}
|
|
@ -713,6 +713,7 @@ use tower::Service;
|
|||
pub(crate) mod macros;
|
||||
|
||||
mod buffer;
|
||||
mod json;
|
||||
mod util;
|
||||
|
||||
pub mod body;
|
||||
|
@ -737,6 +738,8 @@ pub use http;
|
|||
pub use hyper::Server;
|
||||
pub use tower_http::add_extension::{AddExtension, AddExtensionLayer};
|
||||
|
||||
pub use crate::json::Json;
|
||||
|
||||
pub mod prelude {
|
||||
//! Re-exports of important traits, types, and functions used with axum. Meant to be glob
|
||||
//! imported.
|
||||
|
|
|
@ -65,10 +65,10 @@ macro_rules! define_rejection {
|
|||
) => {
|
||||
$(#[$m])*
|
||||
#[derive(Debug)]
|
||||
pub struct $name(pub(super) tower::BoxError);
|
||||
pub struct $name(pub(crate) tower::BoxError);
|
||||
|
||||
impl $name {
|
||||
pub(super) fn from_err<E>(err: E) -> Self
|
||||
pub(crate) fn from_err<E>(err: E) -> Self
|
||||
where
|
||||
E: Into<tower::BoxError>,
|
||||
{
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
use crate::Body;
|
||||
use bytes::Bytes;
|
||||
use http::{header, HeaderMap, HeaderValue, Response, StatusCode};
|
||||
use serde::Serialize;
|
||||
use std::{borrow::Cow, convert::Infallible};
|
||||
use tower::util::Either;
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use crate::Json;
|
||||
|
||||
/// Trait for generating responses.
|
||||
///
|
||||
/// Types that implement `IntoResponse` can be returned from handlers.
|
||||
|
@ -207,64 +209,6 @@ impl<T> From<T> for Html<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A JSON response.
|
||||
///
|
||||
/// Can be created from any type that implements [`serde::Serialize`].
|
||||
///
|
||||
/// Will automatically get `Content-Type: application/json`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use serde_json::json;
|
||||
/// use axum::{body::Body, response::{Json, IntoResponse}};
|
||||
/// use http::{Response, header::CONTENT_TYPE};
|
||||
///
|
||||
/// let json = json!({
|
||||
/// "data": 42,
|
||||
/// });
|
||||
///
|
||||
/// let response: Response<Body> = Json(json).into_response();
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// response.headers().get(CONTENT_TYPE).unwrap(),
|
||||
/// "application/json",
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Json<T>(pub T);
|
||||
|
||||
impl<T> IntoResponse for Json<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn into_response(self) -> Response<Body> {
|
||||
let bytes = match serde_json::to_vec(&self.0) {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
return Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from(err.to_string()))
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
let mut res = Response::new(Body::from(bytes));
|
||||
res.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json"),
|
||||
);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Json<T> {
|
||||
fn from(inner: T) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
Loading…
Reference in a new issue