Graduate contrib 'uuid' into core.

This has the following nice benefits:

  * The 'Uuid' wrapper type is gone.
  * 'Uuid' implements 'UriDisplay', 'FromUriParam'.
  * The 'serialization' example merges in 'uuid'.

Resolves #1299.
This commit is contained in:
Sergio Benitez 2021-05-21 14:29:43 -07:00
parent f6a7087c84
commit a78814f1c5
19 changed files with 222 additions and 299 deletions

View File

@ -27,7 +27,6 @@ serve = []
compression = ["brotli_compression", "gzip_compression"]
brotli_compression = ["brotli"]
gzip_compression = ["flate2"]
uuid = ["serde", "_uuid"]
# The barage of user-facing database features.
diesel_sqlite_pool = ["databases", "diesel/sqlite", "diesel/r2d2"]
@ -69,12 +68,6 @@ r2d2-memcache = { version = "0.6", optional = true }
brotli = { version = "3.3", optional = true }
flate2 = { version = "1.0", optional = true }
[dependencies._uuid]
package = "uuid"
version = ">=0.7.0, <0.9.0"
optional = true
features = ["serde"]
[dev-dependencies]
serde_test = "1.0.114"

View File

@ -19,7 +19,6 @@
//! * [serve*](serve) - Static File Serving
//! * [handlebars_templates](templates) - Handlebars Templating
//! * [tera_templates](templates) - Tera Templating
//! * [uuid](uuid) - UUID (de)serialization
//! * [${database}_pool](databases) - Database Configuration and Pooling
//!
//! The recommend way to include features from this crate via Rocket in your
@ -42,7 +41,6 @@
#[cfg(feature="serve")] pub mod serve;
#[cfg(feature="templates")] pub mod templates;
#[cfg(feature="uuid")] pub mod uuid;
#[cfg(feature="databases")] pub mod databases;
// TODO.async: Migrate compression, reenable this, tests, and add to docs.
//#[cfg(any(feature="brotli_compression", feature="gzip_compression"))] pub mod compression;

View File

@ -1,203 +0,0 @@
//! UUID parameter and form value parsing support.
//!
//! See the [`Uuid`] type for further details.
//!
//! # Enabling
//!
//! This module is only available when the `uuid` feature is enabled. Enable it
//! in `Cargo.toml` as follows:
//!
//! ```toml
//! [dependencies.rocket_contrib]
//! version = "0.5.0-dev"
//! default-features = false
//! features = ["uuid"]
//! ```
pub extern crate _uuid as extern_uuid;
use std::fmt;
use std::str::FromStr;
use std::ops::Deref;
use serde::{Deserialize, Serialize};
use rocket::request::FromParam;
use rocket::form::{self, FromFormField, ValueField};
/// UUID data and form guard: consume UUID values.
///
/// `Uuid` implements [`FromParam`] and [`FromFormField`], allowing UUID values
/// to be accepted directly in paths, queries, and forms.
///
/// # Usage
///
/// To use, add the `uuid` feature to the `rocket_contrib` dependencies section
/// of your `Cargo.toml`:
///
/// ```toml
/// [dependencies.rocket_contrib]
/// version = "0.5.0-dev"
/// default-features = false
/// features = ["uuid"]
/// ```
///
/// You can use the `Uuid` type directly as a target of a dynamic parameter:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # #[macro_use] extern crate rocket_contrib;
/// use rocket_contrib::uuid::Uuid;
///
/// #[get("/users/<id>")]
/// fn user(id: Uuid) -> String {
/// format!("We found: {}", id)
/// }
/// ```
///
/// You can also use the `Uuid` as a form value, including in query strings:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # #[macro_use] extern crate rocket_contrib;
/// use rocket_contrib::uuid::Uuid;
///
/// #[get("/user?<id>")]
/// fn user(id: Uuid) -> String {
/// format!("User ID: {}", id)
/// }
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Uuid(_uuid::Uuid);
pub type Error = <_uuid::Uuid as std::str::FromStr>::Err;
impl Uuid {
/// Consumes the Uuid wrapper, returning the underlying `Uuid` type.
///
/// # Example
/// ```rust
/// # extern crate rocket_contrib;
/// # use std::str::FromStr;
/// # fn main() {
/// use rocket_contrib::uuid::{extern_uuid, Uuid};
///
/// let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
/// let real_uuid = extern_uuid::Uuid::from_str(uuid_str).unwrap();
/// let my_inner_uuid = Uuid::from_str(uuid_str)
/// .expect("valid UUID string")
/// .into_inner();
///
/// assert_eq!(real_uuid, my_inner_uuid);
/// # }
/// ```
#[inline(always)]
pub fn into_inner(self) -> _uuid::Uuid {
self.0
}
}
impl fmt::Display for Uuid {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<'a> FromParam<'a> for Uuid {
type Error = Error;
/// A value is successfully parsed if `param` is a properly formatted Uuid.
/// Otherwise, an error is returned.
#[inline(always)]
fn from_param(param: &'a str) -> Result<Uuid, Self::Error> {
param.parse()
}
}
impl<'v> FromFormField<'v> for Uuid {
fn from_value(field: ValueField<'v>) -> form::Result<'v, Self> {
Ok(field.value.parse().map_err(form::error::Error::custom)?)
}
}
impl FromStr for Uuid {
type Err = Error;
#[inline]
fn from_str(s: &str) -> Result<Uuid, Self::Err> {
s.parse().map(Uuid)
}
}
impl Deref for Uuid {
type Target = _uuid::Uuid;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PartialEq<_uuid::Uuid> for Uuid {
#[inline(always)]
fn eq(&self, other: &_uuid::Uuid) -> bool {
self.0.eq(other)
}
}
#[cfg(test)]
mod test {
use super::Uuid;
use super::FromParam;
use super::FromStr;
#[test]
fn test_from_str() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
let uuid_wrapper = Uuid::from_str(uuid_str).unwrap();
assert_eq!(uuid_str, uuid_wrapper.to_string())
}
#[test]
fn test_from_param() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
let uuid_wrapper = Uuid::from_param(uuid_str.into()).unwrap();
assert_eq!(uuid_str, uuid_wrapper.to_string())
}
#[test]
fn test_into_inner() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
let uuid_wrapper = Uuid::from_param(uuid_str.into()).unwrap();
let real_uuid: _uuid::Uuid = uuid_str.parse().unwrap();
let inner_uuid: _uuid::Uuid = uuid_wrapper.into_inner();
assert_eq!(real_uuid, inner_uuid)
}
#[test]
fn test_partial_eq() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
let uuid_wrapper = Uuid::from_param(uuid_str.into()).unwrap();
let real_uuid: _uuid::Uuid = uuid_str.parse().unwrap();
assert_eq!(uuid_wrapper, real_uuid)
}
#[test]
#[should_panic(expected = "InvalidLength")]
fn test_from_param_invalid() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2p";
Uuid::from_param(uuid_str.into()).unwrap();
}
#[test]
fn test_ser_de() {
// The main reason for this test is to test that UUID only serializes as
// a string token, not anything else. Like a Struct with a named field...
use serde_test::{Token, assert_tokens, Configure};
let uuid: Uuid = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2".parse().unwrap();
assert_tokens(&uuid.readable(), &[
Token::Str("c1aa1e3b-9614-4895-9ebd-705255fa5bc2"),
]);
}
}

View File

@ -19,6 +19,7 @@ default = []
tls = ["tokio-rustls"]
private-cookies = ["cookie/private", "cookie/key-expansion"]
serde = ["uncased/with-serde-alloc", "_serde"]
uuid = ["_uuid"]
[dependencies]
smallvec = "1.0"
@ -56,5 +57,11 @@ optional = true
default-features = false
features = ["std"]
[dependencies._uuid]
package = "uuid"
version = "0.8"
optional = true
default-features = false
[dev-dependencies]
rocket = { version = "0.5.0-dev", path = "../lib" }

View File

@ -259,12 +259,13 @@ use crate::uri::fmt::{Part, Path, Query, Formatter};
/// use rocket::response::Redirect;
///
/// impl UriDisplay<Path> for Name<'_> {
/// // Delegates to the `UriDisplay` implementation for `str` via the call
/// // to `write_value` to ensure that the written string is URI-safe. In
/// // this case, the string will be percent encoded. Prefixes the inner
/// // name with `name:`.
/// // Writes the raw string `name:`, which is URI-safe, and then delegates
/// // to the `UriDisplay` implementation for `str` which ensures that
/// // string is written in a URI-safe manner. In this case, the string will
/// // be percent encoded.
/// fn fmt(&self, f: &mut Formatter<Path>) -> fmt::Result {
/// f.write_value(&format!("name:{}", self.0))
/// f.write_raw("name:")?;
/// UriDisplay::fmt(&self.0, f)
/// }
/// }
///
@ -408,6 +409,9 @@ impl<T: UriDisplay<Query>, E> UriDisplay<Query> for Result<T, E> {
}
}
#[cfg(feature = "uuid")] impl_with_display!(_uuid::Uuid);
#[cfg(feature = "uuid")] crate::impl_from_uri_param_identity!(_uuid::Uuid);
// And finally, the `Ignorable` trait, which has sugar of `_` in the `uri!`
// macro, which expands to a typecheck.

View File

@ -24,11 +24,13 @@ tls = ["rocket_http/tls"]
secrets = ["rocket_http/private-cookies"]
json = ["serde_json", "tokio/io-util"]
msgpack = ["rmp-serde", "tokio/io-util"]
uuid = ["_uuid", "rocket_http/uuid"]
[dependencies]
# Serialization dependencies.
serde_json = { version = "1.0.26", optional = true }
rmp-serde = { version = "0.15.0", optional = true }
_uuid = { package = "uuid", version = "0.8", optional = true, features = ["serde"] }
# Non-optional, core dependencies from here on out.
futures = "0.3.0"

View File

@ -64,6 +64,7 @@
//! | `tls` | Support for [TLS] encrypted connections. |
//! | `json` | Support for [JSON (de)serialization]. |
//! | `msgpack` | Support for [MessagePack (de)serialization]. |
//! | `uuid` | Support for [UUID value parsing and (de)serialization]. |
//!
//! Features can be selectively enabled in `Cargo.toml`:
//!
@ -74,6 +75,7 @@
//!
//! [JSON (de)serialization]: crate::serde::json
//! [MessagePack (de)serialization]: crate::serde::msgpack
//! [UUID value parsing and (de)serialization]: crate::serde::uuid
//! [private cookies]: https://rocket.rs/master/guide/requests/#private-cookies
//! [TLS]: https://rocket.rs/master/guide/configuration/#tls
//!

View File

@ -16,3 +16,7 @@ pub mod json;
#[cfg(feature = "msgpack")]
#[cfg_attr(nightly, doc(cfg(feature = "msgpack")))]
pub mod msgpack;
#[cfg(feature = "uuid")]
#[cfg_attr(nightly, doc(cfg(feature = "uuid")))]
pub mod uuid;

141
core/lib/src/serde/uuid.rs Normal file
View File

@ -0,0 +1,141 @@
//! UUID path/query parameter and form value parsing support.
//!
//! # Enabling
//!
//! This module is only available when the `uuid` feature is enabled. Enable it
//! in `Cargo.toml` as follows:
//!
//! ```toml
//! [dependencies.rocket]
//! version = "0.5.0-dev"
//! features = ["uuid"]
//! ```
//!
//! # Usage
//!
//! `Uuid` implements [`FromParam`] and [`FromFormField`] (i.e,
//! [`FromForm`](crate::form::FromForm)), allowing UUID values to be accepted
//! directly in paths, queries, and forms. You can use the `Uuid` type directly
//! as a target of a dynamic parameter:
//!
//! ```rust
//! # #[macro_use] extern crate rocket;
//! use rocket::serde::uuid::Uuid;
//!
//! #[get("/users/<id>")]
//! fn user(id: Uuid) -> String {
//! format!("We found: {}", id)
//! }
//! ```
//!
//! You can also use the `Uuid` as a form value, including in query strings:
//!
//! ```rust
//! # #[macro_use] extern crate rocket;
//! use rocket::serde::uuid::Uuid;
//!
//! #[get("/user?<id>")]
//! fn user(id: Uuid) -> String {
//! format!("User ID: {}", id)
//! }
//! ```
//!
//! Additionally, `Uuid` implements `UriDisplay<P>` for all `P`. As such, route
//! URIs including `Uuid`s can be generated in a type-safe manner:
//!
//! ```rust
//! # #[macro_use] extern crate rocket;
//! use rocket::serde::uuid::Uuid;
//! use rocket::response::Redirect;
//!
//! #[get("/user/<id>")]
//! fn user(id: Uuid) -> String {
//! format!("User ID: {}", id)
//! }
//!
//! #[get("/user?<id>")]
//! fn old_user_path(id: Uuid) -> Redirect {
//! # let _ = Redirect::to(uri!(user(&id)));
//! # let _ = Redirect::to(uri!(old_user_path(id)));
//! # let _ = Redirect::to(uri!(old_user_path(&id)));
//! Redirect::to(uri!(user(id)))
//! }
//! ```
//!
//! # Extra Features
//!
//! The [`uuid`](https://docs.rs/uuid/0.8) crate exposes extra `v{n}` features
//! for generating UUIDs which are not enabled by Rocket. To enable these
//! features, depend on `uuid` directly. The extra functionality can be accessed
//! via both `rocket::serde::uuid::Uuid` or the direct `uuid::Uuid`; the types
//! are one and the same.
//!
//! ```toml
//! [dependencies.uuid]
//! version = "0.8"
//! features = ["v1", "v4"]
//! ```
use crate::request::FromParam;
use crate::form::{self, FromFormField, ValueField};
/// A Universally Unique Identifier (UUID).
///
/// # Examples
///
/// To parse a UUID and print it as a urn:
///
/// ```rust
/// use rocket::serde::uuid::{Uuid, Error};
///
/// # fn f() -> Result<(), Error> {
/// let uuid = Uuid::parse_str("936DA01F9ABD4d9d80C702AF85C822A8")?;
/// println!("{}", uuid.to_urn());
/// # Ok(())
/// # }
/// ```
///
pub use _uuid::Uuid;
pub use _uuid::{Builder, Variant, Version};
/// Type alias for the error returned on [`FromParam`] or [`FromFormField`]
/// failure.
pub type Error = <_uuid::Uuid as std::str::FromStr>::Err;
impl<'a> FromParam<'a> for Uuid {
type Error = Error;
/// A value is successfully parsed if `param` is a properly formatted Uuid.
/// Otherwise, an error is returned.
#[inline(always)]
fn from_param(param: &'a str) -> Result<Uuid, Self::Error> {
param.parse()
}
}
impl<'v> FromFormField<'v> for Uuid {
#[inline]
fn from_value(field: ValueField<'v>) -> form::Result<'v, Self> {
Ok(field.value.parse().map_err(form::error::Error::custom)?)
}
}
#[cfg(test)]
mod test {
use super::{Uuid, FromParam};
#[test]
fn test_from_param() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2";
let uuid_wrapper = Uuid::from_param(uuid_str.into()).unwrap();
assert_eq!(uuid_str, uuid_wrapper.to_string())
}
#[test]
#[should_panic(expected = "InvalidLength")]
fn test_from_param_invalid() {
let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2p";
Uuid::from_param(uuid_str.into()).unwrap();
}
}

View File

@ -15,7 +15,6 @@ members = [
"templating",
"testing",
"tls",
"uuid",
"pastebin",
"todo",

View File

@ -60,7 +60,7 @@ This directory contains projects showcasing Rocket's features.
* **[`serialization`](./serialization)** - Showcases JSON and MessagePack
(de)serialization support by implementing a CRUD-like message API in JSON
and a simply read/echo API in MessagePack.
and a simply read/echo API in MessagePack. Showcases UUID parsing support.
* **[`state`](./state)** - Illustrates the use of request-local state and
managed state. Uses request-local state to cache "expensive" per-request
@ -80,6 +80,3 @@ This directory contains projects showcasing Rocket's features.
* **[`tls`](./tls)** - Illustrates configuring TLS with a variety of key pair
kinds.
* **[`uuid`](./uuid)** - Uses UUID support in `contrib`, converting between
`contrib::Uuid` type and the `uuid` crate `Uuid`.

View File

@ -7,4 +7,4 @@ publish = false
[dependencies.rocket]
path = "../../core/lib"
features = ["json", "msgpack"]
features = ["json", "msgpack", "uuid"]

View File

@ -4,10 +4,12 @@
mod json;
mod msgpack;
mod uuid;
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(json::stage())
.attach(msgpack::stage())
.attach(uuid::stage())
}

View File

@ -1,6 +1,6 @@
use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType, Accept};
use rocket::serde::{Serialize, Deserialize};
use rocket::serde::{Serialize, Deserialize, uuid::Uuid};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
@ -114,3 +114,25 @@ fn msgpack_post() {
assert_eq!(res.status(), Status::Ok);
assert_eq!(res.into_string().unwrap(), "Goodbye, world!");
}
#[test]
fn uuid() {
let client = Client::tracked(super::rocket()).unwrap();
let pairs = &[
("7f205202-7ba1-4c39-b2fc-3e630722bf9f", "We found: Lacy"),
("4da34121-bc7d-4fc1-aee6-bf8de0795333", "We found: Bob"),
("ad962969-4e3d-4de7-ac4a-2d86d6d10839", "We found: George"),
("e18b3a5c-488f-4159-a240-2101e0da19fd",
"Missing person for UUID: e18b3a5c-488f-4159-a240-2101e0da19fd"),
];
for (uuid, response) in pairs {
let uuid = Uuid::parse_str(uuid).unwrap();
let res = client.get(uri!(super::uuid::people(uuid))).dispatch();
assert_eq!(res.into_string().unwrap(), *response);
}
let res = client.get("/people/not-a-uuid").dispatch();
assert_eq!(res.status(), Status::NotFound);
}

View File

@ -0,0 +1,29 @@
use std::collections::HashMap;
use rocket::State;
use rocket::serde::uuid::Uuid;
// A small people mapping in managed state for the sake of this example. In a
// real application this would be a database.
struct People(HashMap<Uuid, &'static str>);
#[get("/people/<id>")]
fn people(id: Uuid, people: &State<People>) -> Result<String, String> {
Ok(people.0.get(&id)
.map(|person| format!("We found: {}", person))
.ok_or_else(|| format!("Missing person for UUID: {}", id))?)
}
pub fn stage() -> rocket::fairing::AdHoc {
// Seed the "database".
let mut map = HashMap::new();
map.insert("7f205202-7ba1-4c39-b2fc-3e630722bf9f".parse().unwrap(), "Lacy");
map.insert("4da34121-bc7d-4fc1-aee6-bf8de0795333".parse().unwrap(), "Bob");
map.insert("ad962969-4e3d-4de7-ac4a-2d86d6d10839".parse().unwrap(), "George");
rocket::fairing::AdHoc::on_ignite("UUID", |rocket| async {
rocket
.manage(People(map))
.mount("/", routes![people])
})
}

View File

@ -1,14 +0,0 @@
[package]
name = "uuid"
version = "0.1.0"
workspace = "../"
edition = "2018"
publish = false
[dependencies]
rocket = { path = "../../core/lib" }
[dependencies.rocket_contrib]
default-features = false
path = "../../contrib/lib"
features = ["uuid"]

View File

@ -1,36 +0,0 @@
#[macro_use] extern crate rocket;
use std::collections::HashMap;
use rocket::State;
use rocket_contrib::uuid::Uuid;
use rocket_contrib::uuid::extern_uuid;
#[cfg(test)] mod tests;
// A small people mapping in managed state for the sake of this example. In a
// real application this would be a database. Notice that we use the external
// Uuid type here and not the rocket_contrib::uuid::Uuid type. We do this purely
// for demonstrative purposes; in practice, we could use the contrib `Uuid`.
struct People(HashMap<extern_uuid::Uuid, &'static str>);
#[get("/people/<id>")]
fn people(id: Uuid, people: &State<People>) -> Result<String, String> {
// Because Uuid implements the Deref trait, we use Deref coercion to convert
// rocket_contrib::uuid::Uuid to uuid::Uuid.
Ok(people.0.get(&id)
.map(|person| format!("We found: {}", person))
.ok_or_else(|| format!("Person not found for UUID: {}", id))?)
}
#[launch]
fn rocket() -> _ {
let mut map = HashMap::new();
map.insert("7f205202-7ba1-4c39-b2fc-3e630722bf9f".parse().unwrap(), "Lacy");
map.insert("4da34121-bc7d-4fc1-aee6-bf8de0795333".parse().unwrap(), "Bob");
map.insert("ad962969-4e3d-4de7-ac4a-2d86d6d10839".parse().unwrap(), "George");
rocket::build()
.manage(People(map))
.mount("/", routes![people])
}

View File

@ -1,25 +0,0 @@
use super::rocket;
use rocket::local::blocking::Client;
use rocket::http::Status;
fn test(uri: &str, expected: &str) {
let client = Client::tracked(rocket()).unwrap();
let res = client.get(uri).dispatch();
assert_eq!(res.into_string(), Some(expected.into()));
}
fn test_404(uri: &str) {
let client = Client::tracked(rocket()).unwrap();
let res = client.get(uri).dispatch();
assert_eq!(res.status(), Status::NotFound);
}
#[test]
fn test_people() {
test("/people/7f205202-7ba1-4c39-b2fc-3e630722bf9f", "We found: Lacy");
test("/people/4da34121-bc7d-4fc1-aee6-bf8de0795333", "We found: Bob");
test("/people/ad962969-4e3d-4de7-ac4a-2d86d6d10839", "We found: George");
test("/people/e18b3a5c-488f-4159-a240-2101e0da19fd",
"Person not found for UUID: e18b3a5c-488f-4159-a240-2101e0da19fd");
test_404("/people/invalid_uuid");
}

View File

@ -89,6 +89,7 @@ function test_core() {
tls
json
msgpack
uuid
)
pushd "${CORE_LIB_ROOT}" > /dev/null 2>&1