diff --git a/contrib/lib/Cargo.toml b/contrib/lib/Cargo.toml index 38fabde5..fd1ebb83 100644 --- a/contrib/lib/Cargo.toml +++ b/contrib/lib/Cargo.toml @@ -20,9 +20,7 @@ databases = [ ] # User-facing features. -default = ["json", "serve"] -json = ["serde", "serde_json", "tokio/io-util"] -msgpack = ["serde", "rmp-serde", "tokio/io-util"] +default = ["serve"] tera_templates = ["tera", "templates"] handlebars_templates = ["handlebars", "templates"] helmet = ["time"] @@ -50,7 +48,6 @@ log = "0.4" # Serialization and templating dependencies. serde = { version = "1.0", optional = true, features = ["derive"] } serde_json = { version = "1.0.26", optional = true } -rmp-serde = { version = "0.15.0", optional = true } # Templating dependencies. handlebars = { version = "3.0", optional = true } diff --git a/contrib/lib/src/lib.rs b/contrib/lib/src/lib.rs index fe6ea7de..dc054ee0 100644 --- a/contrib/lib/src/lib.rs +++ b/contrib/lib/src/lib.rs @@ -16,9 +16,7 @@ //! common modules exposed by default. The present feature list is below, with //! an asterisk next to the features that are enabled by default: //! -//! * [json*](type@json) - JSON (de)serialization //! * [serve*](serve) - Static File Serving -//! * [msgpack](msgpack) - MessagePack (de)serialization //! * [handlebars_templates](templates) - Handlebars Templating //! * [tera_templates](templates) - Tera Templating //! * [uuid](uuid) - UUID (de)serialization @@ -28,13 +26,14 @@ //! The recommend way to include features from this crate via Rocket in your //! project is by adding a `[dependencies.rocket_contrib]` section to your //! `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 //! [dependencies.rocket_contrib] //! version = "0.5.0-dev" //! default-features = false -//! features = ["json"] +//! features = ["tera_templates"] //! ``` //! //! 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; -#[cfg(feature="json")] #[macro_use] pub mod json; #[cfg(feature="serve")] pub mod serve; -#[cfg(feature="msgpack")] pub mod msgpack; #[cfg(feature="templates")] pub mod templates; #[cfg(feature="uuid")] pub mod uuid; #[cfg(feature="databases")] pub mod databases; diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 112fb0c6..ff327a92 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -22,8 +22,15 @@ all-features = true default = [] tls = ["rocket_http/tls"] secrets = ["rocket_http/private-cookies"] +json = ["serde_json", "tokio/io-util"] +msgpack = ["rmp-serde", "tokio/io-util"] [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" yansi = "0.5" log = { version = "0.4", features = ["std"] } diff --git a/core/lib/src/data/limits.rs b/core/lib/src/data/limits.rs index a93820e3..714e83bc 100644 --- a/core/lib/src/data/limits.rs +++ b/core/lib/src/data/limits.rs @@ -64,8 +64,12 @@ use crate::http::uncased::Uncased; /// | `file/$ext` | _N/A_ | [`TempFile`] | file form field with extension `$ext` | /// | `string` | 8KiB | [`String`] | data guard or data form field | /// | `bytes` | 8KiB | [`Vec`] | data guard | +/// | `json` | 1MiB | [`Json`] | JSON data and form payloads | +/// | `msgpack` | 1MiB | [`MsgPack`] | MessagePack data and form payloads | /// /// [`TempFile`]: crate::data::TempFile +/// [`Json`]: crate::serde::json::Json +/// [`MsgPack`]: crate::serde::msgpack::MsgPack /// /// # Usage /// @@ -78,7 +82,7 @@ use crate::http::uncased::Uncased; /// let limits = Limits::default() /// .limit("form", 64.kibibytes()) /// .limit("file/pdf", 3.mebibytes()) -/// .limit("json", 1.mebibytes()); +/// .limit("json", 2.mebibytes()); /// ``` /// /// The [`Limits::default()`](#impl-Default) method populates the `Limits` @@ -134,6 +138,8 @@ impl Default for Limits { .limit("file", Limits::FILE) .limit("string", Limits::STRING) .limit("bytes", Limits::BYTES) + .limit("json", Limits::JSON) + .limit("msgpack", Limits::MESSAGE_PACK) } } @@ -153,6 +159,12 @@ impl Limits { /// Default limit for bytes. 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. /// /// # Example @@ -181,11 +193,12 @@ impl Limits { /// /// let limits = Limits::default(); /// 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("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()); /// assert_eq!(limits.get("json"), Some(64.mebibytes())); @@ -209,12 +222,12 @@ impl Limits { /// use rocket::data::{Limits, ToByteUnit}; /// /// let limits = Limits::default() - /// .limit("json", 1.mebibytes()) + /// .limit("json", 2.mebibytes()) /// .limit("file/jpeg", 4.mebibytes()) /// .limit("file/jpeg/special", 8.mebibytes()); /// /// 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("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/special"), Some(8.mebibytes())); /// - /// assert!(limits.get("msgpack").is_none()); + /// assert!(limits.get("cats").is_none()); /// ``` pub fn get>(&self, name: S) -> Option { let mut name = name.as_ref(); diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index cf64ecf8..209510b7 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -27,14 +27,6 @@ //! [quickstart]: https://rocket.rs/master/guide/quickstart //! [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 //! //! Depend on `rocket` in `Rocket.toml`: @@ -68,39 +60,44 @@ //! //! ## 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]. -//! * **tls:** Enables support for [TLS]. +//! | Feature | Description | +//! |-----------|---------------------------------------------------------| +//! | `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 //! [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 //! [TLS]: https://rocket.rs/master/guide/configuration/#tls //! //! ## Configuration //! -//! By default, Rocket applications are configured via a `Rocket.toml` file -//! and/or `ROCKET_{PARAM}` environment variables. For more information on how -//! to configure Rocket, including how to completely customize configuration -//! sources, see the [configuration section] of the guide as well as the -//! [`config`] module documentation. -//! -//! [configuration section]: https://rocket.rs/master/guide/configuration/ +//! Rocket offers a rich, extensible configuration system built on [Figment]. By +//! default, Rocket applications are configured via a `Rocket.toml` file +//! and/or `ROCKET_{PARAM}` environment variables, but applications may +//! configure their own sources. See the [configuration guide] for full details. //! //! ## Testing //! //! The [`local`] module contains structures that facilitate unit and //! integration testing of a Rocket application. The top-level [`local`] module -//! documentation and the [testing chapter of the guide] include detailed -//! examples. +//! documentation and the [testing guide] include detailed 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 /// figment's version number in docs. @@ -124,6 +121,7 @@ pub mod fairing; pub mod error; pub mod catcher; pub mod route; +pub mod serde; // Reexport of HTTP everything. pub mod http { diff --git a/core/lib/src/local/asynchronous/response.rs b/core/lib/src/local/asynchronous/response.rs index cda66d7f..9267064d 100644 --- a/core/lib/src/local/asynchronous/response.rs +++ b/core/lib/src/local/asynchronous/response.rs @@ -115,6 +115,73 @@ impl LocalResponse<'_> { self.response.body_mut().to_bytes().await } + #[cfg(feature = "json")] + async fn _into_json(self) -> Option + where T: serde::de::DeserializeOwned + { + self.blocking_read(|r| serde_json::from_reader(r)).await?.ok() + } + + #[cfg(feature = "msgpack")] + async fn _into_msgpack(self) -> Option + 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(mut self, f: F) -> Option + 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>>, + rx: mpsc::Receiver>>, + } + + impl std::io::Read for ChanReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + 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. pub_response_impl!("# use rocket::local::asynchronous::Client;\n\ use rocket::local::asynchronous::LocalResponse;" async await); diff --git a/core/lib/src/local/blocking/response.rs b/core/lib/src/local/blocking/response.rs index a041b43f..fc009398 100644 --- a/core/lib/src/local/blocking/response.rs +++ b/core/lib/src/local/blocking/response.rs @@ -71,6 +71,20 @@ impl LocalResponse<'_> { self.client.block_on(self.inner._into_bytes()) } + #[cfg(feature = "json")] + fn _into_json(self) -> Option + where T: serde::de::DeserializeOwned + { + serde_json::from_reader(self).ok() + } + + #[cfg(feature = "msgpack")] + fn _into_msgpack(self) -> Option + where T: serde::de::DeserializeOwned + { + rmp_serde::from_read(self).ok() + } + // Generates the public API methods, which call the private methods above. pub_response_impl!("# use rocket::local::blocking::Client;\n\ use rocket::local::blocking::LocalResponse;"); diff --git a/core/lib/src/local/mod.rs b/core/lib/src/local/mod.rs index f261aa1d..bed689a4 100644 --- a/core/lib/src/local/mod.rs +++ b/core/lib/src/local/mod.rs @@ -51,8 +51,9 @@ //! // Using the preferred `blocking` API. //! #[test] //! fn test_hello_world_blocking() { -//! // Construct a client to use for dispatching requests. //! use rocket::local::blocking::Client; +//! +//! // Construct a client to use for dispatching requests. //! let client = Client::tracked(super::rocket()) //! .expect("valid `Rocket`"); //! @@ -64,8 +65,9 @@ //! // Using the `asynchronous` API. //! #[rocket::async_test] //! async fn test_hello_world_async() { -//! // Construct a client to use for dispatching requests. //! use rocket::local::asynchronous::Client; +//! +//! // Construct a client to use for dispatching requests. //! let client = Client::tracked(super::rocket()).await //! .expect("valid `Rocket`"); //! diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index a6a6303b..76ce4af3 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -191,12 +191,10 @@ macro_rules! pub_request_impl { self } - /// Set the body (data) of the request. + /// Sets the body data of the request. /// /// # Examples /// - /// Set the body to be a JSON structure; also sets the Content-Type. - /// /// ```rust #[doc = $import] /// use rocket::http::ContentType; @@ -204,8 +202,8 @@ macro_rules! pub_request_impl { /// # Client::_test(|_, request, _| { /// let request: LocalRequest = request; /// let req = request - /// .header(ContentType::JSON) - /// .body(r#"{ "key": "value", "array": [1, 2, 3] }"#); + /// .header(ContentType::Text) + /// .body("Hello, world!"); /// # }); /// ``` #[inline] @@ -219,6 +217,74 @@ macro_rules! pub_request_impl { 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(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(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`. /// /// # Examples diff --git a/core/lib/src/local/response.rs b/core/lib/src/local/response.rs index b005458b..411be73f 100644 --- a/core/lib/src/local/response.rs +++ b/core/lib/src/local/response.rs @@ -85,7 +85,7 @@ macro_rules! pub_response_impl { /// Consumes `self` and reads the entirety of its body into a `Vec` of /// 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 /// is empty. /// @@ -108,6 +108,78 @@ macro_rules! pub_response_impl { 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::(); + /// # }); + /// ``` + #[cfg(feature = "json")] + #[cfg_attr(nightly, doc(cfg(feature = "json")))] + pub $($prefix)? fn into_json(self) -> Option + 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::(); + /// # }); + /// ``` + #[cfg(feature = "msgpack")] + #[cfg_attr(nightly, doc(cfg(feature = "msgpack")))] + pub $($prefix)? fn into_msgpack(self) -> Option + where T: Send + serde::de::DeserializeOwned + 'static + { + if self._response().body().is_none() { + return None; + } + + self._into_msgpack() $(.$suffix)? + } + #[cfg(test)] #[allow(dead_code)] fn _ensure_impls_exist() { diff --git a/contrib/lib/src/json.rs b/core/lib/src/serde/json.rs similarity index 50% rename from contrib/lib/src/json.rs rename to core/lib/src/serde/json.rs index 410b79d3..f3d1eae9 100644 --- a/contrib/lib/src/json.rs +++ b/core/lib/src/serde/json.rs @@ -1,6 +1,6 @@ //! Automatic JSON (de)serialization support. //! -//! See the [`Json`](crate::json::Json) type for further details. +//! See [`Json`](Json) for details. //! //! # Enabling //! @@ -8,28 +8,37 @@ //! in `Cargo.toml` as follows: //! //! ```toml -//! [dependencies.rocket_contrib] +//! [dependencies.rocket] //! version = "0.5.0-dev" -//! default-features = false //! 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::ops::{Deref, DerefMut}; -use std::iter::FromIterator; -use rocket::request::{Request, local_cache}; -use rocket::data::{ByteUnit, Data, FromData, Outcome}; -use rocket::response::{self, Responder, content}; -use rocket::http::Status; -use rocket::form::prelude as form; +use crate::request::{Request, local_cache}; +use crate::data::{Limits, Data, FromData, Outcome}; +use crate::response::{self, Responder, content}; +use crate::http::Status; +use crate::form::prelude as form; -use serde::{Serialize, Serializer, Deserialize, Deserializer}; +use serde::{Serialize, Deserialize}; #[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 /// @@ -43,9 +52,8 @@ pub use serde_json::{json_internal, json_internal_vec}; /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # extern crate rocket_contrib; /// # type User = usize; -/// use rocket_contrib::json::Json; +/// use rocket::serde::json::Json; /// /// #[post("/user", format = "json", data = "")] /// fn new_user(user: Json) { @@ -65,10 +73,9 @@ pub use serde_json::{json_internal, json_internal_vec}; /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # extern crate rocket_contrib; /// # type Metadata = usize; /// use rocket::form::{Form, FromForm}; -/// use rocket_contrib::json::Json; +/// use rocket::serde::json::Json; /// /// #[derive(FromForm)] /// struct User<'r> { @@ -82,27 +89,7 @@ pub use serde_json::{json_internal, json_internal_vec}; /// } /// ``` /// -/// ## Sending JSON -/// -/// If you're responding with JSON data, return a `Json` 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/")] -/// fn user(id: usize) -> Json { -/// let user_from_id = User::from(id); -/// /* ... */ -/// Json(user_from_id) -/// } -/// ``` -/// -/// ## Incoming Data Limits +/// ### Incoming Data Limits /// /// The default size limit for incoming JSON data is 1MiB. Setting a limit /// 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] /// json = 5242880 /// ``` +/// +/// ## Sending JSON +/// +/// If you're responding with JSON data, return a `Json` 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/")] +/// fn user(id: usize) -> Json { +/// let user_from_id = User::from(id); +/// /* ... */ +/// Json(user_from_id) +/// } +/// ``` #[derive(Debug)] pub struct Json(pub T); -/// An error returned by the [`Json`] data guard when incoming data fails to -/// serialize as JSON. +/// Error returned by the [`Json`] guard when JSON deserialization fails. #[derive(Debug)] -pub enum JsonError<'a> { +pub enum Error<'a> { /// An I/O error occurred while reading the incoming request data. Io(io::Error), @@ -132,14 +137,12 @@ pub enum JsonError<'a> { Parse(&'a str, serde_json::error::Error), } -const DEFAULT_LIMIT: ByteUnit = ByteUnit::Mebibyte(1); - impl Json { /// Consumes the JSON wrapper and returns the wrapped item. /// /// # Example /// ```rust - /// # use rocket_contrib::json::Json; + /// # use rocket::serde::json::Json; /// let string = "Hello".to_string(); /// let my_json = Json(string); /// assert_eq!(my_json.into_inner(), "Hello".to_string()); @@ -151,37 +154,37 @@ impl Json { } impl<'r, T: Deserialize<'r>> Json { - fn from_str(s: &'r str) -> Result> { - serde_json::from_str(s).map(Json).map_err(|e| JsonError::Parse(s, e)) + fn from_str(s: &'r str) -> Result> { + serde_json::from_str(s).map(Json).map_err(|e| Error::Parse(s, e)) } - async fn from_data(req: &'r Request<'_>, data: Data) -> Result> { - let size_limit = req.limits().get("json").unwrap_or(DEFAULT_LIMIT); - let string = match data.open(size_limit).into_string().await { + async fn from_data(req: &'r Request<'_>, data: Data) -> Result> { + let limit = req.limits().get("json").unwrap_or(Limits::JSON); + let string = match data.open(limit).into_string().await { Ok(s) if s.is_complete() => s.into_inner(), Ok(_) => { 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)) } } -#[rocket::async_trait] +#[crate::async_trait] impl<'r, T: Deserialize<'r>> FromData<'r> for Json { - type Error = JsonError<'r>; + type Error = Error<'r>; async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { match Self::from_data(req, data).await { Ok(value) => Outcome::Success(value), - Err(JsonError::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { - Outcome::Failure((Status::PayloadTooLarge, JsonError::Io(e))) + Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { + Outcome::Failure((Status::PayloadTooLarge, Error::Io(e))) }, - Err(JsonError::Parse(s, e)) if e.classify() == serde_json::error::Category::Data => { - Outcome::Failure((Status::UnprocessableEntity, JsonError::Parse(s, e))) + Err(Error::Parse(s, e)) if e.classify() == serde_json::error::Category::Data => { + Outcome::Failure((Status::UnprocessableEntity, Error::Parse(s, e))) }, Err(e) => Outcome::Failure((Status::BadRequest, e)), @@ -226,16 +229,16 @@ impl DerefMut for Json { } } -impl From> for form::Error<'_> { - fn from(e: JsonError<'_>) -> Self { +impl From> for form::Error<'_> { + fn from(e: Error<'_>) -> Self { match e { - JsonError::Io(e) => e.into(), - JsonError::Parse(_, e) => form::Error::custom(e) + Error::Io(e) => e.into(), + Error::Parse(_, e) => form::Error::custom(e) } } } -#[rocket::async_trait] +#[crate::async_trait] impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for Json { fn from_value(field: form::ValueField<'v>) -> Result> { Ok(Self::from_str(field.value)?) @@ -253,11 +256,11 @@ impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for Json { /// returned directly from a handler. /// /// [`Value`]: serde_json::value -/// [`Responder`]: rocket::response::Responder +/// [`Responder`]: crate::response::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 /// response with a `Content-Type` of `application/json`. /// @@ -269,190 +272,108 @@ impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for Json { /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # #[macro_use] extern crate rocket_contrib; -/// use rocket_contrib::json::JsonValue; +/// use rocket::serde::json::{json, Value}; /// /// #[get("/json")] -/// fn get_json() -> JsonValue { +/// fn get_json() -> Value { /// json!({ /// "id": 83, /// "values": [1, 2, 3, 4] /// }) /// } /// ``` -#[derive(Debug, Clone, PartialEq, Default)] -pub struct JsonValue(pub serde_json::Value); - -impl Serialize for JsonValue { - fn serialize(&self, serializer: S) -> Result { - self.0.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for JsonValue { - fn deserialize>(deserializer: D) -> Result { - 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 for JsonValue { - #[inline(always)] - fn into(self) -> serde_json::Value { - self.into_inner() - } -} - -impl From for JsonValue { - #[inline(always)] - fn from(value: serde_json::Value) -> JsonValue { - JsonValue(value) - } -} - -impl FromIterator for JsonValue where serde_json::Value: FromIterator { - fn from_iter>(iter: I) -> Self { - JsonValue(serde_json::Value::from_iter(iter)) - } -} +#[doc(inline)] +pub use serde_json::Value; /// Serializes the value into JSON. Returns a response with Content-Type JSON /// 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> { - 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. -/// -/// # Usage -/// -/// To import the macro, add the `#[macro_use]` attribute to the `extern crate -/// rocket_contrib` invocation: -/// -/// ```rust -/// #[macro_use] extern crate rocket_contrib; -/// ``` -/// -/// The return type of a `json!` invocation is -/// [`JsonValue`](crate::json::JsonValue). A value created with this macro can -/// be returned from a handler as follows: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// # #[macro_use] extern crate rocket_contrib; -/// use rocket_contrib::json::JsonValue; -/// -/// #[get("/json")] -/// fn get_json() -> JsonValue { -/// json!({ -/// "key": "value", -/// "array": [1, 2, 3, 4] -/// }) -/// } -/// ``` -/// -/// The [`Responder`](rocket::response::Responder) implementation for -/// `JsonValue` serializes the value into a JSON string and sets it as the body -/// of the response with a `Content-Type` of `application/json`. -/// -/// # Examples -/// -/// Create a simple JSON object with two keys: `"username"` and `"id"`: -/// -/// ```rust -/// # #![allow(unused_variables)] -/// # #[macro_use] extern crate rocket_contrib; -/// # fn main() { -/// let value = json!({ -/// "username": "mjordan", -/// "id": 23 -/// }); -/// # } -/// ``` -/// -/// Create a more complex object with a nested object and array: -/// -/// ```rust -/// # #![allow(unused_variables)] -/// # #[macro_use] extern crate rocket_contrib; -/// # fn main() { -/// let value = json!({ -/// "code": 200, -/// "success": true, -/// "payload": { -/// "features": ["serde", "json"], -/// "ids": [12, 121], -/// }, -/// }); -/// # } -/// ``` -/// -/// 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`. -/// -/// ```rust -/// # #![allow(unused_variables)] -/// # #[macro_use] extern crate rocket_contrib; -/// # fn main() { -/// let code = 200; -/// let features = vec!["serde", "json"]; -/// -/// let value = json!({ -/// "code": code, -/// "success": code == 200, -/// "payload": { -/// features[0]: features[1] -/// } -/// }); -/// # } -/// ``` -/// -/// 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)+)) - }; +crate::export! { + /// A macro to create ad-hoc JSON serializable values using JSON syntax. + /// + /// The return type of a `json!` invocation is [`Value`](Value). A value + /// created with this macro can be returned from a handler as follows: + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::serde::json::{json, Value}; + /// + /// #[get("/json")] + /// fn get_json() -> Value { + /// json!({ + /// "key": "value", + /// "array": [1, 2, 3, 4] + /// }) + /// } + /// ``` + /// + /// The [`Responder`](crate::response::Responder) implementation for + /// `Value` serializes the value into a JSON string and sets it as the body + /// of the response with a `Content-Type` of `application/json`. + /// + /// # Examples + /// + /// Create a simple JSON object with two keys: `"username"` and `"id"`: + /// + /// ```rust + /// use rocket::serde::json::json; + /// + /// let value = json!({ + /// "username": "mjordan", + /// "id": 23 + /// }); + /// ``` + /// + /// Create a more complex object with a nested object and array: + /// + /// ```rust + /// # use rocket::serde::json::json; + /// let value = json!({ + /// "code": 200, + /// "success": true, + /// "payload": { + /// "features": ["serde", "json"], + /// "ids": [12, 121], + /// }, + /// }); + /// ``` + /// + /// 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`. + /// + /// ```rust + /// # use rocket::serde::json::json; + /// let code = 200; + /// let features = vec!["serde", "json"]; + /// + /// let value = json!({ + /// "code": code, + /// "success": code == 200, + /// "payload": { + /// features[0]: features[1] + /// } + /// }); + /// ``` + /// + /// Trailing commas are allowed inside both arrays and objects. + /// + /// ```rust + /// # use rocket::serde::json::json; + /// let value = json!([ + /// "notice", + /// "the", + /// "trailing", + /// "comma -->", + /// ]); + /// ``` + macro_rules! json { + ($($json:tt)+) => ($crate::serde::json::serde_json::json!($($json)*)); + } } - -#[doc(inline)] -pub use json; diff --git a/core/lib/src/serde/mod.rs b/core/lib/src/serde/mod.rs new file mode 100644 index 00000000..932bfbc8 --- /dev/null +++ b/core/lib/src/serde/mod.rs @@ -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; diff --git a/contrib/lib/src/msgpack.rs b/core/lib/src/serde/msgpack.rs similarity index 80% rename from contrib/lib/src/msgpack.rs rename to core/lib/src/serde/msgpack.rs index 573245aa..e471c573 100644 --- a/contrib/lib/src/msgpack.rs +++ b/core/lib/src/serde/msgpack.rs @@ -1,35 +1,44 @@ //! 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 //! -//! This module is only available when the `msgpack` feature is enabled. Enable -//! it in `Cargo.toml` as follows: +//! This module is only available when the `json` feature is enabled. Enable it +//! in `Cargo.toml` as follows: //! //! ```toml -//! [dependencies.rocket_contrib] +//! [dependencies.rocket] //! version = "0.5.0-dev" -//! default-features = false //! 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::ops::{Deref, DerefMut}; -use rocket::request::{Request, local_cache}; -use rocket::data::{ByteUnit, Data, FromData, Outcome}; -use rocket::response::{self, Responder, content}; -use rocket::http::Status; -use rocket::form::prelude as form; +use crate::request::{Request, local_cache}; +use crate::data::{Limits, Data, FromData, Outcome}; +use crate::response::{self, Responder, content}; +use crate::http::Status; +use crate::form::prelude as form; use serde::{Serialize, Deserialize}; +#[doc(inline)] pub use rmp_serde::decode::Error; -/// The `MsgPack` data guard and responder: easily consume and respond with -/// MessagePack. +/// The MessagePack guard: easily consume and return MessagePack. /// /// ## Receiving MessagePack /// @@ -43,9 +52,8 @@ pub use rmp_serde::decode::Error; /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # extern crate rocket_contrib; /// # type User = usize; -/// use rocket_contrib::msgpack::MsgPack; +/// use rocket::serde::msgpack::MsgPack; /// /// #[post("/users", format = "msgpack", data = "")] /// fn new_user(user: MsgPack) { @@ -65,10 +73,9 @@ pub use rmp_serde::decode::Error; /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # extern crate rocket_contrib; /// # type Metadata = usize; /// use rocket::form::{Form, FromForm}; -/// use rocket_contrib::msgpack::MsgPack; +/// use rocket::serde::msgpack::MsgPack; /// /// #[derive(FromForm)] /// struct User<'r> { @@ -82,27 +89,7 @@ pub use rmp_serde::decode::Error; /// } /// ``` /// -/// ## Sending MessagePack -/// -/// If you're responding with MessagePack data, return a `MsgPack` 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/")] -/// fn user(id: usize) -> MsgPack { -/// let user_from_id = User::from(id); -/// /* ... */ -/// MsgPack(user_from_id) -/// } -/// ``` -/// -/// ## Incoming Data Limits +/// ### Incoming Data Limits /// /// The default size limit for incoming MessagePack data is 1MiB. Setting a /// limit protects your application from denial of service (DOS) attacks and @@ -115,6 +102,25 @@ pub use rmp_serde::decode::Error; /// [global.limits] /// msgpack = 5242880 /// ``` +/// +/// ## Sending MessagePack +/// +/// If you're responding with MessagePack data, return a `MsgPack` 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/")] +/// fn user(id: usize) -> MsgPack { +/// let user_from_id = User::from(id); +/// /* ... */ +/// MsgPack(user_from_id) +/// } +/// ``` #[derive(Debug)] pub struct MsgPack(pub T); @@ -124,7 +130,7 @@ impl MsgPack { /// # Example /// /// ```rust - /// # use rocket_contrib::msgpack::MsgPack; + /// # use rocket::serde::msgpack::MsgPack; /// let string = "Hello".to_string(); /// let my_msgpack = MsgPack(string); /// assert_eq!(my_msgpack.into_inner(), "Hello".to_string()); @@ -135,16 +141,14 @@ impl MsgPack { } } -const DEFAULT_LIMIT: ByteUnit = ByteUnit::Mebibyte(1); - impl<'r, T: Deserialize<'r>> MsgPack { fn from_bytes(buf: &'r [u8]) -> Result { rmp_serde::from_slice(buf).map(MsgPack) } async fn from_data(req: &'r Request<'_>, data: Data) -> Result { - let size_limit = req.limits().get("msgpack").unwrap_or(DEFAULT_LIMIT); - let bytes = match data.open(size_limit).into_bytes().await { + let limit = req.limits().get("msgpack").unwrap_or(Limits::MESSAGE_PACK); + let bytes = match data.open(limit).into_bytes().await { Ok(buf) if buf.is_complete() => buf.into_inner(), Ok(_) => { let eof = io::ErrorKind::UnexpectedEof; @@ -156,7 +160,7 @@ impl<'r, T: Deserialize<'r>> MsgPack { Self::from_bytes(local_cache!(req, bytes)) } } -#[rocket::async_trait] +#[crate::async_trait] impl<'r, T: Deserialize<'r>> FromData<'r> for MsgPack { type Error = Error; @@ -192,7 +196,7 @@ impl<'r, T: Serialize> Responder<'r, 'static> for MsgPack { } } -#[rocket::async_trait] +#[crate::async_trait] impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for MsgPack { async fn from_data(f: form::DataField<'v, '_>) -> Result> { Self::from_data(f.request, f.data).await.map_err(|e| { diff --git a/examples/README.md b/examples/README.md index b7e25d13..990c527d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -59,8 +59,8 @@ This directory contains projects showcasing Rocket's features. derived `Responder`. * **[`serialization`](./serialization)** - Showcases JSON and MessagePack - (de)serialization support in `contrib` by implementing a CRUD-like message - API in JSON and a simply read/echo API in MessagePack. + (de)serialization support by implementing a CRUD-like message API in JSON + and a simply read/echo API in MessagePack. * **[`state`](./state)** - Illustrates the use of request-local state and managed state. Uses request-local state to cache "expensive" per-request diff --git a/examples/config/Cargo.toml b/examples/config/Cargo.toml index 8aee719a..20e6ad1b 100644 --- a/examples/config/Cargo.toml +++ b/examples/config/Cargo.toml @@ -7,4 +7,3 @@ publish = false [dependencies] rocket = { path = "../../core/lib", features = ["secrets"] } -serde = { version = "1", features = ["derive"] } diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs index b10d7963..de7bee0a 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -4,10 +4,10 @@ use rocket::{State, Config}; use rocket::fairing::AdHoc; - -use serde::Deserialize; +use rocket::serde::Deserialize; #[derive(Debug, Deserialize)] +#[serde(crate = "rocket::serde")] struct AppConfig { key: String, port: u16 diff --git a/examples/databases/Cargo.toml b/examples/databases/Cargo.toml index 93e6e497..deac721a 100644 --- a/examples/databases/Cargo.toml +++ b/examples/databases/Cargo.toml @@ -6,9 +6,7 @@ edition = "2018" publish = false [dependencies] -rocket = { path = "../../core/lib" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +rocket = { path = "../../core/lib", features = ["json"] } diesel = { version = "1.3", features = ["sqlite", "r2d2"] } diesel_migrations = "1.3" @@ -20,4 +18,4 @@ features = ["runtime-tokio-rustls", "sqlite", "macros", "offline", "migrate"] [dependencies.rocket_contrib] path = "../../contrib/lib" default-features = false -features = ["diesel_sqlite_pool", "sqlite_pool", "json"] +features = ["diesel_sqlite_pool", "sqlite_pool"] diff --git a/examples/databases/src/diesel_sqlite.rs b/examples/databases/src/diesel_sqlite.rs index b7c3bde7..5ea4dd0d 100644 --- a/examples/databases/src/diesel_sqlite.rs +++ b/examples/databases/src/diesel_sqlite.rs @@ -1,12 +1,11 @@ use rocket::{Rocket, Build}; use rocket::fairing::AdHoc; use rocket::response::{Debug, status::Created}; +use rocket::serde::{Serialize, Deserialize, json::Json}; use rocket_contrib::databases::diesel; -use rocket_contrib::json::Json; use self::diesel::prelude::*; -use serde::{Serialize, Deserialize}; #[database("diesel")] struct Db(diesel::SqliteConnection); @@ -14,6 +13,7 @@ struct Db(diesel::SqliteConnection); type Result> = std::result::Result; #[derive(Debug, Clone, Deserialize, Serialize, Queryable, Insertable)] +#[serde(crate = "rocket::serde")] #[table_name="posts"] struct Post { #[serde(skip_deserializing)] diff --git a/examples/databases/src/rusqlite.rs b/examples/databases/src/rusqlite.rs index 5cc1df88..420a4df9 100644 --- a/examples/databases/src/rusqlite.rs +++ b/examples/databases/src/rusqlite.rs @@ -1,16 +1,17 @@ use rocket::{Rocket, Build}; use rocket::fairing::AdHoc; -use rocket_contrib::databases::rusqlite; +use rocket::serde::{Serialize, Deserialize, json::Json}; use rocket::response::{Debug, status::Created}; -use rocket_contrib::json::Json; + +use rocket_contrib::databases::rusqlite; use self::rusqlite::params; -use serde::{Serialize, Deserialize}; #[database("rusqlite")] struct Db(rusqlite::Connection); #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(crate = "rocket::serde")] struct Post { #[serde(skip_deserializing, skip_serializing_if = "Option::is_none")] id: Option, diff --git a/examples/databases/src/sqlx.rs b/examples/databases/src/sqlx.rs index 1f1bf572..a0e0a1a3 100644 --- a/examples/databases/src/sqlx.rs +++ b/examples/databases/src/sqlx.rs @@ -1,11 +1,10 @@ use rocket::{Rocket, State, Build, futures}; use rocket::fairing::{self, AdHoc}; use rocket::response::status::Created; -use rocket_contrib::json::Json; +use rocket::serde::{Serialize, Deserialize, json::Json}; use futures::stream::TryStreamExt; use futures::future::TryFutureExt; -use serde::{Serialize, Deserialize}; use sqlx::ConnectOptions; type Db = sqlx::SqlitePool; @@ -13,6 +12,7 @@ type Db = sqlx::SqlitePool; type Result> = std::result::Result; #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(crate = "rocket::serde")] struct Post { #[serde(skip_deserializing, skip_serializing_if = "Option::is_none")] id: Option, diff --git a/examples/databases/src/tests.rs b/examples/databases/src/tests.rs index b533579e..98457e7f 100644 --- a/examples/databases/src/tests.rs +++ b/examples/databases/src/tests.rs @@ -1,31 +1,10 @@ use rocket::fairing::AdHoc; -use rocket::local::blocking::{Client, LocalResponse, LocalRequest}; -use rocket::http::{Status, ContentType}; -use serde::{Serialize, Deserialize}; - -// Make it easier to work with JSON. -trait LocalResponseExt { - fn into_json(self) -> Option; -} - -trait LocalRequestExt { - fn json(self, value: &T) -> Self; -} - -impl LocalResponseExt for LocalResponse<'_> { - fn into_json(self) -> Option { - serde_json::from_reader(self).ok() - } -} - -impl LocalRequestExt for LocalRequest<'_> { - fn json(self, value: &T) -> Self { - let json = serde_json::to_string(value).expect("JSON serialization"); - self.header(ContentType::JSON).body(json) - } -} +use rocket::local::blocking::Client; +use rocket::serde::{Serialize, Deserialize}; +use rocket::http::Status; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(crate = "rocket::serde")] struct Post { title: String, text: String, diff --git a/examples/responders/src/main.rs b/examples/responders/src/main.rs index 8ce0fc75..bcbf082a 100644 --- a/examples/responders/src/main.rs +++ b/examples/responders/src/main.rs @@ -91,7 +91,7 @@ use rocket::response::content; // NOTE: This example explicitly uses the `Json` type from `response::content` // 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 // 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; // 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`. #[get("/content/")] fn json_or_msgpack(kind: &str) -> Either, MsgPack<&'static [u8]>> { diff --git a/examples/serialization/Cargo.toml b/examples/serialization/Cargo.toml index 1ebe072f..a9be00d0 100644 --- a/examples/serialization/Cargo.toml +++ b/examples/serialization/Cargo.toml @@ -5,11 +5,6 @@ workspace = "../" edition = "2018" publish = false -[dependencies] -rocket = { path = "../../core/lib" } -serde = "1" - -[dependencies.rocket_contrib] -path = "../../contrib/lib" -default-features = false +[dependencies.rocket] +path = "../../core/lib" features = ["json", "msgpack"] diff --git a/examples/serialization/src/json.rs b/examples/serialization/src/json.rs index eda61879..b49a8fe7 100644 --- a/examples/serialization/src/json.rs +++ b/examples/serialization/src/json.rs @@ -2,9 +2,8 @@ use std::borrow::Cow; use rocket::State; use rocket::tokio::sync::Mutex; -use rocket_contrib::json::{Json, JsonValue, json}; - -use serde::{Serialize, Deserialize}; +use rocket::serde::json::{Json, Value, json}; +use rocket::serde::{Serialize, Deserialize}; // The type to represent the ID of a message. type Id = usize; @@ -14,13 +13,14 @@ type MessageList = Mutex>; type Messages<'r> = &'r State; #[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] struct Message<'r> { id: Option, message: Cow<'r, str> } #[post("/", format = "json", data = "")] -async fn new(message: Json>, list: Messages<'_>) -> JsonValue { +async fn new(message: Json>, list: Messages<'_>) -> Value { let mut list = list.lock().await; let id = list.len(); list.push(message.message.to_string()); @@ -28,7 +28,7 @@ async fn new(message: Json>, list: Messages<'_>) -> JsonValue { } #[put("/", format = "json", data = "")] -async fn update(id: Id, message: Json>, list: Messages<'_>) -> Option { +async fn update(id: Id, message: Json>, list: Messages<'_>) -> Option { match list.lock().await.get_mut(id) { Some(existing) => { *existing = message.message.to_string(); @@ -49,7 +49,7 @@ async fn get<'r>(id: Id, list: Messages<'r>) -> Option>> { } #[catch(404)] -fn not_found() -> JsonValue { +fn not_found() -> Value { json!({ "status": "error", "reason": "Resource was not found." diff --git a/examples/serialization/src/msgpack.rs b/examples/serialization/src/msgpack.rs index 63b922c1..f5cd77fd 100644 --- a/examples/serialization/src/msgpack.rs +++ b/examples/serialization/src/msgpack.rs @@ -1,21 +1,20 @@ -use rocket_contrib::msgpack::MsgPack; - -use serde::{Serialize, Deserialize}; +use rocket::serde::{Serialize, Deserialize, msgpack::MsgPack}; #[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] struct Message<'r> { id: usize, - contents: &'r str + message: &'r str } #[get("/", format = "msgpack")] fn get(id: usize) -> MsgPack> { - MsgPack(Message { id, contents: "Hello, world!", }) + MsgPack(Message { id, message: "Hello, world!", }) } #[post("/", data = "", format = "msgpack")] fn echo<'r>(data: MsgPack>) -> &'r str { - data.contents + data.message } pub fn stage() -> rocket::fairing::AdHoc { diff --git a/examples/serialization/src/tests.rs b/examples/serialization/src/tests.rs index 07ea172d..3039a6f4 100644 --- a/examples/serialization/src/tests.rs +++ b/examples/serialization/src/tests.rs @@ -1,5 +1,24 @@ use rocket::local::blocking::Client; use rocket::http::{Status, ContentType, Accept}; +use rocket::serde::{Serialize, Deserialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +struct Message { + id: Option, + message: String +} + +impl Message { + fn new(message: impl Into) -> Self { + Message { message: message.into(), id: None } + } + + fn with_id(mut self, id: usize) -> Self { + self.id = Some(id); + self + } +} #[test] 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. let res = client.put("/json/80") - .header(ContentType::JSON) - .body(r#"{ "message": "Bye bye, world!" }"#) + .json(&Message::new("hi")) .dispatch(); assert_eq!(res.status(), Status::NotFound); @@ -46,40 +64,30 @@ fn json_post_get_put_get() { // Create/read/update/read a few messages. for id in 0..10 { let uri = format!("/json/{}", id); - let message = format!("Hello, JSON {}!", id); // Check that a message with doesn't exist. let res = client.get(&uri).header(ContentType::JSON).dispatch(); assert_eq!(res.status(), Status::NotFound); // Add a new message. This should be ID 0. - let res = client.post("/json") - .header(ContentType::JSON) - .body(format!(r#"{{ "message": "{}" }}"#, message)) - .dispatch(); - + let message = Message::new(format!("Hello, JSON {}!", id)); + let res = client.post("/json").json(&message).dispatch(); assert_eq!(res.status(), Status::Ok); // Check that the message exists with the correct contents. let res = client.get(&uri).header(Accept::JSON).dispatch(); assert_eq!(res.status(), Status::Ok); - let body = res.into_string().unwrap(); - assert!(body.contains(&message)); + assert_eq!(res.into_json::().unwrap(), message.with_id(id)); // Change the message contents. - let res = client.put(&uri) - .header(ContentType::JSON) - .body(r#"{ "message": "Bye bye, world!" }"#) - .dispatch(); - + let message = Message::new("Bye bye, world!"); + let res = client.put(&uri).json(&message).dispatch(); assert_eq!(res.status(), Status::Ok); // Check that the message exists with the updated contents. let res = client.get(&uri).header(Accept::JSON).dispatch(); assert_eq!(res.status(), Status::Ok); - let body = res.into_string().unwrap(); - assert!(!body.contains(&message)); - assert!(body.contains("Bye bye, world!")); + assert_eq!(res.into_json::().unwrap(), message.with_id(id)); } } @@ -91,8 +99,8 @@ fn msgpack_get() { assert_eq!(res.content_type(), Some(ContentType::MsgPack)); // Check that the message is `[1, "Hello, world!"]` - assert_eq!(&res.into_bytes().unwrap(), &[146, 1, 173, 72, 101, 108, 108, - 111, 44, 32, 119, 111, 114, 108, 100, 33]); + let msg = Message::new("Hello, world!").with_id(1); + assert_eq!(res.into_msgpack::().unwrap(), msg); } #[test] @@ -100,11 +108,9 @@ fn msgpack_post() { // Dispatch request with a message of `[2, "Goodbye, world!"]`. let client = Client::tracked(super::rocket()).unwrap(); let res = client.post("/msgpack") - .header(ContentType::MsgPack) - .body(&[146, 2, 175, 71, 111, 111, 100, 98, 121, 101, 44, 32, 119, 111, - 114, 108, 100, 33]) + .msgpack(&Message::new("Goodbye, world!").with_id(2)) .dispatch(); assert_eq!(res.status(), Status::Ok); - assert_eq!(res.into_string(), Some("Goodbye, world!".into())); + assert_eq!(res.into_string().unwrap(), "Goodbye, world!"); } diff --git a/examples/templating/Cargo.toml b/examples/templating/Cargo.toml index 20b83d8f..f6a9b3d5 100644 --- a/examples/templating/Cargo.toml +++ b/examples/templating/Cargo.toml @@ -7,8 +7,6 @@ publish = false [dependencies] rocket = { path = "../../core/lib" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" [dependencies.rocket_contrib] path = "../../contrib/lib" diff --git a/examples/templating/src/hbs.rs b/examples/templating/src/hbs.rs index 212445fa..74ecbaa8 100644 --- a/examples/templating/src/hbs.rs +++ b/examples/templating/src/hbs.rs @@ -1,9 +1,13 @@ use rocket::Request; use rocket::response::Redirect; +use rocket::serde::Serialize; + use rocket_contrib::templates::{Template, handlebars}; + use self::handlebars::{Handlebars, JsonRender}; -#[derive(serde::Serialize)] +#[derive(Serialize)] +#[serde(crate = "rocket::serde")] struct TemplateContext<'r> { title: &'r str, name: Option<&'r str>, diff --git a/examples/templating/src/tera.rs b/examples/templating/src/tera.rs index 572d9d1b..c9ffc5d7 100644 --- a/examples/templating/src/tera.rs +++ b/examples/templating/src/tera.rs @@ -2,9 +2,12 @@ use std::collections::HashMap; use rocket::Request; use rocket::response::Redirect; +use rocket::serde::Serialize; + use rocket_contrib::templates::{Template, tera::Tera}; -#[derive(serde::Serialize)] +#[derive(Serialize)] +#[serde(crate = "rocket::serde")] struct TemplateContext<'r> { title: &'r str, name: Option<&'r str>, diff --git a/examples/todo/Cargo.toml b/examples/todo/Cargo.toml index 3d61695f..2ec8fea5 100644 --- a/examples/todo/Cargo.toml +++ b/examples/todo/Cargo.toml @@ -7,8 +7,6 @@ publish = false [dependencies] rocket = { path = "../../core/lib" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" diesel = { version = "1.3", features = ["sqlite", "r2d2"] } diesel_migrations = "1.3" diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs index 02a09db5..6f4f86a2 100644 --- a/examples/todo/src/main.rs +++ b/examples/todo/src/main.rs @@ -11,6 +11,7 @@ use rocket::{Rocket, Build}; use rocket::fairing::AdHoc; use rocket::request::FlashMessage; use rocket::response::{Flash, Redirect}; +use rocket::serde::Serialize; use rocket::form::Form; use rocket_contrib::templates::Template; @@ -21,7 +22,8 @@ use crate::task::{Task, Todo}; #[database("sqlite_database")] pub struct DbConn(diesel::SqliteConnection); -#[derive(Debug, serde::Serialize)] +#[derive(Debug, Serialize)] +#[serde(crate = "rocket::serde")] struct Context { flash: Option<(String, String)>, tasks: Vec diff --git a/examples/todo/src/task.rs b/examples/todo/src/task.rs index 404e829a..ce4067c1 100644 --- a/examples/todo/src/task.rs +++ b/examples/todo/src/task.rs @@ -1,3 +1,4 @@ +use rocket::serde::Serialize; use diesel::{self, result::QueryResult, prelude::*}; mod schema { @@ -15,7 +16,8 @@ use self::schema::tasks::dsl::{tasks as all_tasks, completed as task_completed}; use crate::DbConn; -#[derive(serde::Serialize, Queryable, Insertable, Debug, Clone)] +#[derive(Serialize, Queryable, Insertable, Debug, Clone)] +#[serde(crate = "rocket::serde")] #[table_name="tasks"] pub struct Task { pub id: Option, diff --git a/scripts/test.sh b/scripts/test.sh index 37afe3e9..78d8a1f9 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -60,8 +60,6 @@ function check_style() { function test_contrib() { FEATURES=( - json - msgpack tera_templates handlebars_templates serve @@ -92,6 +90,8 @@ function test_core() { FEATURES=( secrets tls + json + msgpack ) pushd "${CORE_LIB_ROOT}" > /dev/null 2>&1 diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 3ef5172a..c1c7188b 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -612,18 +612,14 @@ Any type that implements [`FromData`] is also known as _a data guard_. ### JSON -The [`Json`](@api/rocket_contrib/json/struct.Json.html) type from -[`rocket_contrib`] is a data guard that parses the deserialzies body data as -JSON. The only condition is that the generic type `T` implements the -`Deserialize` trait from [Serde](https://github.com/serde-rs/json). +The [`Json`](@api/rocket/serde/json/struct.Json.html) guard deserialzies body +data as JSON. The only condition is that the generic type `T` implements the +`Deserialize` trait from [`serde`](https://serde.rs). ```rust # #[macro_use] extern crate rocket; -# extern crate rocket_contrib; -# fn main() {} -use serde::Deserialize; -use rocket_contrib::json::Json; +use rocket::serde::{Deserialize, json::Json}; #[derive(Deserialize)] struct Task<'r> { diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index ea173167..5b8fe9a4 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -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 [`content::Json`]: @api/rocket/response/content/struct.Json.html @@ -346,21 +346,18 @@ how to detect and handle graceful shutdown requests. ### JSON -The [`Json`] responder in [`rocket_contrib`] allows you to easily respond with -well-formed JSON data: simply return a value of type `Json` where `T` is the -type of a structure to serialize into JSON. The type `T` must implement the -[`Serialize`] trait from [`serde`], which can be automatically derived. +The [`Json`] responder in allows you to easily respond with well-formed JSON +data: simply return a value of type `Json` where `T` is the type of a +structure to serialize into JSON. The type `T` must implement the [`Serialize`] +trait from [`serde`], which can be automatically derived. As an example, to respond with the JSON value of a `Task` structure, we might write: ```rust # #[macro_use] extern crate rocket; -# #[macro_use] extern crate rocket_contrib; -# fn main() {} -use serde::Serialize; -use rocket_contrib::json::Json; +use rocket::serde::{Serialize, json::Json}; #[derive(Serialize)] struct Task { /* .. */ } @@ -377,9 +374,9 @@ fails, a **500 - Internal Server Error** is returned. 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 -[`serde`]: https://docs.serde.rs/serde/ +[`serde`]: https://serde.rs [serialization example]: @example/serialization ## Templates diff --git a/site/guide/8-testing.md b/site/guide/8-testing.md index 6399f01a..4f717fbf 100644 --- a/site/guide/8-testing.md +++ b/site/guide/8-testing.md @@ -69,6 +69,8 @@ a few below: * [`headers`]: returns a map of all of the headers in the response. * [`into_string`]: reads the body data into a `String`. * [`into_bytes`]: reads the body data into a `Vec`. + * [`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 [`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 [`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_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 `assert!` macros as follows: @@ -301,24 +305,26 @@ note: emitting Rocket code generation debug output | ^^^^^^^^^^^^^^^^ | = note: - impl From for rocket::StaticRouteInfo { - fn from(_: world) -> rocket::StaticRouteInfo { + impl world { + fn into_info(self) -> rocket::StaticRouteInfo { fn monomorphized_function<'_b>( __req: &'_b rocket::request::Request<'_>, __data: rocket::data::Data, - ) -> rocket::handler::HandlerFuture<'_b> { + ) -> ::rocket::route::BoxFuture<'_b> { ::std::boxed::Box::pin(async move { let ___responder = world(); - rocket::handler::Outcome::from(__req, ___responder) + ::rocket::handler::Outcome::from(__req, ___responder) }) } - rocket::StaticRouteInfo { + + ::rocket::StaticRouteInfo { name: "world", method: ::rocket::http::Method::Get, path: "/world", handler: monomorphized_function, format: ::std::option::Option::None, rank: ::std::option::Option::None, + sentinels: sentinels![&'static str], } } } diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index dac53657..121f7002 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -57,7 +57,7 @@ selected profile doesn't contain a requested values, while values in the [`Toml`]: @figment/providers/struct.Toml.html [`Json`]: @figment/providers/struct.Json.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 ### Secret Key @@ -205,10 +205,8 @@ from the configured provider, which is exposed via [`Rocket::figment()`]: ```rust # #[macro_use] extern crate rocket; -# extern crate serde; - -use serde::Deserialize; +use rocket::serde::Deserialize; #[launch] fn rocket() -> _ { @@ -242,8 +240,7 @@ Because it is common to store configuration in managed state, Rocket provides an ```rust # #[macro_use] extern crate rocket; -# extern crate serde; -# use serde::Deserialize; +# use rocket::serde::Deserialize; # #[derive(Deserialize)] # struct Config { # port: u16, @@ -278,21 +275,15 @@ more complex cases. ! note: You may need to depend on `figment` and `serde` directly. - Rocket reexports `figment` from its crate root, so you can refer to `figment` - types via `rocket::figment`. However, Rocket does not enable all features from - the figment crate. As such, you may need to import `figment` directly: + Rocket reexports `figment` and `serde` from its crate root, so you can refer + to `figment` types via `rocket::figment` and `serde` types via + `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"] } ` - 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 figment's tuple providers with Rocket's default provider: @@ -320,10 +311,11 @@ and `APP_PROFILE` to configure the selected profile: ```rust # #[macro_use] extern crate rocket; -use serde::{Serialize, Deserialize}; -use figment::{Figment, Profile, providers::{Format, Toml, Serialized, Env}}; +use rocket::serde::{Serialize, Deserialize}; use rocket::fairing::AdHoc; +use figment::{Figment, Profile, providers::{Format, Toml, Serialized, Env}}; + #[derive(Debug, Deserialize, Serialize)] struct Config { app_value: usize, diff --git a/site/tests/Cargo.toml b/site/tests/Cargo.toml index 83db72f2..208e02fc 100644 --- a/site/tests/Cargo.toml +++ b/site/tests/Cargo.toml @@ -9,8 +9,8 @@ publish = false rocket = { path = "../../core/lib", features = ["secrets"] } [dev-dependencies] -rocket = { path = "../../core/lib", features = ["secrets"] } -rocket_contrib = { path = "../../contrib/lib", features = ["json", "tera_templates", "diesel_sqlite_pool"] } +rocket = { path = "../../core/lib", features = ["secrets", "json"] } +rocket_contrib = { path = "../../contrib/lib", features = ["tera_templates", "diesel_sqlite_pool"] } serde = { version = "1.0", features = ["derive"] } rand = "0.8" figment = { version = "0.10", features = ["toml", "env"] }