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