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::http::Status;
/// An infallible form guard that records form fields while parsing any form
/// type.
/// An infallible form guard that records form fields and errors during parsing.
///
/// See the [forms guide](https://rocket.rs/master/guide/requests/#context) for
/// usage details.
/// This form guard _never fails_. It should be use _only_ when the form
/// [`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)]
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>,
/// The context containig all received fields, values, and errors.
pub context: Context<'v>
/// The context with all submitted fields and associated values and errors.
pub context: Context<'v>,
}
/// 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
///
/// When a value of this type is serialized, a `struct` or map with the
/// following fields is emitted:
///
/// | field | type | description |
/// |---------------|-------------------|------------------------------------------------|
/// | `errors` | &str => &[Error] | map from a field name to errors it encountered |
/// | `values` | &str => &[&str] | map from a field name to its submitted values |
/// | `data_values` | &[&str] | field names of all data fields received |
/// | `form_errors` | &[Error] | errors not corresponding to specific fields |
/// | field | type | description |
/// |---------------|------------------------------------|--------------------------------------|
/// | `errors` | map: string to array of [`Error`]s | maps a field name to its errors |
/// | `values` | map: string to array of strings | maps a field name to its form values |
/// | `data_fields` | array of strings | field names of all form data 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)]
pub struct Context<'v> {
errors: IndexMap<NameBuf<'v>, Errors<'v>>,
values: IndexMap<&'v Name, Vec<&'v str>>,
data_values: IndexSet<&'v Name>,
data_fields: IndexSet<&'v Name>,
form_errors: Errors<'v>,
#[serde(skip)]
status: Status,
}
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()
}
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>
{
self.values
@ -57,27 +162,139 @@ impl<'v> Context<'v> {
.flatten()
}
pub fn has_error<N: AsRef<Name>>(&self, name: &N) -> bool {
self.errors(name).next().is_some()
}
pub fn errors<'a, N>(&'a self, name: &'a N) -> impl Iterator<Item = &Error<'v>> + 'a
where N: AsRef<Name> + ?Sized
{
let name = name.as_ref();
name.prefixes()
.filter_map(move |name| self.errors.get(name))
.map(|e| e.iter())
.flatten()
}
pub fn all_errors(&self) -> impl Iterator<Item = &Error<'v>> {
/// Returns an iterator over all of the errors in the context, including
/// those not associated with any field.
///
/// # Example
///
/// ```rust
/// # use rocket::post;
/// # type T = String;
/// use rocket::form::{Form, Contextual};
///
/// #[post("/submit", data = "<form>")]
/// fn submit(form: Form<Contextual<'_, T>>) {
/// let errors = form.context.errors();
/// }
/// ```
pub fn errors(&self) -> impl Iterator<Item = &Error<'v>> {
self.errors.values()
.map(|e| e.iter())
.flatten()
.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 {
self.status
}
@ -119,11 +336,8 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Contextual<'v, T> {
T::push_value(val_ctxt, field);
}
async fn push_data(
(ref mut val_ctxt, ctxt): &mut Self::Context,
field: DataField<'v, '_>
) {
ctxt.data_values.insert(field.name.source());
async fn push_data((ref mut val_ctxt, ctxt): &mut Self::Context, field: DataField<'v, '_>) {
ctxt.data_fields.insert(field.name.source());
T::push_data(val_ctxt, field).await;
}

View File

@ -8,7 +8,7 @@ use indexmap::IndexMap;
use crate::form::prelude::*;
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
/// 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 {
fn eq(&self, other: &NameBuf<'_>) -> bool {
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 std::hash::Hash for Name {

View File

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

View File

@ -67,6 +67,9 @@ fn index<'r>() -> Template {
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>")]
fn submit<'r>(form: Form<Contextual<'r, Submit<'r>>>) -> (Status, Template) {
let template = match form.value {

View File

@ -1521,10 +1521,11 @@ map! {
### Context
The [`Contextual`] type acts as a proxy for any form type, recording all of the
submitted form values and produced errors and associating them with their
corresponding field name. `Contextual` is particularly useful to render a form
with previously submitted values and render errors associated with a form input.
The [`Contextual`] form guard acts as a proxy for any other form guard,
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
@ -1542,18 +1543,22 @@ fn submit(form: Form<Contextual<'_, 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.
let raw_id_value = form.context.value("id");
let id_errors = form.context.errors("id");
let raw_id_value = form.context.field_value("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
`Serialize` types. See
[`Context`](@api/rocket/form/struct.Context.html#Serialization) for details
about its serialization format. The [forms example], too, makes use of form
contexts, as well as every other forms feature.
`Serialize` types. See [`Context`] for details about its serialization format.
The [forms example], too, makes use of form contexts, as well as every other
forms feature.
[`Contextual`]: @api/rocket/form/struct.Contextual.html
[`Context`]: @api/rocket/form/struct.Context.html