mirror of https://github.com/rwf2/Rocket.git
Parse inner form 'T' in 'Option<T>' strictly.
This behavior more closely matches the expectation that a missing field results in 'None'. Also cleans up forms docs for readability, completeness.
This commit is contained in:
parent
33790254f1
commit
e7934a2a3f
|
@ -564,57 +564,51 @@ fn test_nested_multi() {
|
|||
});
|
||||
}
|
||||
|
||||
// fn test_multipart() {
|
||||
// use std::{io, path::Path};
|
||||
//
|
||||
// use crate::*;
|
||||
// use crate::http::ContentType;
|
||||
// use crate::local::blocking::Client;
|
||||
// use crate::{data::TempFile, form::Errors};
|
||||
//
|
||||
// #[derive(FromForm)]
|
||||
// struct MyForm {
|
||||
// names: Vec<String>,
|
||||
// file: String,
|
||||
// }
|
||||
//
|
||||
// #[post("/", data = "<form>")]
|
||||
// async fn form(mut form: Form<MyForm>) -> io::Result<&'static str> {
|
||||
// let path = Path::new("/tmp").join(form.file_name().unwrap_or("upload"));
|
||||
// form.persist(path).await?;
|
||||
// println!("result: {:?}", form);
|
||||
// Ok("hi")
|
||||
// }
|
||||
//
|
||||
// let client = Client::debug_with(routes![form]).unwrap();
|
||||
// let ct = "multipart/form-data; boundary=X-BOUNDARY"
|
||||
// .parse::<ContentType>()
|
||||
// .unwrap();
|
||||
//
|
||||
// let body = &[
|
||||
// // "--X-BOUNDARY",
|
||||
// // r#"Content-Disposition: form-data; name="names[]""#,
|
||||
// // "",
|
||||
// // "abcd",
|
||||
// // "--X-BOUNDARY",
|
||||
// // r#"Content-Disposition: form-data; name="names[]""#,
|
||||
// // "",
|
||||
// // "123",
|
||||
// "--X-BOUNDARY",
|
||||
// r#"Content-Disposition: form-data; name="file"; filename="foo.txt""#,
|
||||
// "Content-Type: text/plain",
|
||||
// "",
|
||||
// "hi there",
|
||||
// "--X-BOUNDARY--",
|
||||
// "",
|
||||
// ].join("\r\n");
|
||||
//
|
||||
// let response = client.post("/")
|
||||
// .header(ct)
|
||||
// .body(body)
|
||||
// .dispatch();
|
||||
//
|
||||
// let string = response.into_string().unwrap();
|
||||
// println!("String: {}", string);
|
||||
// panic!(string);
|
||||
// }
|
||||
#[test]
|
||||
fn test_multipart() {
|
||||
use rocket::http::ContentType;
|
||||
use rocket::local::blocking::Client;
|
||||
use rocket::data::TempFile;
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct MyForm<'r> {
|
||||
names: Vec<&'r str>,
|
||||
file: TempFile<'r>,
|
||||
}
|
||||
|
||||
#[rocket::post("/", data = "<form>")]
|
||||
fn form(form: Form<MyForm>) {
|
||||
assert_eq!(form.names, &["abcd", "123"]);
|
||||
assert_eq!(form.file.file_name(), Some("foo"));
|
||||
}
|
||||
|
||||
let client = Client::debug_with(rocket::routes![form]).unwrap();
|
||||
let ct = "multipart/form-data; boundary=X-BOUNDARY"
|
||||
.parse::<ContentType>()
|
||||
.unwrap();
|
||||
|
||||
let body = &[
|
||||
"--X-BOUNDARY",
|
||||
r#"Content-Disposition: form-data; name="names[]""#,
|
||||
"",
|
||||
"abcd",
|
||||
"--X-BOUNDARY",
|
||||
r#"Content-Disposition: form-data; name="names[]""#,
|
||||
"",
|
||||
"123",
|
||||
"--X-BOUNDARY",
|
||||
r#"Content-Disposition: form-data; name="file"; filename="foo.txt""#,
|
||||
"Content-Type: text/plain",
|
||||
"",
|
||||
"hi there",
|
||||
"--X-BOUNDARY--",
|
||||
"",
|
||||
].join("\r\n");
|
||||
|
||||
let response = client.post("/")
|
||||
.header(ct)
|
||||
.body(body)
|
||||
.dispatch();
|
||||
|
||||
assert!(response.status().class().is_success());
|
||||
}
|
||||
|
|
|
@ -57,6 +57,19 @@ use crate::form::prelude::*;
|
|||
///
|
||||
/// ## Data Limits
|
||||
///
|
||||
/// The total amount of data accepted by the `Form` data guard is limited by the
|
||||
/// following limits:
|
||||
///
|
||||
/// | Limit Name | Default | Description |
|
||||
/// |-------------|---------|------------------------------------|
|
||||
/// | `form` | 32KiB | total limit for url-encoded forms |
|
||||
/// | `data-form` | 2MiB | total limit for multipart forms |
|
||||
/// | `*` | N/A | each field type has its own limits |
|
||||
///
|
||||
/// As noted above, each form field type (a form guard) typically imposes its
|
||||
/// own limits. For example, the `&str` form guard imposes a data limit of
|
||||
/// `string` when multipart data is streamed.
|
||||
///
|
||||
/// ### URL-Encoded Forms
|
||||
///
|
||||
/// The `form` limit specifies the data limit for an entire url-encoded form
|
||||
|
@ -95,7 +108,7 @@ use crate::form::prelude::*;
|
|||
///
|
||||
/// See the [`Limits`](crate::data::Limits) and [`config`](crate::config) docs
|
||||
/// for more.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Form<T>(T);
|
||||
|
||||
impl<T> Form<T> {
|
||||
|
@ -137,28 +150,98 @@ impl<T> From<T> for Form<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Form<()> {
|
||||
/// `string` must represent a decoded string.
|
||||
pub fn values(string: &str) -> impl Iterator<Item = ValueField<'_>> {
|
||||
// WHATWG URL Living Standard 5.1 steps 1, 2, 3.1 - 3.3.
|
||||
string.split('&')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(ValueField::parse)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, T: FromForm<'r>> Form<T> {
|
||||
/// `string` must represent a decoded string.
|
||||
/// Leniently parses a `T` from a **percent-decoded**
|
||||
/// `x-www-form-urlencoded` form string. Specifically, this method
|
||||
/// implements [§5.1 of the WHATWG URL Living Standard] with the exception
|
||||
/// of steps 3.4 and 3.5, which are assumed to already be relfected in
|
||||
/// `string`, and then parses the fields as `T`.
|
||||
///
|
||||
/// [§5.1 of the WHATWG URL Living Standard]: https://url.spec.whatwg.org/#application/x-www-form-urlencoded
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{Form, FromForm};
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Pet<'r> {
|
||||
/// name: &'r str,
|
||||
/// wags: bool,
|
||||
/// }
|
||||
///
|
||||
/// let string = "name=Benson Wagger!&wags=true";
|
||||
/// let pet: Pet<'_> = Form::parse(string).unwrap();
|
||||
/// assert_eq!(pet.name, "Benson Wagger!");
|
||||
/// assert_eq!(pet.wags, true);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn parse(string: &'r str) -> Result<'r, T> {
|
||||
// WHATWG URL Living Standard 5.1 steps 1, 2, 3.1 - 3.3.
|
||||
Self::parse_iter(Form::values(string))
|
||||
}
|
||||
|
||||
/// Leniently parses a `T` from the **percent-decoded** `fields`.
|
||||
/// Specifically, this method implements [§5.1 of the WHATWG URL Living
|
||||
/// Standard] with the exception of step 3.
|
||||
///
|
||||
/// [§5.1 of the WHATWG URL Living Standard]: https://url.spec.whatwg.org/#application/x-www-form-urlencoded
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{Form, FromForm, ValueField};
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Pet<'r> {
|
||||
/// name: &'r str,
|
||||
/// wags: bool,
|
||||
/// }
|
||||
///
|
||||
/// let fields = vec![
|
||||
/// ValueField::parse("name=Bob, the cat. :)"),
|
||||
/// ValueField::parse("wags=no"),
|
||||
/// ];
|
||||
///
|
||||
/// let pet: Pet<'_> = Form::parse_iter(fields).unwrap();
|
||||
/// assert_eq!(pet.name, "Bob, the cat. :)");
|
||||
/// assert_eq!(pet.wags, false);
|
||||
/// ```
|
||||
pub fn parse_iter<I>(fields: I) -> Result<'r, T>
|
||||
where I: IntoIterator<Item = ValueField<'r>>
|
||||
{
|
||||
// WHATWG URL Living Standard 5.1 steps 1, 2, 3.1 - 3.3.
|
||||
let mut ctxt = T::init(Options::Lenient);
|
||||
Form::values(string).for_each(|f| T::push_value(&mut ctxt, f));
|
||||
fields.into_iter().for_each(|f| T::push_value(&mut ctxt, f));
|
||||
T::finalize(ctxt)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: for<'a> FromForm<'a> + 'static> Form<T> {
|
||||
/// `string` must represent an undecoded string.
|
||||
/// Leniently parses a `T` from a raw, `x-www-form-urlencoded` form string.
|
||||
/// Specifically, this method implements [§5.1 of the WHATWG URL Living
|
||||
/// Standard]. Because percent-decoding might modify the input string, the
|
||||
/// output type `T` must be `'static`.
|
||||
///
|
||||
/// [§5.1 of the WHATWG URL Living Standard]:https://url.spec.whatwg.org/#application/x-www-form-urlencoded
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::http::RawStr;
|
||||
/// use rocket::form::{Form, FromForm};
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Pet {
|
||||
/// name: String,
|
||||
/// wags: bool,
|
||||
/// }
|
||||
///
|
||||
/// let string = RawStr::new("name=Benson+Wagger%21&wags=true");
|
||||
/// let pet: Pet = Form::parse_encoded(string).unwrap();
|
||||
/// assert_eq!(pet.name, "Benson Wagger!");
|
||||
/// assert_eq!(pet.wags, true);
|
||||
/// ```
|
||||
pub fn parse_encoded(string: &RawStr) -> Result<'static, T> {
|
||||
let buffer = Buffer::new();
|
||||
let mut ctxt = T::init(Options::Lenient);
|
||||
|
@ -170,6 +253,33 @@ impl<T: for<'a> FromForm<'a> + 'static> Form<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Form<()> {
|
||||
/// Returns an iterator of fields parsed from a `x-www-form-urlencoded` form
|
||||
/// string. Specifically, this method implements steps 1, 2, and 3.1 - 3.3
|
||||
/// of [§5.1 of the WHATWG URL Living Standard]. Fields in the returned
|
||||
/// iterator _are not_ percent-decoded.
|
||||
///
|
||||
/// [§5.1 of the WHATWG URL Living Standard]:https://url.spec.whatwg.org/#application/x-www-form-urlencoded
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{Form, ValueField};
|
||||
///
|
||||
/// let string = "name=Bobby Brown&&&email=me@rocket.rs";
|
||||
/// let mut values = Form::values(string);
|
||||
/// assert_eq!(values.next().unwrap(), ValueField::parse("name=Bobby Brown"));
|
||||
/// assert_eq!(values.next().unwrap(), ValueField::parse("email=me@rocket.rs"));
|
||||
/// assert!(values.next().is_none());
|
||||
/// ```
|
||||
pub fn values(string: &str) -> impl Iterator<Item = ValueField<'_>> {
|
||||
// WHATWG URL Living Standard 5.1 steps 1, 2, 3.1 - 3.3.
|
||||
string.split('&')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(ValueField::parse)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Form<T> {
|
||||
type Target = T;
|
||||
|
||||
|
|
|
@ -66,15 +66,80 @@ use crate::http::uncased::AsUncased;
|
|||
/// [`key()`]: NameView::key()
|
||||
/// [forms guide]: https://rocket.rs/master/guide/requests/#forms
|
||||
///
|
||||
/// # Parsing Strategy
|
||||
///
|
||||
/// Form parsing is either _strict_ or _lenient_, controlled by
|
||||
/// [`Options::strict`]. A _strict_ parse errors when there are missing or extra
|
||||
/// fields, while a _lenient_ parse allows both, providing there is a
|
||||
/// [`default()`](FromForm::default()) in the case of a missing field.
|
||||
///
|
||||
/// Most type inherit their strategy on [`FromForm::init()`], but some types
|
||||
/// like `Option` override the requested strategy. The strategy can also be
|
||||
/// overwritted manually, per-field or per-value, by using the [`Strict`] or
|
||||
/// [`Lenient`] form guard:
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{self, FromForm, Strict, Lenient};
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct TodoTask<'r> {
|
||||
/// strict_bool: Strict<bool>,
|
||||
/// lenient_inner_option: Option<Lenient<bool>>,
|
||||
/// strict_inner_result: form::Result<'r, Strict<bool>>,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Defaults
|
||||
///
|
||||
/// A form guard may have a _default_ which is used in case of a missing field
|
||||
/// when parsing is _lenient_. When parsing is strict, all errors, including
|
||||
/// missing fields, are propagated directly.
|
||||
///
|
||||
/// # Provided Implementations
|
||||
///
|
||||
/// Rocket implements `FromForm` for several types. Their behavior is documented
|
||||
/// here.
|
||||
/// Rocket implements `FromForm` for many common types. As a result, most
|
||||
/// applications will never need a custom implementation of `FromForm` or
|
||||
/// `FromFormField`. Their behavior is documented in the table below.
|
||||
///
|
||||
/// * **`T` where `T: FromFormField`**
|
||||
/// | Type | Strategy | Default | Data | Value | Notes |
|
||||
/// |--------------------|-------------|-------------------|--------|--------|----------------------------------------------------|
|
||||
/// | [`Strict<T>`] | **strict** | if `strict` `T` | if `T` | if `T` | `T: FromForm` |
|
||||
/// | [`Lenient<T>`] | **lenient** | if `lenient` `T` | if `T` | if `T` | `T: FromForm` |
|
||||
/// | `Option<T>` | **strict** | `None` | if `T` | if `T` | Infallible, `T: FromForm` |
|
||||
/// | [`Result<T>`] | _inherit_ | `T::finalize()` | if `T` | if `T` | Infallible, `T: FromForm` |
|
||||
/// | `Vec<T>` | _inherit_ | `vec![]` | if `T` | if `T` | `T: FromForm` |
|
||||
/// | [`HashMap<K, V>`] | _inherit_ | `HashMap::new()` | if `V` | if `V` | `K: FromForm + Eq + Hash`, `V: FromForm` |
|
||||
/// | [`BTreeMap<K, V>`] | _inherit_ | `BTreeMap::new()` | if `V` | if `V` | `K: FromForm + Ord`, `V: FromForm` |
|
||||
/// | `bool` | _inherit_ | `false` | No | Yes | `"yes"/"on"/"true"`, `"no"/"off"/"false"` |
|
||||
/// | (un)signed int | _inherit_ | **no default** | No | Yes | `{u,i}{size,8,16,32,64,128}` |
|
||||
/// | _nonzero_ int | _inherit_ | **no default** | No | Yes | `NonZero{I,U}{size,8,16,32,64,128}` |
|
||||
/// | float | _inherit_ | **no default** | No | Yes | `f{32,64}` |
|
||||
/// | `&str` | _inherit_ | **no default** | Yes | Yes | Percent-decoded. Data limit `string` applies. |
|
||||
/// | `String` | _inherit_ | **no default** | Yes | Yes | Exactly `&str`, but owned. Prefer `&str`. |
|
||||
/// | IP Address | _inherit_ | **no default** | No | Yes | [`IpAddr`], [`Ipv4Addr`], [`Ipv6Addr`] |
|
||||
/// | Socket Address | _inherit_ | **no default** | No | Yes | [`SocketAddr`], [`SocketAddrV4`], [`SocketAddrV6`] |
|
||||
/// | [`TempFile`] | _inherit_ | **no default** | Yes | Yes | Data limits apply. See [`TempFile`]. |
|
||||
/// | [`Capped<C>`] | _inherit_ | **no default** | Yes | Yes | `C` is `&str`, `String`, or `TempFile`. |
|
||||
/// | [`time::Date`] | _inherit_ | **no default** | No | Yes | `%F` (`YYYY-MM-DD`). HTML "date" input. |
|
||||
/// | [`time::DateTime`] | _inherit_ | **no default** | No | Yes | `%FT%R` or `%FT%T` (`YYYY-MM-DDTHH:MM[:SS]`) |
|
||||
/// | [`time::Time`] | _inherit_ | **no default** | No | Yes | `%R` or `%T` (`HH:MM[:SS]`) |
|
||||
///
|
||||
/// This includes types like `&str`, `usize`, and [`Date`](time::Date). See
|
||||
/// [`FromFormField`] for details.
|
||||
/// [`Result<T>`]: crate::form::Result
|
||||
/// [`Strict<T>`]: crate::form::Strict
|
||||
/// [`Lenient<T>`]: crate::form::Lenient
|
||||
/// [`HashMap<K, V>`]: std::collections::HashMap
|
||||
/// [`BTreeMap<K, V>`]: std::collections::BTreeMap
|
||||
/// [`TempFile`]: crate::data::TempFile
|
||||
/// [`Capped<C>`]: crate::data::Capped
|
||||
/// [`time::DateTime`]: time::PrimitiveDateTime
|
||||
/// [`IpAddr`]: std::net::IpAddr
|
||||
/// [`Ipv4Addr`]: std::net::Ipv4Addr
|
||||
/// [`Ipv6Addr`]: std::net::Ipv6Addr
|
||||
/// [`SocketAddr`]: std::net::SocketAddr
|
||||
/// [`SocketAddrV4`]: std::net::SocketAddrV4
|
||||
/// [`SocketAddrV6`]: std::net::SocketAddrV6
|
||||
///
|
||||
/// ## Additional Notes
|
||||
///
|
||||
/// * **`Vec<T>` where `T: FromForm`**
|
||||
///
|
||||
|
@ -106,26 +171,32 @@ use crate::http::uncased::AsUncased;
|
|||
/// Errors are collected as they occur. Finalization finalizes all pairs and
|
||||
/// returns errors, if any, or the map.
|
||||
///
|
||||
/// * **`Option<T>` where `T: FromForm`**
|
||||
/// * **[`time::DateTime`]**
|
||||
///
|
||||
/// _This form guard always succeeds._
|
||||
/// Parses a date in `%FT%R` or `%FT%T` format, that is, `YYYY-MM-DDTHH:MM`
|
||||
/// or `YYYY-MM-DDTHH:MM:SS`. This is the `"datetime-local"` HTML input type
|
||||
/// without support for the millisecond variant.
|
||||
///
|
||||
/// Forwards all pushes to `T` without shifting. Finalizes successfully as
|
||||
/// `Some(T)` if `T` finalizes without error or `None` otherwise.
|
||||
/// * **[`time::Time`]**
|
||||
///
|
||||
/// * **`Result<T, Errors<'r>>` where `T: FromForm`**
|
||||
///
|
||||
/// _This form guard always succeeds._
|
||||
///
|
||||
/// Forwards all pushes to `T` without shifting. Finalizes successfully as
|
||||
/// `Some(T)` if `T` finalizes without error or `Err(Errors)` with the
|
||||
/// errors from `T` otherwise.
|
||||
/// Parses a time in `%R` or `%T` format, that is, `HH:MM` or `HH:MM:SS`.
|
||||
/// This is the `"time"` HTML input type without support for the millisecond
|
||||
/// variant.
|
||||
///
|
||||
/// # Push Parsing
|
||||
///
|
||||
/// `FromForm` describes a 3-stage push-based interface to form parsing. After
|
||||
/// preprocessing (see [the top-level docs](crate::form#parsing)), the three
|
||||
/// stages are:
|
||||
/// `FromForm` describes a push-based parser for Rocket's [field wire format].
|
||||
/// Fields are preprocessed into either [`ValueField`]s or [`DataField`]s which
|
||||
/// are then pushed to the parser in [`FromForm::push_value()`] or
|
||||
/// [`FromForm::push_data()`], respectively. Both url-encoded forms and
|
||||
/// multipart forms are supported. All url-encoded form fields are preprocessed
|
||||
/// as [`ValueField`]s. Multipart form fields with Content-Types are processed
|
||||
/// as [`DataField`]s while those without a set Content-Type are processed as
|
||||
/// [`ValueField`]s. `ValueField` field names and values are percent-decoded.
|
||||
///
|
||||
/// [field wire format]: crate::form#field-wire-format
|
||||
///
|
||||
/// Parsing is split into 3 stages. After preprocessing, the three stages are:
|
||||
///
|
||||
/// 1. **Initialization.** The type sets up a context for later `push`es.
|
||||
///
|
||||
|
@ -215,6 +286,86 @@ use crate::http::uncased::AsUncased;
|
|||
/// [`shift()`]: NameView::shift()
|
||||
/// [`key()`]: NameView::key()
|
||||
///
|
||||
/// ## A Simple Example
|
||||
///
|
||||
/// The following example uses `f1=v1&f2=v2` to illustrate field/value pairs
|
||||
/// `(f1, v2)` and `(f2, v2)`. This is the same encoding used to send HTML forms
|
||||
/// over HTTP, though Rocket's push-parsers are unaware of any specific
|
||||
/// encoding, dealing only with logical `field`s, `index`es, and `value`s.
|
||||
///
|
||||
/// ### A Single Field (`T: FormFormField`)
|
||||
///
|
||||
/// The simplest example parses a single value of type `T` from a string with an
|
||||
/// optional default value: this is `impl<T: FromFormField> FromForm for T`:
|
||||
///
|
||||
/// 1. **Initialization.** The context stores form options and an `Option` of
|
||||
/// `Result<T, form::Error>` for storing the `result` of parsing `T`, which
|
||||
/// is initially set to `None`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{self, FromFormField};
|
||||
///
|
||||
/// struct Context<'r, T: FromFormField<'r>> {
|
||||
/// opts: form::Options,
|
||||
/// result: Option<form::Result<'r, T>>,
|
||||
/// }
|
||||
///
|
||||
/// # impl<'r, T: FromFormField<'r>> Context<'r, T> {
|
||||
/// fn init(opts: form::Options) -> Context<'r, T> {
|
||||
/// Context { opts, result: None }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 2. **Push.** If `ctxt.result` is `None`, `T` is parsed from `field`, and
|
||||
/// the result is stored in `context.result`. Otherwise a field has already
|
||||
/// been parsed and nothing is done.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket::form::{self, ValueField, FromFormField};
|
||||
/// # struct Context<'r, T: FromFormField<'r>> {
|
||||
/// # opts: form::Options,
|
||||
/// # result: Option<form::Result<'r, T>>,
|
||||
/// # }
|
||||
/// # impl<'r, T: FromFormField<'r>> Context<'r, T> {
|
||||
/// fn push_value(ctxt: &mut Context<'r, T>, field: ValueField<'r>) {
|
||||
/// if ctxt.result.is_none() {
|
||||
/// ctxt.result = Some(T::from_value(field));
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 3. **Finalization.** If `ctxt.result` is `None`, parsing is lenient, and
|
||||
/// `T` has a default, the default is returned. Otherwise a `Missing` error
|
||||
/// is returned. If `ctxt.result` is `Some(v)`, the result `v` is returned.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket::form::{self, FromFormField, error::{Errors, ErrorKind}};
|
||||
/// # struct Context<'r, T: FromFormField<'r>> {
|
||||
/// # opts: form::Options,
|
||||
/// # result: Option<form::Result<'r, T>>,
|
||||
/// # }
|
||||
/// # impl<'r, T: FromFormField<'r>> Context<'r, T> {
|
||||
/// fn finalize(ctxt: Context<'r, T>) -> form::Result<'r, T> {
|
||||
/// match ctxt.result {
|
||||
/// Some(result) => result,
|
||||
/// None if ctxt.opts.strict => Err(Errors::from(ErrorKind::Missing)),
|
||||
/// None => match T::default() {
|
||||
/// Some(default) => Ok(default),
|
||||
/// None => Err(Errors::from(ErrorKind::Missing)),
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// This implementation is complete except for the following details:
|
||||
///
|
||||
/// * handling both `push_data` and `push_value`
|
||||
/// * checking for duplicate pushes when parsing is `strict`
|
||||
/// * tracking the field's name and value to generate a complete [`Error`]
|
||||
///
|
||||
/// # Implementing
|
||||
///
|
||||
/// Implementing `FromForm` should be a rare occurrence. Prefer instead to use
|
||||
|
@ -253,11 +404,9 @@ use crate::http::uncased::AsUncased;
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Lifetime
|
||||
///
|
||||
/// The lifetime `'r` correponds to the lifetime of the request.
|
||||
///
|
||||
/// ## Example
|
||||
/// ## A More Involved Example
|
||||
///
|
||||
/// We illustrate implementation of `FromForm` through an example. The example
|
||||
/// implements `FromForm` for a `Pair(A, B)` type where `A: FromForm` and `B:
|
||||
|
@ -278,8 +427,8 @@ use crate::http::uncased::AsUncased;
|
|||
/// * `pair.0=2012-10-12&pair.1=100` as `Pair(time::Date, usize)`
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{self, FromForm, ValueField, DataField, Error, Errors};
|
||||
/// use either::Either;
|
||||
/// use rocket::form::{self, FromForm, ValueField, DataField, Error, Errors};
|
||||
///
|
||||
/// /// A form guard parseable from fields `.0` and `.1`.
|
||||
/// struct Pair<A, B>(A, B);
|
||||
|
@ -377,7 +526,7 @@ pub trait FromForm<'r>: Send + Sized {
|
|||
/// Processes the data field `field`.
|
||||
async fn push_data(ctxt: &mut Self::Context, field: DataField<'r, '_>);
|
||||
|
||||
/// Processes the extern form or field error `_error`.
|
||||
/// Processes the external form or field error `_error`.
|
||||
///
|
||||
/// The default implementation does nothing, which is always correct.
|
||||
fn push_error(_ctxt: &mut Self::Context, _error: Error<'r>) { }
|
||||
|
@ -390,7 +539,9 @@ pub trait FromForm<'r>: Send + Sized {
|
|||
/// parsing fails.
|
||||
///
|
||||
/// The default implementation initializes `Self` with `opts` and finalizes
|
||||
/// immediately, returning the value if finalization succeeds.
|
||||
/// immediately, returning the value if finalization succeeds. This is
|
||||
/// always correct and should likely not be changed. Returning a different
|
||||
/// value may result in ambiguous parses.
|
||||
fn default(opts: Options) -> Option<Self> {
|
||||
Self::finalize(Self::init(opts)).ok()
|
||||
}
|
||||
|
@ -638,7 +789,7 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Option<T> {
|
|||
type Context = <T as FromForm<'v>>::Context;
|
||||
|
||||
fn init(opts: Options) -> Self::Context {
|
||||
T::init(opts)
|
||||
T::init(Options { strict: true, ..opts })
|
||||
}
|
||||
|
||||
fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) {
|
||||
|
@ -650,10 +801,7 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Option<T> {
|
|||
}
|
||||
|
||||
fn finalize(this: Self::Context) -> Result<'v, Self> {
|
||||
match T::finalize(this) {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(_) => Ok(None)
|
||||
}
|
||||
Ok(T::finalize(this).ok())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -674,10 +822,7 @@ impl<'v, T: FromForm<'v>> FromForm<'v> for Result<'v, T> {
|
|||
}
|
||||
|
||||
fn finalize(this: Self::Context) -> Result<'v, Self> {
|
||||
match T::finalize(this) {
|
||||
Ok(v) => Ok(Ok(v)),
|
||||
Err(e) => Ok(Err(e))
|
||||
}
|
||||
Ok(T::finalize(this))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,79 +46,8 @@ use crate::form::prelude::*;
|
|||
///
|
||||
/// # Provided Implementations
|
||||
///
|
||||
/// Rocket implements `FromFormField` for many types. Their behavior is
|
||||
/// documented here.
|
||||
///
|
||||
/// *
|
||||
/// * Numeric types: **`f32`, `f64`, `isize`, `i8`, `i16`, `i32`, `i64`,
|
||||
/// `i128`, `usize`, `u8`, `u16`, `u32`, `u64`, `u128`**
|
||||
/// * Address types: **`IpAddr`, `Ipv4Addr`, `Ipv6Addr`, `SocketAddrV4`,
|
||||
/// `SocketAddrV6`, `SocketAddr`**
|
||||
/// * Non-zero types: **`NonZeroI8`, `NonZeroI16`, `NonZeroI32`,
|
||||
/// `NonZeroI64`, `NonZeroI128`, `NonZeroIsize`, `NonZeroU8`,
|
||||
/// `NonZeroU16`, `NonZeroU32`, `NonZeroU64`, `NonZeroU128`,
|
||||
/// `NonZeroUsize`**
|
||||
///
|
||||
/// A value is validated successfully if the `from_str` method for the given
|
||||
/// type returns successfully. Only accepts form _values_, not binary data.
|
||||
///
|
||||
/// **No type-specific data limit applies.**
|
||||
///
|
||||
/// * **`bool`**
|
||||
///
|
||||
/// A value is validated successfully as `true` if the the form value is one
|
||||
/// of `"on"`, `"yes"`, or `"true"` and `false` if the value is one of
|
||||
/// `"off"`, `"no"`, or `"false"`. Defaults to `false` otherwise. Only
|
||||
/// accepts form _values_, not binary data.
|
||||
///
|
||||
/// **No type-specific data limit applies.**
|
||||
///
|
||||
/// * **`&str`, `String`**
|
||||
///
|
||||
/// The decoded form value or data is returned directly without
|
||||
/// modification.
|
||||
///
|
||||
/// **The data limit `string` applies.**
|
||||
///
|
||||
/// * **[`TempFile`]**
|
||||
///
|
||||
/// Streams the form field value or data to a temporary file.
|
||||
///
|
||||
/// **See [`TempFile`] for details and data limits.**
|
||||
///
|
||||
/// * **[`Capped<TempFile>`], [`Capped<String>`], [`Capped<&str>`]**
|
||||
///
|
||||
/// Streams the form value or data to the inner value, succeeding even if
|
||||
/// the data exceeds the respective type limit by truncating the data.
|
||||
///
|
||||
/// **See [`Capped`] for details.**
|
||||
///
|
||||
/// * **[`time::Date`]**
|
||||
///
|
||||
/// Parses a date in the `%F` format, that is, `%Y-$m-%d` or `YYYY-MM-DD`.
|
||||
/// This is the `"date"` HTML input type. Only accepts form _values_, not
|
||||
/// binary data.
|
||||
///
|
||||
/// **No type-specific data limit applies.**
|
||||
///
|
||||
/// * **[`time::PrimitiveDateTime`]**
|
||||
///
|
||||
/// Parses a date in `%FT%R` or `%FT%T` format, that is, `YYYY-MM-DDTHH:MM`
|
||||
/// or `YYYY-MM-DDTHH:MM:SS`. This is the `"datetime-local"` HTML input type
|
||||
/// without support for the millisecond variant. Only accepts form _values_,
|
||||
/// not binary data.
|
||||
///
|
||||
/// **No type-specific data limit applies.**
|
||||
///
|
||||
/// * **[`time::Time`]**
|
||||
///
|
||||
/// Parses a time in `%R` or `%T` format, that is, `HH:MM` or `HH:MM:SS`.
|
||||
/// This is the `"time"` HTML input type without support for the millisecond
|
||||
/// variant. Only accepts form _values_, not binary data.
|
||||
///
|
||||
/// **No type-specific data limit applies.**
|
||||
///
|
||||
/// [`TempFile`]: crate::data::TempFile
|
||||
/// See [`FromForm`](crate::form::FromForm#provided-implementations) for a list
|
||||
/// of all form guards, including those implemented via `FromFormField`.
|
||||
///
|
||||
/// # Implementing
|
||||
///
|
||||
|
@ -228,19 +157,30 @@ use crate::form::prelude::*;
|
|||
// this concern. Thus, for now, we keep this as one trait.
|
||||
#[crate::async_trait]
|
||||
pub trait FromFormField<'v>: Send + Sized {
|
||||
/// Parse a value of `T` from a form value field.
|
||||
///
|
||||
/// The default implementation returns an error of
|
||||
/// [`ValueField::unexpected()`].
|
||||
fn from_value(field: ValueField<'v>) -> Result<'v, Self> {
|
||||
Err(field.unexpected())?
|
||||
}
|
||||
|
||||
/// Parse a value of `T` from a form data field.
|
||||
///
|
||||
/// The default implementation returns an error of
|
||||
/// [`DataField::unexpected()`].
|
||||
async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> {
|
||||
Err(field.unexpected())?
|
||||
}
|
||||
|
||||
/// Returns a default value to be used when the form field does not exist or
|
||||
/// parsing otherwise fails.
|
||||
/// Returns a default value, if any exists, to be used during lenient
|
||||
/// parsing when the form field is missing.
|
||||
///
|
||||
/// If this returns `None`, the field is required. Otherwise, this should
|
||||
/// return `Some(default_value)`. The default implementation returns `None`.
|
||||
/// A return value of `None` means that field is required to exist and parse
|
||||
/// successfully, always. A return value of `Some(default)` means that
|
||||
/// `default` should be used when a field is missing.
|
||||
///
|
||||
/// The default implementation returns `None`.
|
||||
fn default() -> Option<Self> { None }
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::form::prelude::*;
|
||||
use crate::http::uri::{Query, FromUriParam};
|
||||
|
||||
/// A form guard for parsing form types leniently.
|
||||
///
|
||||
/// This type implements the [`FromForm`] trait and thus can be used as a
|
||||
/// generic parameter to the [`Form`] data guard: `Form<Lenient<T>>`, where `T`
|
||||
/// implements `FromForm`. Unlike using `Form` directly, this type uses a
|
||||
/// _lenient_ parsing strategy: forms that contains a superset of the expected
|
||||
/// fields (i.e, extra fields) will fail to parse and defaults will not be use
|
||||
/// for missing fields.
|
||||
///
|
||||
/// # Lenient Parsing
|
||||
///
|
||||
/// A `Lenient<T>` will parse successfully from an incoming form even if the
|
||||
/// form contains extra or missing fields. If fields are missing, the form field
|
||||
/// type's default will be used, if there is one. Extra fields are ignored; only
|
||||
/// the first is parsed and validated. This is the default strategy for
|
||||
/// [`Form`].
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// `Lenient<T>` implements [`FromForm`] as long as `T` implements `FromForm`.
|
||||
/// As such, `Form<Lenient<T>>` is a data guard.
|
||||
///
|
||||
/// Note that `Form<T>` _already_ parses leniently, so a `Form<Lenient<T>>` is
|
||||
/// redundant and equal to `Form<T>`. `Lenient`, however, can be used to make
|
||||
/// otherwise strict parses lenient, for example, in `Option<Lenient<T>>`:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::form::Lenient;
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct UserInput {
|
||||
/// lenient_inner_option: Option<Lenient<bool>>,
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Lenient<T>(T);
|
||||
|
||||
impl<T> Lenient<T> {
|
||||
/// Consumes `self` and returns the inner value.
|
||||
///
|
||||
/// Note that since `Lenient` implements [`Deref`] and [`DerefMut`] with
|
||||
/// target `T`, reading and writing an inner value can be accomplished
|
||||
/// transparently.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::form::{Form, Lenient};
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct MyForm {
|
||||
/// field: String,
|
||||
/// }
|
||||
///
|
||||
/// #[post("/submit", data = "<form>")]
|
||||
/// fn submit(form: Form<Lenient<MyForm>>) -> String {
|
||||
/// // We can read or mutate a value transparently:
|
||||
/// let field: &str = &form.field;
|
||||
///
|
||||
/// // To gain ownership, however, use `into_inner()`:
|
||||
/// form.into_inner().into_inner().field
|
||||
/// }
|
||||
/// ```
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[crate::async_trait]
|
||||
impl<'v, T: FromForm<'v>> FromForm<'v> for Lenient<T> {
|
||||
type Context = T::Context;
|
||||
|
||||
#[inline(always)]
|
||||
fn init(opts: Options) -> Self::Context {
|
||||
T::init(Options { strict: false, ..opts })
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) {
|
||||
T::push_value(ctxt, field)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) {
|
||||
T::push_data(ctxt, field).await
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn finalize(this: Self::Context) -> Result<'v, Self> {
|
||||
T::finalize(this).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Lenient<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for Lenient<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Lenient<T> {
|
||||
#[inline]
|
||||
fn from(val: T) -> Lenient<T> {
|
||||
Lenient(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f, A, T: FromUriParam<Query, A> + FromForm<'f>> FromUriParam<Query, A> for Lenient<T> {
|
||||
type Target = T::Target;
|
||||
|
||||
#[inline(always)]
|
||||
fn from_uri_param(param: A) -> Self::Target {
|
||||
T::from_uri_param(param)
|
||||
}
|
||||
}
|
|
@ -45,94 +45,7 @@
|
|||
//! * `map[k:1]=Bob`
|
||||
//! * `people[bob]nickname=Stan`
|
||||
//!
|
||||
//! # Parsing
|
||||
//!
|
||||
//! The [`FromForm`] trait describes a push-based parser for this wire format.
|
||||
//! Fields are preprocessed into either [`ValueField`]s or [`DataField`]s which
|
||||
//! are then pushed to the parser in [`FromForm::push_value()`] or
|
||||
//! [`FromForm::push_data()`], respectively. Both url-encoded forms and
|
||||
//! multipart forms are supported. All url-encoded form fields are preprocessed
|
||||
//! as [`ValueField`]s. Multipart form fields with Content-Types are processed
|
||||
//! as [`DataField`]s while those without a set Content-Type are processed as
|
||||
//! [`ValueField`]s. `ValueField` field names and values are percent-decoded.
|
||||
//!
|
||||
//! # Data Limits
|
||||
//!
|
||||
//! The total amount of data accepted by the [`Form`] data guard is limited by
|
||||
//! the following limits:
|
||||
//!
|
||||
//! | Limit Name | Default | Description |
|
||||
//! |-------------|---------|------------------------------------|
|
||||
//! | `form` | 32KiB | total limit for url-encoded forms |
|
||||
//! | `data-form` | 2MiB | total limit for multipart forms |
|
||||
//! | `*` | N/A | each field type has its own limits |
|
||||
//!
|
||||
//! Additionally, as noted above, each form field type (a form guard) typically
|
||||
//! imposes its own limits. For example, the `&str` form guard imposes a data
|
||||
//! limit of `string` when multipart data is streamed.
|
||||
//!
|
||||
//! See the [`Limits`](crate::data::Limits) and [`Form#data-limits`] docs for
|
||||
//! more.
|
||||
//!
|
||||
//! # A Simple Example
|
||||
//!
|
||||
//! The following example uses `f1=v1&f2=v2` to illustrate field/value pairs
|
||||
//! `(f1, v2)` and `(f2, v2)`. This is the same encoding used to send HTML forms
|
||||
//! over HTTP but Rocket's push-parsers are unaware of any specific encoding,
|
||||
//! dealing only with logical `field`s, `index`es, and `value`s.
|
||||
//!
|
||||
//! ## A Single Field (`T: FormFormField`)
|
||||
//!
|
||||
//! The simplest example parses a single value of type `T` from a string with an
|
||||
//! optional default value: this is `impl<T: FromFormField> FromForm for T`:
|
||||
//!
|
||||
//! 1. **Initialization.** The context stores form options and an `Option` of
|
||||
//! `Result<T, form::Error>` for storing the `result` of parsing `T`, which
|
||||
//! is initially set to `None`.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! struct Context<T> {
|
||||
//! opts: Options,
|
||||
//! result: Option<Result<T>>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! 2. **Push.** If `context.result` is `None`, `T` is parsed from `field`,
|
||||
//! and the result is stored in `context.result`. Otherwise a field has
|
||||
//! already been parsed and nothing is done.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! fn push(ctxt: &mut Self::Context, field: Field<'v>) {
|
||||
//! if ctxt.result.is_none() {
|
||||
//! ctxt.result = Some(Self::from_field(field));
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! 3. **Finalization.** If `ctxt.result` is `None` and `T` has a default,
|
||||
//! the default is returned. Otherwise a `Missing` error is returned. If
|
||||
//! `ctxt.result` is `Some(v)`, the result `v` is returned.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! fn finalize(ctxt: Self::Context) -> Result<Self> {
|
||||
//! match ctxt.result {
|
||||
//! Some(value) => Ok(value),
|
||||
//! None => match <T as FromFormField>::default() {
|
||||
//! Some(default) => Ok(default),
|
||||
//! None => Err(ErrorKind::Missing)?,
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This implementation is complete except for the following details:
|
||||
//!
|
||||
//! * not being pseudocode, of course
|
||||
//! * checking for duplicate pushes when parsing is `strict`
|
||||
//! * disallowing defaults when parsing is `strict`
|
||||
//! * tracking the field's name and value to generate a complete `Error`
|
||||
//!
|
||||
//! See [`FromForm`] for full details on push-parsing and a complete example.
|
||||
//! See [`FromForm`] for full details on push-parsing and complete examples.
|
||||
|
||||
// ## Maps w/named Fields (`struct`)
|
||||
//
|
||||
|
@ -453,6 +366,7 @@ mod from_form_field;
|
|||
mod form;
|
||||
mod context;
|
||||
mod strict;
|
||||
mod lenient;
|
||||
mod parser;
|
||||
pub mod validate;
|
||||
pub mod name;
|
||||
|
@ -477,6 +391,7 @@ pub use from_form::*;
|
|||
pub use form::*;
|
||||
pub use context::*;
|
||||
pub use strict::*;
|
||||
pub use lenient::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod prelude {
|
||||
|
|
|
@ -53,7 +53,7 @@ use crate::http::uri::{Query, FromUriParam};
|
|||
/// uses_default: bool
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Strict<T>(T);
|
||||
|
||||
impl<T> Strict<T> {
|
||||
|
|
|
@ -3,9 +3,7 @@ use std::collections::HashMap;
|
|||
use crate::form::*;
|
||||
|
||||
fn parse<'v, T: FromForm<'v>>(values: &[&'v str]) -> Result<'v, T> {
|
||||
let mut context = T::init(Options::Lenient);
|
||||
values.iter().for_each(|v| T::push_value(&mut context, ValueField::parse(*v)));
|
||||
T::finalize(context)
|
||||
Form::parse_iter(values.iter().cloned().map(ValueField::parse))
|
||||
}
|
||||
|
||||
macro_rules! map {
|
||||
|
@ -27,7 +25,7 @@ macro_rules! vec {
|
|||
macro_rules! assert_values_parse_eq {
|
||||
($($v:expr => $T:ty = $expected:expr),* $(,)?) => (
|
||||
$(
|
||||
assert_value_parse_eq!($v => $T = $expected);
|
||||
assert_value_parse_eq!($v as &[&str] => $T = $expected);
|
||||
)*
|
||||
)
|
||||
}
|
||||
|
@ -41,7 +39,25 @@ macro_rules! assert_value_parse_eq {
|
|||
panic!("unexpected parse of {:?}\n {:?} instead of {:?}",
|
||||
$v, actual, expected)
|
||||
}
|
||||
Err(e) => panic!("parse of {:?} failed: {:?}", $v, e)
|
||||
Err(e) => panic!("parse `{:?} {}` failed: {:?}", $v, stringify!(=> $T = $expected), e)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
macro_rules! assert_parses_fail {
|
||||
($($v:expr => $T:ty),* $(,)?) => (
|
||||
$(
|
||||
assert_parse_fails!($v as &[&str] => $T);
|
||||
)*
|
||||
)
|
||||
}
|
||||
|
||||
macro_rules! assert_parse_fails {
|
||||
($v:expr => $T:ty) => (
|
||||
let diag = format!("{:?} {}", $v, stringify!(=> $T = $expected));
|
||||
match parse::<$T>($v) {
|
||||
Ok(actual) => panic!("unexpectedly parsed {} as {:?}", diag, actual),
|
||||
Err(_) => { /* ok */ }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -65,6 +81,41 @@ fn bool() {
|
|||
assert_values_parse_eq! {
|
||||
&["=true", "=yes", "=on"] => Vec<bool> = vec![true, true, true],
|
||||
&["=false", "=no", "=off"] => Vec<bool> = vec![false, false, false],
|
||||
&["=tRuE", "=YES", "=On"] => Vec<bool> = vec![true, true, true],
|
||||
&["=fAlSE", "=NO", "=OFF"] => Vec<bool> = vec![false, false, false],
|
||||
}
|
||||
|
||||
assert_parses_fail! {
|
||||
&[] => Strict<bool>,
|
||||
&["=unknown"] => bool,
|
||||
&["=unknown", "=please"] => Vec<bool>,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults() {
|
||||
assert_values_parse_eq! {
|
||||
&[] => bool = false,
|
||||
&[] => Option<&str> = None,
|
||||
&[] => Option<time::Date> = None,
|
||||
|
||||
&[] => Option<bool> = None,
|
||||
&[] => Option<Strict<bool>> = None,
|
||||
|
||||
&[] => Result<'_, bool> = Ok(false),
|
||||
&[] => Result<'_, Strict<bool>> = Err(error::ErrorKind::Missing.into()),
|
||||
|
||||
&["=unknown"] => Option<bool> = None,
|
||||
&["=unknown"] => Option<Strict<bool>> = None,
|
||||
&["=unknown"] => Option<Lenient<bool>> = None,
|
||||
|
||||
&[] => Option<Lenient<bool>> = Some(false.into()),
|
||||
&["=123"] => Option<time::Date> = None,
|
||||
|
||||
&["=no"] => Option<bool> = Some(false),
|
||||
&["=yes"] => Option<bool> = Some(true),
|
||||
&["=yes"] => Option<Lenient<bool>> = Some(true.into()),
|
||||
&["=yes"] => Option<Strict<bool>> = Some(true.into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -726,7 +726,7 @@ fn new(task: Option<Form<Task>>) { /* .. */ }
|
|||
[`Form`]: @api/rocket/form/struct.Form.html
|
||||
[`FromForm`]: @api/rocket/form/trait.FromForm.html
|
||||
|
||||
### Strict Parsing
|
||||
### Parsing Strategy
|
||||
|
||||
Rocket's `FromForm` parsing is _lenient_ by default: a `Form<T>` will parse
|
||||
successfully from an incoming form even if it contains extra, duplicate, or
|
||||
|
@ -768,7 +768,41 @@ struct Input {
|
|||
fn new(input: Form<Input>) { /* .. */ }
|
||||
```
|
||||
|
||||
[`Lenient`] is the _lenient_ analog to `Strict`, which forces parsing to be
|
||||
lenient. `Form` is lenient by default, so a `Form<Lenient<T>>` is redundant, but
|
||||
`Lenient` can be used to overwrite a strict parse as lenient:
|
||||
`Option<Lenient<T>>`.
|
||||
|
||||
[`Form<Strict<T>>`]: @api/rocket/form/struct.Strict.html
|
||||
[`Lenient`]: @api/rocket/form/struct.Lenient.html
|
||||
|
||||
### Defaults
|
||||
|
||||
A form guard may specify a default value to use when a field is missing. The
|
||||
default value is used only when parsing is _lenient_. When _strict_, all errors,
|
||||
including, missing fields are propagated directly.
|
||||
|
||||
Some types with defaults include `bool`, which defaults to `false`, useful for
|
||||
checkboxes, `Option<T>`, which defaults to `None`, and [`form::Result`], which
|
||||
defaults to `Err(Missing)` or otherwise collects errors in an `Err` of
|
||||
[`Errors<'_>`]. Defaulting guards can be used just like any other form guard:
|
||||
|
||||
```rust
|
||||
# use rocket::form::FromForm;
|
||||
use rocket::form::{self, Errors};
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct MyForm<'v> {
|
||||
maybe_string: Option<&'v str>,
|
||||
ok_or_error: form::Result<'v, Vec<&'v str>>,
|
||||
here_or_false: bool,
|
||||
}
|
||||
|
||||
# rocket_guide_tests::assert_form_parses_ok!(MyForm, "");
|
||||
```
|
||||
|
||||
[`Errors<'_>`]: @api/rocket/form/struct.Errors.html
|
||||
[`form::Result`]: @api/rocket/form/type.Result.html
|
||||
|
||||
### Field Renaming
|
||||
|
||||
|
@ -913,30 +947,6 @@ If a field's validation doesn't depend on other fields (validation is _local_),
|
|||
it is validated prior to those fields that do. For `CreditCard`, `cvv` and
|
||||
`expiration` will be validated prior to `number`.
|
||||
|
||||
### Defaults
|
||||
|
||||
The [`FromForm`] trait allows types to specify a default value if one isn't
|
||||
provided in a submitted form. This includes types such as `bool`, useful for
|
||||
checkboxes, and `Option<T>`. Additionally, `FromForm` is implemented for
|
||||
`Result<T, Errors<'_>>` where the error value is [`Errors<'_>`]. All of these
|
||||
types can be used just like any other form field:
|
||||
|
||||
```rust
|
||||
# use rocket::form::FromForm;
|
||||
use rocket::form::Errors;
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct MyForm<'v> {
|
||||
maybe_string: Option<String>,
|
||||
ok_or_error: Result<Vec<String>, Errors<'v>>,
|
||||
here: bool,
|
||||
}
|
||||
|
||||
# rocket_guide_tests::assert_form_parses_ok!(MyForm, "");
|
||||
```
|
||||
|
||||
[`Errors<'_>`]: @api/rocket/form/struct.Errors.html
|
||||
|
||||
### Collections
|
||||
|
||||
Rocket's form support allows your application to express _any_ structure with
|
||||
|
|
Loading…
Reference in New Issue