Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow responding with named MessagePack data. #19

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@ version_check = "0.9.1"
[dev-dependencies]
figment = { version = "0.10", features = ["test"] }
pretty_assertions = "0.7"
rmp = "0.8"
99 changes: 73 additions & 26 deletions core/lib/src/serde/msgpack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,12 @@ pub use rmp_serde::decode::Error;
///
/// ## Sending MessagePack
///
/// To respond with serialized 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.
/// To respond with serialized MessagePack data, return either [`Named<T>`] or
/// [`Compact<T>`] from your handler. `T` must implement [`serde::Serialize`].
///
/// ```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)
/// }
/// ```
/// Currently, returning `MsgPack<T>` is equivalent to returning `Compact<T>`,
/// but you should prefer to use an explicit option as this default may change
/// in the future.
///
/// ## Receiving MessagePack
///
Expand Down Expand Up @@ -123,9 +113,61 @@ pub use rmp_serde::decode::Error;
/// msgpack = 5242880
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MsgPack<T>(pub T);
pub struct MsgPack<T, const COMPACT: bool = true>(pub T);

/// Serializes responses in a compact MesagePack format, where structs are
/// serialized as arrays of their field values.
///
/// To respond with compact MessagePack data, return a `Compact<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;
///
/// #[get("/users/<id>")]
/// fn user(id: usize) -> msgpack::Compact<User> {
/// let user_from_id = User::from(id);
/// /* ... */
/// msgpack::MsgPack(user_from_id)
/// }
/// ```
///
/// Prefer using [`MsgPack<T>`] for request guards, as the named/compact
/// distinction is not relevant for request data - the correct option is
/// implemented automatically. Using [`Compact<T>`] as a request guard will
/// NOT prevent named requests from being accepted.
pub type Compact<T> = MsgPack<T, true>;

impl<T> MsgPack<T> {
/// Serializes responses in a named MessagePack format, where structs are
/// serialized as maps of their field names and values.
///
/// To respond with named MessagePack data, return a `Named<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;
///
/// #[get("/users/<id>")]
/// fn user(id: usize) -> msgpack::Named<User> {
/// let user_from_id = User::from(id);
/// /* ... */
/// msgpack::MsgPack(user_from_id)
/// }
/// ```
///
/// Prefer using [`MsgPack<T>`] for request guards, as the named/compact
/// distinction is not relevant for request data - the correct option is
/// implemented automatically. Using [`Named<T>`] as a request guard will
/// NOT prevent compact requests from being accepted.
pub type Named<T> = MsgPack<T, false>;

impl<T, const COMPACT: bool> MsgPack<T, COMPACT> {
/// Consumes the `MsgPack` wrapper and returns the wrapped item.
///
/// # Example
Expand All @@ -142,9 +184,9 @@ impl<T> MsgPack<T> {
}
}

impl<'r, T: Deserialize<'r>> MsgPack<T> {
impl<'r, T: Deserialize<'r>, const COMPACT: bool> MsgPack<T, COMPACT> {
fn from_bytes(buf: &'r [u8]) -> Result<Self, Error> {
rmp_serde::from_slice(buf).map(MsgPack)
rmp_serde::from_read_ref(buf).map(MsgPack)
}

async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Result<Self, Error> {
Expand All @@ -163,7 +205,7 @@ impl<'r, T: Deserialize<'r>> MsgPack<T> {
}

#[crate::async_trait]
impl<'r, T: Deserialize<'r>> FromData<'r> for MsgPack<T> {
impl<'r, T: Deserialize<'r>, const COMPACT: bool> FromData<'r> for MsgPack<T, COMPACT> {
type Error = Error;

async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
Expand All @@ -186,9 +228,14 @@ impl<'r, T: Deserialize<'r>> FromData<'r> for MsgPack<T> {
/// Serializes the wrapped value into MessagePack. Returns a response with
/// Content-Type `MsgPack` and a fixed-size body with the serialization. If
/// serialization fails, an `Err` of `Status::InternalServerError` is returned.
impl<'r, T: Serialize> Responder<'r, 'static> for MsgPack<T> {
impl<'r, T: Serialize, const COMPACT: bool> Responder<'r, 'static> for MsgPack<T, COMPACT> {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
let buf = rmp_serde::to_vec(&self.0)
let maybe_buf = if COMPACT {
rmp_serde::to_vec(&self.0)
} else {
rmp_serde::to_vec_named(&self.0)
};
let buf = maybe_buf
.map_err(|e| {
error_!("MsgPack failed to serialize: {:?}", e);
Status::InternalServerError
Expand All @@ -199,7 +246,7 @@ impl<'r, T: Serialize> Responder<'r, 'static> for MsgPack<T> {
}

#[crate::async_trait]
impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for MsgPack<T> {
impl<'v, T: Deserialize<'v> + Send, const COMPACT: bool> form::FromFormField<'v> for MsgPack<T, COMPACT> {
// TODO: To implement `from_value`, we need to the raw string so we can
// decode it into bytes as opposed to a string as it won't be UTF-8.

Expand All @@ -222,13 +269,13 @@ impl<'v, T: Deserialize<'v> + Send> form::FromFormField<'v> for MsgPack<T> {
// }
// }

impl<T> From<T> for MsgPack<T> {
impl<T, const COMPACT: bool> From<T> for MsgPack<T, COMPACT> {
fn from(value: T) -> Self {
MsgPack(value)
}
}

impl<T> Deref for MsgPack<T> {
impl<T, const COMPACT: bool> Deref for MsgPack<T, COMPACT> {
type Target = T;

#[inline(always)]
Expand All @@ -237,7 +284,7 @@ impl<T> Deref for MsgPack<T> {
}
}

impl<T> DerefMut for MsgPack<T> {
impl<T, const COMPACT: bool> DerefMut for MsgPack<T, COMPACT> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut T {
&mut self.0
Expand Down
97 changes: 97 additions & 0 deletions core/lib/tests/msgpack_encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#![cfg(feature = "msgpack")]

use rocket::{Rocket, Build};
use rocket::serde::msgpack;
use rocket::local::blocking::Client;

#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq)]
struct Person {
name: String,
age: u8,
gender: Gender,
}

#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[serde(tag = "gender")]
enum Gender {
Male,
Female,
NonBinary,
}

#[rocket::post("/age_named", data = "<person>")]
fn named(person: msgpack::MsgPack<Person>) -> msgpack::Named<Person> {
let person = Person { age: person.age + 1, ..person.into_inner() };
msgpack::MsgPack(person)
}

#[rocket::post("/age_compact", data = "<person>")]
fn compact(person: msgpack::MsgPack<Person>) -> msgpack::Compact<Person> {
let person = Person { age: person.age + 1, ..person.into_inner() };
msgpack::MsgPack(person)
}

fn rocket() -> Rocket<Build> {
rocket::build()
.mount("/", rocket::routes![named, compact])
}

fn read_string(buf: &mut rmp::decode::Bytes) -> String {
let mut string_buf = vec![0; 32]; // Awful but we're just testing.
rmp::decode::read_str(buf, &mut string_buf).unwrap().to_string()
}

#[test]
fn check_named_roundtrip() {
let client = Client::debug(rocket()).unwrap();
let person = Person {
name: "Cal".to_string(),
age: 17,
gender: Gender::NonBinary,
};
let response = client
.post("/age_named")
.body(rmp_serde::to_vec_named(&person).unwrap())
.dispatch()
.into_bytes()
.unwrap();
let mut bytes = rmp::decode::Bytes::new(&response);
assert_eq!(rmp::decode::read_map_len(&mut bytes).unwrap(), 3);
assert_eq!(&read_string(&mut bytes), "name");
assert_eq!(&read_string(&mut bytes), "Cal");
assert_eq!(&read_string(&mut bytes), "age");
assert_eq!(rmp::decode::read_int::<u8, _>(&mut bytes).unwrap(), 18);
assert_eq!(&read_string(&mut bytes), "gender");
// Enums are complicated in serde. In this test, they're encoded like this:
// (JSON equivalent) `{ "gender": "NonBinary" }`, where that object is itself
// the value of the `gender` key in the outer object. `#[serde(flatten)]`
// on the `gender` key in the outer object fixes this, but it prevents `rmp`
// from using compact mode, which would break the test.
assert_eq!(rmp::decode::read_map_len(&mut bytes).unwrap(), 1);
assert_eq!(&read_string(&mut bytes), "gender");
assert_eq!(&read_string(&mut bytes), "NonBinary");
}

#[test]
fn check_compact_roundtrip() {
let client = Client::debug(rocket()).unwrap();
let person = Person {
name: "Maeve".to_string(),
age: 15,
gender: Gender::Female,
};
let response = client
.post("/age_compact")
.body(rmp_serde::to_vec(&person).unwrap())
.dispatch()
.into_bytes()
.unwrap();
let mut bytes = rmp::decode::Bytes::new(&response);
assert_eq!(rmp::decode::read_array_len(&mut bytes).unwrap(), 3);
assert_eq!(&read_string(&mut bytes), "Maeve");
assert_eq!(rmp::decode::read_int::<u8, _>(&mut bytes).unwrap(), 16);
// Equivalent to the named representation, gender here is encoded like this:
// `[ "Female" ]`.
assert_eq!(rmp::decode::read_array_len(&mut bytes).unwrap(), 1);
assert_eq!(&read_string(&mut bytes), "Female");
}