Document the 'Contextual' form guard.

This commit is contained in:
Sergio Benitez 2021-05-23 18:09:43 -07:00
parent ab13d73b30
commit 8a9000a9cb
7 changed files with 278 additions and 55 deletions

View File

@ -4,50 +4,155 @@ use indexmap::{IndexMap, IndexSet};
use crate::form::prelude::*; use crate::form::prelude::*;
use crate::http::Status; use crate::http::Status;
/// An infallible form guard that records form fields while parsing any form /// An infallible form guard that records form fields and errors during parsing.
/// type.
/// ///
/// See the [forms guide](https://rocket.rs/master/guide/requests/#context) for /// This form guard _never fails_. It should be use _only_ when the form
/// usage details. /// [`Context`] is required. In all other cases, prefer to use `T` directly.
///
/// # Usage
///
/// `Contextual` acts as a proxy for any form type, recording all submitted form
/// values and produced errors and associating them with their corresponding
/// field name. `Contextual` is particularly useful for rendering forms with
/// previously submitted values and errors associated with form input.
///
/// To retrieve the context for a form, use `Form<Contextual<'_, T>>` as a data
/// guard, where `T` implements `FromForm`. The `context` field contains the
/// form's [`Context`]:
///
/// ```rust
/// # use rocket::post;
/// # type T = String;
/// use rocket::form::{Form, Contextual};
///
/// #[post("/submit", data = "<form>")]
/// fn submit(form: Form<Contextual<'_, T>>) {
/// if let Some(ref value) = form.value {
/// // The form parsed successfully. `value` is the `T`.
/// }
///
/// // We can retrieve raw field values and errors.
/// let raw_id_value = form.context.field_value("id");
/// let id_errors = form.context.field_errors("id");
/// }
/// ```
///
/// `Context` serializes as a map, so it can be rendered in templates that
/// require `Serialize` types. See the [forms guide] for further usage details.
///
/// [forms guide]: https://rocket.rs/master/guide/requests/#context
#[derive(Debug)] #[derive(Debug)]
pub struct Contextual<'v, T> { pub struct Contextual<'v, T> {
/// The value, if it could be successfully parsed. /// The value, if it was successfully parsed, or `None` otherwise.
pub value: Option<T>, pub value: Option<T>,
/// The context containig all received fields, values, and errors. /// The context with all submitted fields and associated values and errors.
pub context: Context<'v> pub context: Context<'v>,
} }
/// A form context containing received fields, values, and encountered errors. /// A form context containing received fields, values, and encountered errors.
/// ///
/// A value of this type is produced by the [`Contextual`] form guard in its
/// [`context`](Contextual::context) field. `Context` contains an entry for
/// every form field submitted by the client regardless of whether the field
/// parsed or validated successfully.
///
/// # Field Values
///
/// The original, submitted field value(s) for a _value_ field can be retrieved
/// via [`Context::field_value()`] or [`Context::field_values()`]. Data fields do not have
/// their values recorded. All submitted field names, including data field
/// names, can be retrieved via [`Context::fields()`].
///
/// # Field Errors
///
/// # Serialization /// # Serialization
/// ///
/// When a value of this type is serialized, a `struct` or map with the /// When a value of this type is serialized, a `struct` or map with the
/// following fields is emitted: /// following fields is emitted:
/// ///
/// | field | type | description | /// | field | type | description |
/// |---------------|-------------------|------------------------------------------------| /// |---------------|------------------------------------|--------------------------------------|
/// | `errors` | &str => &[Error] | map from a field name to errors it encountered | /// | `errors` | map: string to array of [`Error`]s | maps a field name to its errors |
/// | `values` | &str => &[&str] | map from a field name to its submitted values | /// | `values` | map: string to array of strings | maps a field name to its form values |
/// | `data_values` | &[&str] | field names of all data fields received | /// | `data_fields` | array of strings | field names of all form data fields |
/// | `form_errors` | &[Error] | errors not corresponding to specific fields | /// | `form_errors` | array of [`Error`]s | errors not associated with a field |
/// ///
/// See [`Error`] for details on how an `Error` is serialized. /// See [`Error`](Error#serialization) for `Error` serialization details.
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct Context<'v> { pub struct Context<'v> {
errors: IndexMap<NameBuf<'v>, Errors<'v>>, errors: IndexMap<NameBuf<'v>, Errors<'v>>,
values: IndexMap<&'v Name, Vec<&'v str>>, values: IndexMap<&'v Name, Vec<&'v str>>,
data_values: IndexSet<&'v Name>, data_fields: IndexSet<&'v Name>,
form_errors: Errors<'v>, form_errors: Errors<'v>,
#[serde(skip)] #[serde(skip)]
status: Status, status: Status,
} }
impl<'v> Context<'v> { impl<'v> Context<'v> {
pub fn value<N: AsRef<Name>>(&self, name: N) -> Option<&'v str> { /// Returns the names of all submitted form fields, both _value_ and _data_
/// fields.
///
/// # Example
///
/// ```rust
/// # use rocket::post;
/// # type T = String;
/// use rocket::form::{Form, Contextual};
///
/// #[post("/submit", data = "<form>")]
/// fn submit(form: Form<Contextual<'_, T>>) {
/// let field_names = form.context.fields();
/// }
/// ```
pub fn fields(&self) -> impl Iterator<Item = &'v Name> + '_ {
self.values.iter()
.map(|(name, _)| *name)
.chain(self.data_fields.iter().copied())
}
/// Returns the _first_ value, if any, submitted for the _value_ field named
/// `name`.
///
/// The type of `name` may be `&Name`, `&str`, or `&RawStr`. Lookup is
/// case-sensitive but key-seperator (`.` or `[]`) insensitive.
///
/// # Example
///
/// ```rust
/// # use rocket::post;
/// # type T = String;
/// use rocket::form::{Form, Contextual};
///
/// #[post("/submit", data = "<form>")]
/// fn submit(form: Form<Contextual<'_, T>>) {
/// let first_value_for_id = form.context.field_value("id");
/// let first_value_for_foo_bar = form.context.field_value("foo.bar");
/// }
/// ```
pub fn field_value<N: AsRef<Name>>(&self, name: N) -> Option<&'v str> {
self.values.get(name.as_ref())?.get(0).cloned() self.values.get(name.as_ref())?.get(0).cloned()
} }
pub fn values<'a, N>(&'a self, name: N) -> impl Iterator<Item = &'v str> + 'a /// Returns the values, if any, submitted for the _value_ field named
/// `name`.
///
/// The type of `name` may be `&Name`, `&str`, or `&RawStr`. Lookup is
/// case-sensitive but key-seperator (`.` or `[]`) insensitive.
///
/// # Example
///
/// ```rust
/// # use rocket::post;
/// # type T = String;
/// use rocket::form::{Form, Contextual};
///
/// #[post("/submit", data = "<form>")]
/// fn submit(form: Form<Contextual<'_, T>>) {
/// let values_for_id = form.context.field_values("id");
/// let values_for_foo_bar = form.context.field_values("foo.bar");
/// }
/// ```
pub fn field_values<N>(&self, name: N) -> impl Iterator<Item = &'v str> + '_
where N: AsRef<Name> where N: AsRef<Name>
{ {
self.values self.values
@ -57,27 +162,139 @@ impl<'v> Context<'v> {
.flatten() .flatten()
} }
pub fn has_error<N: AsRef<Name>>(&self, name: &N) -> bool { /// Returns an iterator over all of the errors in the context, including
self.errors(name).next().is_some() /// those not associated with any field.
} ///
/// # Example
pub fn errors<'a, N>(&'a self, name: &'a N) -> impl Iterator<Item = &Error<'v>> + 'a ///
where N: AsRef<Name> + ?Sized /// ```rust
{ /// # use rocket::post;
let name = name.as_ref(); /// # type T = String;
name.prefixes() /// use rocket::form::{Form, Contextual};
.filter_map(move |name| self.errors.get(name)) ///
.map(|e| e.iter()) /// #[post("/submit", data = "<form>")]
.flatten() /// fn submit(form: Form<Contextual<'_, T>>) {
} /// let errors = form.context.errors();
/// }
pub fn all_errors(&self) -> impl Iterator<Item = &Error<'v>> { /// ```
pub fn errors(&self) -> impl Iterator<Item = &Error<'v>> {
self.errors.values() self.errors.values()
.map(|e| e.iter()) .map(|e| e.iter())
.flatten() .flatten()
.chain(self.form_errors.iter()) .chain(self.form_errors.iter())
} }
/// Returns the errors associated with the field `name`. This method is
/// roughly equivalent to:
///
/// ```rust
/// # use rocket::form::{Context, name::Name};
/// # let context = Context::default();
/// # let name = Name::new("foo");
/// context.errors().filter(|e| e.is_for(name))
/// # ;
/// ```
///
/// That is, it uses [`Error::is_for()`] to determine which errors are
/// associated with the field named `name`. This considers all errors whose
/// associated field name is a prefix of `name` to be an error for the field
/// named `name`. In other words, it associates parent field errors with
/// their children: `a.b`'s errors apply to `a.b.c`, `a.b.d` and so on but
/// not `a.c`.
///
/// Lookup is case-sensitive but key-seperator (`.` or `[]`) insensitive.
///
/// # Example
///
/// ```rust
/// # use rocket::post;
/// # type T = String;
/// use rocket::form::{Form, Contextual};
///
/// #[post("/submit", data = "<form>")]
/// fn submit(form: Form<Contextual<'_, T>>) {
/// // Get all errors for field `id`.
/// let id = form.context.field_errors("id");
///
/// // Get all errors for `foo.bar` or `foo` if `foo` failed first.
/// let foo_bar = form.context.field_errors("foo.bar");
/// }
/// ```
pub fn field_errors<'a, N>(&'a self, name: N) -> impl Iterator<Item = &Error<'v>> + '_
where N: AsRef<Name> + 'a
{
self.errors.values()
.map(|e| e.iter())
.flatten()
.filter(move |e| e.is_for(&name))
}
/// Returns the errors associated _exactly_ with the field `name`. Prefer
/// [`Context::field_errors()`] instead.
///
/// This method is roughly equivalent to:
///
/// ```rust
/// # use rocket::form::{Context, name::Name};
/// # let context = Context::default();
/// # let name = Name::new("foo");
/// context.errors().filter(|e| e.is_for_exactly(name))
/// # ;
/// ```
///
/// That is, it uses [`Error::is_for_exactly()`] to determine which errors
/// are associated with the field named `name`. This considers _only_ errors
/// whose associated field name is _exactly_ `name` to be an error for the
/// field named `name`. This is _not_ what is typically desired as it
/// ignores errors that occur in the parent which will result in missing
/// errors associated with its chilren. Use [`Context::field_errors()`] in
/// almost all cases.
///
/// Lookup is case-sensitive but key-seperator (`.` or `[]`) insensitive.
///
/// # Example
///
/// ```rust
/// # use rocket::post;
/// # type T = String;
/// use rocket::form::{Form, Contextual};
///
/// #[post("/submit", data = "<form>")]
/// fn submit(form: Form<Contextual<'_, T>>) {
/// // Get all errors for field `id`.
/// let id = form.context.exact_field_errors("id");
///
/// // Get all errors exactly for `foo.bar`. If `foo` failed, we will
/// // this will return no erorrs. Use `Context::field_errors()`.
/// let foo_bar = form.context.exact_field_errors("foo.bar");
/// }
/// ```
pub fn exact_field_errors<'a, N>(&'a self, name: N) -> impl Iterator<Item = &Error<'v>> + '_
where N: AsRef<Name> + 'a
{
self.errors.values()
.map(|e| e.iter())
.flatten()
.filter(move |e| e.is_for_exactly(&name))
}
/// Returns the `max` of the statuses associated with all field errors.
///
/// See [`Error::status()`] for details on how an error status is computed.
///
/// # Example
///
/// ```rust
/// # use rocket::post;
/// # type T = String;
/// use rocket::http::Status;
/// use rocket::form::{Form, Contextual};
///
/// #[post("/submit", data = "<form>")]
/// fn submit(form: Form<Contextual<'_, T>>) -> (Status, &'static str) {
/// (form.context.status(), "Thanks!")
/// }
/// ```
pub fn status(&self) -> Status { pub fn status(&self) -> Status {
self.status self.status
} }
@ -119,11 +336,8 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Contextual<'v, T> {
T::push_value(val_ctxt, field); T::push_value(val_ctxt, field);
} }
async fn push_data( async fn push_data((ref mut val_ctxt, ctxt): &mut Self::Context, field: DataField<'v, '_>) {
(ref mut val_ctxt, ctxt): &mut Self::Context, ctxt.data_fields.insert(field.name.source());
field: DataField<'v, '_>
) {
ctxt.data_values.insert(field.name.source());
T::push_data(val_ctxt, field).await; T::push_data(val_ctxt, field).await;
} }

View File

@ -8,7 +8,7 @@ use indexmap::IndexMap;
use crate::form::prelude::*; use crate::form::prelude::*;
use crate::http::uncased::AsUncased; use crate::http::uncased::AsUncased;
/// Trait for implementing form guards: types parseable from HTTP form fields. /// Trait implemented by form guards: types parseable from HTTP forms.
/// ///
/// Only form guards that are _collections_, that is, collect more than one form /// Only form guards that are _collections_, that is, collect more than one form
/// field while parsing, should implement `FromForm`. All other types should /// field while parsing, should implement `FromForm`. All other types should

View File

@ -178,12 +178,6 @@ impl<N: AsRef<Name> + ?Sized> PartialEq<N> for NameBuf<'_> {
} }
} }
impl PartialEq<Name> for NameBuf<'_> {
fn eq(&self, other: &Name) -> bool {
self.keys().eq(other.keys())
}
}
impl PartialEq<NameBuf<'_>> for Name { impl PartialEq<NameBuf<'_>> for Name {
fn eq(&self, other: &NameBuf<'_>) -> bool { fn eq(&self, other: &NameBuf<'_>) -> bool {
self.keys().eq(other.keys()) self.keys().eq(other.keys())

View File

@ -210,6 +210,12 @@ impl AsRef<Name> for RawStr {
} }
} }
impl AsRef<Name> for Name {
fn as_ref(&self) -> &Name {
self
}
}
impl Eq for Name { } impl Eq for Name { }
impl std::hash::Hash for Name { impl std::hash::Hash for Name {

View File

@ -7,6 +7,7 @@
#![cfg_attr(nightly, feature(decl_macro))] #![cfg_attr(nightly, feature(decl_macro))]
#![warn(rust_2018_idioms)] #![warn(rust_2018_idioms)]
#![warn(missing_docs)]
//! # Rocket - Core API Documentation //! # Rocket - Core API Documentation
//! //!

View File

@ -67,6 +67,9 @@ fn index<'r>() -> Template {
Template::render("index", &Context::default()) Template::render("index", &Context::default())
} }
// NOTE: We use `Contextual` here because we want to collect all submitted form
// fields to re-render forms with submitted values on error. If you have no such
// need, do not use `Contextual`. Use the equivalent of `Form<Submit<'_>>`.
#[post("/", data = "<form>")] #[post("/", data = "<form>")]
fn submit<'r>(form: Form<Contextual<'r, Submit<'r>>>) -> (Status, Template) { fn submit<'r>(form: Form<Contextual<'r, Submit<'r>>>) -> (Status, Template) {
let template = match form.value { let template = match form.value {

View File

@ -1521,10 +1521,11 @@ map! {
### Context ### Context
The [`Contextual`] type acts as a proxy for any form type, recording all of the The [`Contextual`] form guard acts as a proxy for any other form guard,
submitted form values and produced errors and associating them with their recording all submitted form values and produced errors and associating them
corresponding field name. `Contextual` is particularly useful to render a form with their corresponding field name. `Contextual` is particularly useful for
with previously submitted values and render errors associated with a form input. rendering forms with previously submitted values and errors associated with form
input.
To retrieve the context for a form, use `Form<Contextual<'_, T>>` as a data To retrieve the context for a form, use `Form<Contextual<'_, T>>` as a data
guard, where `T` implements `FromForm`. The `context` field contains the form's guard, where `T` implements `FromForm`. The `context` field contains the form's
@ -1542,18 +1543,22 @@ fn submit(form: Form<Contextual<'_, T>>) {
// The form parsed successfully. `value` is the `T`. // The form parsed successfully. `value` is the `T`.
} }
// In all cases, `form.context` contains the `Context`.
// We can retrieve raw field values and errors. // We can retrieve raw field values and errors.
let raw_id_value = form.context.value("id"); let raw_id_value = form.context.field_value("id");
let id_errors = form.context.errors("id"); let id_errors = form.context.field_errors("id");
} }
``` ```
`Context` is nesting-aware for errors. When `Context` is queried for errors for
a field named `foo.bar`, it returns errors for fields that are a prefix of
`foo.bar`, namely `foo` and `foo.bar`. Similarly, if queried for errors for a
field named `foo.bar.baz`, errors for field `foo`, `foo.bar`, and `foo.bar.baz`
will be returned.
`Context` serializes as a map, so it can be rendered in templates that require `Context` serializes as a map, so it can be rendered in templates that require
`Serialize` types. See `Serialize` types. See [`Context`] for details about its serialization format.
[`Context`](@api/rocket/form/struct.Context.html#Serialization) for details The [forms example], too, makes use of form contexts, as well as every other
about its serialization format. The [forms example], too, makes use of form forms feature.
contexts, as well as every other forms feature.
[`Contextual`]: @api/rocket/form/struct.Contextual.html [`Contextual`]: @api/rocket/form/struct.Contextual.html
[`Context`]: @api/rocket/form/struct.Context.html [`Context`]: @api/rocket/form/struct.Context.html