From 8a9000a9cbee3988781fe125759e5b507e7b0c55 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 23 May 2021 18:09:43 -0700 Subject: [PATCH] Document the 'Contextual' form guard. --- core/lib/src/form/context.rs | 288 ++++++++++++++++++++++++++++----- core/lib/src/form/from_form.rs | 2 +- core/lib/src/form/name/buf.rs | 6 - core/lib/src/form/name/name.rs | 6 + core/lib/src/lib.rs | 1 + examples/forms/src/main.rs | 3 + site/guide/4-requests.md | 27 ++-- 7 files changed, 278 insertions(+), 55 deletions(-) diff --git a/core/lib/src/form/context.rs b/core/lib/src/form/context.rs index bacfea93..f43e0485 100644 --- a/core/lib/src/form/context.rs +++ b/core/lib/src/form/context.rs @@ -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>` 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 = "
")] +/// fn submit(form: Form>) { +/// 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, - /// 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, 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>(&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 = "")] + /// fn submit(form: Form>) { + /// let field_names = form.context.fields(); + /// } + /// ``` + pub fn fields(&self) -> impl Iterator + '_ { + 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 = "")] + /// fn submit(form: Form>) { + /// 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>(&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 + '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 = "")] + /// fn submit(form: Form>) { + /// let values_for_id = form.context.field_values("id"); + /// let values_for_foo_bar = form.context.field_values("foo.bar"); + /// } + /// ``` + pub fn field_values(&self, name: N) -> impl Iterator + '_ where N: AsRef { self.values @@ -57,27 +162,139 @@ impl<'v> Context<'v> { .flatten() } - pub fn has_error>(&self, name: &N) -> bool { - self.errors(name).next().is_some() - } - - pub fn errors<'a, N>(&'a self, name: &'a N) -> impl Iterator> + 'a - where N: AsRef + ?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> { + /// 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 = "")] + /// fn submit(form: Form>) { + /// let errors = form.context.errors(); + /// } + /// ``` + pub fn errors(&self) -> impl Iterator> { 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 = "")] + /// fn submit(form: Form>) { + /// // 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> + '_ + where N: AsRef + '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 = "")] + /// fn submit(form: Form>) { + /// // 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> + '_ + where N: AsRef + '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 = "")] + /// fn submit(form: Form>) -> (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; } diff --git a/core/lib/src/form/from_form.rs b/core/lib/src/form/from_form.rs index 4b336ee9..cef4e26e 100644 --- a/core/lib/src/form/from_form.rs +++ b/core/lib/src/form/from_form.rs @@ -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 diff --git a/core/lib/src/form/name/buf.rs b/core/lib/src/form/name/buf.rs index 9cd15eaf..708d4c57 100644 --- a/core/lib/src/form/name/buf.rs +++ b/core/lib/src/form/name/buf.rs @@ -178,12 +178,6 @@ impl + ?Sized> PartialEq for NameBuf<'_> { } } -impl PartialEq for NameBuf<'_> { - fn eq(&self, other: &Name) -> bool { - self.keys().eq(other.keys()) - } -} - impl PartialEq> for Name { fn eq(&self, other: &NameBuf<'_>) -> bool { self.keys().eq(other.keys()) diff --git a/core/lib/src/form/name/name.rs b/core/lib/src/form/name/name.rs index c9caa4ee..af9570ff 100644 --- a/core/lib/src/form/name/name.rs +++ b/core/lib/src/form/name/name.rs @@ -210,6 +210,12 @@ impl AsRef for RawStr { } } +impl AsRef for Name { + fn as_ref(&self) -> &Name { + self + } +} + impl Eq for Name { } impl std::hash::Hash for Name { diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index 08252409..504e8342 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -7,6 +7,7 @@ #![cfg_attr(nightly, feature(decl_macro))] #![warn(rust_2018_idioms)] +#![warn(missing_docs)] //! # Rocket - Core API Documentation //! diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 8ea49b05..09b8af40 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -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>`. #[post("/", data = "")] fn submit<'r>(form: Form>>) -> (Status, Template) { let template = match form.value { diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 61a50ff7..8f60fc29 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -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>` as a data guard, where `T` implements `FromForm`. The `context` field contains the form's @@ -1542,18 +1543,22 @@ fn submit(form: Form>) { // 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