diff --git a/src/extract/json.rs b/src/extract/json.rs deleted file mode 100644 index e9d65061..00000000 --- a/src/extract/json.rs +++ /dev/null @@ -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) { -/// 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(pub T); - -#[async_trait] -impl FromRequest for Json -where - T: DeserializeOwned, - B: http_body::Body + Send, - B::Data: Send, - B::Error: Into, -{ - type Rejection = JsonRejection; - - async fn from_request(req: &mut RequestParts) -> Result { - 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 Deref for Json { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 6115feb1..036425e5 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -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( +pub(crate) fn has_content_type( req: &RequestParts, expected_content_type: &str, ) -> Result { @@ -591,6 +591,6 @@ fn has_content_type( Ok(content_type.starts_with(expected_content_type)) } -fn take_body(req: &mut RequestParts) -> Result { +pub(crate) fn take_body(req: &mut RequestParts) -> Result { req.take_body().ok_or(BodyAlreadyExtracted) } diff --git a/src/json.rs b/src/json.rs new file mode 100644 index 00000000..1c99b774 --- /dev/null +++ b/src/json.rs @@ -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) { +/// // 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) -> Json { +/// 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(pub T); + +#[async_trait] +impl FromRequest for Json +where + T: DeserializeOwned, + B: http_body::Body + Send, + B::Data: Send, + B::Error: Into, +{ + type Rejection = JsonRejection; + + async fn from_request(req: &mut RequestParts) -> Result { + 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 Deref for Json { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Json { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for Json { + fn from(inner: T) -> Self { + Self(inner) + } +} + +impl IntoResponse for Json +where + T: Serialize, +{ + fn into_response(self) -> Response { + 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 + } +} diff --git a/src/lib.rs b/src/lib.rs index ed32eb90..0717f6c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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. diff --git a/src/macros.rs b/src/macros.rs index a4801842..e7dd96e5 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -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(err: E) -> Self + pub(crate) fn from_err(err: E) -> Self where E: Into, { diff --git a/src/response.rs b/src/response.rs index 0ed511e4..48fbb500 100644 --- a/src/response.rs +++ b/src/response.rs @@ -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 From for Html { } } -/// 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 = Json(json).into_response(); -/// -/// assert_eq!( -/// response.headers().get(CONTENT_TYPE).unwrap(), -/// "application/json", -/// ); -/// ``` -#[derive(Clone, Copy, Debug)] -pub struct Json(pub T); - -impl IntoResponse for Json -where - T: Serialize, -{ - fn into_response(self) -> Response { - 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 From for Json { - fn from(inner: T) -> Self { - Self(inner) - } -} - #[cfg(test)] mod tests { use super::*;