mirror of https://github.com/rwf2/Rocket.git
Complete forms documentation. Improve 'validate'.
* Add a `msg!()` macro to easily change a field validation message. * Allow a field to refer to itself via `self.field`. * Improve the various field validation traits.
This commit is contained in:
parent
78e2f8a3c9
commit
398a044eb0
|
@ -445,4 +445,5 @@ macro_rules! json {
|
|||
};
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use json;
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use devise::{*, ext::{TypeExt, SpanDiagnosticExt}};
|
||||
|
||||
use syn::visit_mut::VisitMut;
|
||||
use syn::visit::Visit;
|
||||
|
||||
use crate::exports::*;
|
||||
use crate::proc_macro2::Span;
|
||||
use crate::proc_macro2::{Span, TokenStream, TokenTree};
|
||||
use crate::name::Name;
|
||||
|
||||
pub struct FormField {
|
||||
|
@ -86,62 +89,139 @@ impl FieldExt for Field<'_> {
|
|||
|
||||
fn name_view(&self) -> Result<syn::Expr> {
|
||||
let field_name = self.field_name()?;
|
||||
Ok(syn::parse_quote!(#_form::NameBuf::from((__c.__parent, #field_name))))
|
||||
let name_view = quote_spanned! { self.span() =>
|
||||
#_form::NameBuf::from((__c.__parent, #field_name))
|
||||
};
|
||||
|
||||
Ok(syn::parse2(name_view).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
struct RecordMemberAccesses(Vec<syn::Member>);
|
||||
|
||||
impl<'a> Visit<'a> for RecordMemberAccesses {
|
||||
fn visit_expr_field(&mut self, i: &syn::ExprField) {
|
||||
if let syn::Expr::Path(e) = &*i.base {
|
||||
if e.path.is_ident("self") {
|
||||
self.0.push(i.member.clone());
|
||||
}
|
||||
}
|
||||
|
||||
syn::visit::visit_expr_field(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
struct ValidationMutator<'a> {
|
||||
field: &'a syn::Ident,
|
||||
parent: &'a syn::Ident,
|
||||
local: bool,
|
||||
visited: bool,
|
||||
}
|
||||
|
||||
impl ValidationMutator<'_> {
|
||||
fn visit_token_stream(&mut self, tt: TokenStream) -> TokenStream {
|
||||
use quote::{ToTokens, TokenStreamExt};
|
||||
use TokenTree::*;
|
||||
|
||||
let mut iter = tt.into_iter();
|
||||
let mut stream = TokenStream::new();
|
||||
while let Some(tt) = iter.next() {
|
||||
match tt {
|
||||
Ident(s3lf) if s3lf == "self" => {
|
||||
match (iter.next(), iter.next()) {
|
||||
(Some(Punct(p)), Some(Ident(i))) if p.as_char() == '.' => {
|
||||
let field = syn::parse_quote!(#s3lf #p #i);
|
||||
let mut expr = syn::Expr::Field(field);
|
||||
self.visit_expr_mut(&mut expr);
|
||||
expr.to_tokens(&mut stream);
|
||||
},
|
||||
(tt1, tt2) => stream.append_all(&[Some(Ident(s3lf)), tt1, tt2]),
|
||||
}
|
||||
},
|
||||
TokenTree::Group(group) => {
|
||||
let tt = self.visit_token_stream(group.stream());
|
||||
let mut new = proc_macro2::Group::new(group.delimiter(), tt);
|
||||
new.set_span(group.span());
|
||||
let group = TokenTree::Group(new);
|
||||
stream.append(group);
|
||||
}
|
||||
tt => stream.append(tt),
|
||||
}
|
||||
}
|
||||
|
||||
stream
|
||||
}
|
||||
}
|
||||
|
||||
impl VisitMut for ValidationMutator<'_> {
|
||||
fn visit_expr_call_mut(&mut self, call: &mut syn::ExprCall) {
|
||||
syn::visit_mut::visit_expr_call_mut(self, call);
|
||||
|
||||
// Only modify the first call we see.
|
||||
if self.visited { return; }
|
||||
|
||||
let (parent, field) = (self.parent, self.field);
|
||||
let form_field = match self.local {
|
||||
true => syn::parse2(quote_spanned!(field.span() => &#field)).unwrap(),
|
||||
false => syn::parse2(quote_spanned!(field.span() => &#parent.#field)).unwrap(),
|
||||
};
|
||||
|
||||
call.args.insert(0, form_field);
|
||||
self.visited = true;
|
||||
}
|
||||
|
||||
fn visit_ident_mut(&mut self, i: &mut syn::Ident) {
|
||||
if !self.local && i == "self" {
|
||||
*i = self.parent.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_macro_mut(&mut self, mac: &mut syn::Macro) {
|
||||
mac.tokens = self.visit_token_stream(mac.tokens.clone());
|
||||
syn::visit_mut::visit_macro_mut(self, mac);
|
||||
}
|
||||
|
||||
fn visit_expr_mut(&mut self, i: &mut syn::Expr) {
|
||||
// If this is a local, replace accesses of `self.field` with `field`.
|
||||
if let syn::Expr::Field(e) = i {
|
||||
if let syn::Expr::Path(e) = &*e.base {
|
||||
if e.path.is_ident("self") && self.local {
|
||||
let new_expr = &self.field;
|
||||
*i = syn::parse_quote!(#new_expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return syn::visit_mut::visit_expr_mut(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validators<'v>(
|
||||
field: Field<'v>,
|
||||
out: &'v syn::Ident,
|
||||
parent: &'v syn::Ident, // field ident (if local) or form ident (if !local)
|
||||
local: bool,
|
||||
) -> Result<impl Iterator<Item = syn::Expr> + 'v> {
|
||||
use syn::visit_mut::VisitMut;
|
||||
|
||||
struct ValidationMutator<'a> {
|
||||
field: syn::Expr,
|
||||
self_expr: syn::Expr,
|
||||
form: &'a syn::Ident,
|
||||
visited: bool,
|
||||
rec: bool,
|
||||
}
|
||||
|
||||
impl<'a> ValidationMutator<'a> {
|
||||
fn new(field: &'a syn::Ident, form: &'a syn::Ident) -> Self {
|
||||
let self_expr = syn::parse_quote!(&#form.#field);
|
||||
let field = syn::parse_quote!(&#field);
|
||||
ValidationMutator { field, self_expr, form, visited: false, rec: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl VisitMut for ValidationMutator<'_> {
|
||||
fn visit_expr_call_mut(&mut self, call: &mut syn::ExprCall) {
|
||||
syn::visit_mut::visit_expr_call_mut(self, call);
|
||||
|
||||
let ident = if self.rec { &self.self_expr } else { &self.field };
|
||||
if !self.visited {
|
||||
call.args.insert(0, ident.clone());
|
||||
self.visited = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_ident_mut(&mut self, i: &mut syn::Ident) {
|
||||
if i == "self" {
|
||||
*i = self.form.clone();
|
||||
self.rec = true;
|
||||
}
|
||||
|
||||
syn::visit_mut::visit_ident_mut(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)?
|
||||
.into_iter()
|
||||
.filter_map(|a| a.validate)
|
||||
.map(move |mut expr| {
|
||||
let mut mutator = ValidationMutator::new(field.ident(), out);
|
||||
mutator.visit_expr_mut(&mut expr);
|
||||
(expr, mutator.rec)
|
||||
.map(move |expr| {
|
||||
let mut members = RecordMemberAccesses(vec![]);
|
||||
members.visit_expr(&expr);
|
||||
|
||||
let field_ident = field.ident();
|
||||
let is_local_validation = members.0.iter()
|
||||
.all(|member| match member {
|
||||
syn::Member::Named(i) => i == field_ident,
|
||||
_ => false
|
||||
});
|
||||
|
||||
(expr, is_local_validation)
|
||||
})
|
||||
.filter(move |(_, global)| local != *global)
|
||||
.map(|(e, _)| e))
|
||||
.filter(move |(_, is_local)| *is_local == local)
|
||||
.map(move |(mut expr, _)| {
|
||||
let field = field.ident();
|
||||
let mut v = ValidationMutator { parent, local, field, visited: false };
|
||||
v.visit_expr_mut(&mut expr);
|
||||
expr
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ fn context_type(input: Input<'_>) -> (TokenStream, Option<syn::WhereClause>) {
|
|||
|
||||
pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream {
|
||||
DeriveGenerator::build_for(input, quote!(impl<'__f> #_form::FromForm<'__f>))
|
||||
// NOTE: If support is widened, fix `FieldExt::ident()` `expect()`.
|
||||
.support(Support::NamedStruct | Support::Lifetime | Support::Type)
|
||||
.replace_generic(0, 0)
|
||||
.type_bound(quote!(#_form::FromForm<'__f> + '__f))
|
||||
|
@ -189,13 +190,6 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream {
|
|||
.split2();
|
||||
|
||||
Ok(quote_spanned! { fields.span() =>
|
||||
// if __c.__parent.is_none() {
|
||||
// let _e = #_form::Error::from(#_form::ErrorKind::Missing)
|
||||
// .with_entity(#_form::Entity::Form);
|
||||
//
|
||||
// return #_Err(_e.into());
|
||||
// }
|
||||
|
||||
#(let #ident = match #finalize_field {
|
||||
#_ok(#ident) => #_some(#ident),
|
||||
#_err(_e) => { __c.__errors.extend(_e); #_none }
|
||||
|
@ -231,7 +225,6 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream {
|
|||
.unwrap_or_else(|| <#ty as #_form::FromForm<'__f>>::default()
|
||||
.ok_or_else(|| #_form::ErrorKind::Missing.into())
|
||||
)
|
||||
// <#ty as #_form::FromForm<'__f>>::finalize(__c.#ident)
|
||||
.and_then(|#ident| {
|
||||
let mut _es = #_form::Errors::new();
|
||||
#(if let #_err(_e) = #validator { _es.extend(_e); })*
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,52 +3,225 @@ use crate::form::error::{Error, ErrorKind, Entity};
|
|||
use crate::http::{ContentType, RawStr};
|
||||
use crate::{Request, Data};
|
||||
|
||||
/// A form field with a string value.
|
||||
///
|
||||
/// Rocket preprocesses all form fields into either [`ValueField`]s or
|
||||
/// [`DataField`]s. All fields from url-encoded forms, and fields without
|
||||
/// Content-Types from multipart forms, are preprocessed as a `ValueField`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueField<'r> {
|
||||
/// The (decoded) name of the form field.
|
||||
pub name: NameView<'r>,
|
||||
/// The (decoded) value of the form field.
|
||||
pub value: &'r str,
|
||||
}
|
||||
|
||||
/// A multipart form field with an underlying data stream.
|
||||
///
|
||||
/// Rocket preprocesses all form fields into either [`ValueField`]s or
|
||||
/// [`DataField`]s. Multipart form fields with a `Content-Type` are preprocessed
|
||||
/// as a `DataField`. The underlying data is _not_ read into memory, but
|
||||
/// instead, streamable from the contained [`Data`] structure.
|
||||
pub struct DataField<'r, 'i> {
|
||||
/// The (decoded) name of the form field.
|
||||
pub name: NameView<'r>,
|
||||
/// The form field's sanitized file name.
|
||||
///
|
||||
/// ## Santization
|
||||
///
|
||||
/// A "sanitized" file name is a non-empty string, stripped of its file
|
||||
/// exntesion, which does not contain any path delimiters (`/` or `\`), is
|
||||
/// not a hidden (`.`) or `*`-prefixed name, and does not contain special
|
||||
/// characters (`:`, `>`, or `<`). If the submitted file name matches any of
|
||||
/// these properties or none was submitted, `file_name` will be `None`.
|
||||
pub file_name: Option<&'r str>,
|
||||
/// The form field's Content-Type, as submitted, which may or may not
|
||||
/// reflect on `data`.
|
||||
pub content_type: ContentType,
|
||||
/// The request in which the form field was submitted.
|
||||
pub request: &'r Request<'i>,
|
||||
/// The raw data stream.
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
impl<'v> ValueField<'v> {
|
||||
/// `raw` must already be URL-decoded. This is weird.
|
||||
/// Parse a field string, where both the key and value are assumed to be
|
||||
/// URL-decoded while preserving the `=` delimiter, into a `ValueField`.
|
||||
///
|
||||
/// This implements 3.2, 3.3 of [section 5.1 of the WHATWG living standard].
|
||||
///
|
||||
/// [section 5.1 of the WHATWG living standard]: https://url.spec.whatwg.org/#urlencoded-parsing
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::ValueField;
|
||||
///
|
||||
/// let parsed = ValueField::parse("a cat=an A+ pet");
|
||||
/// assert_eq!(parsed.name, "a cat");
|
||||
/// assert_eq!(parsed.value, "an A+ pet");
|
||||
///
|
||||
/// let parsed = ValueField::parse("a cat is an A+ pet");
|
||||
/// assert_eq!(parsed.name, "a cat is an A+ pet");
|
||||
/// assert_eq!(parsed.value, "");
|
||||
///
|
||||
/// let parsed = ValueField::parse("cat.food=yum?");
|
||||
/// assert_eq!(parsed.name, "cat");
|
||||
/// assert_eq!(parsed.name.source(), "cat.food");
|
||||
/// assert_eq!(parsed.value, "yum?");
|
||||
/// ```
|
||||
pub fn parse(field: &'v str) -> Self {
|
||||
// WHATWG URL Living Standard 5.1 steps 3.2, 3.3.
|
||||
let (name, val) = RawStr::new(field).split_at_byte(b'=');
|
||||
ValueField::from((name.as_str(), val.as_str()))
|
||||
}
|
||||
|
||||
/// Create a `ValueField` from a value, which is assumed to be URL-decoded.
|
||||
/// The field `name` will be empty.
|
||||
///
|
||||
/// This is equivalent to `ValueField::from(("", value))`. To create a
|
||||
/// `ValueField` from both a `name` and a `value`, use
|
||||
/// `ValueField::from((name, value))`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::ValueField;
|
||||
///
|
||||
/// let parsed = ValueField::from_value("A+=kitten");
|
||||
/// assert_eq!(parsed.name, "");
|
||||
/// assert_eq!(parsed.value, "A+=kitten");
|
||||
/// ```
|
||||
pub fn from_value(value: &'v str) -> Self {
|
||||
ValueField::from(("", value))
|
||||
}
|
||||
|
||||
/// Shift the `name` of `self` and return `self` with the shfited `name`.
|
||||
///
|
||||
/// See [`NameView::shift()`] for the details on name "shifting".
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::ValueField;
|
||||
///
|
||||
/// let parsed = ValueField::parse("cat.food=yum?");
|
||||
/// assert_eq!(parsed.name, "cat");
|
||||
/// assert_eq!(parsed.name.source(), "cat.food");
|
||||
/// assert_eq!(parsed.name.key_lossy(), "cat");
|
||||
///
|
||||
/// let shifted = parsed.shift();
|
||||
/// assert_eq!(shifted.name, "cat.food");
|
||||
/// assert_eq!(shifted.name.key_lossy(), "food");
|
||||
/// ```
|
||||
pub fn shift(mut self) -> Self {
|
||||
self.name.shift();
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a complete unexpected value field [`Error`] from `self`.
|
||||
///
|
||||
/// The error will have the following properties:
|
||||
/// * `kind`: [`ErrorKind::Unexpected`]
|
||||
/// * `name`: [`self.name.source()`](NameView::source())
|
||||
/// * `value`: [`self.value`](ValueField::value)
|
||||
/// * `entity`: [`Entity::ValueField`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::ValueField;
|
||||
/// use rocket::form::error::{ErrorKind, Entity};
|
||||
///
|
||||
/// let field = ValueField::parse("cat.food=yum?");
|
||||
/// let error = field.unexpected();
|
||||
///
|
||||
/// assert_eq!(error.name.as_ref().unwrap(), "cat.food");
|
||||
/// assert_eq!(error.value.as_ref().unwrap(), "yum?");
|
||||
/// assert_eq!(error.kind, ErrorKind::Unexpected);
|
||||
/// assert_eq!(error.entity, Entity::ValueField);
|
||||
/// ```
|
||||
pub fn unexpected(&self) -> Error<'v> {
|
||||
Error::from(ErrorKind::Unexpected)
|
||||
.with_name(NameView::new(self.name.source()))
|
||||
.with_name(self.name.source())
|
||||
.with_value(self.value)
|
||||
.with_entity(Entity::ValueField)
|
||||
}
|
||||
|
||||
/// Creates a complete mising value field [`Error`] from `self`.
|
||||
///
|
||||
/// The error will have the following properties:
|
||||
/// * `kind`: [`ErrorKind::Missing`]
|
||||
/// * `name`: [`self.name.source()`](NameView::source())
|
||||
/// * `value`: [`self.value`](ValueField::value)
|
||||
/// * `entity`: [`Entity::ValueField`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::ValueField;
|
||||
/// use rocket::form::error::{ErrorKind, Entity};
|
||||
///
|
||||
/// let field = ValueField::parse("cat.food=yum?");
|
||||
/// let error = field.missing();
|
||||
///
|
||||
/// assert_eq!(error.name.as_ref().unwrap(), "cat.food");
|
||||
/// assert_eq!(error.value.as_ref().unwrap(), "yum?");
|
||||
/// assert_eq!(error.kind, ErrorKind::Missing);
|
||||
/// assert_eq!(error.entity, Entity::ValueField);
|
||||
/// ```
|
||||
pub fn missing(&self) -> Error<'v> {
|
||||
Error::from(ErrorKind::Missing)
|
||||
.with_name(NameView::new(self.name.source()))
|
||||
.with_name(self.name.source())
|
||||
.with_value(self.value)
|
||||
.with_entity(Entity::ValueField)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'v> DataField<'v, '_> {
|
||||
/// Shift the `name` of `self` and return `self` with the shfited `name`.
|
||||
///
|
||||
/// This is identical to [`ValueField::shift()`] but for `DataFields`s. See
|
||||
/// [`NameView::shift()`] for the details on name "shifting".
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::DataField;
|
||||
///
|
||||
/// fn push_data(field: DataField<'_, '_>) {
|
||||
/// let shifted = field.shift();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn shift(mut self) -> Self {
|
||||
self.name.shift();
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a complete unexpected data field [`Error`] from `self`.
|
||||
///
|
||||
/// The error will have the following properties:
|
||||
/// * `kind`: [`ErrorKind::Unexpected`]
|
||||
/// * `name`: [`self.name.source()`](NameView::source())
|
||||
/// * `value`: `None`
|
||||
/// * `entity`: [`Entity::DataField`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::DataField;
|
||||
///
|
||||
/// fn push_data(field: DataField<'_, '_>) {
|
||||
/// let error = field.unexpected();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn unexpected(&self) -> Error<'v> {
|
||||
Error::from(ErrorKind::Unexpected)
|
||||
.with_name(self.name.source())
|
||||
.with_entity(Entity::DataField)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<(&'a str, &'a str)> for ValueField<'a> {
|
||||
fn from((name, value): (&'a str, &'a str)) -> Self {
|
||||
ValueField { name: NameView::new(name), value }
|
||||
|
@ -60,16 +233,3 @@ impl<'a, 'b> PartialEq<ValueField<'b>> for ValueField<'a> {
|
|||
self.name == other.name && self.value == other.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<'v> DataField<'v, '_> {
|
||||
pub fn shift(mut self) -> Self {
|
||||
self.name.shift();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unexpected(&self) -> Error<'v> {
|
||||
Error::from(ErrorKind::Unexpected)
|
||||
.with_name(self.name)
|
||||
.with_entity(Entity::DataField)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::form::prelude::*;
|
|||
/// A data guard for [`FromForm`] types.
|
||||
///
|
||||
/// This type implements the [`FromData`] trait. It provides a generic means to
|
||||
/// parse arbitrary structures from incoming form data of any kind.
|
||||
/// parse arbitrary structures from incoming form data.
|
||||
///
|
||||
/// See the [forms guide](https://rocket.rs/master/guide/requests#forms) for
|
||||
/// general form support documentation.
|
||||
|
@ -57,19 +57,44 @@ use crate::form::prelude::*;
|
|||
///
|
||||
/// ## Data Limits
|
||||
///
|
||||
/// The default size limit for incoming form data is 32KiB. Setting a limit
|
||||
/// protects your application from denial of service (DOS) attacks and from
|
||||
/// resource exhaustion through high memory consumption. The limit can be
|
||||
/// modified by setting the `limits.form` configuration parameter. For instance,
|
||||
/// to increase the forms limit to 512KiB for all environments, you may add the
|
||||
/// following to your `Rocket.toml`:
|
||||
/// ### URL-Encoded Forms
|
||||
///
|
||||
/// The `form` limit specifies the data limit for an entire url-encoded form
|
||||
/// data. It defaults to 32KiB. URL-encoded form data is percent-decoded, stored
|
||||
/// in-memory, and parsed into [`ValueField`]s. If the incoming data exceeds
|
||||
/// this limit, the `Form` data guard fails without attempting to parse fields
|
||||
/// with a `413: Payload Too Large` error.
|
||||
///
|
||||
/// ### Multipart Forms
|
||||
///
|
||||
/// The `data-form` limit specifies the data limit for an entire multipart form
|
||||
/// data stream. It defaults to 2MiB. Multipart data is streamed, and form
|
||||
/// fields are processed into [`DataField`]s or [`ValueField`]s as they arrive.
|
||||
/// If the commulative data received while streaming exceeds the limit, parsing
|
||||
/// is aborted, an error is created and pushed via [`FromForm::push_error()`],
|
||||
/// and the form is finalized.
|
||||
///
|
||||
/// ### Individual Fields
|
||||
///
|
||||
/// Individual fields _may_ have data limits as well. The type of the field
|
||||
/// determines whether there is a data limit. For instance, the `&str` type
|
||||
/// imposes the `string` data limit. Consult the type's documentation or
|
||||
/// [`FromFormField`] for details.
|
||||
///
|
||||
/// ### Changing Limits
|
||||
///
|
||||
/// To change data limits, set the `limits.form` and/or `limits.data-form`
|
||||
/// configuration parameters. For instance, to increase the URL-encoded forms
|
||||
/// limit to 128KiB for all environments, you might add the following to your
|
||||
/// `Rocket.toml`:
|
||||
///
|
||||
/// ```toml
|
||||
/// [global.limits]
|
||||
/// form = 524288
|
||||
/// form = 128KiB
|
||||
/// ```
|
||||
///
|
||||
/// See the [`Limits`](crate::data::Limits) docs for more.
|
||||
/// See the [`Limits`](crate::data::Limits) and [`config`](crate::config) docs
|
||||
/// for more.
|
||||
#[derive(Debug)]
|
||||
pub struct Form<T>(T);
|
||||
|
||||
|
|
|
@ -532,7 +532,7 @@ impl<'v, K, V> MapContext<'v, K, V>
|
|||
}
|
||||
_ => {
|
||||
let error = Error::from(ErrorKind::Missing)
|
||||
.with_entity(Entity::Indices)
|
||||
.with_entity(Entity::Key)
|
||||
.with_name(name);
|
||||
|
||||
self.errors.push(error);
|
||||
|
@ -566,14 +566,14 @@ impl<'v, K, V> MapContext<'v, K, V>
|
|||
.zip(key_map.values().map(|(_, name)| name))
|
||||
.filter_map(|(ctxt, name)| match K::finalize(ctxt) {
|
||||
Ok(value) => Some(value),
|
||||
Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None }
|
||||
Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None },
|
||||
});
|
||||
|
||||
let values = values.into_iter()
|
||||
.zip(key_map.values().map(|(_, name)| name))
|
||||
.filter_map(|(ctxt, name)| match V::finalize(ctxt) {
|
||||
Ok(value) => Some(value),
|
||||
Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None }
|
||||
Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None },
|
||||
});
|
||||
|
||||
let map: T = keys.zip(values).collect();
|
||||
|
|
|
@ -61,6 +61,7 @@ use crate::form::prelude::*;
|
|||
///
|
||||
/// A value is validated successfully if the `from_str` method for the given
|
||||
/// type returns successfully. Only accepts form _values_, not binary data.
|
||||
/// No data limit applies.
|
||||
///
|
||||
/// * **`bool`**
|
||||
///
|
||||
|
@ -69,17 +70,21 @@ use crate::form::prelude::*;
|
|||
/// `"off"`, `"no"`, or `"false"`. Defaults to `false` otherwise. Only
|
||||
/// accepts form _values_, not binary data.
|
||||
///
|
||||
/// **No 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.
|
||||
/// [`TempFile`] for details and data limits.
|
||||
///
|
||||
/// * **[`Capped<TempFile>`], [`Capped<String>`]**
|
||||
/// * **[`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
|
||||
|
@ -91,6 +96,8 @@ use crate::form::prelude::*;
|
|||
/// This is the `"date"` HTML input type. Only accepts form _values_, not
|
||||
/// binary data.
|
||||
///
|
||||
/// **No data limit applies.**
|
||||
///
|
||||
/// * **[`time::PrimitiveDateTime`]**
|
||||
///
|
||||
/// Parses a date in `%FT%R` or `%FT%T` format, that is, `YYYY-MM-DDTHH:MM`
|
||||
|
@ -98,12 +105,16 @@ use crate::form::prelude::*;
|
|||
/// without support for the millisecond variant. Only accepts form _values_,
|
||||
/// not binary data.
|
||||
///
|
||||
/// **No 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 data limit applies.**
|
||||
///
|
||||
/// [`TempFile`]: crate::data::TempFile
|
||||
///
|
||||
/// # Implementing
|
||||
|
@ -240,7 +251,7 @@ pub struct FromFieldContext<'v, T: FromFormField<'v>> {
|
|||
}
|
||||
|
||||
impl<'v, T: FromFormField<'v>> FromFieldContext<'v, T> {
|
||||
fn can_push(&mut self) -> bool {
|
||||
fn should_push(&mut self) -> bool {
|
||||
self.pushes += 1;
|
||||
self.value.is_none()
|
||||
}
|
||||
|
@ -273,14 +284,14 @@ impl<'v, T: FromFormField<'v>> FromForm<'v> for T {
|
|||
}
|
||||
|
||||
fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) {
|
||||
if ctxt.can_push() {
|
||||
if ctxt.should_push() {
|
||||
ctxt.field_value = Some(field.value);
|
||||
ctxt.push(field.name, Self::from_value(field))
|
||||
}
|
||||
}
|
||||
|
||||
async fn push_data(ctxt: &mut FromFieldContext<'v, T>, field: DataField<'v, '_>) {
|
||||
if ctxt.can_push() {
|
||||
if ctxt.should_push() {
|
||||
ctxt.push(field.name, Self::from_data(field).await);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
//! Parsing and validation of HTTP forms and fields.
|
||||
//!
|
||||
//! See the [forms guide](https://rocket.rs/master/guide/requests#forms) for
|
||||
//! general form support documentation.
|
||||
//!
|
||||
//! # Field Wire Format
|
||||
//!
|
||||
//! Rocket's field wire format is a flexible, non-self-descriptive, text-based
|
||||
|
@ -17,13 +20,13 @@
|
|||
//!
|
||||
//! indices := index (':' index)*
|
||||
//!
|
||||
//! index := STRING except ':'
|
||||
//! index := STRING except ':', ']'
|
||||
//!
|
||||
//! value := STRING
|
||||
//! ```
|
||||
//!
|
||||
//! Each field name consists of any number of `key`s and at most one `value`.
|
||||
//! Keys are delimited by `[]` or `.`. A `key` consists of indices delimited by
|
||||
//! Keys are delimited by `[` or `.`. A `key` consists of indices delimited by
|
||||
//! `:`.
|
||||
//!
|
||||
//! The meaning of a key or index is type-dependent, hence the format is
|
||||
|
@ -51,7 +54,7 @@
|
|||
//! 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`]s. `ValueField` field names and values are percent-decoded.
|
||||
//!
|
||||
//! # Data Limits
|
||||
//!
|
||||
|
@ -68,372 +71,379 @@
|
|||
//! 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) docs for more.
|
||||
//! See the [`Limits`](crate::data::Limits) and [`Form#data-limits`] docs for
|
||||
//! more.
|
||||
//!
|
||||
/// # Examples
|
||||
///
|
||||
/// The following examples use `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 Value (`T: FormFormValue`)
|
||||
///
|
||||
/// The simplest example parses a single value of type `T` from a string with an
|
||||
/// optional default value: this is `impl<T: FromFormValue> FromForm for T`:
|
||||
///
|
||||
/// 1. **Initialization.** The context stores parsing options and an `Option`
|
||||
/// of `Result<T, T::Error>` for storing the `result` of parsing `T`, which is
|
||||
/// initially set to `None`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// struct Context<T> {
|
||||
/// opts: FormOptions,
|
||||
/// result: Option<Result<T, T::Error>>,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 2. **Push.** The field is treated as a string. If `context.result` is `None`,
|
||||
/// `T` is parsed from `field`, and the result is stored in `context.result`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||
/// if this.result.is_none() {
|
||||
/// this.result = Some(Self::from_value(field));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 3. **Finalization.** If `context.result` is `None` and `T` has a default,
|
||||
/// the default is returned; otherwise a `Missing` error is returned. If
|
||||
/// `context.result` is `Some(v)`, the result `v` is returned.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn finalize(this: Self::Context) -> Result<Self, Self::Error> {
|
||||
/// match this.result {
|
||||
/// Some(value) => Ok(value),
|
||||
/// None => match <T as FromFormValue>::default() {
|
||||
/// Some(default) => Ok(default),
|
||||
/// None => Err(Error::Missing)
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This implementation is complete, barring checking for duplicate pushes when
|
||||
/// paring is requested as `strict`.
|
||||
///
|
||||
/// ## Maps w/named Fields (`struct`)
|
||||
///
|
||||
/// A `struct` with named fields parses values of multiple types, indexed by the
|
||||
/// name of its fields:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// struct Dog { name: String, barks: bool, friends: Vec<Cat>, }
|
||||
/// struct Cat { name: String, meows: bool }
|
||||
/// ```
|
||||
///
|
||||
/// Candidates for parsing into a `Dog` include:
|
||||
///
|
||||
/// * `name=Fido&barks=0`
|
||||
///
|
||||
/// `Dog { "Fido", false }`
|
||||
///
|
||||
/// * `name=Fido&barks=1&friends[0]name=Sally&friends[0]meows=0`
|
||||
/// `name=Fido&barks=1&friends[0].name=Sally&friends[0].meows=0`
|
||||
/// `name=Fido&barks=1&friends.0.name=Sally&friends.0.meows=0`
|
||||
///
|
||||
/// `Dog { "Fido", true, vec![Cat { "Sally", false }] }`
|
||||
///
|
||||
/// Parsers for structs are code-generated to proceed as follows:
|
||||
///
|
||||
/// 1. **Initialization.** The context stores parsing options, a `T::Context`
|
||||
/// for each field of type `T`, and a vector called `extra`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// struct Context<'v> {
|
||||
/// opts: FormOptions,
|
||||
/// field_a: A::Context,
|
||||
/// field_b: B::Context,
|
||||
/// /* ... */
|
||||
/// extra: Vec<FormField<'v>>
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 2. **Push.** The index of the first key is compared to known field names.
|
||||
/// If none matches, the index is added to `extra`. Otherwise the key is
|
||||
/// stripped from the field, and the remaining field is pushed to `T`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||
/// match field.key() {
|
||||
/// "field_a" => A::push(&mut this.field_a, field.next()),
|
||||
/// "field_b" => B::push(&mut this.field_b, field.next()),
|
||||
/// /* ... */
|
||||
/// _ => this.extra.push(field)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 3. **Finalization.** Every context is finalized; errors and `Ok` values
|
||||
/// are collected. If parsing is strict and extras is non-empty, an error
|
||||
/// added to the collection of errors. If there are no errors, all `Ok`
|
||||
/// values are used to create the `struct`, and the created struct is
|
||||
/// returned. Otherwise, `Err(errors)` is returned.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
||||
/// let mut errors = vec![];
|
||||
///
|
||||
/// let field_a = A::finalize(&mut this.field_a)
|
||||
/// .map_err(|e| errors.push(e))
|
||||
/// .map(Some).unwrap_or(None);
|
||||
///
|
||||
/// let field_b = B::finblize(&mut this.field_b)
|
||||
/// .map_err(|e| errors.push(e))
|
||||
/// .map(Some).unwrap_or(None);
|
||||
///
|
||||
/// /* .. */
|
||||
///
|
||||
/// if !errors.is_empty() {
|
||||
/// return Err(Values(errors));
|
||||
/// } else if this.opts.is_strict() && !this.extra.is_empty() {
|
||||
/// return Err(Extra(this.extra));
|
||||
/// } else {
|
||||
/// // NOTE: All unwraps will succeed since `errors.is_empty()`.
|
||||
/// Struct {
|
||||
/// field_a: field_a.unwrap(),
|
||||
/// field_b: field_b.unwrap(),
|
||||
/// /* .. */
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Sequences: (`Vec<T: FromForm>`)
|
||||
///
|
||||
/// A `Vec<T: FromForm>` invokes `T`'s push-parser on every push, adding instances
|
||||
/// of `T` to an internal vector. The instance of `T` whose parser is invoked
|
||||
/// depends on the index of the first key:
|
||||
///
|
||||
/// * if it is the first push, the index differs from the previous, or there is no
|
||||
/// index, a new `T::Context` is `init`ialized and added to the internal vector
|
||||
/// * if the index matches the previously seen index, the last initialized
|
||||
/// `T::Context` is `push`ed to.
|
||||
///
|
||||
/// For instance, the sequentially pushed values `=1`, `=2`, and `=3` for a
|
||||
/// `Vec<usize>` (or any other integer) is expected to parse as `vec![1, 2, 3]`. The
|
||||
/// same is true for `[]=1&[]=2&[]=3`. In the first example (`=1&..`), the fields
|
||||
/// passed to `Vec`'s push-parser (`=1`, ..) have no key and thus no index. In the
|
||||
/// second example (`[]=1&..`), the key is `[]` (`[]=1`) without an index. In both
|
||||
/// cases, there is no index. The `Vec` parser takes this to mean that a _new_ `T`
|
||||
/// should be parsed using the field's value.
|
||||
///
|
||||
/// If, instead, the index was non-empty and equal to the index of the field in the
|
||||
/// _previous_ push, `Vec` pushes the value to the parser of the previously parsed
|
||||
/// `T`: `[]=1&[0]=2&[0]=3` results in `vec![1, 2]` and `[0]=1&[0]=2&[]=3` results
|
||||
/// in `vec![1, 3]` (see [`FromFormValue`]).
|
||||
///
|
||||
/// This generalizes. Consider a `Vec<Vec<usize>>` named `x`, so `x` and an
|
||||
/// optional `=` are stripped before being passed to `Vec`'s push-parser:
|
||||
///
|
||||
/// * `x=1&x=2&x=3` parses as `vec![vec![1], vec![2], vec![3]]`
|
||||
///
|
||||
/// Every push (`1`, `2`, `3`) has no key, thus no index: a new `T` (here,
|
||||
/// `Vec<usize>`) is thus initialized for every `push()` and passed the
|
||||
/// value (here, `1`, `2`, and `3`). Each of these `push`es proceeds
|
||||
/// recursively: every push again has no key, thus no index, so a new `T` is
|
||||
/// initialized for every push (now a `usize`), which finally parse as
|
||||
/// integers `1`, `2`, and `3`.
|
||||
///
|
||||
/// Note: `x=1&x=2&x=3` _also_ can also parse as `vec![1, 2, 3]` when viewed
|
||||
/// as a `Vec<usize>`; this is the non-self-descriptive part of the format.
|
||||
///
|
||||
/// * `x[]=1&x[]=2&x[]=3` parses as `vec![vec![1], vec![2], vec![3]]`
|
||||
///
|
||||
/// This proceeds nearly identically to the previous example, with the exception
|
||||
/// that the top-level `Vec` sees the values `[]=1`, `[]=2`, and `[]=3`.
|
||||
///
|
||||
/// * `x[0]=1&x[0]=2&x[]=3` parses as `vec![vec![1, 2], vec![3]]`
|
||||
///
|
||||
/// The top-level `Vec` sees the values `[0]=1`, `[0]=2`, and `[]=3`. The first
|
||||
/// value results in a new `Vec<usize>` being initialized, as before, which is
|
||||
/// pushed a `1`. The second value has the same index as the first, `0`, and so
|
||||
/// `2` is pushed to the previous `T`, the `Vec` which contains the `1`.
|
||||
/// Finally, the third value has no index, so a new `Vec<usize>` is initialized
|
||||
/// and pushed a `3`.
|
||||
///
|
||||
/// * `x[0]=1&x[0]=2&x[]=3&x[]=4` parses as `vec![vec![1, 2], vec![3], vec![4]]`
|
||||
/// * `x[0]=1&x[0]=2&x[1]=3&x[1]=4` parses as `vec![vec![1, 2], vec![3, 4]]`
|
||||
///
|
||||
/// The indexing kind `[]` is purely by convention: the first two examples are
|
||||
/// equivalent to `x.=1&x.=2`, while the third to `x.0=1&x.0=&x.=3`.
|
||||
///
|
||||
/// The parser proceeds as follows:
|
||||
///
|
||||
/// 1. **Initialization.** The context stores parsing options, the
|
||||
/// `last_index` encountered in a `push`, an `Option` of a `T::Context` for
|
||||
/// the `current` value being parsed, a `Vec<T::Errors>` of `errors`, and
|
||||
/// finally a `Vec<T>` of already parsed `items`.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// struct VecContext<'v, T: FromForm<'v>> {
|
||||
/// opts: FormOptions,
|
||||
/// last_index: Index<'v>,
|
||||
/// current: Option<T::Context>,
|
||||
/// errors: Vec<T::Error>,
|
||||
/// items: Vec<T>
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 2. **Push.** The index of the first key is compared against `last_index`.
|
||||
/// If it differs, a new context for `T` is created and the previous is
|
||||
/// finalized. The `Ok` result from finalization is stored in `items` and
|
||||
/// the `Err` in `errors`. Otherwise the `index` is the same, the `current`
|
||||
/// context is retrieved, and the field stripped of the current key is
|
||||
/// pushed to `T`. `last_index` is updated.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||
/// if this.last_index != field.index() {
|
||||
/// this.shift(); // finalize `current`, add to `items`, `errors`
|
||||
/// let mut context = T::init(this.opts);
|
||||
/// T::push(&mut context, field.next());
|
||||
/// this.current = Some(context);
|
||||
/// } else {
|
||||
/// let context = this.current.as_mut();
|
||||
/// T::push(context, field.next())
|
||||
/// }
|
||||
///
|
||||
/// this.last_index = field.index();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 3. **Finalization.** Any `current` context is finalized, storing the `Ok`
|
||||
/// or `Err` as before. `Ok(items)` is returned if `errors` is empty,
|
||||
/// otherwise `Err(errors)` is returned.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
||||
/// this.shift(); // finalizes `current`, as before.
|
||||
/// match this.errors.is_empty() {
|
||||
/// true => Ok(this.items),
|
||||
/// false => Err(this.errors)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Arbitrary Maps (`HashMap<K: FromForm, V: FromForm>`)
|
||||
///
|
||||
/// A `HashMap<K, V>` can be parsed from keys with one index or, for composite
|
||||
/// key values, such as structures or sequences, multiple indices. We begin with
|
||||
/// a discussion of the simpler case: non-composite keys.
|
||||
///
|
||||
/// ### Non-Composite Keys
|
||||
///
|
||||
/// A non-composite value can be described by a single field with no indices.
|
||||
/// Strings and integers are examples of non-composite values. The push-parser
|
||||
/// for `HashMap<K, V>` for a non-composite `K` uses the index of the first key
|
||||
/// as the value of `K`; the remainder of the field is pushed to `V`'s parser:
|
||||
///
|
||||
/// 1. **Initialization.** The context stores a column-based representation of
|
||||
/// `keys` and `values`, a `key_map` from a string key to the column index,
|
||||
/// an `errors` vector for storing errors as they arise, and the parsing
|
||||
/// options.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// struct MapContext<'v, K: FromForm<'v>, V: FromForm<'v>> {
|
||||
/// opts: FormOptions,
|
||||
/// key_map: HashMap<&'v str, usize>,
|
||||
/// keys: Vec<K::Context>,
|
||||
/// values: Vec<V::Context>,
|
||||
/// errors: Vec<MapError<'v, K::Error, V::Error>>,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 2. **Push.** The `key_map` index for the key associated with the index of
|
||||
/// the first key in the field is retrieved. If such a key has not yet been
|
||||
/// seen, a new key and value context are created, the key is pushed to
|
||||
/// `K`'s parser, and the field minus the first key is pushed to `V`'s
|
||||
/// parser.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||
/// let key = field.index();
|
||||
/// let value_context = match this.key_map.get(Key) {
|
||||
/// Some(i) => &mut this.values[i],
|
||||
/// None => {
|
||||
/// let i = this.keys.len();
|
||||
/// this.key_map.insert(key, i);
|
||||
/// this.keys.push(K::init(this.opts));
|
||||
/// this.values.push(V::init(this.opts));
|
||||
/// K::push(&mut this.keys[i], key.into());
|
||||
/// &mut this.values[i]
|
||||
/// }
|
||||
/// };
|
||||
///
|
||||
/// V::push(value_context, field.next());
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 3. **Finalization.** All key and value contexts are finalized; any errors
|
||||
/// are collected in `errors`. If there are no errors, `keys` and `values`
|
||||
/// are collected into a `HashMap` and returned. Otherwise, the errors are
|
||||
/// returned.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
||||
/// this.finalize_keys();
|
||||
/// this.finalize_values();
|
||||
/// if this.errors.is_empty() {
|
||||
/// Ok(this.keys.into_iter().zip(this.values.into_iter()).collect())
|
||||
/// } else {
|
||||
/// Err(this.errors)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Examples of forms parseable via this parser are:
|
||||
///
|
||||
/// * `x[0].name=Bob&x[0].meows=true`as a `HashMap<usize, Cat>` parses with
|
||||
/// `0` mapping to `Cat { name: "Bob", meows: true }`
|
||||
/// * `x[0]name=Bob&x[0]meows=true`as a `HashMap<usize, Cat>` parses just as
|
||||
/// above.
|
||||
/// * `x[0]=Bob&x[0]=Sally&x[1]=Craig`as a `HashMap<usize, Vec<String>>`
|
||||
/// just as `{ 0 => vec!["Bob", "Sally"], 1 => vec!["Craig"] }`.
|
||||
///
|
||||
/// A `HashMap<K, V>` can be thought of as a vector of key-value pairs: `Vec<(K,
|
||||
/// V)` (row-based) or equivalently, as two vectors of keys and values: `Vec<K>`
|
||||
/// and `Vec<V>` (column-based). The implication is that indexing into a
|
||||
/// specific key or value requires _two_ indexes: the first to determine whether
|
||||
/// a key or value is being indexed to, and the second to determine _which_ key
|
||||
/// or value. The push-parser for maps thus optionally accepts two indexes for a
|
||||
/// single key to allow piece-by-piece build-up of arbitrary keys and values.
|
||||
///
|
||||
/// The parser proceeds as follows:
|
||||
///
|
||||
/// 1. **Initialization.** The context stores parsing options, a vector of
|
||||
/// `key_contexts: Vec<K::Context>`, a vector of `value_contexts:
|
||||
/// Vec<V::Context>`, a `mapping` from a string index to an integer index
|
||||
/// into the `contexts`, and a vector of `errors`.
|
||||
/// 2. **Push.** An index is required; an error is emitted and `push` returns
|
||||
/// if they field's first key does not contain an index. If the first key
|
||||
/// contains _one_ index, a new `K::Context` and `V::Context` are created.
|
||||
/// The key is pushed as the value to `K` and the remaining field as the
|
||||
/// value to `V`. The key and value are finalized; if both succeed, the key
|
||||
/// and value are stored in `keys` and `values`; otherwise the error(s) is
|
||||
/// stored in `errors`.
|
||||
///
|
||||
/// If the first keys contains _two_ indices, the first must starts with
|
||||
/// `k` or `v`, while the `second` is arbitrary. `mapping` is indexed by
|
||||
/// `second`; the integer is retrieved. If none exists, new contexts are
|
||||
/// created an added to `{key,value}_contexts`, and their index is mapped
|
||||
/// to `second` in `mapping`. If the first index is `k`, the field,
|
||||
/// stripped of the first key, is pushed to the key's context; the same is
|
||||
/// done for the value's context is the first index is `v`.
|
||||
/// 3. **Finalization.** Every context is finalized; errors and `Ok` values
|
||||
/// are collected. TODO: FINISH. Split this into two: one for single-index,
|
||||
/// another for two-indices.
|
||||
//! # 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 paring is requested as `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.
|
||||
|
||||
// ## Maps w/named Fields (`struct`)
|
||||
//
|
||||
// A `struct` with named fields parses values of multiple types, indexed by the
|
||||
// name of its fields:
|
||||
//
|
||||
// ```rust,ignore
|
||||
// struct Dog { name: String, barks: bool, friends: Vec<Cat>, }
|
||||
// struct Cat { name: String, meows: bool }
|
||||
// ```
|
||||
//
|
||||
// Candidates for parsing into a `Dog` include:
|
||||
//
|
||||
// * `name=Fido&barks=0`
|
||||
//
|
||||
// `Dog { "Fido", false }`
|
||||
//
|
||||
// * `name=Fido&barks=1&friends[0]name=Sally&friends[0]meows=0`
|
||||
// `name=Fido&barks=1&friends[0].name=Sally&friends[0].meows=0`
|
||||
// `name=Fido&barks=1&friends.0.name=Sally&friends.0.meows=0`
|
||||
//
|
||||
// `Dog { "Fido", true, vec![Cat { "Sally", false }] }`
|
||||
//
|
||||
// Parsers for structs are code-generated to proceed as follows:
|
||||
//
|
||||
// 1. **Initialization.** The context stores parsing options, a `T::Context`
|
||||
// for each field of type `T`, and a vector called `extra`.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// struct Context<'v> {
|
||||
// opts: FormOptions,
|
||||
// field_a: A::Context,
|
||||
// field_b: B::Context,
|
||||
// /* ... */
|
||||
// extra: Vec<FormField<'v>>
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// 2. **Push.** The index of the first key is compared to known field names.
|
||||
// If none matches, the index is added to `extra`. Otherwise the key is
|
||||
// stripped from the field, and the remaining field is pushed to `T`.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||
// match field.key() {
|
||||
// "field_a" => A::push(&mut this.field_a, field.next()),
|
||||
// "field_b" => B::push(&mut this.field_b, field.next()),
|
||||
// /* ... */
|
||||
// _ => this.extra.push(field)
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// 3. **Finalization.** Every context is finalized; errors and `Ok` values
|
||||
// are collected. If parsing is strict and extras is non-empty, an error
|
||||
// added to the collection of errors. If there are no errors, all `Ok`
|
||||
// values are used to create the `struct`, and the created struct is
|
||||
// returned. Otherwise, `Err(errors)` is returned.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
||||
// let mut errors = vec![];
|
||||
//
|
||||
// let field_a = A::finalize(&mut this.field_a)
|
||||
// .map_err(|e| errors.push(e))
|
||||
// .map(Some).unwrap_or(None);
|
||||
//
|
||||
// let field_b = B::finblize(&mut this.field_b)
|
||||
// .map_err(|e| errors.push(e))
|
||||
// .map(Some).unwrap_or(None);
|
||||
//
|
||||
// /* .. */
|
||||
//
|
||||
// if !errors.is_empty() {
|
||||
// return Err(Values(errors));
|
||||
// } else if this.opts.is_strict() && !this.extra.is_empty() {
|
||||
// return Err(Extra(this.extra));
|
||||
// } else {
|
||||
// // NOTE: All unwraps will succeed since `errors.is_empty()`.
|
||||
// Struct {
|
||||
// field_a: field_a.unwrap(),
|
||||
// field_b: field_b.unwrap(),
|
||||
// /* .. */
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// ## Sequences: (`Vec<T: FromForm>`)
|
||||
//
|
||||
// A `Vec<T: FromForm>` invokes `T`'s push-parser on every push, adding instances
|
||||
// of `T` to an internal vector. The instance of `T` whose parser is invoked
|
||||
// depends on the index of the first key:
|
||||
//
|
||||
// * if it is the first push, the index differs from the previous, or there is no
|
||||
// index, a new `T::Context` is `init`ialized and added to the internal vector
|
||||
// * if the index matches the previously seen index, the last initialized
|
||||
// `T::Context` is `push`ed to.
|
||||
//
|
||||
// For instance, the sequentially pushed values `=1`, `=2`, and `=3` for a
|
||||
// `Vec<usize>` (or any other integer) is expected to parse as `vec![1, 2, 3]`. The
|
||||
// same is true for `[]=1&[]=2&[]=3`. In the first example (`=1&..`), the fields
|
||||
// passed to `Vec`'s push-parser (`=1`, ..) have no key and thus no index. In the
|
||||
// second example (`[]=1&..`), the key is `[]` (`[]=1`) without an index. In both
|
||||
// cases, there is no index. The `Vec` parser takes this to mean that a _new_ `T`
|
||||
// should be parsed using the field's value.
|
||||
//
|
||||
// If, instead, the index was non-empty and equal to the index of the field in the
|
||||
// _previous_ push, `Vec` pushes the value to the parser of the previously parsed
|
||||
// `T`: `[]=1&[0]=2&[0]=3` results in `vec![1, 2]` and `[0]=1&[0]=2&[]=3` results
|
||||
// in `vec![1, 3]` (see [`FromFormValue`]).
|
||||
//
|
||||
// This generalizes. Consider a `Vec<Vec<usize>>` named `x`, so `x` and an
|
||||
// optional `=` are stripped before being passed to `Vec`'s push-parser:
|
||||
//
|
||||
// * `x=1&x=2&x=3` parses as `vec![vec![1], vec![2], vec![3]]`
|
||||
//
|
||||
// Every push (`1`, `2`, `3`) has no key, thus no index: a new `T` (here,
|
||||
// `Vec<usize>`) is thus initialized for every `push()` and passed the
|
||||
// value (here, `1`, `2`, and `3`). Each of these `push`es proceeds
|
||||
// recursively: every push again has no key, thus no index, so a new `T` is
|
||||
// initialized for every push (now a `usize`), which finally parse as
|
||||
// integers `1`, `2`, and `3`.
|
||||
//
|
||||
// Note: `x=1&x=2&x=3` _also_ can also parse as `vec![1, 2, 3]` when viewed
|
||||
// as a `Vec<usize>`; this is the non-self-descriptive part of the format.
|
||||
//
|
||||
// * `x[]=1&x[]=2&x[]=3` parses as `vec![vec![1], vec![2], vec![3]]`
|
||||
//
|
||||
// This proceeds nearly identically to the previous example, with the exception
|
||||
// that the top-level `Vec` sees the values `[]=1`, `[]=2`, and `[]=3`.
|
||||
//
|
||||
// * `x[0]=1&x[0]=2&x[]=3` parses as `vec![vec![1, 2], vec![3]]`
|
||||
//
|
||||
// The top-level `Vec` sees the values `[0]=1`, `[0]=2`, and `[]=3`. The first
|
||||
// value results in a new `Vec<usize>` being initialized, as before, which is
|
||||
// pushed a `1`. The second value has the same index as the first, `0`, and so
|
||||
// `2` is pushed to the previous `T`, the `Vec` which contains the `1`.
|
||||
// Finally, the third value has no index, so a new `Vec<usize>` is initialized
|
||||
// and pushed a `3`.
|
||||
//
|
||||
// * `x[0]=1&x[0]=2&x[]=3&x[]=4` parses as `vec![vec![1, 2], vec![3], vec![4]]`
|
||||
// * `x[0]=1&x[0]=2&x[1]=3&x[1]=4` parses as `vec![vec![1, 2], vec![3, 4]]`
|
||||
//
|
||||
// The indexing kind `[]` is purely by convention: the first two examples are
|
||||
// equivalent to `x.=1&x.=2`, while the third to `x.0=1&x.0=&x.=3`.
|
||||
//
|
||||
// The parser proceeds as follows:
|
||||
//
|
||||
// 1. **Initialization.** The context stores parsing options, the
|
||||
// `last_index` encountered in a `push`, an `Option` of a `T::Context` for
|
||||
// the `current` value being parsed, a `Vec<T::Errors>` of `errors`, and
|
||||
// finally a `Vec<T>` of already parsed `items`.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// struct VecContext<'v, T: FromForm<'v>> {
|
||||
// opts: FormOptions,
|
||||
// last_index: Index<'v>,
|
||||
// current: Option<T::Context>,
|
||||
// errors: Vec<T::Error>,
|
||||
// items: Vec<T>
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// 2. **Push.** The index of the first key is compared against `last_index`.
|
||||
// If it differs, a new context for `T` is created and the previous is
|
||||
// finalized. The `Ok` result from finalization is stored in `items` and
|
||||
// the `Err` in `errors`. Otherwise the `index` is the same, the `current`
|
||||
// context is retrieved, and the field stripped of the current key is
|
||||
// pushed to `T`. `last_index` is updated.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||
// if this.last_index != field.index() {
|
||||
// this.shift(); // finalize `current`, add to `items`, `errors`
|
||||
// let mut context = T::init(this.opts);
|
||||
// T::push(&mut context, field.next());
|
||||
// this.current = Some(context);
|
||||
// } else {
|
||||
// let context = this.current.as_mut();
|
||||
// T::push(context, field.next())
|
||||
// }
|
||||
//
|
||||
// this.last_index = field.index();
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// 3. **Finalization.** Any `current` context is finalized, storing the `Ok`
|
||||
// or `Err` as before. `Ok(items)` is returned if `errors` is empty,
|
||||
// otherwise `Err(errors)` is returned.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
||||
// this.shift(); // finalizes `current`, as before.
|
||||
// match this.errors.is_empty() {
|
||||
// true => Ok(this.items),
|
||||
// false => Err(this.errors)
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// ## Arbitrary Maps (`HashMap<K: FromForm, V: FromForm>`)
|
||||
//
|
||||
// A `HashMap<K, V>` can be parsed from keys with one index or, for composite
|
||||
// key values, such as structures or sequences, multiple indices. We begin with
|
||||
// a discussion of the simpler case: non-composite keys.
|
||||
//
|
||||
// ### Non-Composite Keys
|
||||
//
|
||||
// A non-composite value can be described by a single field with no indices.
|
||||
// Strings and integers are examples of non-composite values. The push-parser
|
||||
// for `HashMap<K, V>` for a non-composite `K` uses the index of the first key
|
||||
// as the value of `K`; the remainder of the field is pushed to `V`'s parser:
|
||||
//
|
||||
// 1. **Initialization.** The context stores a column-based representation of
|
||||
// `keys` and `values`, a `key_map` from a string key to the column index,
|
||||
// an `errors` vector for storing errors as they arise, and the parsing
|
||||
// options.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// struct MapContext<'v, K: FromForm<'v>, V: FromForm<'v>> {
|
||||
// opts: FormOptions,
|
||||
// key_map: HashMap<&'v str, usize>,
|
||||
// keys: Vec<K::Context>,
|
||||
// values: Vec<V::Context>,
|
||||
// errors: Vec<MapError<'v, K::Error, V::Error>>,
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// 2. **Push.** The `key_map` index for the key associated with the index of
|
||||
// the first key in the field is retrieved. If such a key has not yet been
|
||||
// seen, a new key and value context are created, the key is pushed to
|
||||
// `K`'s parser, and the field minus the first key is pushed to `V`'s
|
||||
// parser.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||
// let key = field.index();
|
||||
// let value_context = match this.key_map.get(Key) {
|
||||
// Some(i) => &mut this.values[i],
|
||||
// None => {
|
||||
// let i = this.keys.len();
|
||||
// this.key_map.insert(key, i);
|
||||
// this.keys.push(K::init(this.opts));
|
||||
// this.values.push(V::init(this.opts));
|
||||
// K::push(&mut this.keys[i], key.into());
|
||||
// &mut this.values[i]
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// V::push(value_context, field.next());
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// 3. **Finalization.** All key and value contexts are finalized; any errors
|
||||
// are collected in `errors`. If there are no errors, `keys` and `values`
|
||||
// are collected into a `HashMap` and returned. Otherwise, the errors are
|
||||
// returned.
|
||||
//
|
||||
// ```rust,ignore
|
||||
// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
||||
// this.finalize_keys();
|
||||
// this.finalize_values();
|
||||
// if this.errors.is_empty() {
|
||||
// Ok(this.keys.into_iter().zip(this.values.into_iter()).collect())
|
||||
// } else {
|
||||
// Err(this.errors)
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// Examples of forms parseable via this parser are:
|
||||
//
|
||||
// * `x[0].name=Bob&x[0].meows=true`as a `HashMap<usize, Cat>` parses with
|
||||
// `0` mapping to `Cat { name: "Bob", meows: true }`
|
||||
// * `x[0]name=Bob&x[0]meows=true`as a `HashMap<usize, Cat>` parses just as
|
||||
// above.
|
||||
// * `x[0]=Bob&x[0]=Sally&x[1]=Craig`as a `HashMap<usize, Vec<String>>`
|
||||
// just as `{ 0 => vec!["Bob", "Sally"], 1 => vec!["Craig"] }`.
|
||||
//
|
||||
// A `HashMap<K, V>` can be thought of as a vector of key-value pairs: `Vec<(K,
|
||||
// V)` (row-based) or equivalently, as two vectors of keys and values: `Vec<K>`
|
||||
// and `Vec<V>` (column-based). The implication is that indexing into a
|
||||
// specific key or value requires _two_ indexes: the first to determine whether
|
||||
// a key or value is being indexed to, and the second to determine _which_ key
|
||||
// or value. The push-parser for maps thus optionally accepts two indexes for a
|
||||
// single key to allow piece-by-piece build-up of arbitrary keys and values.
|
||||
//
|
||||
// The parser proceeds as follows:
|
||||
//
|
||||
// 1. **Initialization.** The context stores parsing options, a vector of
|
||||
// `key_contexts: Vec<K::Context>`, a vector of `value_contexts:
|
||||
// Vec<V::Context>`, a `mapping` from a string index to an integer index
|
||||
// into the `contexts`, and a vector of `errors`.
|
||||
// 2. **Push.** An index is required; an error is emitted and `push` returns
|
||||
// if they field's first key does not contain an index. If the first key
|
||||
// contains _one_ index, a new `K::Context` and `V::Context` are created.
|
||||
// The key is pushed as the value to `K` and the remaining field as the
|
||||
// value to `V`. The key and value are finalized; if both succeed, the key
|
||||
// and value are stored in `keys` and `values`; otherwise the error(s) is
|
||||
// stored in `errors`.
|
||||
//
|
||||
// If the first keys contains _two_ indices, the first must starts with
|
||||
// `k` or `v`, while the `second` is arbitrary. `mapping` is indexed by
|
||||
// `second`; the integer is retrieved. If none exists, new contexts are
|
||||
// created an added to `{key,value}_contexts`, and their index is mapped
|
||||
// to `second` in `mapping`. If the first index is `k`, the field,
|
||||
// stripped of the first key, is pushed to the key's context; the same is
|
||||
// done for the value's context is the first index is `v`.
|
||||
// 3. **Finalization.** Every context is finalized; errors and `Ok` values
|
||||
// are collected. TODO: FINISH. Split this into two: one for single-index,
|
||||
// another for two-indices.
|
||||
|
||||
mod field;
|
||||
mod options;
|
||||
|
@ -450,6 +460,7 @@ pub mod error;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Type alias for `Result` with an error type of [`Errors`].
|
||||
pub type Result<'v, T> = std::result::Result<T, Errors<'v>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
|
|
|
@ -66,7 +66,7 @@ impl Name {
|
|||
type Item = &'v Key;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.0.is_terminal() {
|
||||
if self.0.exhausted() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ impl Name {
|
|||
type Item = &'v Name;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.0.is_terminal() {
|
||||
if self.0.exhausted() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -476,16 +476,55 @@ impl<'v> NameView<'v> {
|
|||
|
||||
/// Shifts the current key once to the right.
|
||||
///
|
||||
/// # Example
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::name::NameView;
|
||||
///
|
||||
/// let mut view = NameView::new("a.b[c:d]");
|
||||
/// let mut view = NameView::new("a.b[c:d][d.e]");
|
||||
/// assert_eq!(view.key().unwrap(), "a");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key().unwrap(), "b");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key().unwrap(), "c:d");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key().unwrap(), "d.e");
|
||||
/// ```
|
||||
///
|
||||
/// Malformed strings can have interesting results:
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::name::NameView;
|
||||
///
|
||||
/// let mut view = NameView::new("a[c.d");
|
||||
/// assert_eq!(view.key_lossy(), "a");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key_lossy(), "c.d");
|
||||
///
|
||||
/// let mut view = NameView::new("a[c[.d]");
|
||||
/// assert_eq!(view.key_lossy(), "a");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key_lossy(), "c[.d");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key(), None);
|
||||
///
|
||||
/// let mut view = NameView::new("foo[c[.d]]");
|
||||
/// assert_eq!(view.key_lossy(), "foo");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key_lossy(), "c[.d");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key_lossy(), "]");
|
||||
///
|
||||
/// view.shift();
|
||||
/// assert_eq!(view.key(), None);
|
||||
/// ```
|
||||
pub fn shift(&mut self) {
|
||||
const START_DELIMS: &'static [char] = &['.', '['];
|
||||
|
@ -494,13 +533,10 @@ impl<'v> NameView<'v> {
|
|||
let bytes = string.as_bytes();
|
||||
let shift = match bytes.get(0) {
|
||||
None | Some(b'=') => 0,
|
||||
Some(b'[') => match string[1..].find(&[']', '.'][..]) {
|
||||
Some(j) => match string[1..].as_bytes()[j] {
|
||||
b']' => j + 2,
|
||||
_ => j + 1,
|
||||
}
|
||||
Some(b'[') => match memchr::memchr(b']', bytes) {
|
||||
Some(j) => j + 1,
|
||||
None => bytes.len(),
|
||||
}
|
||||
},
|
||||
Some(b'.') => match string[1..].find(START_DELIMS) {
|
||||
Some(j) => j + 1,
|
||||
None => bytes.len(),
|
||||
|
@ -569,6 +605,7 @@ impl<'v> NameView<'v> {
|
|||
let key = match view.as_bytes().get(0) {
|
||||
Some(b'.') => &view[1..],
|
||||
Some(b'[') if view.ends_with(']') => &view[1..view.len() - 1],
|
||||
Some(b'[') if self.is_at_last() => &view[1..],
|
||||
_ => view
|
||||
};
|
||||
|
||||
|
@ -643,7 +680,13 @@ impl<'v> NameView<'v> {
|
|||
self.name
|
||||
}
|
||||
|
||||
fn is_terminal(&self) -> bool {
|
||||
// This is the last key. The next `shift()` will exhaust `self`.
|
||||
fn is_at_last(&self) -> bool {
|
||||
self.end == self.name.len()
|
||||
}
|
||||
|
||||
// There are no more keys. A `shift` will do nothing.
|
||||
fn exhausted(&self) -> bool {
|
||||
self.start == self.name.len()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
/// Form guard options.
|
||||
///
|
||||
/// See [`Form#leniency`](crate::form::Form#leniency) for details.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Options {
|
||||
/// Whether parsing should be strict (no extra parameters) or not.
|
||||
pub strict: bool,
|
||||
}
|
||||
|
||||
#[allow(non_upper_case_globals, dead_code)]
|
||||
impl Options {
|
||||
/// `Options` with `strict` set to `false`.
|
||||
pub const Lenient: Self = Options { strict: false };
|
||||
|
||||
/// `Options` with `strict` set to `true`.
|
||||
pub const Strict: Self = Options { strict: true };
|
||||
}
|
||||
|
|
|
@ -148,6 +148,8 @@ mod raw_str_parse_tests {
|
|||
}
|
||||
|
||||
impl<'r, 'i> MultipartParser<'r, 'i> {
|
||||
/// Returns `None` when there are no further fields. Otherwise tries to
|
||||
/// parse the next multipart form field and returns the result.
|
||||
async fn next(&mut self) -> Option<Result<'r, Field<'r, 'i>>> {
|
||||
if self.done {
|
||||
return None;
|
||||
|
|
|
@ -1,68 +1,332 @@
|
|||
use std::{borrow::Cow, convert::TryInto, fmt::Display, ops::{RangeBounds, Bound}};
|
||||
//! Form field validation routines.
|
||||
//!
|
||||
//! Each function in this module can be used as the target of the
|
||||
//! `field(validate)` field attribute of the `FromForm` derive.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use rocket::form::FromForm;
|
||||
//!
|
||||
//! #[derive(FromForm)]
|
||||
//! struct MyForm<'r> {
|
||||
//! #[field(validate = range(2..10))]
|
||||
//! id: usize,
|
||||
//! #[field(validate = omits("password"))]
|
||||
//! password: &'r str,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The `validate` parameter takes any expression that returns a
|
||||
//! [`form::Result<()>`](crate::form::Result). If the expression is a function
|
||||
//! call, a reference to the field is inserted as the first parameter. Thus,
|
||||
//! functions calls to `validate` must take a reference to _some_ type,
|
||||
//! typically a generic with some bounds, as their first argument.
|
||||
//!
|
||||
//! ## Custom Error Messages
|
||||
//!
|
||||
//! To set a custom error messages, it is useful to chain results:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use rocket::form::{Errors, Error, FromForm};
|
||||
//!
|
||||
//! #[derive(FromForm)]
|
||||
//! struct MyForm<'r> {
|
||||
//! // By defining another function...
|
||||
//! #[field(validate = omits("password").map_err(pass_help))]
|
||||
//! password: &'r str,
|
||||
//! // or inline using the `msg` helper.
|
||||
//! #[field(validate = omits("password").or_else(msg!("please omit `password`")))]
|
||||
//! password2: &'r str,
|
||||
//! // You can even refer to the field in the message...
|
||||
//! #[field(validate = range(1..).or_else(msg!("`{}` < 1", self.n)))]
|
||||
//! n: isize,
|
||||
//! // ..or other fields!
|
||||
//! #[field(validate = range(..self.n).or_else(msg!("`{}` > `{}`", self.z, self.n)))]
|
||||
//! z: isize,
|
||||
//! }
|
||||
//!
|
||||
//! fn pass_help<'a>(errors: Errors<'_>) -> Errors<'a> {
|
||||
//! Error::validation("passwords can't contain the text \"password\"").into()
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Custom Validation
|
||||
//!
|
||||
//! Custom validation routines can be defined as regular functions. Consider a
|
||||
//! routine that tries to validate a credit card number:
|
||||
//!
|
||||
//! ```rust
|
||||
//! extern crate time;
|
||||
//!
|
||||
//! use rocket::form::{self, FromForm, Error};
|
||||
//!
|
||||
//! #[derive(FromForm)]
|
||||
//! struct CreditCard {
|
||||
//! #[field(validate = luhn(self.cvv, &self.expiration))]
|
||||
//! number: u64,
|
||||
//! cvv: u16,
|
||||
//! expiration: time::Date,
|
||||
//! }
|
||||
//!
|
||||
//! // Implementation of Luhn validator.
|
||||
//! fn luhn<'v>(number: &u64, cvv: u16, exp: &time::Date) -> form::Result<'v, ()> {
|
||||
//! # let valid = false;
|
||||
//! if !valid {
|
||||
//! Err(Error::validation("invalid credit card number"))?;
|
||||
//! }
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::convert::TryInto;
|
||||
use std::ops::{RangeBounds, Bound};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::data::ByteUnit;
|
||||
use rocket_http::ContentType;
|
||||
|
||||
use crate::{data::TempFile, form::error::{Error, Errors}};
|
||||
use crate::{data::TempFile, form::{Result, Error}};
|
||||
|
||||
pub fn eq<'v, A, B>(a: &A, b: B) -> Result<(), Errors<'v>>
|
||||
/// A helper macro for custom validation error messages.
|
||||
///
|
||||
/// The macro works identically to [`std::format!`] except it does not allocate
|
||||
/// when the expression is a string literal. It returns a function (a closure)
|
||||
/// that takes one parameter and evaluates to an `Err` of validation [`Error`]
|
||||
/// with the formatted message. While useful in other contexts, it is designed
|
||||
/// to be chained to validation results via `.or_else()` and `.and_then()`.
|
||||
///
|
||||
/// Note that the macro never needs to be imported when used with a `FromForm`
|
||||
/// derive; all items in [`form::validate`](crate::form::validate) are already
|
||||
/// in scope.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::FromForm;
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Person<'r> {
|
||||
/// #[field(validate = len(3..).or_else(msg!("that's a short name...")))]
|
||||
/// name: &'r str,
|
||||
/// #[field(validate = contains('f').and_then(msg!("please, no `f`!")))]
|
||||
/// non_f_name: &'r str,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See the [top-level docs](crate::form::validate) for more examples.
|
||||
#[macro_export]
|
||||
macro_rules! msg {
|
||||
($e:expr) => (
|
||||
|_| {
|
||||
Err($crate::form::Errors::from($crate::form::Error::validation($e)))
|
||||
as $crate::form::Result<()>
|
||||
}
|
||||
);
|
||||
($($arg:tt)*) => (
|
||||
|_| {
|
||||
Err($crate::form::Errors::from($crate::form::Error::validation(format!($($arg)*))))
|
||||
as $crate::form::Result<()>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use msg;
|
||||
|
||||
/// Equality validator: succeeds exactly when `a` == `b`, using [`PartialEq`].
|
||||
///
|
||||
/// On failure, returns a validation error with the following message:
|
||||
///
|
||||
/// ```text
|
||||
/// value does not match expected value
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{FromForm, FromFormField};
|
||||
///
|
||||
/// #[derive(FromFormField, PartialEq)]
|
||||
/// enum Kind {
|
||||
/// Car,
|
||||
/// Truck
|
||||
/// }
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo<'r> {
|
||||
/// #[field(validate = eq("Bob Marley"))]
|
||||
/// name: &'r str,
|
||||
/// #[field(validate = eq(Kind::Car))]
|
||||
/// vehicle: Kind,
|
||||
/// #[field(validate = eq(&[5, 7, 8]))]
|
||||
/// numbers: Vec<usize>,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn eq<'v, A, B>(a: &A, b: B) -> Result<'v, ()>
|
||||
where A: PartialEq<B>
|
||||
{
|
||||
if a != &b {
|
||||
Err(Error::validation("value does not match"))?
|
||||
Err(Error::validation("value does not match expected value"))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait Len {
|
||||
fn len(&self) -> usize;
|
||||
|
||||
fn len_u64(&self) -> u64 {
|
||||
self.len() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl Len for str {
|
||||
fn len(&self) -> usize { self.len() }
|
||||
}
|
||||
|
||||
impl Len for String {
|
||||
fn len(&self) -> usize { self.len() }
|
||||
}
|
||||
|
||||
impl<T> Len for Vec<T> {
|
||||
fn len(&self) -> usize { <Vec<T>>::len(self) }
|
||||
}
|
||||
|
||||
impl Len for TempFile<'_> {
|
||||
fn len(&self) -> usize { TempFile::len(self) as usize }
|
||||
|
||||
fn len_u64(&self) -> u64 { TempFile::len(self) }
|
||||
}
|
||||
|
||||
impl<K, V> Len for std::collections::HashMap<K, V> {
|
||||
fn len(&self) -> usize { <std::collections::HashMap<K, V>>::len(self) }
|
||||
}
|
||||
|
||||
impl<T: Len + ?Sized> Len for &T {
|
||||
fn len(&self) -> usize {
|
||||
<T as Len>::len(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len<'v, V, R>(value: V, range: R) -> Result<(), Errors<'v>>
|
||||
where V: Len, R: RangeBounds<u64>
|
||||
/// Negative equality validator: succeeds exactly when `a` != `b`, using
|
||||
/// [`PartialEq`].
|
||||
///
|
||||
/// On failure, returns a validation error with the following message:
|
||||
///
|
||||
/// ```text
|
||||
/// value is equal to an invalid value
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{FromForm, FromFormField};
|
||||
///
|
||||
/// #[derive(FromFormField, PartialEq)]
|
||||
/// enum Kind {
|
||||
/// Car,
|
||||
/// Truck
|
||||
/// }
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo<'r> {
|
||||
/// #[field(validate = neq("Bob Marley"))]
|
||||
/// name: &'r str,
|
||||
/// #[field(validate = neq(Kind::Car))]
|
||||
/// vehicle: Kind,
|
||||
/// #[field(validate = neq(&[5, 7, 8]))]
|
||||
/// numbers: Vec<usize>,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn neq<'v, A, B>(a: &A, b: B) -> Result<'v, ()>
|
||||
where A: PartialEq<B>
|
||||
{
|
||||
if !range.contains(&value.len_u64()) {
|
||||
if a == &b {
|
||||
Err(Error::validation("value is equal to an invalid value"))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Types for values that have a length.
|
||||
///
|
||||
/// At present, these are:
|
||||
///
|
||||
/// | type | length description |
|
||||
/// |-----------------------------------|--------------------------------------|
|
||||
/// | `&str`, `String` | length in bytes |
|
||||
/// | `Vec<T>` | number of elements in the vector |
|
||||
/// | `HashMap<K, V>`, `BTreeMap<K, V>` | number of key/value pairs in the map |
|
||||
/// | [`TempFile`] | length of the file in bytes |
|
||||
/// | `Option<T>` where `T: Len` | length of `T` or 0 if `None` |
|
||||
/// | [`form::Result<'_, T>`] | length of `T` or 0 if `Err` |
|
||||
///
|
||||
/// [`form::Result<'_, T>`]: crate::form::Result
|
||||
pub trait Len<L> {
|
||||
/// The length of the value.
|
||||
fn len(&self) -> L;
|
||||
|
||||
/// Convert `len` into `u64`.
|
||||
fn len_into_u64(len: L) -> u64;
|
||||
|
||||
/// The zero value for `L`.
|
||||
fn zero_len() -> L;
|
||||
}
|
||||
|
||||
macro_rules! impl_len {
|
||||
(<$($gen:ident),*> $T:ty => $L:ty) => (
|
||||
impl <$($gen),*> Len<$L> for $T {
|
||||
fn len(&self) -> $L { self.len() }
|
||||
fn len_into_u64(len: $L) -> u64 { len as u64 }
|
||||
fn zero_len() -> $L { 0 }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
impl_len!(<> str => usize);
|
||||
impl_len!(<> String => usize);
|
||||
impl_len!(<T> Vec<T> => usize);
|
||||
impl_len!(<> TempFile<'_> => u64);
|
||||
impl_len!(<K, V> std::collections::HashMap<K, V> => usize);
|
||||
impl_len!(<K, V> std::collections::BTreeMap<K, V> => usize);
|
||||
|
||||
impl Len<ByteUnit> for TempFile<'_> {
|
||||
fn len(&self) -> ByteUnit { self.len().into() }
|
||||
fn len_into_u64(len: ByteUnit) -> u64 { len.into() }
|
||||
fn zero_len() -> ByteUnit { ByteUnit::from(0) }
|
||||
}
|
||||
|
||||
impl<L, T: Len<L> + ?Sized> Len<L> for &T {
|
||||
fn len(&self) -> L { <T as Len<L>>::len(self) }
|
||||
fn len_into_u64(len: L) -> u64 { T::len_into_u64(len) }
|
||||
fn zero_len() -> L { T::zero_len() }
|
||||
}
|
||||
|
||||
impl<L, T: Len<L>> Len<L> for Option<T> {
|
||||
fn len(&self) -> L { self.as_ref().map(|v| v.len()).unwrap_or(T::zero_len()) }
|
||||
fn len_into_u64(len: L) -> u64 { T::len_into_u64(len) }
|
||||
fn zero_len() -> L { T::zero_len() }
|
||||
}
|
||||
|
||||
impl<L, T: Len<L>> Len<L> for Result<'_, T> {
|
||||
fn len(&self) -> L { self.as_ref().ok().len() }
|
||||
fn len_into_u64(len: L) -> u64 { T::len_into_u64(len) }
|
||||
fn zero_len() -> L { T::zero_len() }
|
||||
}
|
||||
|
||||
/// Length validator: succeeds when the length of a value is within a `range`.
|
||||
///
|
||||
/// The value must implement [`Len`]. On failure, returns an [`InvalidLength`]
|
||||
/// error. See [`Len`] for supported types and how their length is computed.
|
||||
///
|
||||
/// [`InvalidLength`]: rocket::form::error::ErrorKind::InvalidLength
|
||||
///
|
||||
/// # Data Limits
|
||||
///
|
||||
/// All form types are constrained by a data limit. As such, the `len()`
|
||||
/// validator should be used only when a data limit is insufficiently specific.
|
||||
/// For example, prefer to use data [`Limits`](crate::data::Limits) to validate
|
||||
/// the length of files as not doing so will result in writing more data to disk
|
||||
/// than necessary.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::http::ContentType;
|
||||
/// use rocket::form::{FromForm, FromFormField};
|
||||
/// use rocket::data::{TempFile, ToByteUnit};
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo<'r> {
|
||||
/// #[field(validate = len(5..20))]
|
||||
/// name: &'r str,
|
||||
/// #[field(validate = len(..=200))]
|
||||
/// maybe_name: Option<&'r str>,
|
||||
/// #[field(validate = len(..=2.mebibytes()))]
|
||||
/// #[field(validate = ext(ContentType::Plain))]
|
||||
/// file: TempFile<'r>,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn len<'v, V, L, R>(value: V, range: R) -> Result<'v, ()>
|
||||
where V: Len<L>,
|
||||
L: Copy + PartialOrd,
|
||||
R: RangeBounds<L>
|
||||
{
|
||||
if !range.contains(&value.len()) {
|
||||
let start = match range.start_bound() {
|
||||
Bound::Included(v) => Some(*v),
|
||||
Bound::Excluded(v) => Some(v.saturating_add(1)),
|
||||
Bound::Included(v) => Some(V::len_into_u64(*v)),
|
||||
Bound::Excluded(v) => Some(V::len_into_u64(*v).saturating_add(1)),
|
||||
Bound::Unbounded => None
|
||||
};
|
||||
|
||||
let end = match range.end_bound() {
|
||||
Bound::Included(v) => Some(*v),
|
||||
Bound::Excluded(v) => Some(v.saturating_sub(1)),
|
||||
Bound::Included(v) => Some(V::len_into_u64(*v)),
|
||||
Bound::Excluded(v) => Some(V::len_into_u64(*v).saturating_sub(1)),
|
||||
Bound::Unbounded => None,
|
||||
};
|
||||
|
||||
|
@ -72,87 +336,42 @@ pub fn len<'v, V, R>(value: V, range: R) -> Result<(), Errors<'v>>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Types for values that contain items.
|
||||
///
|
||||
/// At present, these are:
|
||||
///
|
||||
/// | type | contains |
|
||||
/// |-------------------------|----------------------------|
|
||||
/// | `&str`, `String` | `&str`, `char` |
|
||||
/// | `Vec<T>` | `T`, `&T` |
|
||||
/// | `Option<T>` | `I` where `T: Contains<I>` |
|
||||
/// | [`form::Result<'_, T>`] | `I` where `T: Contains<I>` |
|
||||
///
|
||||
/// [`form::Result<'_, T>`]: crate::form::Result
|
||||
pub trait Contains<I> {
|
||||
/// Returns `true` if `self` contains `item`.
|
||||
fn contains(&self, item: I) -> bool;
|
||||
}
|
||||
|
||||
impl<I, T: Contains<I>> Contains<I> for &T {
|
||||
fn contains(&self, item: I) -> bool {
|
||||
<T as Contains<I>>::contains(self, item)
|
||||
}
|
||||
macro_rules! impl_contains {
|
||||
([$($gen:tt)*] $T:ty [contains] $I:ty [via] $P:ty) => {
|
||||
impl_contains!([$($gen)*] $T [contains] $I [via] $P [with] |v| v);
|
||||
};
|
||||
|
||||
([$($gen:tt)*] $T:ty [contains] $I:ty [via] $P:ty [with] $f:expr) => {
|
||||
impl<$($gen)*> Contains<$I> for $T {
|
||||
fn contains(&self, item: $I) -> bool {
|
||||
<$P>::contains(self, $f(item))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Contains<&str> for str {
|
||||
fn contains(&self, string: &str) -> bool {
|
||||
<str>::contains(self, string)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<&&str> for str {
|
||||
fn contains(&self, string: &&str) -> bool {
|
||||
<str>::contains(self, string)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<char> for str {
|
||||
fn contains(&self, c: char) -> bool {
|
||||
<str>::contains(self, c)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<&char> for str {
|
||||
fn contains(&self, c: &char) -> bool {
|
||||
<str>::contains(self, *c)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<&str> for &str {
|
||||
fn contains(&self, string: &str) -> bool {
|
||||
<str>::contains(self, string)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<&&str> for &str {
|
||||
fn contains(&self, string: &&str) -> bool {
|
||||
<str>::contains(self, string)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<char> for &str {
|
||||
fn contains(&self, c: char) -> bool {
|
||||
<str>::contains(self, c)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<&char> for &str {
|
||||
fn contains(&self, c: &char) -> bool {
|
||||
<str>::contains(self, *c)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<&str> for String {
|
||||
fn contains(&self, string: &str) -> bool {
|
||||
<str>::contains(self, string)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<&&str> for String {
|
||||
fn contains(&self, string: &&str) -> bool {
|
||||
<str>::contains(self, string)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<char> for String {
|
||||
fn contains(&self, c: char) -> bool {
|
||||
<str>::contains(self, c)
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains<&char> for String {
|
||||
fn contains(&self, c: &char) -> bool {
|
||||
<str>::contains(self, *c)
|
||||
}
|
||||
}
|
||||
impl_contains!([] str [contains] &str [via] str);
|
||||
impl_contains!([] str [contains] char [via] str);
|
||||
impl_contains!([] String [contains] &str [via] str);
|
||||
impl_contains!([] String [contains] char [via] str);
|
||||
impl_contains!([T: PartialEq] Vec<T> [contains] &T [via] [T]);
|
||||
|
||||
impl<T: PartialEq> Contains<T> for Vec<T> {
|
||||
fn contains(&self, item: T) -> bool {
|
||||
|
@ -160,33 +379,202 @@ impl<T: PartialEq> Contains<T> for Vec<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> Contains<&T> for Vec<T> {
|
||||
fn contains(&self, item: &T) -> bool {
|
||||
<[T]>::contains(self, item)
|
||||
impl<I, T: Contains<I>> Contains<I> for Option<T> {
|
||||
fn contains(&self, item: I) -> bool {
|
||||
self.as_ref().map(|v| v.contains(item)).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains<'v, V, I>(value: V, item: I) -> Result<(), Errors<'v>>
|
||||
where V: for<'a> Contains<&'a I>, I: std::fmt::Debug
|
||||
impl<I, T: Contains<I>> Contains<I> for Result<'_, T> {
|
||||
fn contains(&self, item: I) -> bool {
|
||||
self.as_ref().map(|v| v.contains(item)).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, T: Contains<I> + ?Sized> Contains<I> for &T {
|
||||
fn contains(&self, item: I) -> bool {
|
||||
<T as Contains<I>>::contains(self, item)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains validator: succeeds when a value contains `item`.
|
||||
///
|
||||
/// The value must implement [`Contains<I>`](Contains) where `I` is the type of
|
||||
/// the `item`. See [`Contains`] for supported types and items.
|
||||
///
|
||||
/// On failure, returns a validation error with the following message:
|
||||
///
|
||||
/// ```text
|
||||
/// value is equal to an invalid value
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{FromForm, FromFormField};
|
||||
///
|
||||
/// #[derive(PartialEq, FromFormField)]
|
||||
/// enum Pet { Cat, Dog }
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo<'r> {
|
||||
/// best_pet: Pet,
|
||||
/// #[field(validate = contains(Pet::Cat))]
|
||||
/// #[field(validate = contains(&self.best_pet))]
|
||||
/// pets: Vec<Pet>,
|
||||
/// #[field(validate = contains('/'))]
|
||||
/// license: &'r str,
|
||||
/// #[field(validate = contains("@rust-lang.org"))]
|
||||
/// rust_lang_email: &'r str,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn contains<'v, V, I>(value: V, item: I) -> Result<'v, ()>
|
||||
where V: Contains<I>
|
||||
{
|
||||
if !value.contains(&item) {
|
||||
Err(Error::validation(format!("must contain {:?}", item)))?
|
||||
if !value.contains(item) {
|
||||
Err(Error::validation("value does not contain expected item"))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn omits<'v, V, I>(value: V, item: I) -> Result<(), Errors<'v>>
|
||||
where V: for<'a> Contains<&'a I>, I: std::fmt::Debug
|
||||
/// Verbose contains validator: like `contains` but mentions `item` in the
|
||||
/// error message.
|
||||
///
|
||||
/// The is identical to [`contains()`] except that `item` must be `Debug + Copy`
|
||||
/// and the error message is as follows, where `$item` is the [`Debug`]
|
||||
/// representation of `item`:
|
||||
///
|
||||
/// ```text
|
||||
/// values must contains $item
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{FromForm, FromFormField};
|
||||
///
|
||||
/// #[derive(PartialEq, Debug, Clone, Copy, FromFormField)]
|
||||
/// enum Pet { Cat, Dog }
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo {
|
||||
/// best_pet: Pet,
|
||||
/// #[field(validate = verbose_contains(Pet::Dog))]
|
||||
/// #[field(validate = verbose_contains(&self.best_pet))]
|
||||
/// pets: Vec<Pet>,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn verbose_contains<'v, V, I>(value: V, item: I) -> Result<'v, ()>
|
||||
where V: Contains<I>, I: Debug + Copy
|
||||
{
|
||||
if value.contains(&item) {
|
||||
Err(Error::validation(format!("cannot contain {:?}", item)))?
|
||||
if !value.contains(item) {
|
||||
Err(Error::validation(format!("value must contain {:?}", item)))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn range<'v, V, R>(value: &V, range: R) -> Result<(), Errors<'v>>
|
||||
/// Omits validator: succeeds when a value _does not_ contains `item`.
|
||||
/// error message.
|
||||
///
|
||||
/// The value must implement [`Contains<I>`](Contains) where `I` is the type of
|
||||
/// the `item`. See [`Contains`] for supported types and items.
|
||||
///
|
||||
/// On failure, returns a validation error with the following message:
|
||||
///
|
||||
/// ```text
|
||||
/// value contains a disallowed item
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{FromForm, FromFormField};
|
||||
///
|
||||
/// #[derive(PartialEq, FromFormField)]
|
||||
/// enum Pet { Cat, Dog }
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo<'r> {
|
||||
/// #[field(validate = omits(Pet::Cat))]
|
||||
/// pets: Vec<Pet>,
|
||||
/// #[field(validate = omits('@'))]
|
||||
/// not_email: &'r str,
|
||||
/// #[field(validate = omits("@gmail.com"))]
|
||||
/// non_gmail_email: &'r str,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn omits<'v, V, I>(value: V, item: I) -> Result<'v, ()>
|
||||
where V: Contains<I>
|
||||
{
|
||||
if value.contains(item) {
|
||||
Err(Error::validation("value contains a disallowed item"))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verbose omits validator: like `omits` but mentions `item` in the error
|
||||
/// message.
|
||||
///
|
||||
/// The is identical to [`omits()`] except that `item` must be `Debug + Copy`
|
||||
/// and the error message is as follows, where `$item` is the [`Debug`]
|
||||
/// representation of `item`:
|
||||
///
|
||||
/// ```text
|
||||
/// value cannot contain $item
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::{FromForm, FromFormField};
|
||||
///
|
||||
/// #[derive(PartialEq, Debug, Clone, Copy, FromFormField)]
|
||||
/// enum Pet { Cat, Dog }
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo<'r> {
|
||||
/// #[field(validate = verbose_omits(Pet::Cat))]
|
||||
/// pets: Vec<Pet>,
|
||||
/// #[field(validate = verbose_omits('@'))]
|
||||
/// not_email: &'r str,
|
||||
/// #[field(validate = verbose_omits("@gmail.com"))]
|
||||
/// non_gmail_email: &'r str,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn verbose_omits<'v, V, I>(value: V, item: I) -> Result<'v, ()>
|
||||
where V: Contains<I>, I: Copy + Debug
|
||||
{
|
||||
if value.contains(item) {
|
||||
Err(Error::validation(format!("value cannot contain {:?}", item)))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Integer range validator: succeeds when an integer value is within a range.
|
||||
///
|
||||
/// The value must be an integer type that implement `TryInto<isize> + Copy`. On
|
||||
/// failure, returns an [`OutOfRange`] error.
|
||||
///
|
||||
/// [`OutOfRange`]: rocket::form::error::ErrorKind::OutOfRange
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::FromForm;
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo {
|
||||
/// #[field(validate = range(0..))]
|
||||
/// non_negative: isize,
|
||||
/// #[field(validate = range(18..=130))]
|
||||
/// maybe_adult: u8,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn range<'v, V, R>(value: &V, range: R) -> Result<'v, ()>
|
||||
where V: TryInto<isize> + Copy, R: RangeBounds<isize>
|
||||
{
|
||||
if let Ok(v) = (*value).try_into() {
|
||||
|
@ -211,36 +599,93 @@ pub fn range<'v, V, R>(value: &V, range: R) -> Result<(), Errors<'v>>
|
|||
Err((start, end))?
|
||||
}
|
||||
|
||||
pub fn one_of<'v, V, I>(value: V, items: &[I]) -> Result<(), Errors<'v>>
|
||||
where V: for<'a> Contains<&'a I>, I: Display
|
||||
/// Contains one of validator: succeeds when a value contains at least one item
|
||||
/// in an `items` iterator.
|
||||
///
|
||||
/// The value must implement [`Contains<I>`](Contains) where `I` is the type of
|
||||
/// the `item`. The iterator must be [`Clone`]. See [`Contains`] for supported
|
||||
/// types and items. The item must be [`Debug`].
|
||||
///
|
||||
/// On failure, returns a [`InvalidChoice`] error with the debug representation
|
||||
/// of each item in `items`.
|
||||
///
|
||||
/// [`InvalidChoice`]: rocket::form::error::ErrorKind::InvalidChoice
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::FromForm;
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo<'r> {
|
||||
/// #[field(validate = one_of(&[3, 5, 7]))]
|
||||
/// single_digit_primes: Vec<u8>,
|
||||
/// #[field(validate = one_of(" \t\n".chars()))]
|
||||
/// has_space_char: &'r str,
|
||||
/// #[field(validate = one_of(" \t\n".chars()).and_then(msg!("no spaces")))]
|
||||
/// no_space: &'r str,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn one_of<'v, V, I, R>(value: V, items: R) -> Result<'v, ()>
|
||||
where V: Contains<I>,
|
||||
I: Debug,
|
||||
R: IntoIterator<Item = I>,
|
||||
<R as IntoIterator>::IntoIter: Clone
|
||||
{
|
||||
for item in items {
|
||||
let items = items.into_iter();
|
||||
for item in items.clone() {
|
||||
if value.contains(item) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let choices = items.iter()
|
||||
.map(|item| item.to_string().into())
|
||||
.collect::<Vec<Cow<'v, str>>>();
|
||||
let choices: Vec<Cow<'_, str>> = items
|
||||
.map(|item| format!("{:?}", item).into())
|
||||
.collect();
|
||||
|
||||
Err(choices)?
|
||||
}
|
||||
|
||||
pub fn ext<'v>(file: &TempFile<'_>, ext: &str) -> Result<(), Errors<'v>> {
|
||||
/// File type validator: succeeds when a [`TempFile`] has the Content-Type
|
||||
/// `content_type`.
|
||||
///
|
||||
/// On failure, returns a validation error with one of the following messages:
|
||||
///
|
||||
/// ```text
|
||||
/// // the file has an incorrect extension
|
||||
/// file type was .$file_ext but must be $type
|
||||
///
|
||||
/// // the file does not have an extension
|
||||
/// file type must be $type
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::form::FromForm;
|
||||
/// use rocket::data::{ToByteUnit, TempFile};
|
||||
/// use rocket::http::ContentType;
|
||||
///
|
||||
/// #[derive(FromForm)]
|
||||
/// struct Foo<'r> {
|
||||
/// #[field(validate = ext(ContentType::PDF))]
|
||||
/// #[field(validate = len(..1.mebibytes()))]
|
||||
/// document: TempFile<'r>,
|
||||
/// }
|
||||
/// ```
|
||||
pub fn ext<'v>(file: &TempFile<'_>, r#type: ContentType) -> Result<'v, ()> {
|
||||
if let Some(file_ct) = file.content_type() {
|
||||
if let Some(ext_ct) = ContentType::from_extension(ext) {
|
||||
if file_ct == &ext_ct {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let m = file_ct.extension()
|
||||
.map(|fext| format!("file type was .{} but must be .{}", fext, ext))
|
||||
.unwrap_or_else(|| format!("file type must be .{}", ext));
|
||||
|
||||
Err(Error::validation(m))?
|
||||
if file_ct == &r#type {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::validation(format!("invalid extension: expected {}", ext)))?
|
||||
let msg = match (file.content_type().and_then(|c| c.extension()), r#type.extension()) {
|
||||
(Some(a), Some(b)) => format!("invalid file type: .{}, must be .{}", a, b),
|
||||
(Some(a), None) => format!("invalid file type: .{}, must be {}", a, r#type),
|
||||
(None, Some(b)) => format!("file type must be .{}", b),
|
||||
(None, None) => format!("file type must be {}", r#type),
|
||||
};
|
||||
|
||||
Err(Error::validation(msg))?
|
||||
}
|
||||
|
|
|
@ -832,30 +832,35 @@ means that validation was successful while an `Err` of [`Errors<'_>`] indicates
|
|||
the error(s) that occured. For instance, if you wanted to implement an ad-hoc
|
||||
Luhn validator for credit-card-like numbers, you might write:
|
||||
|
||||
|
||||
```rust
|
||||
# #[macro_use] extern crate rocket;
|
||||
extern crate time;
|
||||
|
||||
use rocket::form::{self, Error};
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct CreditCard<'v> {
|
||||
#[field(validate = luhn())]
|
||||
number: &'v str,
|
||||
# #[field(validate = luhn())]
|
||||
# other: String,
|
||||
struct CreditCard {
|
||||
#[field(validate = luhn(self.cvv, &self.expiration))]
|
||||
number: u64,
|
||||
#[field(validate = range(..9999))]
|
||||
cvv: u16,
|
||||
expiration: time::Date,
|
||||
}
|
||||
|
||||
fn luhn<'v, S: AsRef<str>>(field: S) -> rocket::form::Result<'v, ()> {
|
||||
let num = field.as_ref().parse::<u64>()?;
|
||||
fn luhn<'v>(number: &u64, cvv: u16, exp: &time::Date) -> form::Result<'v, ()> {
|
||||
# let valid = false;
|
||||
if !valid {
|
||||
Err(Error::validation("invalid credit card number"))?;
|
||||
}
|
||||
|
||||
/* implementation of Luhn validator... */
|
||||
# Ok(())
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue