mirror of https://github.com/rwf2/Rocket.git
parent
1f82d4bbcd
commit
df71f79bd9
|
@ -140,3 +140,4 @@ version_check = "0.9.1"
|
||||||
tokio = { version = "1", features = ["macros", "io-std"] }
|
tokio = { version = "1", features = ["macros", "io-std"] }
|
||||||
figment = { version = "0.10.17", features = ["test"] }
|
figment = { version = "0.10.17", features = ["test"] }
|
||||||
pretty_assertions = "1"
|
pretty_assertions = "1"
|
||||||
|
rmp = "0.8"
|
||||||
|
|
|
@ -43,22 +43,12 @@ pub use rmp_serde::decode::Error;
|
||||||
///
|
///
|
||||||
/// ## Sending MessagePack
|
/// ## Sending MessagePack
|
||||||
///
|
///
|
||||||
/// To respond with serialized MessagePack data, return a `MsgPack<T>` type,
|
/// To respond with serialized MessagePack data, return either [`Named<T>`] or
|
||||||
/// where `T` implements [`Serialize`] from [`serde`]. The content type of the
|
/// [`Compact<T>`] from your handler. `T` must implement [`serde::Serialize`].
|
||||||
/// response is set to `application/msgpack` automatically.
|
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// Currently, returning `MsgPack<T>` is equivalent to returning `Compact<T>`,
|
||||||
/// # #[macro_use] extern crate rocket;
|
/// but you should prefer to use an explicit option as this default may change
|
||||||
/// # type User = usize;
|
/// in the future.
|
||||||
/// 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)
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// ## Receiving MessagePack
|
/// ## Receiving MessagePack
|
||||||
///
|
///
|
||||||
|
@ -123,9 +113,61 @@ pub use rmp_serde::decode::Error;
|
||||||
/// msgpack = 5242880
|
/// msgpack = 5242880
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[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.
|
/// Consumes the `MsgPack` wrapper and returns the wrapped item.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # 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> {
|
fn from_bytes(buf: &'r [u8]) -> Result<Self, Error> {
|
||||||
rmp_serde::from_slice(buf).map(MsgPack)
|
rmp_serde::from_slice(buf).map(MsgPack)
|
||||||
}
|
}
|
||||||
|
@ -163,7 +205,7 @@ impl<'r, T: Deserialize<'r>> MsgPack<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[crate::async_trait]
|
#[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;
|
type Error = Error;
|
||||||
|
|
||||||
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
|
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
|
/// Serializes the wrapped value into MessagePack. Returns a response with
|
||||||
/// Content-Type `MsgPack` and a fixed-size body with the serialization. If
|
/// Content-Type `MsgPack` and a fixed-size body with the serialization. If
|
||||||
/// serialization fails, an `Err` of `Status::InternalServerError` is returned.
|
/// 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> {
|
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| {
|
.map_err(|e| {
|
||||||
error!("MsgPack serialize failure: {}", e);
|
error!("MsgPack serialize failure: {}", e);
|
||||||
Status::InternalServerError
|
Status::InternalServerError
|
||||||
|
@ -199,7 +246,7 @@ impl<'r, T: Serialize> Responder<'r, 'static> for MsgPack<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[crate::async_trait]
|
#[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
|
// 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.
|
// 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 {
|
fn from(value: T) -> Self {
|
||||||
MsgPack(value)
|
MsgPack(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Deref for MsgPack<T> {
|
impl<T, const COMPACT: bool> Deref for MsgPack<T, COMPACT> {
|
||||||
type Target = T;
|
type Target = T;
|
||||||
|
|
||||||
#[inline(always)]
|
#[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)]
|
#[inline(always)]
|
||||||
fn deref_mut(&mut self) -> &mut T {
|
fn deref_mut(&mut self) -> &mut T {
|
||||||
&mut self.0
|
&mut self.0
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
Loading…
Reference in New Issue