Graduate contrib 'json' and 'msgpack' into core.

This has the following nice benefits:

  * The 'JsonValue' wrapper type is gone.
  * 'Local{Request, Response}' natively support JSON/MessagePack.
  * The 'json' and 'msgpack' limits are officially recognized.
  * Soon, Rocket application will not require an explicit 'serde' dep.

This marks the beginning of the end of 'rocket_contrib'.
This commit is contained in:
Sergio Benitez 2021-04-29 05:19:24 -07:00
parent 7fb18cbe0f
commit c74bcfd40a
38 changed files with 619 additions and 468 deletions

View File

@ -20,9 +20,7 @@ databases = [
] ]
# User-facing features. # User-facing features.
default = ["json", "serve"] default = ["serve"]
json = ["serde", "serde_json", "tokio/io-util"]
msgpack = ["serde", "rmp-serde", "tokio/io-util"]
tera_templates = ["tera", "templates"] tera_templates = ["tera", "templates"]
handlebars_templates = ["handlebars", "templates"] handlebars_templates = ["handlebars", "templates"]
helmet = ["time"] helmet = ["time"]
@ -50,7 +48,6 @@ log = "0.4"
# Serialization and templating dependencies. # Serialization and templating dependencies.
serde = { version = "1.0", optional = true, features = ["derive"] } serde = { version = "1.0", optional = true, features = ["derive"] }
serde_json = { version = "1.0.26", optional = true } serde_json = { version = "1.0.26", optional = true }
rmp-serde = { version = "0.15.0", optional = true }
# Templating dependencies. # Templating dependencies.
handlebars = { version = "3.0", optional = true } handlebars = { version = "3.0", optional = true }

View File

@ -16,9 +16,7 @@
//! common modules exposed by default. The present feature list is below, with //! common modules exposed by default. The present feature list is below, with
//! an asterisk next to the features that are enabled by default: //! an asterisk next to the features that are enabled by default:
//! //!
//! * [json*](type@json) - JSON (de)serialization
//! * [serve*](serve) - Static File Serving //! * [serve*](serve) - Static File Serving
//! * [msgpack](msgpack) - MessagePack (de)serialization
//! * [handlebars_templates](templates) - Handlebars Templating //! * [handlebars_templates](templates) - Handlebars Templating
//! * [tera_templates](templates) - Tera Templating //! * [tera_templates](templates) - Tera Templating
//! * [uuid](uuid) - UUID (de)serialization //! * [uuid](uuid) - UUID (de)serialization
@ -28,13 +26,14 @@
//! The recommend way to include features from this crate via Rocket in your //! The recommend way to include features from this crate via Rocket in your
//! project is by adding a `[dependencies.rocket_contrib]` section to your //! project is by adding a `[dependencies.rocket_contrib]` section to your
//! `Cargo.toml` file, setting `default-features` to false, and specifying //! `Cargo.toml` file, setting `default-features` to false, and specifying
//! features manually. For example, to use the JSON module, you would add: //! features manually. For example, to use the `tera_templates` module, you
//! would add:
//! //!
//! ```toml //! ```toml
//! [dependencies.rocket_contrib] //! [dependencies.rocket_contrib]
//! version = "0.5.0-dev" //! version = "0.5.0-dev"
//! default-features = false //! default-features = false
//! features = ["json"] //! features = ["tera_templates"]
//! ``` //! ```
//! //!
//! This crate is expected to grow with time, bringing in outside crates to be //! This crate is expected to grow with time, bringing in outside crates to be
@ -42,9 +41,7 @@
#[allow(unused_imports)] #[macro_use] extern crate rocket; #[allow(unused_imports)] #[macro_use] extern crate rocket;
#[cfg(feature="json")] #[macro_use] pub mod json;
#[cfg(feature="serve")] pub mod serve; #[cfg(feature="serve")] pub mod serve;
#[cfg(feature="msgpack")] pub mod msgpack;
#[cfg(feature="templates")] pub mod templates; #[cfg(feature="templates")] pub mod templates;
#[cfg(feature="uuid")] pub mod uuid; #[cfg(feature="uuid")] pub mod uuid;
#[cfg(feature="databases")] pub mod databases; #[cfg(feature="databases")] pub mod databases;

View File

@ -22,8 +22,15 @@ all-features = true
default = [] default = []
tls = ["rocket_http/tls"] tls = ["rocket_http/tls"]
secrets = ["rocket_http/private-cookies"] secrets = ["rocket_http/private-cookies"]
json = ["serde_json", "tokio/io-util"]
msgpack = ["rmp-serde", "tokio/io-util"]
[dependencies] [dependencies]
# Serialization dependencies.
serde_json = { version = "1.0.26", optional = true }
rmp-serde = { version = "0.15.0", optional = true }
# Non-optional, core dependencies from here on out.
futures = "0.3.0" futures = "0.3.0"
yansi = "0.5" yansi = "0.5"
log = { version = "0.4", features = ["std"] } log = { version = "0.4", features = ["std"] }

View File

@ -64,8 +64,12 @@ use crate::http::uncased::Uncased;
/// | `file/$ext` | _N/A_ | [`TempFile`] | file form field with extension `$ext` | /// | `file/$ext` | _N/A_ | [`TempFile`] | file form field with extension `$ext` |
/// | `string` | 8KiB | [`String`] | data guard or data form field | /// | `string` | 8KiB | [`String`] | data guard or data form field |
/// | `bytes` | 8KiB | [`Vec<u8>`] | data guard | /// | `bytes` | 8KiB | [`Vec<u8>`] | data guard |
/// | `json` | 1MiB | [`Json`] | JSON data and form payloads |
/// | `msgpack` | 1MiB | [`MsgPack`] | MessagePack data and form payloads |
/// ///
/// [`TempFile`]: crate::data::TempFile /// [`TempFile`]: crate::data::TempFile
/// [`Json`]: crate::serde::json::Json
/// [`MsgPack`]: crate::serde::msgpack::MsgPack
/// ///
/// # Usage /// # Usage
/// ///
@ -78,7 +82,7 @@ use crate::http::uncased::Uncased;
/// let limits = Limits::default() /// let limits = Limits::default()
/// .limit("form", 64.kibibytes()) /// .limit("form", 64.kibibytes())
/// .limit("file/pdf", 3.mebibytes()) /// .limit("file/pdf", 3.mebibytes())
/// .limit("json", 1.mebibytes()); /// .limit("json", 2.mebibytes());
/// ``` /// ```
/// ///
/// The [`Limits::default()`](#impl-Default) method populates the `Limits` /// The [`Limits::default()`](#impl-Default) method populates the `Limits`
@ -134,6 +138,8 @@ impl Default for Limits {
.limit("file", Limits::FILE) .limit("file", Limits::FILE)
.limit("string", Limits::STRING) .limit("string", Limits::STRING)
.limit("bytes", Limits::BYTES) .limit("bytes", Limits::BYTES)
.limit("json", Limits::JSON)
.limit("msgpack", Limits::MESSAGE_PACK)
} }
} }
@ -153,6 +159,12 @@ impl Limits {
/// Default limit for bytes. /// Default limit for bytes.
pub const BYTES: ByteUnit = ByteUnit::Kibibyte(8); pub const BYTES: ByteUnit = ByteUnit::Kibibyte(8);
/// Default limit for JSON payloads.
pub const JSON: ByteUnit = ByteUnit::Mebibyte(1);
/// Default limit for MessagePack payloads.
pub const MESSAGE_PACK: ByteUnit = ByteUnit::Mebibyte(1);
/// Construct a new `Limits` structure with no limits set. /// Construct a new `Limits` structure with no limits set.
/// ///
/// # Example /// # Example
@ -181,11 +193,12 @@ impl Limits {
/// ///
/// let limits = Limits::default(); /// let limits = Limits::default();
/// assert_eq!(limits.get("form"), Some(32.kibibytes())); /// assert_eq!(limits.get("form"), Some(32.kibibytes()));
/// assert_eq!(limits.get("json"), None);
///
/// let limits = limits.limit("json", 1.mebibytes());
/// assert_eq!(limits.get("form"), Some(32.kibibytes()));
/// assert_eq!(limits.get("json"), Some(1.mebibytes())); /// assert_eq!(limits.get("json"), Some(1.mebibytes()));
/// assert_eq!(limits.get("cat"), None);
///
/// let limits = limits.limit("cat", 1.mebibytes());
/// assert_eq!(limits.get("form"), Some(32.kibibytes()));
/// assert_eq!(limits.get("cat"), Some(1.mebibytes()));
/// ///
/// let limits = limits.limit("json", 64.mebibytes()); /// let limits = limits.limit("json", 64.mebibytes());
/// assert_eq!(limits.get("json"), Some(64.mebibytes())); /// assert_eq!(limits.get("json"), Some(64.mebibytes()));
@ -209,12 +222,12 @@ impl Limits {
/// use rocket::data::{Limits, ToByteUnit}; /// use rocket::data::{Limits, ToByteUnit};
/// ///
/// let limits = Limits::default() /// let limits = Limits::default()
/// .limit("json", 1.mebibytes()) /// .limit("json", 2.mebibytes())
/// .limit("file/jpeg", 4.mebibytes()) /// .limit("file/jpeg", 4.mebibytes())
/// .limit("file/jpeg/special", 8.mebibytes()); /// .limit("file/jpeg/special", 8.mebibytes());
/// ///
/// assert_eq!(limits.get("form"), Some(32.kibibytes())); /// assert_eq!(limits.get("form"), Some(32.kibibytes()));
/// assert_eq!(limits.get("json"), Some(1.mebibytes())); /// assert_eq!(limits.get("json"), Some(2.mebibytes()));
/// assert_eq!(limits.get("data-form"), Some(Limits::DATA_FORM)); /// assert_eq!(limits.get("data-form"), Some(Limits::DATA_FORM));
/// ///
/// assert_eq!(limits.get("file"), Some(1.mebibytes())); /// assert_eq!(limits.get("file"), Some(1.mebibytes()));
@ -223,7 +236,7 @@ impl Limits {
/// assert_eq!(limits.get("file/jpeg/inner"), Some(4.mebibytes())); /// assert_eq!(limits.get("file/jpeg/inner"), Some(4.mebibytes()));
/// assert_eq!(limits.get("file/jpeg/special"), Some(8.mebibytes())); /// assert_eq!(limits.get("file/jpeg/special"), Some(8.mebibytes()));
/// ///
/// assert!(limits.get("msgpack").is_none()); /// assert!(limits.get("cats").is_none());
/// ``` /// ```
pub fn get<S: AsRef<str>>(&self, name: S) -> Option<ByteUnit> { pub fn get<S: AsRef<str>>(&self, name: S) -> Option<ByteUnit> {
let mut name = name.as_ref(); let mut name = name.as_ref();

View File

@ -27,14 +27,6 @@
//! [quickstart]: https://rocket.rs/master/guide/quickstart //! [quickstart]: https://rocket.rs/master/guide/quickstart
//! [getting started]: https://rocket.rs/master/guide/getting-started //! [getting started]: https://rocket.rs/master/guide/getting-started
//! //!
//! ## Libraries
//!
//! Rocket's functionality is split into two crates:
//!
//! 1. Core - This core library. Needed by every Rocket application.
//! 2. [Contrib](../rocket_contrib) - Provides useful functionality for many
//! Rocket applications. Completely optional.
//!
//! ## Usage //! ## Usage
//! //!
//! Depend on `rocket` in `Rocket.toml`: //! Depend on `rocket` in `Rocket.toml`:
@ -68,39 +60,44 @@
//! //!
//! ## Features //! ## Features
//! //!
//! There are two optional, disabled-by-default features: //! To avoid unused dependencies, Rocket _feaure-gates_ functionalities, all of
//! which are disabled-by-default:
//! //!
//! * **secrets:** Enables support for [private cookies]. //! | Feature | Description |
//! * **tls:** Enables support for [TLS]. //! |-----------|---------------------------------------------------------|
//! | `secrets` | Support for authenticated, encrypted [private cookies]. |
//! | `tls` | Support for [TLS] encrypted connections. |
//! | `json` | Support for [JSON (de)serialization]. |
//! | `msgpack` | Support for [MessagePack (de)serialization]. |
//! //!
//! The features can be enabled in `Rocket.toml`: //! Features can be selectively enabled in `Cargo.toml`:
//! //!
//! ```toml //! ```toml
//! [dependencies] //! [dependencies]
//! rocket = { version = "0.5.0-dev", features = ["secrets", "tls"] } //! rocket = { version = "0.5.0-dev", features = ["secrets", "tls", "json"] }
//! ``` //! ```
//! //!
//! [JSON (de)serialization]: crate::serde::json
//! [MessagePack (de)serialization]: crate::serde::msgpack
//! [private cookies]: https://rocket.rs/master/guide/requests/#private-cookies //! [private cookies]: https://rocket.rs/master/guide/requests/#private-cookies
//! [TLS]: https://rocket.rs/master/guide/configuration/#tls //! [TLS]: https://rocket.rs/master/guide/configuration/#tls
//! //!
//! ## Configuration //! ## Configuration
//! //!
//! By default, Rocket applications are configured via a `Rocket.toml` file //! Rocket offers a rich, extensible configuration system built on [Figment]. By
//! and/or `ROCKET_{PARAM}` environment variables. For more information on how //! default, Rocket applications are configured via a `Rocket.toml` file
//! to configure Rocket, including how to completely customize configuration //! and/or `ROCKET_{PARAM}` environment variables, but applications may
//! sources, see the [configuration section] of the guide as well as the //! configure their own sources. See the [configuration guide] for full details.
//! [`config`] module documentation.
//!
//! [configuration section]: https://rocket.rs/master/guide/configuration/
//! //!
//! ## Testing //! ## Testing
//! //!
//! The [`local`] module contains structures that facilitate unit and //! The [`local`] module contains structures that facilitate unit and
//! integration testing of a Rocket application. The top-level [`local`] module //! integration testing of a Rocket application. The top-level [`local`] module
//! documentation and the [testing chapter of the guide] include detailed //! documentation and the [testing guide] include detailed examples.
//! examples.
//! //!
//! [testing chapter of the guide]: https://rocket.rs/master/guide/testing/#testing //! [configuration guide]: https://rocket.rs/master/guide/configuration/
//! [testing guide]: https://rocket.rs/master/guide/testing/#testing
//! [Figment]: https://docs.rs/figment
/// These are public dependencies! Update docs if these are changed, especially /// These are public dependencies! Update docs if these are changed, especially
/// figment's version number in docs. /// figment's version number in docs.
@ -124,6 +121,7 @@ pub mod fairing;
pub mod error; pub mod error;
pub mod catcher; pub mod catcher;
pub mod route; pub mod route;
pub mod serde;
// Reexport of HTTP everything. // Reexport of HTTP everything.
pub mod http { pub mod http {

View File

@ -115,6 +115,73 @@ impl LocalResponse<'_> {
self.response.body_mut().to_bytes().await self.response.body_mut().to_bytes().await
} }
#[cfg(feature = "json")]
async fn _into_json<T: Send + 'static>(self) -> Option<T>
where T: serde::de::DeserializeOwned
{
self.blocking_read(|r| serde_json::from_reader(r)).await?.ok()
}
#[cfg(feature = "msgpack")]
async fn _into_msgpack<T: Send + 'static>(self) -> Option<T>
where T: serde::de::DeserializeOwned
{
self.blocking_read(|r| rmp_serde::from_read(r)).await?.ok()
}
#[cfg(any(feature = "json", feature = "msgpack"))]
async fn blocking_read<T, F>(mut self, f: F) -> Option<T>
where T: Send + 'static,
F: FnOnce(&mut dyn io::Read) -> T + Send + 'static
{
use tokio::sync::mpsc;
use tokio::io::AsyncReadExt;
struct ChanReader {
last: Option<io::Cursor<Vec<u8>>>,
rx: mpsc::Receiver<io::Result<Vec<u8>>>,
}
impl std::io::Read for ChanReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
loop {
if let Some(ref mut cursor) = self.last {
if cursor.position() < cursor.get_ref().len() as u64 {
return std::io::Read::read(cursor, buf);
}
}
if let Some(buf) = self.rx.blocking_recv() {
self.last = Some(io::Cursor::new(buf?));
} else {
return Ok(0);
}
}
}
}
let (tx, rx) = mpsc::channel(2);
let reader = tokio::task::spawn_blocking(move || {
let mut reader = ChanReader { last: None, rx };
f(&mut reader)
});
loop {
let mut buf = Vec::with_capacity(1024);
// TODO: Try to fill as much as the buffer before send it off?
match self.read_buf(&mut buf).await {
Ok(n) if n == 0 => break,
Ok(_) => tx.send(Ok(buf)).await.ok()?,
Err(e) => {
tx.send(Err(e)).await.ok()?;
break;
}
}
}
reader.await.ok()
}
// Generates the public API methods, which call the private methods above. // Generates the public API methods, which call the private methods above.
pub_response_impl!("# use rocket::local::asynchronous::Client;\n\ pub_response_impl!("# use rocket::local::asynchronous::Client;\n\
use rocket::local::asynchronous::LocalResponse;" async await); use rocket::local::asynchronous::LocalResponse;" async await);

View File

@ -71,6 +71,20 @@ impl LocalResponse<'_> {
self.client.block_on(self.inner._into_bytes()) self.client.block_on(self.inner._into_bytes())
} }
#[cfg(feature = "json")]
fn _into_json<T: Send + 'static>(self) -> Option<T>
where T: serde::de::DeserializeOwned
{
serde_json::from_reader(self).ok()
}
#[cfg(feature = "msgpack")]
fn _into_msgpack<T: Send + 'static>(self) -> Option<T>
where T: serde::de::DeserializeOwned
{
rmp_serde::from_read(self).ok()
}
// Generates the public API methods, which call the private methods above. // Generates the public API methods, which call the private methods above.
pub_response_impl!("# use rocket::local::blocking::Client;\n\ pub_response_impl!("# use rocket::local::blocking::Client;\n\
use rocket::local::blocking::LocalResponse;"); use rocket::local::blocking::LocalResponse;");

View File

@ -51,8 +51,9 @@
//! // Using the preferred `blocking` API. //! // Using the preferred `blocking` API.
//! #[test] //! #[test]
//! fn test_hello_world_blocking() { //! fn test_hello_world_blocking() {
//! // Construct a client to use for dispatching requests.
//! use rocket::local::blocking::Client; //! use rocket::local::blocking::Client;
//!
//! // Construct a client to use for dispatching requests.
//! let client = Client::tracked(super::rocket()) //! let client = Client::tracked(super::rocket())
//! .expect("valid `Rocket`"); //! .expect("valid `Rocket`");
//! //!
@ -64,8 +65,9 @@
//! // Using the `asynchronous` API. //! // Using the `asynchronous` API.
//! #[rocket::async_test] //! #[rocket::async_test]
//! async fn test_hello_world_async() { //! async fn test_hello_world_async() {
//! // Construct a client to use for dispatching requests.
//! use rocket::local::asynchronous::Client; //! use rocket::local::asynchronous::Client;
//!
//! // Construct a client to use for dispatching requests.
//! let client = Client::tracked(super::rocket()).await //! let client = Client::tracked(super::rocket()).await
//! .expect("valid `Rocket`"); //! .expect("valid `Rocket`");
//! //!

View File

@ -191,12 +191,10 @@ macro_rules! pub_request_impl {
self self
} }
/// Set the body (data) of the request. /// Sets the body data of the request.
/// ///
/// # Examples /// # Examples
/// ///
/// Set the body to be a JSON structure; also sets the Content-Type.
///
/// ```rust /// ```rust
#[doc = $import] #[doc = $import]
/// use rocket::http::ContentType; /// use rocket::http::ContentType;
@ -204,8 +202,8 @@ macro_rules! pub_request_impl {
/// # Client::_test(|_, request, _| { /// # Client::_test(|_, request, _| {
/// let request: LocalRequest = request; /// let request: LocalRequest = request;
/// let req = request /// let req = request
/// .header(ContentType::JSON) /// .header(ContentType::Text)
/// .body(r#"{ "key": "value", "array": [1, 2, 3] }"#); /// .body("Hello, world!");
/// # }); /// # });
/// ``` /// ```
#[inline] #[inline]
@ -219,6 +217,74 @@ macro_rules! pub_request_impl {
self self
} }
/// Sets the body to `value` serialized as JSON with `Content-Type`
/// [`ContentType::JSON`](crate::http::ContentType::JSON).
///
/// If `value` fails to serialize, the body is set to empty. The
/// `Content-Type` header is _always_ set.
///
/// # Examples
///
/// ```rust
#[doc = $import]
/// use rocket::serde::Serialize;
/// use rocket::http::ContentType;
///
/// #[derive(Serialize)]
/// struct Task {
/// id: usize,
/// complete: bool,
/// }
///
/// # Client::_test(|_, request, _| {
/// let task = Task { id: 10, complete: false };
///
/// let request: LocalRequest = request;
/// let req = request.json(&task);
/// assert_eq!(req.content_type(), Some(&ContentType::JSON));
/// # });
/// ```
#[cfg(feature = "json")]
#[cfg_attr(nightly, doc(cfg(feature = "json")))]
pub fn json<T: crate::serde::Serialize>(self, value: &T) -> Self {
let json = serde_json::to_vec(&value).unwrap_or_default();
self.header(crate::http::ContentType::JSON).body(json)
}
/// Sets the body to `value` serialized as MessagePack with `Content-Type`
/// [`ContentType::MsgPack`](crate::http::ContentType::MsgPack).
///
/// If `value` fails to serialize, the body is set to empty. The
/// `Content-Type` header is _always_ set.
///
/// # Examples
///
/// ```rust
#[doc = $import]
/// use rocket::serde::Serialize;
/// use rocket::http::ContentType;
///
/// #[derive(Serialize)]
/// struct Task {
/// id: usize,
/// complete: bool,
/// }
///
/// # Client::_test(|_, request, _| {
/// let task = Task { id: 10, complete: false };
///
/// let request: LocalRequest = request;
/// let req = request.msgpack(&task);
/// assert_eq!(req.content_type(), Some(&ContentType::MsgPack));
/// # });
/// ```
#[cfg(feature = "msgpack")]
#[cfg_attr(nightly, doc(cfg(feature = "msgpack")))]
pub fn msgpack<T: crate::serde::Serialize>(self, value: &T) -> Self {
let msgpack = rmp_serde::to_vec(value).unwrap_or_default();
self.header(crate::http::ContentType::MsgPack).body(msgpack)
}
/// Set the body (data) of the request without consuming `self`. /// Set the body (data) of the request without consuming `self`.
/// ///
/// # Examples /// # Examples

View File

@ -85,7 +85,7 @@ macro_rules! pub_response_impl {
/// Consumes `self` and reads the entirety of its body into a `Vec` of /// Consumes `self` and reads the entirety of its body into a `Vec` of
/// bytes. /// bytes.
/// ///
/// If reading fails or the body is unset in the response, return `None`. /// If reading fails or the body is unset in the response, returns `None`.
/// Otherwise, returns `Some`. The returned vector may be empty if the body /// Otherwise, returns `Some`. The returned vector may be empty if the body
/// is empty. /// is empty.
/// ///
@ -108,6 +108,78 @@ macro_rules! pub_response_impl {
self._into_bytes() $(.$suffix)? .ok() self._into_bytes() $(.$suffix)? .ok()
} }
/// Consumes `self` and deserializes its body as JSON without buffering in
/// memory.
///
/// If deserialization fails or the body is unset in the response, returns
/// `None`. Otherwise, returns `Some`.
///
/// # Example
///
/// ```rust
#[doc = $doc_prelude]
/// use rocket::serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Task {
/// id: usize,
/// complete: bool,
/// text: String,
/// }
///
/// # Client::_test(|_, _, response| {
/// let response: LocalResponse = response;
/// let task = response.into_json::<Task>();
/// # });
/// ```
#[cfg(feature = "json")]
#[cfg_attr(nightly, doc(cfg(feature = "json")))]
pub $($prefix)? fn into_json<T>(self) -> Option<T>
where T: Send + serde::de::DeserializeOwned + 'static
{
if self._response().body().is_none() {
return None;
}
self._into_json() $(.$suffix)?
}
/// Consumes `self` and deserializes its body as MessagePack without
/// buffering in memory.
///
/// If deserialization fails or the body is unset in the response, returns
/// `None`. Otherwise, returns `Some`.
///
/// # Example
///
/// ```rust
#[doc = $doc_prelude]
/// use rocket::serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Task {
/// id: usize,
/// complete: bool,
/// text: String,
/// }
///
/// # Client::_test(|_, _, response| {
/// let response: LocalResponse = response;
/// let task = response.into_msgpack::<Task>();
/// # });
/// ```
#[cfg(feature = "msgpack")]
#[cfg_attr(nightly, doc(cfg(feature = "msgpack")))]
pub $($prefix)? fn into_msgpack<T>(self) -> Option<T>
where T: Send + serde::de::DeserializeOwned + 'static
{
if self._response().body().is_none() {
return None;
}
self._into_msgpack() $(.$suffix)?
}
#[cfg(test)] #[cfg(test)]
#[allow(dead_code)] #[allow(dead_code)]
fn _ensure_impls_exist() { fn _ensure_impls_exist() {

View File

@ -1,6 +1,6 @@
//! Automatic JSON (de)serialization support. //! Automatic JSON (de)serialization support.
//! //!
//! See the [`Json`](crate::json::Json) type for further details. //! See [`Json`](Json) for details.
//! //!
//! # Enabling //! # Enabling
//! //!
@ -8,28 +8,37 @@
//! in `Cargo.toml` as follows: //! in `Cargo.toml` as follows:
//! //!
//! ```toml //! ```toml
//! [dependencies.rocket_contrib] //! [dependencies.rocket]
//! version = "0.5.0-dev" //! version = "0.5.0-dev"
//! default-features = false
//! features = ["json"] //! features = ["json"]
//! ``` //! ```
//!
//! # Testing
//!
//! The [`LocalRequest`] and [`LocalResponse`] types provide [`json()`] and
//! [`into_json()`] methods to create a request with serialized JSON and
//! deserialize a response as JSON, respectively.
//!
//! [`LocalRequest`]: crate::local::blocking::LocalRequest
//! [`LocalResponse`]: crate::local::blocking::LocalResponse
//! [`json()`]: crate::local::blocking::LocalRequest::json()
//! [`into_json()`]: crate::local::blocking::LocalResponse::into_json()
use std::io; use std::io;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::iter::FromIterator;
use rocket::request::{Request, local_cache}; use crate::request::{Request, local_cache};
use rocket::data::{ByteUnit, Data, FromData, Outcome}; use crate::data::{Limits, Data, FromData, Outcome};
use rocket::response::{self, Responder, content}; use crate::response::{self, Responder, content};
use rocket::http::Status; use crate::http::Status;
use rocket::form::prelude as form; use crate::form::prelude as form;
use serde::{Serialize, Serializer, Deserialize, Deserializer}; use serde::{Serialize, Deserialize};
#[doc(hidden)] #[doc(hidden)]
pub use serde_json::{json_internal, json_internal_vec}; pub use serde_json;
/// The JSON data guard: easily consume and respond with JSON. /// The JSON guard: easily consume and return JSON.
/// ///
/// ## Receiving JSON /// ## Receiving JSON
/// ///
@ -43,9 +52,8 @@ pub use serde_json::{json_internal, json_internal_vec};
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # extern crate rocket_contrib;
/// # type User = usize; /// # type User = usize;
/// use rocket_contrib::json::Json; /// use rocket::serde::json::Json;
/// ///
/// #[post("/user", format = "json", data = "<user>")] /// #[post("/user", format = "json", data = "<user>")]
/// fn new_user(user: Json<User>) { /// fn new_user(user: Json<User>) {
@ -65,10 +73,9 @@ pub use serde_json::{json_internal, json_internal_vec};
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # extern crate rocket_contrib;
/// # type Metadata = usize; /// # type Metadata = usize;
/// use rocket::form::{Form, FromForm}; /// use rocket::form::{Form, FromForm};
/// use rocket_contrib::json::Json; /// use rocket::serde::json::Json;
/// ///
/// #[derive(FromForm)] /// #[derive(FromForm)]
/// struct User<'r> { /// struct User<'r> {
@ -82,27 +89,7 @@ pub use serde_json::{json_internal, json_internal_vec};
/// } /// }
/// ``` /// ```
/// ///
/// ## Sending JSON /// ### Incoming Data Limits
///
/// If you're responding with JSON data, return a `Json<T>` type, where `T`
/// implements [`Serialize`] from [`serde`]. The content type of the response is
/// set to `application/json` automatically.
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # extern crate rocket_contrib;
/// # type User = usize;
/// use rocket_contrib::json::Json;
///
/// #[get("/users/<id>")]
/// fn user(id: usize) -> Json<User> {
/// let user_from_id = User::from(id);
/// /* ... */
/// Json(user_from_id)
/// }
/// ```
///
/// ## Incoming Data Limits
/// ///
/// The default size limit for incoming JSON data is 1MiB. Setting a limit /// The default size limit for incoming JSON data is 1MiB. Setting a limit
/// protects your application from denial of service (DoS) attacks and from /// protects your application from denial of service (DoS) attacks and from
@ -115,13 +102,31 @@ pub use serde_json::{json_internal, json_internal_vec};
/// [global.limits] /// [global.limits]
/// json = 5242880 /// json = 5242880
/// ``` /// ```
///
/// ## Sending JSON
///
/// If you're responding with JSON data, return a `Json<T>` type, where `T`
/// implements [`Serialize`] from [`serde`]. The content type of the response is
/// set to `application/json` automatically.
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # type User = usize;
/// use rocket::serde::json::Json;
///
/// #[get("/users/<id>")]
/// fn user(id: usize) -> Json<User> {
/// let user_from_id = User::from(id);
/// /* ... */
/// Json(user_from_id)
/// }
/// ```
#[derive(Debug)] #[derive(Debug)]
pub struct Json<T>(pub T); pub struct Json<T>(pub T);
/// An error returned by the [`Json`] data guard when incoming data fails to /// Error returned by the [`Json`] guard when JSON deserialization fails.
/// serialize as JSON.
#[derive(Debug)] #[derive(Debug)]
pub enum JsonError<'a> { pub enum Error<'a> {
/// An I/O error occurred while reading the incoming request data. /// An I/O error occurred while reading the incoming request data.
Io(io::Error), Io(io::Error),
@ -132,14 +137,12 @@ pub enum JsonError<'a> {
Parse(&'a str, serde_json::error::Error), Parse(&'a str, serde_json::error::Error),
} }
const DEFAULT_LIMIT: ByteUnit = ByteUnit::Mebibyte(1);
impl<T> Json<T> { impl<T> Json<T> {
/// Consumes the JSON wrapper and returns the wrapped item. /// Consumes the JSON wrapper and returns the wrapped item.
/// ///
/// # Example /// # Example
/// ```rust /// ```rust
/// # use rocket_contrib::json::Json; /// # use rocket::serde::json::Json;
/// let string = "Hello".to_string(); /// let string = "Hello".to_string();
/// let my_json = Json(string); /// let my_json = Json(string);
/// assert_eq!(my_json.into_inner(), "Hello".to_string()); /// assert_eq!(my_json.into_inner(), "Hello".to_string());
@ -151,37 +154,37 @@ impl<T> Json<T> {
} }
impl<'r, T: Deserialize<'r>> Json<T> { impl<'r, T: Deserialize<'r>> Json<T> {
fn from_str(s: &'r str) -> Result<Self, JsonError<'r>> { fn from_str(s: &'r str) -> Result<Self, Error<'r>> {
serde_json::from_str(s).map(Json).map_err(|e| JsonError::Parse(s, e)) serde_json::from_str(s).map(Json).map_err(|e| Error::Parse(s, e))
} }
async fn from_data(req: &'r Request<'_>, data: Data) -> Result<Self, JsonError<'r>> { async fn from_data(req: &'r Request<'_>, data: Data) -> Result<Self, Error<'r>> {
let size_limit = req.limits().get("json").unwrap_or(DEFAULT_LIMIT); let limit = req.limits().get("json").unwrap_or(Limits::JSON);
let string = match data.open(size_limit).into_string().await { let string = match data.open(limit).into_string().await {
Ok(s) if s.is_complete() => s.into_inner(), Ok(s) if s.is_complete() => s.into_inner(),
Ok(_) => { Ok(_) => {
let eof = io::ErrorKind::UnexpectedEof; let eof = io::ErrorKind::UnexpectedEof;
return Err(JsonError::Io(io::Error::new(eof, "data limit exceeded"))); return Err(Error::Io(io::Error::new(eof, "data limit exceeded")));
}, },
Err(e) => return Err(JsonError::Io(e)), Err(e) => return Err(Error::Io(e)),
}; };
Self::from_str(local_cache!(req, string)) Self::from_str(local_cache!(req, string))
} }
} }
#[rocket::async_trait] #[crate::async_trait]
impl<'r, T: Deserialize<'r>> FromData<'r> for Json<T> { impl<'r, T: Deserialize<'r>> FromData<'r> for Json<T> {
type Error = JsonError<'r>; type Error = Error<'r>;
async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome<Self, Self::Error> { async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome<Self, Self::Error> {
match Self::from_data(req, data).await { match Self::from_data(req, data).await {
Ok(value) => Outcome::Success(value), Ok(value) => Outcome::Success(value),
Err(JsonError::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => {
Outcome::Failure((Status::PayloadTooLarge, JsonError::Io(e))) Outcome::Failure((Status::PayloadTooLarge, Error::Io(e)))
}, },
Err(JsonError::Parse(s, e)) if e.classify() == serde_json::error::Category::Data => { Err(Error::Parse(s, e)) if e.classify() == serde_json::error::Category::Data => {
Outcome::Failure((Status::UnprocessableEntity, JsonError::Parse(s, e))) Outcome::Failure((Status::UnprocessableEntity, Error::Parse(s, e)))
}, },
Err(e) => Outcome::Failure((Status::BadRequest, e)), Err(e) => Outcome::Failure((Status::BadRequest, e)),
@ -226,16 +229,16 @@ impl<T> DerefMut for Json<T> {
} }
} }
impl From<JsonError<'_>> for form::Error<'_> { impl From<Error<'_>> for form::Error<'_> {
fn from(e: JsonError<'_>) -> Self { fn from(e: Error<'_>) -> Self {
match e { match e {
JsonError::Io(e) => e.into(), Error::Io(e) => e.into(),
JsonError::Parse(_, e) => form::Error::custom(e) Error::Parse(_, e) => form::Error::custom(e)
} }
} }
} }
#[rocket::async_trait] #[crate::async_trait]
impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for Json<T> { impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for Json<T> {
fn from_value(field: form::ValueField<'v>) -> Result<Self, form::Errors<'v>> { fn from_value(field: form::ValueField<'v>) -> Result<Self, form::Errors<'v>> {
Ok(Self::from_str(field.value)?) Ok(Self::from_str(field.value)?)
@ -253,11 +256,11 @@ impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for Json<T> {
/// returned directly from a handler. /// returned directly from a handler.
/// ///
/// [`Value`]: serde_json::value /// [`Value`]: serde_json::value
/// [`Responder`]: rocket::response::Responder /// [`Responder`]: crate::response::Responder
/// ///
/// # `Responder` /// # `Responder`
/// ///
/// The `Responder` implementation for `JsonValue` serializes the represented /// The `Responder` implementation for `Value` serializes the represented
/// value into a JSON string and sets the string as the body of a fixed-sized /// value into a JSON string and sets the string as the body of a fixed-sized
/// response with a `Content-Type` of `application/json`. /// response with a `Content-Type` of `application/json`.
/// ///
@ -269,190 +272,108 @@ impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for Json<T> {
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # #[macro_use] extern crate rocket_contrib; /// use rocket::serde::json::{json, Value};
/// use rocket_contrib::json::JsonValue;
/// ///
/// #[get("/json")] /// #[get("/json")]
/// fn get_json() -> JsonValue { /// fn get_json() -> Value {
/// json!({ /// json!({
/// "id": 83, /// "id": 83,
/// "values": [1, 2, 3, 4] /// "values": [1, 2, 3, 4]
/// }) /// })
/// } /// }
/// ``` /// ```
#[derive(Debug, Clone, PartialEq, Default)] #[doc(inline)]
pub struct JsonValue(pub serde_json::Value); pub use serde_json::Value;
impl Serialize for JsonValue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for JsonValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
serde_json::Value::deserialize(deserializer).map(JsonValue)
}
}
impl JsonValue {
#[inline(always)]
fn into_inner(self) -> serde_json::Value {
self.0
}
}
impl Deref for JsonValue {
type Target = serde_json::Value;
#[inline(always)]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for JsonValue {
#[inline(always)]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Into<serde_json::Value> for JsonValue {
#[inline(always)]
fn into(self) -> serde_json::Value {
self.into_inner()
}
}
impl From<serde_json::Value> for JsonValue {
#[inline(always)]
fn from(value: serde_json::Value) -> JsonValue {
JsonValue(value)
}
}
impl<T> FromIterator<T> for JsonValue where serde_json::Value: FromIterator<T> {
fn from_iter<I: IntoIterator<Item=T>>(iter: I) -> Self {
JsonValue(serde_json::Value::from_iter(iter))
}
}
/// Serializes the value into JSON. Returns a response with Content-Type JSON /// Serializes the value into JSON. Returns a response with Content-Type JSON
/// and a fixed-size body with the serialized value. /// and a fixed-size body with the serialized value.
impl<'r> Responder<'r, 'static> for JsonValue { impl<'r> Responder<'r, 'static> for Value {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
content::Json(self.0.to_string()).respond_to(req) content::Json(self.to_string()).respond_to(req)
} }
} }
/// A macro to create ad-hoc JSON serializable values using JSON syntax. crate::export! {
/// /// A macro to create ad-hoc JSON serializable values using JSON syntax.
/// # Usage ///
/// /// The return type of a `json!` invocation is [`Value`](Value). A value
/// To import the macro, add the `#[macro_use]` attribute to the `extern crate /// created with this macro can be returned from a handler as follows:
/// rocket_contrib` invocation: ///
/// /// ```rust
/// ```rust /// # #[macro_use] extern crate rocket;
/// #[macro_use] extern crate rocket_contrib; /// use rocket::serde::json::{json, Value};
/// ``` ///
/// /// #[get("/json")]
/// The return type of a `json!` invocation is /// fn get_json() -> Value {
/// [`JsonValue`](crate::json::JsonValue). A value created with this macro can /// json!({
/// be returned from a handler as follows: /// "key": "value",
/// /// "array": [1, 2, 3, 4]
/// ```rust /// })
/// # #[macro_use] extern crate rocket; /// }
/// # #[macro_use] extern crate rocket_contrib; /// ```
/// use rocket_contrib::json::JsonValue; ///
/// /// The [`Responder`](crate::response::Responder) implementation for
/// #[get("/json")] /// `Value` serializes the value into a JSON string and sets it as the body
/// fn get_json() -> JsonValue { /// of the response with a `Content-Type` of `application/json`.
/// json!({ ///
/// "key": "value", /// # Examples
/// "array": [1, 2, 3, 4] ///
/// }) /// Create a simple JSON object with two keys: `"username"` and `"id"`:
/// } ///
/// ``` /// ```rust
/// /// use rocket::serde::json::json;
/// The [`Responder`](rocket::response::Responder) implementation for ///
/// `JsonValue` serializes the value into a JSON string and sets it as the body /// let value = json!({
/// of the response with a `Content-Type` of `application/json`. /// "username": "mjordan",
/// /// "id": 23
/// # Examples /// });
/// /// ```
/// Create a simple JSON object with two keys: `"username"` and `"id"`: ///
/// /// Create a more complex object with a nested object and array:
/// ```rust ///
/// # #![allow(unused_variables)] /// ```rust
/// # #[macro_use] extern crate rocket_contrib; /// # use rocket::serde::json::json;
/// # fn main() { /// let value = json!({
/// let value = json!({ /// "code": 200,
/// "username": "mjordan", /// "success": true,
/// "id": 23 /// "payload": {
/// }); /// "features": ["serde", "json"],
/// # } /// "ids": [12, 121],
/// ``` /// },
/// /// });
/// Create a more complex object with a nested object and array: /// ```
/// ///
/// ```rust /// Variables or expressions can be interpolated into the JSON literal. Any type
/// # #![allow(unused_variables)] /// interpolated into an array element or object value must implement serde's
/// # #[macro_use] extern crate rocket_contrib; /// `Serialize` trait, while any type interpolated into a object key must
/// # fn main() { /// implement `Into<String>`.
/// let value = json!({ ///
/// "code": 200, /// ```rust
/// "success": true, /// # use rocket::serde::json::json;
/// "payload": { /// let code = 200;
/// "features": ["serde", "json"], /// let features = vec!["serde", "json"];
/// "ids": [12, 121], ///
/// }, /// let value = json!({
/// }); /// "code": code,
/// # } /// "success": code == 200,
/// ``` /// "payload": {
/// /// features[0]: features[1]
/// Variables or expressions can be interpolated into the JSON literal. Any type /// }
/// interpolated into an array element or object value must implement serde's /// });
/// `Serialize` trait, while any type interpolated into a object key must /// ```
/// implement `Into<String>`. ///
/// /// Trailing commas are allowed inside both arrays and objects.
/// ```rust ///
/// # #![allow(unused_variables)] /// ```rust
/// # #[macro_use] extern crate rocket_contrib; /// # use rocket::serde::json::json;
/// # fn main() { /// let value = json!([
/// let code = 200; /// "notice",
/// let features = vec!["serde", "json"]; /// "the",
/// /// "trailing",
/// let value = json!({ /// "comma -->",
/// "code": code, /// ]);
/// "success": code == 200, /// ```
/// "payload": { macro_rules! json {
/// features[0]: features[1] ($($json:tt)+) => ($crate::serde::json::serde_json::json!($($json)*));
/// } }
/// });
/// # }
/// ```
///
/// Trailing commas are allowed inside both arrays and objects.
///
/// ```rust
/// # #![allow(unused_variables)]
/// # #[macro_use] extern crate rocket_contrib;
/// # fn main() {
/// let value = json!([
/// "notice",
/// "the",
/// "trailing",
/// "comma -->",
/// ]);
/// # }
/// ```
#[macro_export]
macro_rules! json {
($($json:tt)+) => {
$crate::json::JsonValue($crate::json::json_internal!($($json)+))
};
} }
#[doc(inline)]
pub use json;

18
core/lib/src/serde/mod.rs Normal file
View File

@ -0,0 +1,18 @@
//! Automatic serialization and deserialization support.
#[doc(inline)]
pub use serde::ser::{Serialize, Serializer};
#[doc(inline)]
pub use serde::de::{Deserialize, DeserializeOwned, Deserializer};
#[doc(hidden)]
pub use serde::*;
#[cfg(feature = "json")]
#[cfg_attr(nightly, doc(cfg(feature = "json")))]
pub mod json;
#[cfg(feature = "msgpack")]
#[cfg_attr(nightly, doc(cfg(feature = "msgpack")))]
pub mod msgpack;

View File

@ -1,35 +1,44 @@
//! Automatic MessagePack (de)serialization support. //! Automatic MessagePack (de)serialization support.
//! //!
//! See the [`MsgPack`](crate::msgpack::MsgPack) type for further details. //! See [`MsgPack`](crate::serde::msgpack::MsgPack) for further details.
//! //!
//! # Enabling //! # Enabling
//! //!
//! This module is only available when the `msgpack` feature is enabled. Enable //! This module is only available when the `json` feature is enabled. Enable it
//! it in `Cargo.toml` as follows: //! in `Cargo.toml` as follows:
//! //!
//! ```toml //! ```toml
//! [dependencies.rocket_contrib] //! [dependencies.rocket]
//! version = "0.5.0-dev" //! version = "0.5.0-dev"
//! default-features = false
//! features = ["msgpack"] //! features = ["msgpack"]
//! ``` //! ```
//!
//! # Testing
//!
//! The [`LocalRequest`] and [`LocalResponse`] types provide [`msgpack()`] and
//! [`into_msgpack()`] methods to create a request with serialized MessagePack
//! and deserialize a response as MessagePack, respectively.
//!
//! [`LocalRequest`]: crate::local::blocking::LocalRequest
//! [`LocalResponse`]: crate::local::blocking::LocalResponse
//! [`msgpack()`]: crate::local::blocking::LocalRequest::msgpack()
//! [`into_msgpack()`]: crate::local::blocking::LocalResponse::into_msgpack()
use std::io; use std::io;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use rocket::request::{Request, local_cache}; use crate::request::{Request, local_cache};
use rocket::data::{ByteUnit, Data, FromData, Outcome}; use crate::data::{Limits, Data, FromData, Outcome};
use rocket::response::{self, Responder, content}; use crate::response::{self, Responder, content};
use rocket::http::Status; use crate::http::Status;
use rocket::form::prelude as form; use crate::form::prelude as form;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
#[doc(inline)]
pub use rmp_serde::decode::Error; pub use rmp_serde::decode::Error;
/// The `MsgPack` data guard and responder: easily consume and respond with /// The MessagePack guard: easily consume and return MessagePack.
/// MessagePack.
/// ///
/// ## Receiving MessagePack /// ## Receiving MessagePack
/// ///
@ -43,9 +52,8 @@ pub use rmp_serde::decode::Error;
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # extern crate rocket_contrib;
/// # type User = usize; /// # type User = usize;
/// use rocket_contrib::msgpack::MsgPack; /// use rocket::serde::msgpack::MsgPack;
/// ///
/// #[post("/users", format = "msgpack", data = "<user>")] /// #[post("/users", format = "msgpack", data = "<user>")]
/// fn new_user(user: MsgPack<User>) { /// fn new_user(user: MsgPack<User>) {
@ -65,10 +73,9 @@ pub use rmp_serde::decode::Error;
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # extern crate rocket_contrib;
/// # type Metadata = usize; /// # type Metadata = usize;
/// use rocket::form::{Form, FromForm}; /// use rocket::form::{Form, FromForm};
/// use rocket_contrib::msgpack::MsgPack; /// use rocket::serde::msgpack::MsgPack;
/// ///
/// #[derive(FromForm)] /// #[derive(FromForm)]
/// struct User<'r> { /// struct User<'r> {
@ -82,27 +89,7 @@ pub use rmp_serde::decode::Error;
/// } /// }
/// ``` /// ```
/// ///
/// ## Sending MessagePack /// ### Incoming Data Limits
///
/// If you're responding with MessagePack data, return a `MsgPack<T>` type,
/// where `T` implements [`Serialize`] from [`serde`]. The content type of the
/// response is set to `application/msgpack` automatically.
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # extern crate rocket_contrib;
/// # type User = usize;
/// use rocket_contrib::msgpack::MsgPack;
///
/// #[get("/users/<id>")]
/// fn user(id: usize) -> MsgPack<User> {
/// let user_from_id = User::from(id);
/// /* ... */
/// MsgPack(user_from_id)
/// }
/// ```
///
/// ## Incoming Data Limits
/// ///
/// The default size limit for incoming MessagePack data is 1MiB. Setting a /// The default size limit for incoming MessagePack data is 1MiB. Setting a
/// limit protects your application from denial of service (DOS) attacks and /// limit protects your application from denial of service (DOS) attacks and
@ -115,6 +102,25 @@ pub use rmp_serde::decode::Error;
/// [global.limits] /// [global.limits]
/// msgpack = 5242880 /// msgpack = 5242880
/// ``` /// ```
///
/// ## Sending MessagePack
///
/// If you're responding with MessagePack data, return a `MsgPack<T>` type,
/// where `T` implements [`Serialize`] from [`serde`]. The content type of the
/// response is set to `application/msgpack` automatically.
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # type User = usize;
/// use rocket::serde::msgpack::MsgPack;
///
/// #[get("/users/<id>")]
/// fn user(id: usize) -> MsgPack<User> {
/// let user_from_id = User::from(id);
/// /* ... */
/// MsgPack(user_from_id)
/// }
/// ```
#[derive(Debug)] #[derive(Debug)]
pub struct MsgPack<T>(pub T); pub struct MsgPack<T>(pub T);
@ -124,7 +130,7 @@ impl<T> MsgPack<T> {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # use rocket_contrib::msgpack::MsgPack; /// # use rocket::serde::msgpack::MsgPack;
/// let string = "Hello".to_string(); /// let string = "Hello".to_string();
/// let my_msgpack = MsgPack(string); /// let my_msgpack = MsgPack(string);
/// assert_eq!(my_msgpack.into_inner(), "Hello".to_string()); /// assert_eq!(my_msgpack.into_inner(), "Hello".to_string());
@ -135,16 +141,14 @@ impl<T> MsgPack<T> {
} }
} }
const DEFAULT_LIMIT: ByteUnit = ByteUnit::Mebibyte(1);
impl<'r, T: Deserialize<'r>> MsgPack<T> { impl<'r, T: Deserialize<'r>> MsgPack<T> {
fn from_bytes(buf: &'r [u8]) -> Result<Self, Error> { fn from_bytes(buf: &'r [u8]) -> Result<Self, Error> {
rmp_serde::from_slice(buf).map(MsgPack) rmp_serde::from_slice(buf).map(MsgPack)
} }
async fn from_data(req: &'r Request<'_>, data: Data) -> Result<Self, Error> { async fn from_data(req: &'r Request<'_>, data: Data) -> Result<Self, Error> {
let size_limit = req.limits().get("msgpack").unwrap_or(DEFAULT_LIMIT); let limit = req.limits().get("msgpack").unwrap_or(Limits::MESSAGE_PACK);
let bytes = match data.open(size_limit).into_bytes().await { let bytes = match data.open(limit).into_bytes().await {
Ok(buf) if buf.is_complete() => buf.into_inner(), Ok(buf) if buf.is_complete() => buf.into_inner(),
Ok(_) => { Ok(_) => {
let eof = io::ErrorKind::UnexpectedEof; let eof = io::ErrorKind::UnexpectedEof;
@ -156,7 +160,7 @@ impl<'r, T: Deserialize<'r>> MsgPack<T> {
Self::from_bytes(local_cache!(req, bytes)) Self::from_bytes(local_cache!(req, bytes))
} }
} }
#[rocket::async_trait] #[crate::async_trait]
impl<'r, T: Deserialize<'r>> FromData<'r> for MsgPack<T> { impl<'r, T: Deserialize<'r>> FromData<'r> for MsgPack<T> {
type Error = Error; type Error = Error;
@ -192,7 +196,7 @@ impl<'r, T: Serialize> Responder<'r, 'static> for MsgPack<T> {
} }
} }
#[rocket::async_trait] #[crate::async_trait]
impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for MsgPack<T> { impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for MsgPack<T> {
async fn from_data(f: form::DataField<'v, '_>) -> Result<Self, form::Errors<'v>> { async fn from_data(f: form::DataField<'v, '_>) -> Result<Self, form::Errors<'v>> {
Self::from_data(f.request, f.data).await.map_err(|e| { Self::from_data(f.request, f.data).await.map_err(|e| {

View File

@ -59,8 +59,8 @@ This directory contains projects showcasing Rocket's features.
derived `Responder`. derived `Responder`.
* **[`serialization`](./serialization)** - Showcases JSON and MessagePack * **[`serialization`](./serialization)** - Showcases JSON and MessagePack
(de)serialization support in `contrib` by implementing a CRUD-like message (de)serialization support by implementing a CRUD-like message API in JSON
API in JSON and a simply read/echo API in MessagePack. and a simply read/echo API in MessagePack.
* **[`state`](./state)** - Illustrates the use of request-local state and * **[`state`](./state)** - Illustrates the use of request-local state and
managed state. Uses request-local state to cache "expensive" per-request managed state. Uses request-local state to cache "expensive" per-request

View File

@ -7,4 +7,3 @@ publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib", features = ["secrets"] } rocket = { path = "../../core/lib", features = ["secrets"] }
serde = { version = "1", features = ["derive"] }

View File

@ -4,10 +4,10 @@
use rocket::{State, Config}; use rocket::{State, Config};
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
use rocket::serde::Deserialize;
use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct AppConfig { struct AppConfig {
key: String, key: String,
port: u16 port: u16

View File

@ -6,9 +6,7 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diesel = { version = "1.3", features = ["sqlite", "r2d2"] } diesel = { version = "1.3", features = ["sqlite", "r2d2"] }
diesel_migrations = "1.3" diesel_migrations = "1.3"
@ -20,4 +18,4 @@ features = ["runtime-tokio-rustls", "sqlite", "macros", "offline", "migrate"]
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
path = "../../contrib/lib" path = "../../contrib/lib"
default-features = false default-features = false
features = ["diesel_sqlite_pool", "sqlite_pool", "json"] features = ["diesel_sqlite_pool", "sqlite_pool"]

View File

@ -1,12 +1,11 @@
use rocket::{Rocket, Build}; use rocket::{Rocket, Build};
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
use rocket::response::{Debug, status::Created}; use rocket::response::{Debug, status::Created};
use rocket::serde::{Serialize, Deserialize, json::Json};
use rocket_contrib::databases::diesel; use rocket_contrib::databases::diesel;
use rocket_contrib::json::Json;
use self::diesel::prelude::*; use self::diesel::prelude::*;
use serde::{Serialize, Deserialize};
#[database("diesel")] #[database("diesel")]
struct Db(diesel::SqliteConnection); struct Db(diesel::SqliteConnection);
@ -14,6 +13,7 @@ struct Db(diesel::SqliteConnection);
type Result<T, E = Debug<diesel::result::Error>> = std::result::Result<T, E>; type Result<T, E = Debug<diesel::result::Error>> = std::result::Result<T, E>;
#[derive(Debug, Clone, Deserialize, Serialize, Queryable, Insertable)] #[derive(Debug, Clone, Deserialize, Serialize, Queryable, Insertable)]
#[serde(crate = "rocket::serde")]
#[table_name="posts"] #[table_name="posts"]
struct Post { struct Post {
#[serde(skip_deserializing)] #[serde(skip_deserializing)]

View File

@ -1,16 +1,17 @@
use rocket::{Rocket, Build}; use rocket::{Rocket, Build};
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
use rocket_contrib::databases::rusqlite; use rocket::serde::{Serialize, Deserialize, json::Json};
use rocket::response::{Debug, status::Created}; use rocket::response::{Debug, status::Created};
use rocket_contrib::json::Json;
use rocket_contrib::databases::rusqlite;
use self::rusqlite::params; use self::rusqlite::params;
use serde::{Serialize, Deserialize};
#[database("rusqlite")] #[database("rusqlite")]
struct Db(rusqlite::Connection); struct Db(rusqlite::Connection);
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
struct Post { struct Post {
#[serde(skip_deserializing, skip_serializing_if = "Option::is_none")] #[serde(skip_deserializing, skip_serializing_if = "Option::is_none")]
id: Option<i64>, id: Option<i64>,

View File

@ -1,11 +1,10 @@
use rocket::{Rocket, State, Build, futures}; use rocket::{Rocket, State, Build, futures};
use rocket::fairing::{self, AdHoc}; use rocket::fairing::{self, AdHoc};
use rocket::response::status::Created; use rocket::response::status::Created;
use rocket_contrib::json::Json; use rocket::serde::{Serialize, Deserialize, json::Json};
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
use futures::future::TryFutureExt; use futures::future::TryFutureExt;
use serde::{Serialize, Deserialize};
use sqlx::ConnectOptions; use sqlx::ConnectOptions;
type Db = sqlx::SqlitePool; type Db = sqlx::SqlitePool;
@ -13,6 +12,7 @@ type Db = sqlx::SqlitePool;
type Result<T, E = rocket::response::Debug<sqlx::Error>> = std::result::Result<T, E>; type Result<T, E = rocket::response::Debug<sqlx::Error>> = std::result::Result<T, E>;
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
struct Post { struct Post {
#[serde(skip_deserializing, skip_serializing_if = "Option::is_none")] #[serde(skip_deserializing, skip_serializing_if = "Option::is_none")]
id: Option<i64>, id: Option<i64>,

View File

@ -1,31 +1,10 @@
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
use rocket::local::blocking::{Client, LocalResponse, LocalRequest}; use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType}; use rocket::serde::{Serialize, Deserialize};
use serde::{Serialize, Deserialize}; use rocket::http::Status;
// Make it easier to work with JSON.
trait LocalResponseExt {
fn into_json<T: serde::de::DeserializeOwned>(self) -> Option<T>;
}
trait LocalRequestExt {
fn json<T: serde::Serialize>(self, value: &T) -> Self;
}
impl LocalResponseExt for LocalResponse<'_> {
fn into_json<T: serde::de::DeserializeOwned>(self) -> Option<T> {
serde_json::from_reader(self).ok()
}
}
impl LocalRequestExt for LocalRequest<'_> {
fn json<T: serde::Serialize>(self, value: &T) -> Self {
let json = serde_json::to_string(value).expect("JSON serialization");
self.header(ContentType::JSON).body(json)
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
struct Post { struct Post {
title: String, title: String,
text: String, text: String,

View File

@ -91,7 +91,7 @@ use rocket::response::content;
// NOTE: This example explicitly uses the `Json` type from `response::content` // NOTE: This example explicitly uses the `Json` type from `response::content`
// for demonstration purposes. In a real application, _always_ prefer to use // for demonstration purposes. In a real application, _always_ prefer to use
// `rocket_contrib::json::Json` instead! // `rocket::serde::json::Json` instead!
// In a `GET` request and all other non-payload supporting request types, the // In a `GET` request and all other non-payload supporting request types, the
// preferred media type in the Accept header is matched against the `format` in // preferred media type in the Accept header is matched against the `format` in
@ -128,7 +128,7 @@ use rocket::response::content::{Json, MsgPack};
use rocket::http::uncased::AsUncased; use rocket::http::uncased::AsUncased;
// NOTE: In a real application, we'd use `Json` and `MsgPack` from // NOTE: In a real application, we'd use `Json` and `MsgPack` from
// `rocket_contrib`, which perform automatic serialization of responses and // `rocket::serde`, which perform automatic serialization of responses and
// automatically set the `Content-Type`. // automatically set the `Content-Type`.
#[get("/content/<kind>")] #[get("/content/<kind>")]
fn json_or_msgpack(kind: &str) -> Either<Json<&'static str>, MsgPack<&'static [u8]>> { fn json_or_msgpack(kind: &str) -> Either<Json<&'static str>, MsgPack<&'static [u8]>> {

View File

@ -5,11 +5,6 @@ workspace = "../"
edition = "2018" edition = "2018"
publish = false publish = false
[dependencies] [dependencies.rocket]
rocket = { path = "../../core/lib" } path = "../../core/lib"
serde = "1"
[dependencies.rocket_contrib]
path = "../../contrib/lib"
default-features = false
features = ["json", "msgpack"] features = ["json", "msgpack"]

View File

@ -2,9 +2,8 @@ use std::borrow::Cow;
use rocket::State; use rocket::State;
use rocket::tokio::sync::Mutex; use rocket::tokio::sync::Mutex;
use rocket_contrib::json::{Json, JsonValue, json}; use rocket::serde::json::{Json, Value, json};
use rocket::serde::{Serialize, Deserialize};
use serde::{Serialize, Deserialize};
// The type to represent the ID of a message. // The type to represent the ID of a message.
type Id = usize; type Id = usize;
@ -14,13 +13,14 @@ type MessageList = Mutex<Vec<String>>;
type Messages<'r> = &'r State<MessageList>; type Messages<'r> = &'r State<MessageList>;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Message<'r> { struct Message<'r> {
id: Option<Id>, id: Option<Id>,
message: Cow<'r, str> message: Cow<'r, str>
} }
#[post("/", format = "json", data = "<message>")] #[post("/", format = "json", data = "<message>")]
async fn new(message: Json<Message<'_>>, list: Messages<'_>) -> JsonValue { async fn new(message: Json<Message<'_>>, list: Messages<'_>) -> Value {
let mut list = list.lock().await; let mut list = list.lock().await;
let id = list.len(); let id = list.len();
list.push(message.message.to_string()); list.push(message.message.to_string());
@ -28,7 +28,7 @@ async fn new(message: Json<Message<'_>>, list: Messages<'_>) -> JsonValue {
} }
#[put("/<id>", format = "json", data = "<message>")] #[put("/<id>", format = "json", data = "<message>")]
async fn update(id: Id, message: Json<Message<'_>>, list: Messages<'_>) -> Option<JsonValue> { async fn update(id: Id, message: Json<Message<'_>>, list: Messages<'_>) -> Option<Value> {
match list.lock().await.get_mut(id) { match list.lock().await.get_mut(id) {
Some(existing) => { Some(existing) => {
*existing = message.message.to_string(); *existing = message.message.to_string();
@ -49,7 +49,7 @@ async fn get<'r>(id: Id, list: Messages<'r>) -> Option<Json<Message<'r>>> {
} }
#[catch(404)] #[catch(404)]
fn not_found() -> JsonValue { fn not_found() -> Value {
json!({ json!({
"status": "error", "status": "error",
"reason": "Resource was not found." "reason": "Resource was not found."

View File

@ -1,21 +1,20 @@
use rocket_contrib::msgpack::MsgPack; use rocket::serde::{Serialize, Deserialize, msgpack::MsgPack};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Message<'r> { struct Message<'r> {
id: usize, id: usize,
contents: &'r str message: &'r str
} }
#[get("/<id>", format = "msgpack")] #[get("/<id>", format = "msgpack")]
fn get(id: usize) -> MsgPack<Message<'static>> { fn get(id: usize) -> MsgPack<Message<'static>> {
MsgPack(Message { id, contents: "Hello, world!", }) MsgPack(Message { id, message: "Hello, world!", })
} }
#[post("/", data = "<data>", format = "msgpack")] #[post("/", data = "<data>", format = "msgpack")]
fn echo<'r>(data: MsgPack<Message<'r>>) -> &'r str { fn echo<'r>(data: MsgPack<Message<'r>>) -> &'r str {
data.contents data.message
} }
pub fn stage() -> rocket::fairing::AdHoc { pub fn stage() -> rocket::fairing::AdHoc {

View File

@ -1,5 +1,24 @@
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType, Accept}; use rocket::http::{Status, ContentType, Accept};
use rocket::serde::{Serialize, Deserialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Message {
id: Option<usize>,
message: String
}
impl Message {
fn new(message: impl Into<String>) -> Self {
Message { message: message.into(), id: None }
}
fn with_id(mut self, id: usize) -> Self {
self.id = Some(id);
self
}
}
#[test] #[test]
fn json_bad_get_put() { fn json_bad_get_put() {
@ -32,8 +51,7 @@ fn json_bad_get_put() {
// Try to put a message for an ID that doesn't exist. // Try to put a message for an ID that doesn't exist.
let res = client.put("/json/80") let res = client.put("/json/80")
.header(ContentType::JSON) .json(&Message::new("hi"))
.body(r#"{ "message": "Bye bye, world!" }"#)
.dispatch(); .dispatch();
assert_eq!(res.status(), Status::NotFound); assert_eq!(res.status(), Status::NotFound);
@ -46,40 +64,30 @@ fn json_post_get_put_get() {
// Create/read/update/read a few messages. // Create/read/update/read a few messages.
for id in 0..10 { for id in 0..10 {
let uri = format!("/json/{}", id); let uri = format!("/json/{}", id);
let message = format!("Hello, JSON {}!", id);
// Check that a message with doesn't exist. // Check that a message with doesn't exist.
let res = client.get(&uri).header(ContentType::JSON).dispatch(); let res = client.get(&uri).header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::NotFound); assert_eq!(res.status(), Status::NotFound);
// Add a new message. This should be ID 0. // Add a new message. This should be ID 0.
let res = client.post("/json") let message = Message::new(format!("Hello, JSON {}!", id));
.header(ContentType::JSON) let res = client.post("/json").json(&message).dispatch();
.body(format!(r#"{{ "message": "{}" }}"#, message))
.dispatch();
assert_eq!(res.status(), Status::Ok); assert_eq!(res.status(), Status::Ok);
// Check that the message exists with the correct contents. // Check that the message exists with the correct contents.
let res = client.get(&uri).header(Accept::JSON).dispatch(); let res = client.get(&uri).header(Accept::JSON).dispatch();
assert_eq!(res.status(), Status::Ok); assert_eq!(res.status(), Status::Ok);
let body = res.into_string().unwrap(); assert_eq!(res.into_json::<Message>().unwrap(), message.with_id(id));
assert!(body.contains(&message));
// Change the message contents. // Change the message contents.
let res = client.put(&uri) let message = Message::new("Bye bye, world!");
.header(ContentType::JSON) let res = client.put(&uri).json(&message).dispatch();
.body(r#"{ "message": "Bye bye, world!" }"#)
.dispatch();
assert_eq!(res.status(), Status::Ok); assert_eq!(res.status(), Status::Ok);
// Check that the message exists with the updated contents. // Check that the message exists with the updated contents.
let res = client.get(&uri).header(Accept::JSON).dispatch(); let res = client.get(&uri).header(Accept::JSON).dispatch();
assert_eq!(res.status(), Status::Ok); assert_eq!(res.status(), Status::Ok);
let body = res.into_string().unwrap(); assert_eq!(res.into_json::<Message>().unwrap(), message.with_id(id));
assert!(!body.contains(&message));
assert!(body.contains("Bye bye, world!"));
} }
} }
@ -91,8 +99,8 @@ fn msgpack_get() {
assert_eq!(res.content_type(), Some(ContentType::MsgPack)); assert_eq!(res.content_type(), Some(ContentType::MsgPack));
// Check that the message is `[1, "Hello, world!"]` // Check that the message is `[1, "Hello, world!"]`
assert_eq!(&res.into_bytes().unwrap(), &[146, 1, 173, 72, 101, 108, 108, let msg = Message::new("Hello, world!").with_id(1);
111, 44, 32, 119, 111, 114, 108, 100, 33]); assert_eq!(res.into_msgpack::<Message>().unwrap(), msg);
} }
#[test] #[test]
@ -100,11 +108,9 @@ fn msgpack_post() {
// Dispatch request with a message of `[2, "Goodbye, world!"]`. // Dispatch request with a message of `[2, "Goodbye, world!"]`.
let client = Client::tracked(super::rocket()).unwrap(); let client = Client::tracked(super::rocket()).unwrap();
let res = client.post("/msgpack") let res = client.post("/msgpack")
.header(ContentType::MsgPack) .msgpack(&Message::new("Goodbye, world!").with_id(2))
.body(&[146, 2, 175, 71, 111, 111, 100, 98, 121, 101, 44, 32, 119, 111,
114, 108, 100, 33])
.dispatch(); .dispatch();
assert_eq!(res.status(), Status::Ok); assert_eq!(res.status(), Status::Ok);
assert_eq!(res.into_string(), Some("Goodbye, world!".into())); assert_eq!(res.into_string().unwrap(), "Goodbye, world!");
} }

View File

@ -7,8 +7,6 @@ publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
path = "../../contrib/lib" path = "../../contrib/lib"

View File

@ -1,9 +1,13 @@
use rocket::Request; use rocket::Request;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::serde::Serialize;
use rocket_contrib::templates::{Template, handlebars}; use rocket_contrib::templates::{Template, handlebars};
use self::handlebars::{Handlebars, JsonRender}; use self::handlebars::{Handlebars, JsonRender};
#[derive(serde::Serialize)] #[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct TemplateContext<'r> { struct TemplateContext<'r> {
title: &'r str, title: &'r str,
name: Option<&'r str>, name: Option<&'r str>,

View File

@ -2,9 +2,12 @@ use std::collections::HashMap;
use rocket::Request; use rocket::Request;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::serde::Serialize;
use rocket_contrib::templates::{Template, tera::Tera}; use rocket_contrib::templates::{Template, tera::Tera};
#[derive(serde::Serialize)] #[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct TemplateContext<'r> { struct TemplateContext<'r> {
title: &'r str, title: &'r str,
name: Option<&'r str>, name: Option<&'r str>,

View File

@ -7,8 +7,6 @@ publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diesel = { version = "1.3", features = ["sqlite", "r2d2"] } diesel = { version = "1.3", features = ["sqlite", "r2d2"] }
diesel_migrations = "1.3" diesel_migrations = "1.3"

View File

@ -11,6 +11,7 @@ use rocket::{Rocket, Build};
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
use rocket::request::FlashMessage; use rocket::request::FlashMessage;
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket::serde::Serialize;
use rocket::form::Form; use rocket::form::Form;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
@ -21,7 +22,8 @@ use crate::task::{Task, Todo};
#[database("sqlite_database")] #[database("sqlite_database")]
pub struct DbConn(diesel::SqliteConnection); pub struct DbConn(diesel::SqliteConnection);
#[derive(Debug, serde::Serialize)] #[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct Context { struct Context {
flash: Option<(String, String)>, flash: Option<(String, String)>,
tasks: Vec<Task> tasks: Vec<Task>

View File

@ -1,3 +1,4 @@
use rocket::serde::Serialize;
use diesel::{self, result::QueryResult, prelude::*}; use diesel::{self, result::QueryResult, prelude::*};
mod schema { mod schema {
@ -15,7 +16,8 @@ use self::schema::tasks::dsl::{tasks as all_tasks, completed as task_completed};
use crate::DbConn; use crate::DbConn;
#[derive(serde::Serialize, Queryable, Insertable, Debug, Clone)] #[derive(Serialize, Queryable, Insertable, Debug, Clone)]
#[serde(crate = "rocket::serde")]
#[table_name="tasks"] #[table_name="tasks"]
pub struct Task { pub struct Task {
pub id: Option<i32>, pub id: Option<i32>,

View File

@ -60,8 +60,6 @@ function check_style() {
function test_contrib() { function test_contrib() {
FEATURES=( FEATURES=(
json
msgpack
tera_templates tera_templates
handlebars_templates handlebars_templates
serve serve
@ -92,6 +90,8 @@ function test_core() {
FEATURES=( FEATURES=(
secrets secrets
tls tls
json
msgpack
) )
pushd "${CORE_LIB_ROOT}" > /dev/null 2>&1 pushd "${CORE_LIB_ROOT}" > /dev/null 2>&1

View File

@ -612,18 +612,14 @@ Any type that implements [`FromData`] is also known as _a data guard_.
### JSON ### JSON
The [`Json<T>`](@api/rocket_contrib/json/struct.Json.html) type from The [`Json<T>`](@api/rocket/serde/json/struct.Json.html) guard deserialzies body
[`rocket_contrib`] is a data guard that parses the deserialzies body data as data as JSON. The only condition is that the generic type `T` implements the
JSON. The only condition is that the generic type `T` implements the `Deserialize` trait from [`serde`](https://serde.rs).
`Deserialize` trait from [Serde](https://github.com/serde-rs/json).
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
# extern crate rocket_contrib;
# fn main() {}
use serde::Deserialize; use rocket::serde::{Deserialize, json::Json};
use rocket_contrib::json::Json;
#[derive(Deserialize)] #[derive(Deserialize)]
struct Task<'r> { struct Task<'r> {

View File

@ -63,7 +63,7 @@ fn json() -> content::Json<&'static str> {
} }
``` ```
! warning: This is _not_ the same as the [`Json`] in [`rocket_contrib`]! ! warning: This is _not_ the same as the [`Json`] in [`serde`]!
[`Accepted`]: @api/rocket/response/status/struct.Accepted.html [`Accepted`]: @api/rocket/response/status/struct.Accepted.html
[`content::Json`]: @api/rocket/response/content/struct.Json.html [`content::Json`]: @api/rocket/response/content/struct.Json.html
@ -346,21 +346,18 @@ how to detect and handle graceful shutdown requests.
### JSON ### JSON
The [`Json`] responder in [`rocket_contrib`] allows you to easily respond with The [`Json`] responder in allows you to easily respond with well-formed JSON
well-formed JSON data: simply return a value of type `Json<T>` where `T` is the data: simply return a value of type `Json<T>` where `T` is the type of a
type of a structure to serialize into JSON. The type `T` must implement the structure to serialize into JSON. The type `T` must implement the [`Serialize`]
[`Serialize`] trait from [`serde`], which can be automatically derived. trait from [`serde`], which can be automatically derived.
As an example, to respond with the JSON value of a `Task` structure, we might As an example, to respond with the JSON value of a `Task` structure, we might
write: write:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
# #[macro_use] extern crate rocket_contrib;
# fn main() {}
use serde::Serialize; use rocket::serde::{Serialize, json::Json};
use rocket_contrib::json::Json;
#[derive(Serialize)] #[derive(Serialize)]
struct Task { /* .. */ } struct Task { /* .. */ }
@ -377,9 +374,9 @@ fails, a **500 - Internal Server Error** is returned.
The [serialization example] provides further illustration. The [serialization example] provides further illustration.
[`Json`]: @api/rocket_contrib/json/struct.Json.html [`Json`]: @api/rocket/serde/json/struct.Json.html
[`Serialize`]: https://docs.serde.rs/serde/trait.Serialize.html [`Serialize`]: https://docs.serde.rs/serde/trait.Serialize.html
[`serde`]: https://docs.serde.rs/serde/ [`serde`]: https://serde.rs
[serialization example]: @example/serialization [serialization example]: @example/serialization
## Templates ## Templates

View File

@ -69,6 +69,8 @@ a few below:
* [`headers`]: returns a map of all of the headers in the response. * [`headers`]: returns a map of all of the headers in the response.
* [`into_string`]: reads the body data into a `String`. * [`into_string`]: reads the body data into a `String`.
* [`into_bytes`]: reads the body data into a `Vec<u8>`. * [`into_bytes`]: reads the body data into a `Vec<u8>`.
* [`into_json`]: deserializes the body data on-the-fly as JSON.
* [`into_msgpack`]: deserializes the body data on-the-fly as MessagePack.
[`LocalResponse`]: @api/rocket/local/blocking/struct.LocalResponse.html [`LocalResponse`]: @api/rocket/local/blocking/struct.LocalResponse.html
[`status`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.status [`status`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.status
@ -76,6 +78,8 @@ a few below:
[`headers`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.headers [`headers`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.headers
[`into_string`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.into_string [`into_string`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.into_string
[`into_bytes`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.into_bytes [`into_bytes`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.into_bytes
[`into_json`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.into_json
[`into_msgpack`]: @api/rocket/local/blocking/struct.LocalResponse.html#method.into_msgpack
These methods are typically used in combination with the `assert_eq!` or These methods are typically used in combination with the `assert_eq!` or
`assert!` macros as follows: `assert!` macros as follows:
@ -301,24 +305,26 @@ note: emitting Rocket code generation debug output
| ^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^
| |
= note: = note:
impl From<world> for rocket::StaticRouteInfo { impl world {
fn from(_: world) -> rocket::StaticRouteInfo { fn into_info(self) -> rocket::StaticRouteInfo {
fn monomorphized_function<'_b>( fn monomorphized_function<'_b>(
__req: &'_b rocket::request::Request<'_>, __req: &'_b rocket::request::Request<'_>,
__data: rocket::data::Data, __data: rocket::data::Data,
) -> rocket::handler::HandlerFuture<'_b> { ) -> ::rocket::route::BoxFuture<'_b> {
::std::boxed::Box::pin(async move { ::std::boxed::Box::pin(async move {
let ___responder = world(); let ___responder = world();
rocket::handler::Outcome::from(__req, ___responder) ::rocket::handler::Outcome::from(__req, ___responder)
}) })
} }
rocket::StaticRouteInfo {
::rocket::StaticRouteInfo {
name: "world", name: "world",
method: ::rocket::http::Method::Get, method: ::rocket::http::Method::Get,
path: "/world", path: "/world",
handler: monomorphized_function, handler: monomorphized_function,
format: ::std::option::Option::None, format: ::std::option::Option::None,
rank: ::std::option::Option::None, rank: ::std::option::Option::None,
sentinels: sentinels![&'static str],
} }
} }
} }

View File

@ -57,7 +57,7 @@ selected profile doesn't contain a requested values, while values in the
[`Toml`]: @figment/providers/struct.Toml.html [`Toml`]: @figment/providers/struct.Toml.html
[`Json`]: @figment/providers/struct.Json.html [`Json`]: @figment/providers/struct.Json.html
[`Figment`]: @api/rocket/struct.Figment.html [`Figment`]: @api/rocket/struct.Figment.html
[`Deserialize`]: @serde/trait.Deserialize.html [`Deserialize`]: @api/rocket/serde/trait.Deserialize.html
[`Limits::default()`]: @api/rocket/data/struct.Limits.html#impl-Default [`Limits::default()`]: @api/rocket/data/struct.Limits.html#impl-Default
### Secret Key ### Secret Key
@ -205,10 +205,8 @@ from the configured provider, which is exposed via [`Rocket::figment()`]:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
# extern crate serde;
use serde::Deserialize;
use rocket::serde::Deserialize;
#[launch] #[launch]
fn rocket() -> _ { fn rocket() -> _ {
@ -242,8 +240,7 @@ Because it is common to store configuration in managed state, Rocket provides an
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
# extern crate serde; # use rocket::serde::Deserialize;
# use serde::Deserialize;
# #[derive(Deserialize)] # #[derive(Deserialize)]
# struct Config { # struct Config {
# port: u16, # port: u16,
@ -278,21 +275,15 @@ more complex cases.
! note: You may need to depend on `figment` and `serde` directly. ! note: You may need to depend on `figment` and `serde` directly.
Rocket reexports `figment` from its crate root, so you can refer to `figment` Rocket reexports `figment` and `serde` from its crate root, so you can refer
types via `rocket::figment`. However, Rocket does not enable all features from to `figment` types via `rocket::figment` and `serde` types via
the figment crate. As such, you may need to import `figment` directly: `rocket::serde`. However, Rocket does not enable all features from either
crate. As such, you may need to import crates directly:
` `
figment = { version = "0.9", features = ["env", "toml", "json"] } figment = { version = "0.9", features = ["env", "toml", "json"] }
` `
Furthermore, you should directly depend on `serde` when using its `derive`
feature, which is also not enabled by Rocket:
`
serde = { version = "1", features = ["derive"] }
`
As a first example, we override configuration values at runtime by merging As a first example, we override configuration values at runtime by merging
figment's tuple providers with Rocket's default provider: figment's tuple providers with Rocket's default provider:
@ -320,10 +311,11 @@ and `APP_PROFILE` to configure the selected profile:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
use serde::{Serialize, Deserialize}; use rocket::serde::{Serialize, Deserialize};
use figment::{Figment, Profile, providers::{Format, Toml, Serialized, Env}};
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
use figment::{Figment, Profile, providers::{Format, Toml, Serialized, Env}};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
struct Config { struct Config {
app_value: usize, app_value: usize,

View File

@ -9,8 +9,8 @@ publish = false
rocket = { path = "../../core/lib", features = ["secrets"] } rocket = { path = "../../core/lib", features = ["secrets"] }
[dev-dependencies] [dev-dependencies]
rocket = { path = "../../core/lib", features = ["secrets"] } rocket = { path = "../../core/lib", features = ["secrets", "json"] }
rocket_contrib = { path = "../../contrib/lib", features = ["json", "tera_templates", "diesel_sqlite_pool"] } rocket_contrib = { path = "../../contrib/lib", features = ["tera_templates", "diesel_sqlite_pool"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
rand = "0.8" rand = "0.8"
figment = { version = "0.10", features = ["toml", "env"] } figment = { version = "0.10", features = ["toml", "env"] }