From d43678c35ea3cbaab9343faf1565638f44f94570 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Tue, 7 Feb 2017 22:40:14 -0500 Subject: [PATCH] Add MsgPack implementation to contrib. --- Cargo.toml | 1 + contrib/Cargo.toml | 4 +- contrib/src/lib.rs | 8 ++ contrib/src/msgpack/mod.rs | 134 ++++++++++++++++++++++++++++++++++ examples/msgpack/Cargo.toml | 18 +++++ examples/msgpack/src/main.rs | 38 ++++++++++ examples/msgpack/src/tests.rs | 44 +++++++++++ lib/src/http/content_type.rs | 1 + lib/src/response/content.rs | 1 + lib/src/testing.rs | 4 +- 10 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 contrib/src/msgpack/mod.rs create mode 100644 examples/msgpack/Cargo.toml create mode 100644 examples/msgpack/src/main.rs create mode 100644 examples/msgpack/src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 3934e3aa..130c84ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "examples/from_request", "examples/stream", "examples/json", + "examples/msgpack", "examples/handlebars_templates", "examples/form_kitchen_sink", "examples/config", diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 48b2998b..849e8ecf 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT/Apache-2.0" [features] default = ["json"] json = ["serde", "serde_json"] +msgpack = ["serde", "rmp-serde"] tera_templates = ["tera", "templates"] handlebars_templates = ["handlebars", "templates"] @@ -27,9 +28,10 @@ log = "^0.3" # UUID dependencies. uuid = { version = "^0.4", optional = true } -# JSON and templating dependencies. +# Serialization and templating dependencies. serde = { version = "^0.9", optional = true } serde_json = { version = "^0.9.3", optional = true } +rmp-serde = { version = "^0.12", optional = true } # Templating dependencies only. handlebars = { version = "^0.25", optional = true, features = ["serde_type"] } diff --git a/contrib/src/lib.rs b/contrib/src/lib.rs index 18945897..a90c4dbc 100644 --- a/contrib/src/lib.rs +++ b/contrib/src/lib.rs @@ -14,6 +14,7 @@ //! an asterisk next to the features that are enabled by default: //! //! * [json*](struct.JSON.html) +//! * [msgpack](struct.MsgPack.html) //! * [handlebars_templates](struct.Template.html) //! * [tera_templates](struct.Template.html) //! * [uuid](struct.UUID.html) @@ -55,6 +56,13 @@ pub mod json; #[cfg(feature = "json")] pub use json::{JSON, SerdeError, Value}; +#[cfg(feature = "msgpack")] +#[doc(hidden)] +pub mod msgpack; + +#[cfg(feature = "msgpack")] +pub use msgpack::{MsgPack, MsgPackError}; + #[cfg(feature = "templates")] mod templates; diff --git a/contrib/src/msgpack/mod.rs b/contrib/src/msgpack/mod.rs new file mode 100644 index 00000000..acf47520 --- /dev/null +++ b/contrib/src/msgpack/mod.rs @@ -0,0 +1,134 @@ +extern crate rmp_serde; + +use std::ops::{Deref, DerefMut}; +use std::io::{Cursor, Read}; + +use rocket::outcome::Outcome; +use rocket::request::Request; +use rocket::data::{self, Data, FromData}; +use rocket::response::{self, Responder, Response}; +use rocket::http::{ContentType, Status}; + +use serde::{Serialize, Deserialize}; + +pub use self::rmp_serde::decode::Error as MsgPackError; + +/// The `MsgPack` type: implements `FromData` and `Responder`, allowing you to easily +/// consume and respond with MessagePack data. +/// +/// If you're receiving MessagePack data, simply add a `data` parameter to your route +/// arguments and ensure the type of the parameter is a `MsgPack`, where `T` is +/// some type you'd like to parse from MessagePack. `T` must implement `Deserialize` +/// from [Serde](https://github.com/serde-rs/serde). The data is parsed from the +/// HTTP request body. +/// +/// ```rust,ignore +/// #[post("/users/", format = "application/msgpack", data = "")] +/// fn new_user(user: MsgPack) { +/// ... +/// } +/// ``` +/// +/// You don't _need_ to use `format = "application/msgpack"`, but it _may_ be what +/// you want. Using `format = application/msgpack` means that any request that +/// doesn't specify "application/msgpack" as its first `Content-Type:` header +/// parameter will not be routed to this handler. By default, Rocket will accept a +/// Content Type of any of the following for MessagePack data: +/// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, or `bin/x-msgpack`. +/// +/// If you're responding with MessagePack data, return a `MsgPack` type, where `T` +/// implements `Serialize` from [Serde](https://github.com/serde-rs/serde). The +/// content type of the response is set to `application/msgpack` automatically. +/// +/// ```rust,ignore +/// #[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); + +impl MsgPack { + /// Consumes the `MsgPack` wrapper and returns the wrapped item. + /// + /// # Example + /// ```rust + /// # use rocket_contrib::MsgPack; + /// let string = "Hello".to_string(); + /// let my_msgpack = MsgPack(string); + /// assert_eq!(my_msgpack.into_inner(), "Hello".to_string()); + /// ``` + pub fn into_inner(self) -> T { + self.0 + } +} + +/// Maximum size of MessagePack data is 1MB. +/// TODO: Determine this size from some configuration parameter. +const MAX_SIZE: u64 = 1048576; + +/// Accepted content types are: +/// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, and `bin/x-msgpack` +fn is_msgpack_content_type(ct: &ContentType) -> bool { + (ct.ttype == "application" || ct.ttype == "bin") + && (ct.subtype == "msgpack" || ct.subtype == "x-msgpack") +} + +impl FromData for MsgPack { + type Error = MsgPackError; + + fn from_data(request: &Request, data: Data) -> data::Outcome { + if !request.content_type().map_or(false, |ct| is_msgpack_content_type(&ct)) { + error_!("Content-Type is not MessagePack."); + return Outcome::Forward(data); + } + + let mut buf = Vec::new(); + if let Err(e) = data.open().take(MAX_SIZE).read_to_end(&mut buf) { + let e = MsgPackError::InvalidDataRead(e); + error_!("Couldn't read request data: {:?}", e); + return Outcome::Failure((Status::BadRequest, e)); + }; + + match rmp_serde::from_slice(&buf).map(|val| MsgPack(val)) { + Ok(value) => Outcome::Success(value), + Err(e) => { + error_!("Couldn't parse MessagePack body: {:?}", e); + Outcome::Failure((Status::BadRequest, e)) + } + } + } +} + +/// Serializes the wrapped value into MessagePack. Returns a response with Content-Type +/// MessagePack and a fixed-size body with the serialization. If serialization fails, an +/// `Err` of `Status::InternalServerError` is returned. +impl Responder<'static> for MsgPack { + fn respond(self) -> response::Result<'static> { + rmp_serde::to_vec(&self.0).map_err(|e| { + error_!("MsgPack failed to serialize: {:?}", e); + Status::InternalServerError + }).and_then(|buf| { + Response::build() + .sized_body(Cursor::new(buf)) + .ok() + }) + } +} + +impl Deref for MsgPack { + type Target = T; + + fn deref<'a>(&'a self) -> &'a T { + &self.0 + } +} + +impl DerefMut for MsgPack { + fn deref_mut<'a>(&'a mut self) -> &'a mut T { + &mut self.0 + } +} diff --git a/examples/msgpack/Cargo.toml b/examples/msgpack/Cargo.toml new file mode 100644 index 00000000..590e3349 --- /dev/null +++ b/examples/msgpack/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "msgpack" +version = "0.0.1" +workspace = "../../" + +[dependencies] +rocket = { path = "../../lib" } +rocket_codegen = { path = "../../codegen" } +serde = "0.9" +serde_derive = "0.9" + +[dependencies.rocket_contrib] +path = "../../contrib" +default-features = false +features = ["msgpack"] + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/msgpack/src/main.rs b/examples/msgpack/src/main.rs new file mode 100644 index 00000000..ffff2400 --- /dev/null +++ b/examples/msgpack/src/main.rs @@ -0,0 +1,38 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; +extern crate rocket_contrib; +#[macro_use] extern crate serde_derive; + +#[cfg(test)] mod tests; + +use rocket_contrib::MsgPack; + +#[derive(Serialize, Deserialize)] +struct Message { + id: usize, + contents: String +} + +#[get("/", format = "application/msgpack")] +fn get(id: usize) -> MsgPack { + MsgPack(Message { + id: id, + contents: "Hello, world!".to_string(), + }) +} + +#[post("/", data = "", format = "application/msgpack")] +fn create(data: MsgPack) -> Result { + Ok(data.into_inner().contents) +} + +fn rocket() -> rocket::Rocket { + rocket::ignite() + .mount("/message", routes![get, create]) +} + +fn main() { + rocket().launch(); +} diff --git a/examples/msgpack/src/tests.rs b/examples/msgpack/src/tests.rs new file mode 100644 index 00000000..0419a075 --- /dev/null +++ b/examples/msgpack/src/tests.rs @@ -0,0 +1,44 @@ +use rocket; +use rocket::testing::MockRequest; +use rocket::http::Method::*; +use rocket::http::{Status, ContentType}; +use rocket::Response; + +#[derive(Serialize, Deserialize)] +struct Message { + id: usize, + contents: String +} + +macro_rules! run_test { + ($rocket: expr, $req:expr, $test_fn:expr) => ({ + let mut req = $req; + $test_fn(req.dispatch_with($rocket)); + }) +} + +#[test] +fn msgpack_get() { + let rocket = rocket(); + let req = MockRequest::new(Get, "/message/1").header(ContentType::MsgPack); + run_test!(&rocket, req, |mut response: Response| { + assert_eq!(response.status(), Status::Ok); + let body = response.body().unwrap().into_bytes().unwrap(); + // Represents a message of `[1, "Hello, world!"]` + assert_eq!(&body, &[146, 1, 173, 72, 101, 108, 108, 111, 44, 32, 119, 111, + 114, 108, 100, 33]); + }); +} + +#[test] +fn msgpack_post() { + let rocket = rocket(); + let req = MockRequest::new(Post, "/message") + .header(ContentType::MsgPack) + // Represents a message of `[2, "Goodbye, world!"]` + .body(&[146, 2, 175, 71, 111, 111, 100, 98, 121, 101, 44, 32, 119, 111, 114, 108, 100, 33]); + run_test!(&rocket, req, |mut response: Response| { + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.body().unwrap().into_string().unwrap(), "Goodbye, world!"); + }); +} diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index be35667f..7ec8e46c 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -104,6 +104,7 @@ impl ContentType { "HTML", HTML, is_html => "text", "html" ; "charset=utf-8", "Plain", Plain, is_plain => "text", "plain" ; "charset=utf-8", "JSON", JSON, is_json => "application", "json", + "MsgPack", MsgPack, is_msgpack => "application", "msgpack", "form", Form, is_form => "application", "x-www-form-urlencoded", "JavaScript", JavaScript, is_javascript => "application", "javascript", "CSS", CSS, is_css => "text", "css" ; "charset=utf-8", diff --git a/lib/src/response/content.rs b/lib/src/response/content.rs index dc15776d..38403e4b 100644 --- a/lib/src/response/content.rs +++ b/lib/src/response/content.rs @@ -82,6 +82,7 @@ macro_rules! ctrs { ctrs! { JSON: "JSON", "application/json", XML: "XML", "text/xml", + MsgPack: "MessagePack", "application/msgpack", HTML: "HTML", "text/html", Plain: "plain text", "text/plain", CSS: "CSS", "text/css", diff --git a/lib/src/testing.rs b/lib/src/testing.rs index d23ae494..a7e71eb8 100644 --- a/lib/src/testing.rs +++ b/lib/src/testing.rs @@ -224,8 +224,8 @@ impl<'r> MockRequest<'r> { /// .body(r#"{ "key": "value", "array": [1, 2, 3], }"#); /// ``` #[inline] - pub fn body>(mut self, body: S) -> Self { - self.data = Data::new(body.as_ref().as_bytes().into()); + pub fn body>(mut self, body: S) -> Self { + self.data = Data::new(body.as_ref().into()); self }