Rocket

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit d1cfdbaa8e575c4f0b8a0509b06356cd0ffb1987
parent cd776d5b6a98e57bb5901cda4754985bc7a183b5
Author: jeb <jeb@jebrosen.com>
Date:   Tue, 31 Jul 2018 19:22:36 -0600

Add documentation and examples for request-local state.

Closes #654.

Diffstat:
Mcore/lib/src/fairing/mod.rs | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/lib/src/request/from_request.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msite/guide/state.md | 44++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 237 insertions(+), 0 deletions(-)

diff --git a/core/lib/src/fairing/mod.rs b/core/lib/src/fairing/mod.rs @@ -260,6 +260,67 @@ pub use self::info_kind::{Info, Kind}; /// } /// } /// ``` +/// +/// ## Request-Local Cache +/// +/// Fairings can use the *request-local cache* to persist data between the +/// request and the response, or to pass data to a request guard. +/// +/// ``` +/// # use std::time::{Duration, SystemTime}; +/// # use rocket::Outcome; +/// # use rocket::{Request, Data, Response}; +/// # use rocket::fairing::{Fairing, Info, Kind}; +/// # use rocket::http::Status; +/// # use rocket::request::{self, FromRequest}; +/// # +/// struct RequestTimer; +/// #[derive(Copy, Clone)] +/// struct StartTime(pub Option<SystemTime>); +/// +/// impl Fairing for RequestTimer { +/// fn info(&self) -> Info { +/// Info { +/// name: "Request Timer", +/// kind: Kind::Request | Kind::Response +/// } +/// } +/// +/// /// Stores the start time of the request +/// fn on_request(&self, request: &mut Request, _: &Data) { +/// // Store a StartTime instead of directly storing a SystemTime, +/// // to ensure that this usage doesn't conflict with anything else +/// // that might store a SystemTime in request-local cache. +/// request.local_cache(|| StartTime(Some(SystemTime::now()))); +/// } +/// +/// /// Adds a header to the response indicating how long the server took to +/// /// process the request +/// fn on_response(&self, request: &Request, response: &mut Response) { +/// let start_time = request.local_cache(|| StartTime(None)); +/// if let Some(Ok(duration)) = start_time.0.map(|st| st.elapsed()) { +/// response.set_raw_header("X-Response-Time", format!("{} ms", +/// duration.as_secs() * 1000 + duration.subsec_millis() as u64)); +/// } +/// } +/// } +/// +/// // Allows a route to access the time the request was initiated. +/// // This guard will fail if the RequestTimer fairing was not attached, +/// // and will never return a StartTime(None). +/// impl<'a, 'r> FromRequest<'a, 'r> for StartTime { +/// type Error = (); +/// +/// fn from_request(request: &'a Request<'r>) -> request::Outcome<StartTime, ()> { +/// let start_time = request.local_cache(|| StartTime(None)); +/// match *start_time { +/// st@StartTime(Some(_)) => Outcome::Success(st), +/// StartTime(None) => Outcome::Failure((Status::InternalServerError, ())), +/// } +/// } +/// } +/// ``` + pub trait Fairing: Send + Sync + 'static { /// Returns an [`Info`](/rocket/fairing/struct.Info.html) structure /// containing the `name` and [`Kind`](/rocket/fairing/struct.Kind.html) of diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs @@ -207,6 +207,138 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> { /// /// # fn main() { } /// ``` +/// +/// # Request-Local Cache +/// +/// Request guards that perform expensive operations, such as querying a +/// database or an external service, should use the *request-local cache* to +/// store the result if they might be invoked multiple times during the routing +/// of a single request. +/// +/// For example, consider a pair of `User` and `Admin` guards: +/// +/// ```rust +/// # #![feature(plugin, decl_macro)] +/// # #![plugin(rocket_codegen)] +/// # extern crate rocket; +/// # +/// # use rocket::outcome::{IntoOutcome, Outcome}; +/// # use rocket::request::{self, FromRequest, Request}; +/// # struct User { id: String, is_admin: bool }; +/// # struct Database; +/// # impl Database { +/// # fn get_user(&self, id: String) -> Result<User, ()> { +/// # Ok(User { id, is_admin: false }) +/// # } +/// # } +/// # impl<'a, 'r> FromRequest<'a, 'r> for Database { +/// # type Error = (); +/// # fn from_request(request: &'a Request<'r>) -> request::Outcome<Database, ()> { +/// # Outcome::Success(Database) +/// # } +/// # } +/// # +/// # struct Admin { user: User }; +/// # +/// impl<'a, 'r> FromRequest<'a, 'r> for User { +/// type Error = (); +/// +/// fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> { +/// let db = request.guard::<Database>()?; +/// request.cookies() +/// .get_private("user_id") +/// .and_then(|cookie| cookie.value().parse().ok()) +/// .and_then(|id| db.get_user(id).ok()) +/// .or_forward(()) +/// } +/// } +/// +/// impl<'a, 'r> FromRequest<'a, 'r> for Admin { +/// type Error = (); +/// +/// fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> { +/// // This will unconditionally query the database! +/// let user = request.guard::<User>()?; +/// +/// if user.is_admin { +/// Outcome::Success(Admin { user }) +/// } else { +/// Outcome::Forward(()) +/// } +/// } +/// } +/// +/// #[get("/dashboard")] +/// fn admin_dashboard(admin: Admin) { } +/// +/// #[get("/dashboard", rank = 2)] +/// fn user_dashboard(user: User) { } +/// ``` +/// +/// When a non-admin user is logged in, the database will be queried twice: Once +/// via the `Admin` guard invoking the `User` guard, and a second time via the +/// `User` guard directly. For cases such as these, the request-local cache +/// should be used: +/// +/// ```rust +/// # #![feature(plugin, decl_macro)] +/// # #![plugin(rocket_codegen)] +/// # extern crate rocket; +/// # +/// # use rocket::outcome::{IntoOutcome, Outcome}; +/// # use rocket::request::{self, FromRequest, Request}; +/// # struct User { id: String, is_admin: bool }; +/// # struct Database; +/// # impl Database { +/// # fn get_user(&self, id: String) -> Result<User, ()> { +/// # Ok(User { id, is_admin: false }) +/// # } +/// # } +/// # impl<'a, 'r> FromRequest<'a, 'r> for Database { +/// # type Error = (); +/// # fn from_request(request: &'a Request<'r>) -> request::Outcome<Database, ()> { +/// # Outcome::Success(Database) +/// # } +/// # } +/// # +/// # struct Admin<'a> { user: &'a User }; +/// # +/// impl<'a, 'r> FromRequest<'a, 'r> for &'a User { +/// type Error = (); +/// +/// fn from_request(request: &'a Request<'r>) -> request::Outcome<&'a User, ()> { +/// // The closure will run only once per request, and future +/// // invocations will reuse the result of the first calculation +/// let user_result = request.local_cache(|| { +/// let db = request.guard::<Database>().succeeded()?; +/// request.cookies() +/// .get_private("user_id") +/// .and_then(|cookie| cookie.value().parse().ok()) +/// .and_then(|id| db.get_user(id).ok()) +/// }); +/// user_result.as_ref().or_forward(()) +/// } +/// } +/// +/// impl<'a, 'r> FromRequest<'a, 'r> for Admin<'a> { +/// type Error = (); +/// +/// fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin<'a>, ()> { +/// let user = request.guard::<&User>()?; +/// +/// if user.is_admin { +/// Outcome::Success(Admin { user }) +/// } else { +/// Outcome::Forward(()) +/// } +/// } +/// } +/// ``` +/// +/// Notice that these request guards provide access to *borrowed* data +/// (`&'a User` and `Admin<'a>`). The data is now owned by the request's cache, +/// so it must either be borrowed or cloned by the guards. + pub trait FromRequest<'a, 'r>: Sized { /// The associated error to be returned if derivation fails. type Error: Debug; diff --git a/site/guide/state.md b/site/guide/state.md @@ -85,6 +85,50 @@ fn from_request(req: &'a Request<'r>) -> request::Outcome<T, ()> { [`Request::guard()`]: https://api.rocket.rs/rocket/struct.Request.html#method.guard +### Request-Local State + +While managed state is *global* and available application-wide, request-local +state is *local* to a given request, carried along with the request, and dropped +once the request is completed. Request-local state can be used whenever a +`Request` is available, such as in a fairing, a request guard, or a responder. + +Request-local state is *cached*: if data of a given type has already been +stored, it will be reused. This is especially useful for request guards that +might be invoked multiple times during the routing and processing of a single +request, such as those dealing with authentication. + +```rust +/// A global counter for arbitrary request IDs +static request_id_counter: AtomicUsize = AtomicUsize::new(0); +/// A type that represents request IDs +struct RequestId(pub usize); + +/// Returns the current request's RequestId, assigning one +/// if the current request does not have one already. +impl<'a, 'r> FromRequest<'a, 'r> for RequestId { + fn from_request(request: &'a Request<'r>) -> request::Outcome { + // The closure passed to local_cache will be executed at most once per + // request, the first time the RequestId guard is used. If it is + // requested again, local_cache will return the same value. + Outcome::Success(request.local_cache(|| { + RequestId(request_id_counter.fetch_add(1, Ordering::Relaxed)) + })) + } +} +``` + +Another use case for request-local state is request validation. A fairing can +read request headers, query a database or other external service, and store the +result in request-local state. The result of the validation is available to each +individual route and also to any custom `Responder`s, for example from an +authentication library. + +Refer to the documentation for the [`FromRequest`] and [`Fairing`] traits for +more examples of this functionality. + +[`FromRequest`]: https://api.rocket.rs/rocket/request/trait.FromRequest.html +[`Fairing`]: https://api.rocket.rs/rocket/fairing/trait.Fairing.html + ### Unmanaged State If you request a `State<T>` for a `T` that is not `managed`, Rocket won't call