Add documentation and examples for request-local state.

Closes #654.
This commit is contained in:
jeb 2018-07-31 19:22:36 -06:00 committed by Sergio Benitez
parent cd776d5b6a
commit d1cfdbaa8e
3 changed files with 237 additions and 0 deletions

View File

@ -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

View File

@ -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;

View File

@ -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