Allow responding with named MessagePack data.

Closes #2107
This commit is contained in:
Artemis 2023-06-18 21:43:10 +01:00 committed by Matthew Pomes
parent 1f82d4bbcd
commit df71f79bd9
No known key found for this signature in database
GPG Key ID: B8C0D93B8D8FBDB7
3 changed files with 170 additions and 25 deletions

View File

@ -140,3 +140,4 @@ version_check = "0.9.1"
tokio = { version = "1", features = ["macros", "io-std"] }
figment = { version = "0.10.17", features = ["test"] }
pretty_assertions = "1"
rmp = "0.8"

View File

@ -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
///
@ -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);
impl<T> MsgPack<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>;
/// 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
@ -142,7 +184,7 @@ 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)
}
@ -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> {
@ -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 serialize failure: {}", e);
Status::InternalServerError
@ -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.
@ -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)]
@ -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

View File

@ -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");
}