rocket_community/form/context.rs
1use indexmap::{IndexMap, IndexSet};
2use serde::Serialize;
3
4use crate::form::prelude::*;
5use crate::http::Status;
6
7/// An infallible form guard that records form fields and errors during parsing.
8///
9/// This form guard _never fails_. It should be use _only_ when the form
10/// [`Context`] is required. In all other cases, prefer to use `T` directly.
11///
12/// # Usage
13///
14/// `Contextual` acts as a proxy for any form type, recording all submitted form
15/// values and produced errors and associating them with their corresponding
16/// field name. `Contextual` is particularly useful for rendering forms with
17/// previously submitted values and errors associated with form input.
18///
19/// To retrieve the context for a form, use `Form<Contextual<'_, T>>` as a data
20/// guard, where `T` implements `FromForm`. The `context` field contains the
21/// form's [`Context`]:
22///
23/// ```rust
24/// # extern crate rocket_community as rocket;
25/// # use rocket::post;
26/// # type T = String;
27/// use rocket::form::{Form, Contextual};
28///
29/// #[post("/submit", data = "<form>")]
30/// fn submit(form: Form<Contextual<'_, T>>) {
31/// if let Some(ref value) = form.value {
32/// // The form parsed successfully. `value` is the `T`.
33/// }
34///
35/// // We can retrieve raw field values and errors.
36/// let raw_id_value = form.context.field_value("id");
37/// let id_errors = form.context.field_errors("id");
38/// }
39/// ```
40///
41/// `Context` serializes as a map, so it can be rendered in templates that
42/// require `Serialize` types. See the [forms guide] for further usage details.
43///
44/// [forms guide]: https://rocket.rs/master/guide/requests/#context
45#[derive(Debug)]
46pub struct Contextual<'v, T> {
47 /// The value, if it was successfully parsed, or `None` otherwise.
48 pub value: Option<T>,
49 /// The context with all submitted fields and associated values and errors.
50 pub context: Context<'v>,
51}
52
53/// A form context containing received fields, values, and encountered errors.
54///
55/// A value of this type is produced by the [`Contextual`] form guard in its
56/// [`context`](Contextual::context) field. `Context` contains an entry for
57/// every form field submitted by the client regardless of whether the field
58/// parsed or validated successfully.
59///
60/// # Field Values
61///
62/// The original, submitted field value(s) for a _value_ field can be retrieved
63/// via [`Context::field_value()`] or [`Context::field_values()`]. Data fields do not have
64/// their values recorded. All submitted field names, including data field
65/// names, can be retrieved via [`Context::fields()`].
66///
67/// # Field Errors
68///
69/// # Serialization
70///
71/// When a value of this type is serialized, a `struct` or map with the
72/// following fields is emitted:
73///
74/// | field | type | description |
75/// |---------------|------------------------------------|--------------------------------------|
76/// | `errors` | map: string to array of [`Error`]s | maps a field name to its errors |
77/// | `values` | map: string to array of strings | maps a field name to its form values |
78/// | `data_fields` | array of strings | field names of all form data fields |
79/// | `form_errors` | array of [`Error`]s | errors not associated with a field |
80///
81/// See [`Error`](Error#serialization) for `Error` serialization details.
82#[derive(Debug, Default, Serialize)]
83pub struct Context<'v> {
84 errors: IndexMap<NameBuf<'v>, Errors<'v>>,
85 values: IndexMap<&'v Name, Vec<&'v str>>,
86 data_fields: IndexSet<&'v Name>,
87 form_errors: Errors<'v>,
88 #[serde(skip)]
89 status: Status,
90}
91
92impl<'v> Context<'v> {
93 /// Returns the names of all submitted form fields, both _value_ and _data_
94 /// fields.
95 ///
96 /// # Example
97 ///
98 /// ```rust
99 /// # extern crate rocket_community as rocket;
100 /// # use rocket::post;
101 /// # type T = String;
102 /// use rocket::form::{Form, Contextual};
103 ///
104 /// #[post("/submit", data = "<form>")]
105 /// fn submit(form: Form<Contextual<'_, T>>) {
106 /// let field_names = form.context.fields();
107 /// }
108 /// ```
109 pub fn fields(&self) -> impl Iterator<Item = &'v Name> + '_ {
110 self.values
111 .iter()
112 .map(|(name, _)| *name)
113 .chain(self.data_fields.iter().copied())
114 }
115
116 /// Returns the _first_ value, if any, submitted for the _value_ field named
117 /// `name`.
118 ///
119 /// The type of `name` may be `&Name`, `&str`, or `&RawStr`. Lookup is
120 /// case-sensitive but key-separator (`.` or `[]`) insensitive.
121 ///
122 /// # Example
123 ///
124 /// ```rust
125 /// # extern crate rocket_community as rocket;
126 /// # use rocket::post;
127 /// # type T = String;
128 /// use rocket::form::{Form, Contextual};
129 ///
130 /// #[post("/submit", data = "<form>")]
131 /// fn submit(form: Form<Contextual<'_, T>>) {
132 /// let first_value_for_id = form.context.field_value("id");
133 /// let first_value_for_foo_bar = form.context.field_value("foo.bar");
134 /// }
135 /// ```
136 pub fn field_value<N: AsRef<Name>>(&self, name: N) -> Option<&'v str> {
137 self.values.get(name.as_ref())?.first().cloned()
138 }
139
140 /// Returns the values, if any, submitted for the _value_ field named
141 /// `name`.
142 ///
143 /// The type of `name` may be `&Name`, `&str`, or `&RawStr`. Lookup is
144 /// case-sensitive but key-separator (`.` or `[]`) insensitive.
145 ///
146 /// # Example
147 ///
148 /// ```rust
149 /// # extern crate rocket_community as rocket;
150 /// # use rocket::post;
151 /// # type T = String;
152 /// use rocket::form::{Form, Contextual};
153 ///
154 /// #[post("/submit", data = "<form>")]
155 /// fn submit(form: Form<Contextual<'_, T>>) {
156 /// let values_for_id = form.context.field_values("id");
157 /// let values_for_foo_bar = form.context.field_values("foo.bar");
158 /// }
159 /// ```
160 pub fn field_values<N>(&self, name: N) -> impl Iterator<Item = &'v str> + '_
161 where
162 N: AsRef<Name>,
163 {
164 self.values
165 .get(name.as_ref())
166 .map(|e| e.iter().cloned())
167 .into_iter()
168 .flatten()
169 }
170
171 /// Returns an iterator over all of the errors in the context, including
172 /// those not associated with any field.
173 ///
174 /// # Example
175 ///
176 /// ```rust
177 /// # extern crate rocket_community as rocket;
178 /// # use rocket::post;
179 /// # type T = String;
180 /// use rocket::form::{Form, Contextual};
181 ///
182 /// #[post("/submit", data = "<form>")]
183 /// fn submit(form: Form<Contextual<'_, T>>) {
184 /// let errors = form.context.errors();
185 /// }
186 /// ```
187 pub fn errors(&self) -> impl Iterator<Item = &Error<'v>> {
188 self.errors
189 .values()
190 .flat_map(|e| e.iter())
191 .chain(self.form_errors.iter())
192 }
193
194 /// Returns the errors associated with the field `name`. This method is
195 /// roughly equivalent to:
196 ///
197 /// ```rust
198 /// # extern crate rocket_community as rocket;
199 /// # use rocket::form::{Context, name::Name};
200 /// # let context = Context::default();
201 /// # let name = Name::new("foo");
202 /// context.errors().filter(|e| e.is_for(name))
203 /// # ;
204 /// ```
205 ///
206 /// That is, it uses [`Error::is_for()`] to determine which errors are
207 /// associated with the field named `name`. This considers all errors whose
208 /// associated field name is a prefix of `name` to be an error for the field
209 /// named `name`. In other words, it associates parent field errors with
210 /// their children: `a.b`'s errors apply to `a.b.c`, `a.b.d` and so on but
211 /// not `a.c`.
212 ///
213 /// Lookup is case-sensitive but key-separator (`.` or `[]`) insensitive.
214 ///
215 /// # Example
216 ///
217 /// ```rust
218 /// # extern crate rocket_community as rocket;
219 /// # use rocket::post;
220 /// # type T = String;
221 /// use rocket::form::{Form, Contextual};
222 ///
223 /// #[post("/submit", data = "<form>")]
224 /// fn submit(form: Form<Contextual<'_, T>>) {
225 /// // Get all errors for field `id`.
226 /// let id = form.context.field_errors("id");
227 ///
228 /// // Get all errors for `foo.bar` or `foo` if `foo` failed first.
229 /// let foo_bar = form.context.field_errors("foo.bar");
230 /// }
231 /// ```
232 pub fn field_errors<'a, N>(&'a self, name: N) -> impl Iterator<Item = &'a Error<'v>> + 'a
233 where
234 N: AsRef<Name> + 'a,
235 {
236 self.errors
237 .values()
238 .flat_map(|e| e.iter())
239 .filter(move |e| e.is_for(&name))
240 }
241
242 /// Returns the errors associated _exactly_ with the field `name`. Prefer
243 /// [`Context::field_errors()`] instead.
244 ///
245 /// This method is roughly equivalent to:
246 ///
247 /// ```rust
248 /// # extern crate rocket_community as rocket;
249 /// # use rocket::form::{Context, name::Name};
250 /// # let context = Context::default();
251 /// # let name = Name::new("foo");
252 /// context.errors().filter(|e| e.is_for_exactly(name))
253 /// # ;
254 /// ```
255 ///
256 /// That is, it uses [`Error::is_for_exactly()`] to determine which errors
257 /// are associated with the field named `name`. This considers _only_ errors
258 /// whose associated field name is _exactly_ `name` to be an error for the
259 /// field named `name`. This is _not_ what is typically desired as it
260 /// ignores errors that occur in the parent which will result in missing
261 /// errors associated with its children. Use [`Context::field_errors()`] in
262 /// almost all cases.
263 ///
264 /// Lookup is case-sensitive but key-separator (`.` or `[]`) insensitive.
265 ///
266 /// # Example
267 ///
268 /// ```rust
269 /// # extern crate rocket_community as rocket;
270 /// # use rocket::post;
271 /// # type T = String;
272 /// use rocket::form::{Form, Contextual};
273 ///
274 /// #[post("/submit", data = "<form>")]
275 /// fn submit(form: Form<Contextual<'_, T>>) {
276 /// // Get all errors for field `id`.
277 /// let id = form.context.exact_field_errors("id");
278 ///
279 /// // Get all errors exactly for `foo.bar`. If `foo` failed, we will
280 /// // this will return no errors. Use `Context::field_errors()`.
281 /// let foo_bar = form.context.exact_field_errors("foo.bar");
282 /// }
283 /// ```
284 pub fn exact_field_errors<'a, N>(&'a self, name: N) -> impl Iterator<Item = &'a Error<'v>> + 'a
285 where
286 N: AsRef<Name> + 'a,
287 {
288 self.errors
289 .values()
290 .flat_map(|e| e.iter())
291 .filter(move |e| e.is_for_exactly(&name))
292 }
293
294 /// Returns the `max` of the statuses associated with all field errors.
295 ///
296 /// See [`Error::status()`] for details on how an error status is computed.
297 ///
298 /// # Example
299 ///
300 /// ```rust
301 /// # extern crate rocket_community as rocket;
302 /// # use rocket::post;
303 /// # type T = String;
304 /// use rocket::http::Status;
305 /// use rocket::form::{Form, Contextual};
306 ///
307 /// #[post("/submit", data = "<form>")]
308 /// fn submit(form: Form<Contextual<'_, T>>) -> (Status, &'static str) {
309 /// (form.context.status(), "Thanks!")
310 /// }
311 /// ```
312 pub fn status(&self) -> Status {
313 self.status
314 }
315
316 /// Inject a single error `error` into the context.
317 ///
318 /// # Example
319 ///
320 /// ```rust
321 /// # extern crate rocket_community as rocket;
322 /// # use rocket::post;
323 /// # type T = String;
324 /// use rocket::http::Status;
325 /// use rocket::form::{Form, Contextual, Error};
326 ///
327 /// #[post("/submit", data = "<form>")]
328 /// fn submit(mut form: Form<Contextual<'_, T>>) {
329 /// let error = Error::validation("a good error message")
330 /// .with_name("field_name")
331 /// .with_value("some field value");
332 ///
333 /// form.context.push_error(error);
334 /// }
335 /// ```
336 pub fn push_error(&mut self, error: Error<'v>) {
337 self.status = std::cmp::max(self.status, error.status());
338 match error.name {
339 Some(ref name) => match self.errors.get_mut(name) {
340 Some(errors) => errors.push(error),
341 None => {
342 self.errors.insert(name.clone(), error.into());
343 }
344 },
345 None => self.form_errors.push(error),
346 }
347 }
348
349 /// Inject all of the errors in `errors` into the context.
350 ///
351 /// # Example
352 ///
353 /// ```rust
354 /// # extern crate rocket_community as rocket;
355 /// # use rocket::post;
356 /// # type T = String;
357 /// use rocket::http::Status;
358 /// use rocket::form::{Form, Contextual, Error};
359 ///
360 /// #[post("/submit", data = "<form>")]
361 /// fn submit(mut form: Form<Contextual<'_, T>>) {
362 /// let error = Error::validation("a good error message")
363 /// .with_name("field_name")
364 /// .with_value("some field value");
365 ///
366 /// form.context.push_errors(vec![error]);
367 /// }
368 /// ```
369 pub fn push_errors<E: Into<Errors<'v>>>(&mut self, errors: E) {
370 errors.into().into_iter().for_each(|e| self.push_error(e))
371 }
372}
373
374impl<'f> From<Errors<'f>> for Context<'f> {
375 fn from(errors: Errors<'f>) -> Self {
376 let mut context = Context::default();
377 context.push_errors(errors);
378 context
379 }
380}
381
382#[crate::async_trait]
383impl<'v, T: FromForm<'v>> FromForm<'v> for Contextual<'v, T> {
384 type Context = (<T as FromForm<'v>>::Context, Context<'v>);
385
386 fn init(opts: Options) -> Self::Context {
387 (T::init(opts), Context::default())
388 }
389
390 fn push_value((ref mut val_ctxt, ctxt): &mut Self::Context, field: ValueField<'v>) {
391 ctxt.values
392 .entry(field.name.source())
393 .or_default()
394 .push(field.value);
395 T::push_value(val_ctxt, field);
396 }
397
398 async fn push_data((ref mut val_ctxt, ctxt): &mut Self::Context, field: DataField<'v, '_>) {
399 ctxt.data_fields.insert(field.name.source());
400 T::push_data(val_ctxt, field).await;
401 }
402
403 fn push_error((_, ref mut ctxt): &mut Self::Context, e: Error<'v>) {
404 ctxt.push_error(e);
405 }
406
407 fn finalize((val_ctxt, mut context): Self::Context) -> Result<'v, Self> {
408 let value = match T::finalize(val_ctxt) {
409 Ok(value) => Some(value),
410 Err(errors) => {
411 context.push_errors(errors);
412 None
413 }
414 };
415
416 Ok(Contextual { value, context })
417 }
418}