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;
|
pub use json;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use devise::{*, ext::{TypeExt, SpanDiagnosticExt}};
|
use devise::{*, ext::{TypeExt, SpanDiagnosticExt}};
|
||||||
|
|
||||||
|
use syn::visit_mut::VisitMut;
|
||||||
|
use syn::visit::Visit;
|
||||||
|
|
||||||
use crate::exports::*;
|
use crate::exports::*;
|
||||||
use crate::proc_macro2::Span;
|
use crate::proc_macro2::{Span, TokenStream, TokenTree};
|
||||||
use crate::name::Name;
|
use crate::name::Name;
|
||||||
|
|
||||||
pub struct FormField {
|
pub struct FormField {
|
||||||
|
@ -86,62 +89,139 @@ impl FieldExt for Field<'_> {
|
||||||
|
|
||||||
fn name_view(&self) -> Result<syn::Expr> {
|
fn name_view(&self) -> Result<syn::Expr> {
|
||||||
let field_name = self.field_name()?;
|
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>(
|
pub fn validators<'v>(
|
||||||
field: Field<'v>,
|
field: Field<'v>,
|
||||||
out: &'v syn::Ident,
|
parent: &'v syn::Ident, // field ident (if local) or form ident (if !local)
|
||||||
local: bool,
|
local: bool,
|
||||||
) -> Result<impl Iterator<Item = syn::Expr> + 'v> {
|
) -> 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)?
|
Ok(FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|a| a.validate)
|
.filter_map(|a| a.validate)
|
||||||
.map(move |mut expr| {
|
.map(move |expr| {
|
||||||
let mut mutator = ValidationMutator::new(field.ident(), out);
|
let mut members = RecordMemberAccesses(vec![]);
|
||||||
mutator.visit_expr_mut(&mut expr);
|
members.visit_expr(&expr);
|
||||||
(expr, mutator.rec)
|
|
||||||
|
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)
|
.filter(move |(_, is_local)| *is_local == local)
|
||||||
.map(|(e, _)| e))
|
.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 {
|
pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream {
|
||||||
DeriveGenerator::build_for(input, quote!(impl<'__f> #_form::FromForm<'__f>))
|
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)
|
.support(Support::NamedStruct | Support::Lifetime | Support::Type)
|
||||||
.replace_generic(0, 0)
|
.replace_generic(0, 0)
|
||||||
.type_bound(quote!(#_form::FromForm<'__f> + '__f))
|
.type_bound(quote!(#_form::FromForm<'__f> + '__f))
|
||||||
|
@ -189,13 +190,6 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream {
|
||||||
.split2();
|
.split2();
|
||||||
|
|
||||||
Ok(quote_spanned! { fields.span() =>
|
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 {
|
#(let #ident = match #finalize_field {
|
||||||
#_ok(#ident) => #_some(#ident),
|
#_ok(#ident) => #_some(#ident),
|
||||||
#_err(_e) => { __c.__errors.extend(_e); #_none }
|
#_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()
|
.unwrap_or_else(|| <#ty as #_form::FromForm<'__f>>::default()
|
||||||
.ok_or_else(|| #_form::ErrorKind::Missing.into())
|
.ok_or_else(|| #_form::ErrorKind::Missing.into())
|
||||||
)
|
)
|
||||||
// <#ty as #_form::FromForm<'__f>>::finalize(__c.#ident)
|
|
||||||
.and_then(|#ident| {
|
.and_then(|#ident| {
|
||||||
let mut _es = #_form::Errors::new();
|
let mut _es = #_form::Errors::new();
|
||||||
#(if let #_err(_e) = #validator { _es.extend(_e); })*
|
#(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::http::{ContentType, RawStr};
|
||||||
use crate::{Request, Data};
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueField<'r> {
|
pub struct ValueField<'r> {
|
||||||
|
/// The (decoded) name of the form field.
|
||||||
pub name: NameView<'r>,
|
pub name: NameView<'r>,
|
||||||
|
/// The (decoded) value of the form field.
|
||||||
pub value: &'r str,
|
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> {
|
pub struct DataField<'r, 'i> {
|
||||||
|
/// The (decoded) name of the form field.
|
||||||
pub name: NameView<'r>,
|
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>,
|
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,
|
pub content_type: ContentType,
|
||||||
|
/// The request in which the form field was submitted.
|
||||||
pub request: &'r Request<'i>,
|
pub request: &'r Request<'i>,
|
||||||
|
/// The raw data stream.
|
||||||
pub data: Data,
|
pub data: Data,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'v> ValueField<'v> {
|
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 {
|
pub fn parse(field: &'v str) -> Self {
|
||||||
// WHATWG URL Living Standard 5.1 steps 3.2, 3.3.
|
// WHATWG URL Living Standard 5.1 steps 3.2, 3.3.
|
||||||
let (name, val) = RawStr::new(field).split_at_byte(b'=');
|
let (name, val) = RawStr::new(field).split_at_byte(b'=');
|
||||||
ValueField::from((name.as_str(), val.as_str()))
|
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 {
|
pub fn from_value(value: &'v str) -> Self {
|
||||||
ValueField::from(("", value))
|
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 {
|
pub fn shift(mut self) -> Self {
|
||||||
self.name.shift();
|
self.name.shift();
|
||||||
self
|
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> {
|
pub fn unexpected(&self) -> Error<'v> {
|
||||||
Error::from(ErrorKind::Unexpected)
|
Error::from(ErrorKind::Unexpected)
|
||||||
.with_name(NameView::new(self.name.source()))
|
.with_name(self.name.source())
|
||||||
.with_value(self.value)
|
.with_value(self.value)
|
||||||
.with_entity(Entity::ValueField)
|
.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> {
|
pub fn missing(&self) -> Error<'v> {
|
||||||
Error::from(ErrorKind::Missing)
|
Error::from(ErrorKind::Missing)
|
||||||
.with_name(NameView::new(self.name.source()))
|
.with_name(self.name.source())
|
||||||
.with_value(self.value)
|
.with_value(self.value)
|
||||||
.with_entity(Entity::ValueField)
|
.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> {
|
impl<'a> From<(&'a str, &'a str)> for ValueField<'a> {
|
||||||
fn from((name, value): (&'a str, &'a str)) -> Self {
|
fn from((name, value): (&'a str, &'a str)) -> Self {
|
||||||
ValueField { name: NameView::new(name), value }
|
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
|
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.
|
/// A data guard for [`FromForm`] types.
|
||||||
///
|
///
|
||||||
/// This type implements the [`FromData`] trait. It provides a generic means to
|
/// 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
|
/// See the [forms guide](https://rocket.rs/master/guide/requests#forms) for
|
||||||
/// general form support documentation.
|
/// general form support documentation.
|
||||||
|
@ -57,19 +57,44 @@ use crate::form::prelude::*;
|
||||||
///
|
///
|
||||||
/// ## Data Limits
|
/// ## Data Limits
|
||||||
///
|
///
|
||||||
/// The default size limit for incoming form data is 32KiB. Setting a limit
|
/// ### URL-Encoded Forms
|
||||||
/// protects your application from denial of service (DOS) attacks and from
|
///
|
||||||
/// resource exhaustion through high memory consumption. The limit can be
|
/// The `form` limit specifies the data limit for an entire url-encoded form
|
||||||
/// modified by setting the `limits.form` configuration parameter. For instance,
|
/// data. It defaults to 32KiB. URL-encoded form data is percent-decoded, stored
|
||||||
/// to increase the forms limit to 512KiB for all environments, you may add the
|
/// in-memory, and parsed into [`ValueField`]s. If the incoming data exceeds
|
||||||
/// following to your `Rocket.toml`:
|
/// 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
|
/// ```toml
|
||||||
/// [global.limits]
|
/// [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)]
|
#[derive(Debug)]
|
||||||
pub struct Form<T>(T);
|
pub struct Form<T>(T);
|
||||||
|
|
||||||
|
|
|
@ -532,7 +532,7 @@ impl<'v, K, V> MapContext<'v, K, V>
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let error = Error::from(ErrorKind::Missing)
|
let error = Error::from(ErrorKind::Missing)
|
||||||
.with_entity(Entity::Indices)
|
.with_entity(Entity::Key)
|
||||||
.with_name(name);
|
.with_name(name);
|
||||||
|
|
||||||
self.errors.push(error);
|
self.errors.push(error);
|
||||||
|
@ -566,14 +566,14 @@ impl<'v, K, V> MapContext<'v, K, V>
|
||||||
.zip(key_map.values().map(|(_, name)| name))
|
.zip(key_map.values().map(|(_, name)| name))
|
||||||
.filter_map(|(ctxt, name)| match K::finalize(ctxt) {
|
.filter_map(|(ctxt, name)| match K::finalize(ctxt) {
|
||||||
Ok(value) => Some(value),
|
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()
|
let values = values.into_iter()
|
||||||
.zip(key_map.values().map(|(_, name)| name))
|
.zip(key_map.values().map(|(_, name)| name))
|
||||||
.filter_map(|(ctxt, name)| match V::finalize(ctxt) {
|
.filter_map(|(ctxt, name)| match V::finalize(ctxt) {
|
||||||
Ok(value) => Some(value),
|
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();
|
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
|
/// A value is validated successfully if the `from_str` method for the given
|
||||||
/// type returns successfully. Only accepts form _values_, not binary data.
|
/// type returns successfully. Only accepts form _values_, not binary data.
|
||||||
|
/// No data limit applies.
|
||||||
///
|
///
|
||||||
/// * **`bool`**
|
/// * **`bool`**
|
||||||
///
|
///
|
||||||
|
@ -69,17 +70,21 @@ use crate::form::prelude::*;
|
||||||
/// `"off"`, `"no"`, or `"false"`. Defaults to `false` otherwise. Only
|
/// `"off"`, `"no"`, or `"false"`. Defaults to `false` otherwise. Only
|
||||||
/// accepts form _values_, not binary data.
|
/// accepts form _values_, not binary data.
|
||||||
///
|
///
|
||||||
|
/// **No data limit applies.**
|
||||||
|
///
|
||||||
/// * **`&str`, `String`**
|
/// * **`&str`, `String`**
|
||||||
///
|
///
|
||||||
/// The decoded form value or data is returned directly without
|
/// The decoded form value or data is returned directly without
|
||||||
/// modification.
|
/// modification.
|
||||||
///
|
///
|
||||||
|
/// **The data limit `string` applies.**
|
||||||
|
///
|
||||||
/// * **[`TempFile`]**
|
/// * **[`TempFile`]**
|
||||||
///
|
///
|
||||||
/// Streams the form field value or data to a temporary file. See
|
/// 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
|
/// 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
|
/// 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
|
/// This is the `"date"` HTML input type. Only accepts form _values_, not
|
||||||
/// binary data.
|
/// binary data.
|
||||||
///
|
///
|
||||||
|
/// **No data limit applies.**
|
||||||
|
///
|
||||||
/// * **[`time::PrimitiveDateTime`]**
|
/// * **[`time::PrimitiveDateTime`]**
|
||||||
///
|
///
|
||||||
/// Parses a date in `%FT%R` or `%FT%T` format, that is, `YYYY-MM-DDTHH:MM`
|
/// 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_,
|
/// without support for the millisecond variant. Only accepts form _values_,
|
||||||
/// not binary data.
|
/// not binary data.
|
||||||
///
|
///
|
||||||
|
/// **No data limit applies.**
|
||||||
|
///
|
||||||
/// * **[`time::Time`]**
|
/// * **[`time::Time`]**
|
||||||
///
|
///
|
||||||
/// Parses a time in `%R` or `%T` format, that is, `HH:MM` or `HH:MM:SS`.
|
/// 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
|
/// This is the `"time"` HTML input type without support for the millisecond
|
||||||
/// variant. Only accepts form _values_, not binary data.
|
/// variant. Only accepts form _values_, not binary data.
|
||||||
///
|
///
|
||||||
|
/// **No data limit applies.**
|
||||||
|
///
|
||||||
/// [`TempFile`]: crate::data::TempFile
|
/// [`TempFile`]: crate::data::TempFile
|
||||||
///
|
///
|
||||||
/// # Implementing
|
/// # Implementing
|
||||||
|
@ -240,7 +251,7 @@ pub struct FromFieldContext<'v, T: FromFormField<'v>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'v, T: FromFormField<'v>> FromFieldContext<'v, T> {
|
impl<'v, T: FromFormField<'v>> FromFieldContext<'v, T> {
|
||||||
fn can_push(&mut self) -> bool {
|
fn should_push(&mut self) -> bool {
|
||||||
self.pushes += 1;
|
self.pushes += 1;
|
||||||
self.value.is_none()
|
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>) {
|
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.field_value = Some(field.value);
|
||||||
ctxt.push(field.name, Self::from_value(field))
|
ctxt.push(field.name, Self::from_value(field))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn push_data(ctxt: &mut FromFieldContext<'v, T>, field: DataField<'v, '_>) {
|
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);
|
ctxt.push(field.name, Self::from_data(field).await);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
//! Parsing and validation of HTTP forms and fields.
|
//! 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
|
//! # Field Wire Format
|
||||||
//!
|
//!
|
||||||
//! Rocket's field wire format is a flexible, non-self-descriptive, text-based
|
//! Rocket's field wire format is a flexible, non-self-descriptive, text-based
|
||||||
|
@ -17,13 +20,13 @@
|
||||||
//!
|
//!
|
||||||
//! indices := index (':' index)*
|
//! indices := index (':' index)*
|
||||||
//!
|
//!
|
||||||
//! index := STRING except ':'
|
//! index := STRING except ':', ']'
|
||||||
//!
|
//!
|
||||||
//! value := STRING
|
//! value := STRING
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Each field name consists of any number of `key`s and at most one `value`.
|
//! 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
|
//! 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
|
//! multipart forms are supported. All url-encoded form fields are preprocessed
|
||||||
//! as [`ValueField`]s. Multipart form fields with Content-Types are processed
|
//! as [`ValueField`]s. Multipart form fields with Content-Types are processed
|
||||||
//! as [`DataField`]s while those without a set Content-Type are processed as
|
//! 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
|
//! # Data Limits
|
||||||
//!
|
//!
|
||||||
|
@ -68,372 +71,379 @@
|
||||||
//! imposes its own limits. For example, the `&str` form guard imposes a data
|
//! imposes its own limits. For example, the `&str` form guard imposes a data
|
||||||
//! limit of `string` when multipart data is streamed.
|
//! 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
|
//! # A Simple Example
|
||||||
///
|
//!
|
||||||
/// The following examples use `f1=v1&f2=v2` to illustrate field/value pairs
|
//! 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
|
//! `(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,
|
//! over HTTP but Rocket's push-parsers are unaware of any specific encoding,
|
||||||
/// dealing only with logical `field`s, `index`es, and `value`s.
|
//! dealing only with logical `field`s, `index`es, and `value`s.
|
||||||
///
|
//!
|
||||||
/// ## A Single Value (`T: FormFormValue`)
|
//! ## A Single Field (`T: FormFormField`)
|
||||||
///
|
//!
|
||||||
/// The simplest example parses a single value of type `T` from a string with an
|
//! 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`:
|
//! optional default value: this is `impl<T: FromFormField> FromForm for T`:
|
||||||
///
|
//!
|
||||||
/// 1. **Initialization.** The context stores parsing options and an `Option`
|
//! 1. **Initialization.** The context stores form options and an `Option` of
|
||||||
/// of `Result<T, T::Error>` for storing the `result` of parsing `T`, which is
|
//! `Result<T, form::Error>` for storing the `result` of parsing `T`, which
|
||||||
/// initially set to `None`.
|
//! is initially set to `None`.
|
||||||
///
|
//!
|
||||||
/// ```rust,ignore
|
//! ```rust,ignore
|
||||||
/// struct Context<T> {
|
//! struct Context<T> {
|
||||||
/// opts: FormOptions,
|
//! opts: Options,
|
||||||
/// result: Option<Result<T, T::Error>>,
|
//! result: Option<Result<T>>,
|
||||||
/// }
|
//! }
|
||||||
/// ```
|
//! ```
|
||||||
///
|
//!
|
||||||
/// 2. **Push.** The field is treated as a string. If `context.result` is `None`,
|
//! 2. **Push.** If `context.result` is `None`, `T` is parsed from `field`,
|
||||||
/// `T` is parsed from `field`, and the result is stored in `context.result`.
|
//! and the result is stored in `context.result`. Otherwise a field has
|
||||||
///
|
//! already been parsed and nothing is done.
|
||||||
/// ```rust,ignore
|
//!
|
||||||
/// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
//! ```rust,ignore
|
||||||
/// if this.result.is_none() {
|
//! fn push(ctxt: &mut Self::Context, field: Field<'v>) {
|
||||||
/// this.result = Some(Self::from_value(field));
|
//! if ctxt.result.is_none() {
|
||||||
/// }
|
//! ctxt.result = Some(Self::from_field(field));
|
||||||
/// }
|
//! }
|
||||||
/// ```
|
//! }
|
||||||
///
|
//! ```
|
||||||
/// 3. **Finalization.** If `context.result` is `None` and `T` has a default,
|
//!
|
||||||
/// the default is returned; otherwise a `Missing` error is returned. If
|
//! 3. **Finalization.** If `ctxt.result` is `None` and `T` has a default,
|
||||||
/// `context.result` is `Some(v)`, the result `v` is returned.
|
//! 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(this: Self::Context) -> Result<Self, Self::Error> {
|
//! ```rust,ignore
|
||||||
/// match this.result {
|
//! fn finalize(ctxt: Self::Context) -> Result<Self> {
|
||||||
/// Some(value) => Ok(value),
|
//! match ctxt.result {
|
||||||
/// None => match <T as FromFormValue>::default() {
|
//! Some(value) => Ok(value),
|
||||||
/// Some(default) => Ok(default),
|
//! None => match <T as FromFormField>::default() {
|
||||||
/// None => Err(Error::Missing)
|
//! Some(default) => Ok(default),
|
||||||
/// }
|
//! None => Err(ErrorKind::Missing)?,
|
||||||
/// }
|
//! }
|
||||||
/// }
|
//! }
|
||||||
/// ```
|
//! }
|
||||||
///
|
//! ```
|
||||||
/// This implementation is complete, barring checking for duplicate pushes when
|
//!
|
||||||
/// paring is requested as `strict`.
|
//! This implementation is complete except for the following details:
|
||||||
///
|
//!
|
||||||
/// ## Maps w/named Fields (`struct`)
|
//! * not being pseudocode, of course
|
||||||
///
|
//! * checking for duplicate pushes when paring is requested as `strict`
|
||||||
/// A `struct` with named fields parses values of multiple types, indexed by the
|
//! * tracking the field's name and value to generate a complete `Error`
|
||||||
/// name of its fields:
|
//!
|
||||||
///
|
//! See [`FromForm`] for full details on push-parsing and a complete example.
|
||||||
/// ```rust,ignore
|
|
||||||
/// struct Dog { name: String, barks: bool, friends: Vec<Cat>, }
|
// ## Maps w/named Fields (`struct`)
|
||||||
/// struct Cat { name: String, meows: bool }
|
//
|
||||||
/// ```
|
// A `struct` with named fields parses values of multiple types, indexed by the
|
||||||
///
|
// name of its fields:
|
||||||
/// Candidates for parsing into a `Dog` include:
|
//
|
||||||
///
|
// ```rust,ignore
|
||||||
/// * `name=Fido&barks=0`
|
// struct Dog { name: String, barks: bool, friends: Vec<Cat>, }
|
||||||
///
|
// struct Cat { name: String, meows: bool }
|
||||||
/// `Dog { "Fido", false }`
|
// ```
|
||||||
///
|
//
|
||||||
/// * `name=Fido&barks=1&friends[0]name=Sally&friends[0]meows=0`
|
// Candidates for parsing into a `Dog` include:
|
||||||
/// `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=0`
|
||||||
///
|
//
|
||||||
/// `Dog { "Fido", true, vec![Cat { "Sally", false }] }`
|
// `Dog { "Fido", false }`
|
||||||
///
|
//
|
||||||
/// Parsers for structs are code-generated to proceed as follows:
|
// * `name=Fido&barks=1&friends[0]name=Sally&friends[0]meows=0`
|
||||||
///
|
// `name=Fido&barks=1&friends[0].name=Sally&friends[0].meows=0`
|
||||||
/// 1. **Initialization.** The context stores parsing options, a `T::Context`
|
// `name=Fido&barks=1&friends.0.name=Sally&friends.0.meows=0`
|
||||||
/// for each field of type `T`, and a vector called `extra`.
|
//
|
||||||
///
|
// `Dog { "Fido", true, vec![Cat { "Sally", false }] }`
|
||||||
/// ```rust,ignore
|
//
|
||||||
/// struct Context<'v> {
|
// Parsers for structs are code-generated to proceed as follows:
|
||||||
/// opts: FormOptions,
|
//
|
||||||
/// field_a: A::Context,
|
// 1. **Initialization.** The context stores parsing options, a `T::Context`
|
||||||
/// field_b: B::Context,
|
// for each field of type `T`, and a vector called `extra`.
|
||||||
/// /* ... */
|
//
|
||||||
/// extra: Vec<FormField<'v>>
|
// ```rust,ignore
|
||||||
/// }
|
// struct Context<'v> {
|
||||||
/// ```
|
// opts: FormOptions,
|
||||||
///
|
// field_a: A::Context,
|
||||||
/// 2. **Push.** The index of the first key is compared to known field names.
|
// field_b: B::Context,
|
||||||
/// 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`.
|
// extra: Vec<FormField<'v>>
|
||||||
///
|
// }
|
||||||
/// ```rust,ignore
|
// ```
|
||||||
/// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
//
|
||||||
/// match field.key() {
|
// 2. **Push.** The index of the first key is compared to known field names.
|
||||||
/// "field_a" => A::push(&mut this.field_a, field.next()),
|
// If none matches, the index is added to `extra`. Otherwise the key is
|
||||||
/// "field_b" => B::push(&mut this.field_b, field.next()),
|
// stripped from the field, and the remaining field is pushed to `T`.
|
||||||
/// /* ... */
|
//
|
||||||
/// _ => this.extra.push(field)
|
// ```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()),
|
||||||
/// 3. **Finalization.** Every context is finalized; errors and `Ok` values
|
// /* ... */
|
||||||
/// are collected. If parsing is strict and extras is non-empty, an error
|
// _ => this.extra.push(field)
|
||||||
/// 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
|
// 3. **Finalization.** Every context is finalized; errors and `Ok` values
|
||||||
/// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
// are collected. If parsing is strict and extras is non-empty, an error
|
||||||
/// let mut errors = vec![];
|
// 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
|
||||||
/// let field_a = A::finalize(&mut this.field_a)
|
// returned. Otherwise, `Err(errors)` is returned.
|
||||||
/// .map_err(|e| errors.push(e))
|
//
|
||||||
/// .map(Some).unwrap_or(None);
|
// ```rust,ignore
|
||||||
///
|
// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
||||||
/// let field_b = B::finblize(&mut this.field_b)
|
// let mut errors = vec![];
|
||||||
/// .map_err(|e| errors.push(e))
|
//
|
||||||
/// .map(Some).unwrap_or(None);
|
// let field_a = A::finalize(&mut this.field_a)
|
||||||
///
|
// .map_err(|e| errors.push(e))
|
||||||
/// /* .. */
|
// .map(Some).unwrap_or(None);
|
||||||
///
|
//
|
||||||
/// if !errors.is_empty() {
|
// let field_b = B::finblize(&mut this.field_b)
|
||||||
/// return Err(Values(errors));
|
// .map_err(|e| errors.push(e))
|
||||||
/// } else if this.opts.is_strict() && !this.extra.is_empty() {
|
// .map(Some).unwrap_or(None);
|
||||||
/// return Err(Extra(this.extra));
|
//
|
||||||
/// } else {
|
// /* .. */
|
||||||
/// // NOTE: All unwraps will succeed since `errors.is_empty()`.
|
//
|
||||||
/// Struct {
|
// if !errors.is_empty() {
|
||||||
/// field_a: field_a.unwrap(),
|
// return Err(Values(errors));
|
||||||
/// field_b: field_b.unwrap(),
|
// } 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
|
// ## Sequences: (`Vec<T: FromForm>`)
|
||||||
/// 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
|
// A `Vec<T: FromForm>` invokes `T`'s push-parser on every push, adding instances
|
||||||
/// `T::Context` is `push`ed to.
|
// of `T` to an internal vector. The instance of `T` whose parser is invoked
|
||||||
///
|
// depends on the index of the first key:
|
||||||
/// 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
|
// * if it is the first push, the index differs from the previous, or there is no
|
||||||
/// same is true for `[]=1&[]=2&[]=3`. In the first example (`=1&..`), the fields
|
// index, a new `T::Context` is `init`ialized and added to the internal vector
|
||||||
/// passed to `Vec`'s push-parser (`=1`, ..) have no key and thus no index. In the
|
// * if the index matches the previously seen index, the last initialized
|
||||||
/// second example (`[]=1&..`), the key is `[]` (`[]=1`) without an index. In both
|
// `T::Context` is `push`ed to.
|
||||||
/// cases, there is no index. The `Vec` parser takes this to mean that a _new_ `T`
|
//
|
||||||
/// should be parsed using the field's value.
|
// 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
|
||||||
/// If, instead, the index was non-empty and equal to the index of the field in the
|
// same is true for `[]=1&[]=2&[]=3`. In the first example (`=1&..`), the fields
|
||||||
/// _previous_ push, `Vec` pushes the value to the parser of the previously parsed
|
// passed to `Vec`'s push-parser (`=1`, ..) have no key and thus no index. In the
|
||||||
/// `T`: `[]=1&[0]=2&[0]=3` results in `vec![1, 2]` and `[0]=1&[0]=2&[]=3` results
|
// second example (`[]=1&..`), the key is `[]` (`[]=1`) without an index. In both
|
||||||
/// in `vec![1, 3]` (see [`FromFormValue`]).
|
// cases, there is no index. The `Vec` parser takes this to mean that a _new_ `T`
|
||||||
///
|
// should be parsed using the field's value.
|
||||||
/// This generalizes. Consider a `Vec<Vec<usize>>` named `x`, so `x` and an
|
//
|
||||||
/// optional `=` are stripped before being passed to `Vec`'s push-parser:
|
// 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
|
||||||
/// * `x=1&x=2&x=3` parses as `vec![vec![1], vec![2], vec![3]]`
|
// `T`: `[]=1&[0]=2&[0]=3` results in `vec![1, 2]` and `[0]=1&[0]=2&[]=3` results
|
||||||
///
|
// in `vec![1, 3]` (see [`FromFormValue`]).
|
||||||
/// 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
|
// This generalizes. Consider a `Vec<Vec<usize>>` named `x`, so `x` and an
|
||||||
/// value (here, `1`, `2`, and `3`). Each of these `push`es proceeds
|
// optional `=` are stripped before being passed to `Vec`'s push-parser:
|
||||||
/// 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
|
// * `x=1&x=2&x=3` parses as `vec![vec![1], vec![2], vec![3]]`
|
||||||
/// integers `1`, `2`, and `3`.
|
//
|
||||||
///
|
// Every push (`1`, `2`, `3`) has no key, thus no index: a new `T` (here,
|
||||||
/// Note: `x=1&x=2&x=3` _also_ can also parse as `vec![1, 2, 3]` when viewed
|
// `Vec<usize>`) is thus initialized for every `push()` and passed the
|
||||||
/// as a `Vec<usize>`; this is the non-self-descriptive part of the format.
|
// 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
|
||||||
/// * `x[]=1&x[]=2&x[]=3` parses as `vec![vec![1], vec![2], vec![3]]`
|
// initialized for every push (now a `usize`), which finally parse as
|
||||||
///
|
// integers `1`, `2`, and `3`.
|
||||||
/// This proceeds nearly identically to the previous example, with the exception
|
//
|
||||||
/// that the top-level `Vec` sees the values `[]=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[0]=1&x[0]=2&x[]=3` parses as `vec![vec![1, 2], vec![3]]`
|
//
|
||||||
///
|
// * `x[]=1&x[]=2&x[]=3` parses as `vec![vec![1], vec![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
|
// This proceeds nearly identically to the previous example, with the exception
|
||||||
/// pushed a `1`. The second value has the same index as the first, `0`, and so
|
// that the top-level `Vec` sees the values `[]=1`, `[]=2`, and `[]=3`.
|
||||||
/// `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
|
// * `x[0]=1&x[0]=2&x[]=3` parses as `vec![vec![1, 2], vec![3]]`
|
||||||
/// and pushed a `3`.
|
//
|
||||||
///
|
// The top-level `Vec` sees the values `[0]=1`, `[0]=2`, and `[]=3`. The first
|
||||||
/// * `x[0]=1&x[0]=2&x[]=3&x[]=4` parses as `vec![vec![1, 2], vec![3], vec![4]]`
|
// value results in a new `Vec<usize>` being initialized, as before, which is
|
||||||
/// * `x[0]=1&x[0]=2&x[1]=3&x[1]=4` parses as `vec![vec![1, 2], vec![3, 4]]`
|
// 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`.
|
||||||
/// The indexing kind `[]` is purely by convention: the first two examples are
|
// Finally, the third value has no index, so a new `Vec<usize>` is initialized
|
||||||
/// equivalent to `x.=1&x.=2`, while the third to `x.0=1&x.0=&x.=3`.
|
// and pushed a `3`.
|
||||||
///
|
//
|
||||||
/// The parser proceeds as follows:
|
// * `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]]`
|
||||||
/// 1. **Initialization.** The context stores parsing options, the
|
//
|
||||||
/// `last_index` encountered in a `push`, an `Option` of a `T::Context` for
|
// The indexing kind `[]` is purely by convention: the first two examples are
|
||||||
/// the `current` value being parsed, a `Vec<T::Errors>` of `errors`, and
|
// equivalent to `x.=1&x.=2`, while the third to `x.0=1&x.0=&x.=3`.
|
||||||
/// finally a `Vec<T>` of already parsed `items`.
|
//
|
||||||
///
|
// The parser proceeds as follows:
|
||||||
/// ```rust,ignore
|
//
|
||||||
/// struct VecContext<'v, T: FromForm<'v>> {
|
// 1. **Initialization.** The context stores parsing options, the
|
||||||
/// opts: FormOptions,
|
// `last_index` encountered in a `push`, an `Option` of a `T::Context` for
|
||||||
/// last_index: Index<'v>,
|
// the `current` value being parsed, a `Vec<T::Errors>` of `errors`, and
|
||||||
/// current: Option<T::Context>,
|
// finally a `Vec<T>` of already parsed `items`.
|
||||||
/// errors: Vec<T::Error>,
|
//
|
||||||
/// items: Vec<T>
|
// ```rust,ignore
|
||||||
/// }
|
// struct VecContext<'v, T: FromForm<'v>> {
|
||||||
/// ```
|
// opts: FormOptions,
|
||||||
///
|
// last_index: Index<'v>,
|
||||||
/// 2. **Push.** The index of the first key is compared against `last_index`.
|
// current: Option<T::Context>,
|
||||||
/// If it differs, a new context for `T` is created and the previous is
|
// errors: Vec<T::Error>,
|
||||||
/// finalized. The `Ok` result from finalization is stored in `items` and
|
// items: Vec<T>
|
||||||
/// 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.
|
//
|
||||||
///
|
// 2. **Push.** The index of the first key is compared against `last_index`.
|
||||||
/// ```rust,ignore
|
// If it differs, a new context for `T` is created and the previous is
|
||||||
/// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
// finalized. The `Ok` result from finalization is stored in `items` and
|
||||||
/// if this.last_index != field.index() {
|
// the `Err` in `errors`. Otherwise the `index` is the same, the `current`
|
||||||
/// this.shift(); // finalize `current`, add to `items`, `errors`
|
// context is retrieved, and the field stripped of the current key is
|
||||||
/// let mut context = T::init(this.opts);
|
// pushed to `T`. `last_index` is updated.
|
||||||
/// T::push(&mut context, field.next());
|
//
|
||||||
/// this.current = Some(context);
|
// ```rust,ignore
|
||||||
/// } else {
|
// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||||
/// let context = this.current.as_mut();
|
// if this.last_index != field.index() {
|
||||||
/// T::push(context, field.next())
|
// this.shift(); // finalize `current`, add to `items`, `errors`
|
||||||
/// }
|
// let mut context = T::init(this.opts);
|
||||||
///
|
// T::push(&mut context, field.next());
|
||||||
/// this.last_index = field.index();
|
// this.current = Some(context);
|
||||||
/// }
|
// } else {
|
||||||
/// ```
|
// let context = this.current.as_mut();
|
||||||
///
|
// T::push(context, field.next())
|
||||||
/// 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.
|
// this.last_index = field.index();
|
||||||
///
|
// }
|
||||||
/// ```rust,ignore
|
// ```
|
||||||
/// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
//
|
||||||
/// this.shift(); // finalizes `current`, as before.
|
// 3. **Finalization.** Any `current` context is finalized, storing the `Ok`
|
||||||
/// match this.errors.is_empty() {
|
// or `Err` as before. `Ok(items)` is returned if `errors` is empty,
|
||||||
/// true => Ok(this.items),
|
// otherwise `Err(errors)` is returned.
|
||||||
/// false => Err(this.errors)
|
//
|
||||||
/// }
|
// ```rust,ignore
|
||||||
/// }
|
// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
||||||
/// ```
|
// this.shift(); // finalizes `current`, as before.
|
||||||
///
|
// match this.errors.is_empty() {
|
||||||
/// ## Arbitrary Maps (`HashMap<K: FromForm, V: FromForm>`)
|
// true => Ok(this.items),
|
||||||
///
|
// false => Err(this.errors)
|
||||||
/// 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
|
// ## Arbitrary Maps (`HashMap<K: FromForm, V: FromForm>`)
|
||||||
///
|
//
|
||||||
/// A non-composite value can be described by a single field with no indices.
|
// A `HashMap<K, V>` can be parsed from keys with one index or, for composite
|
||||||
/// Strings and integers are examples of non-composite values. The push-parser
|
// key values, such as structures or sequences, multiple indices. We begin with
|
||||||
/// for `HashMap<K, V>` for a non-composite `K` uses the index of the first key
|
// a discussion of the simpler case: non-composite keys.
|
||||||
/// as the value of `K`; the remainder of the field is pushed to `V`'s parser:
|
//
|
||||||
///
|
// ### Non-Composite Keys
|
||||||
/// 1. **Initialization.** The context stores a column-based representation of
|
//
|
||||||
/// `keys` and `values`, a `key_map` from a string key to the column index,
|
// A non-composite value can be described by a single field with no indices.
|
||||||
/// an `errors` vector for storing errors as they arise, and the parsing
|
// Strings and integers are examples of non-composite values. The push-parser
|
||||||
/// options.
|
// 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:
|
||||||
/// ```rust,ignore
|
//
|
||||||
/// struct MapContext<'v, K: FromForm<'v>, V: FromForm<'v>> {
|
// 1. **Initialization.** The context stores a column-based representation of
|
||||||
/// opts: FormOptions,
|
// `keys` and `values`, a `key_map` from a string key to the column index,
|
||||||
/// key_map: HashMap<&'v str, usize>,
|
// an `errors` vector for storing errors as they arise, and the parsing
|
||||||
/// keys: Vec<K::Context>,
|
// options.
|
||||||
/// values: Vec<V::Context>,
|
//
|
||||||
/// errors: Vec<MapError<'v, K::Error, V::Error>>,
|
// ```rust,ignore
|
||||||
/// }
|
// struct MapContext<'v, K: FromForm<'v>, V: FromForm<'v>> {
|
||||||
/// ```
|
// opts: FormOptions,
|
||||||
///
|
// key_map: HashMap<&'v str, usize>,
|
||||||
/// 2. **Push.** The `key_map` index for the key associated with the index of
|
// keys: Vec<K::Context>,
|
||||||
/// the first key in the field is retrieved. If such a key has not yet been
|
// values: Vec<V::Context>,
|
||||||
/// seen, a new key and value context are created, the key is pushed to
|
// errors: Vec<MapError<'v, K::Error, V::Error>>,
|
||||||
/// `K`'s parser, and the field minus the first key is pushed to `V`'s
|
// }
|
||||||
/// parser.
|
// ```
|
||||||
///
|
//
|
||||||
/// ```rust,ignore
|
// 2. **Push.** The `key_map` index for the key associated with the index of
|
||||||
/// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
// the first key in the field is retrieved. If such a key has not yet been
|
||||||
/// let key = field.index();
|
// seen, a new key and value context are created, the key is pushed to
|
||||||
/// let value_context = match this.key_map.get(Key) {
|
// `K`'s parser, and the field minus the first key is pushed to `V`'s
|
||||||
/// Some(i) => &mut this.values[i],
|
// parser.
|
||||||
/// None => {
|
//
|
||||||
/// let i = this.keys.len();
|
// ```rust,ignore
|
||||||
/// this.key_map.insert(key, i);
|
// fn push(this: &mut Self::Context, field: FormField<'v>) {
|
||||||
/// this.keys.push(K::init(this.opts));
|
// let key = field.index();
|
||||||
/// this.values.push(V::init(this.opts));
|
// let value_context = match this.key_map.get(Key) {
|
||||||
/// K::push(&mut this.keys[i], key.into());
|
// Some(i) => &mut this.values[i],
|
||||||
/// &mut this.values[i]
|
// None => {
|
||||||
/// }
|
// let i = this.keys.len();
|
||||||
/// };
|
// this.key_map.insert(key, i);
|
||||||
///
|
// this.keys.push(K::init(this.opts));
|
||||||
/// V::push(value_context, field.next());
|
// this.values.push(V::init(this.opts));
|
||||||
/// }
|
// K::push(&mut this.keys[i], key.into());
|
||||||
/// ```
|
// &mut this.values[i]
|
||||||
///
|
// }
|
||||||
/// 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
|
// V::push(value_context, field.next());
|
||||||
/// returned.
|
// }
|
||||||
///
|
// ```
|
||||||
/// ```rust,ignore
|
//
|
||||||
/// fn finalize(mut this: Self::Context) -> Result<Self, Self::Error> {
|
// 3. **Finalization.** All key and value contexts are finalized; any errors
|
||||||
/// this.finalize_keys();
|
// are collected in `errors`. If there are no errors, `keys` and `values`
|
||||||
/// this.finalize_values();
|
// are collected into a `HashMap` and returned. Otherwise, the errors are
|
||||||
/// if this.errors.is_empty() {
|
// returned.
|
||||||
/// Ok(this.keys.into_iter().zip(this.values.into_iter()).collect())
|
//
|
||||||
/// } else {
|
// ```rust,ignore
|
||||||
/// Err(this.errors)
|
// 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())
|
||||||
/// Examples of forms parseable via this parser are:
|
// } else {
|
||||||
///
|
// Err(this.errors)
|
||||||
/// * `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>>`
|
// Examples of forms parseable via this parser are:
|
||||||
/// just as `{ 0 => vec!["Bob", "Sally"], 1 => vec!["Craig"] }`.
|
//
|
||||||
///
|
// * `x[0].name=Bob&x[0].meows=true`as a `HashMap<usize, Cat>` parses with
|
||||||
/// A `HashMap<K, V>` can be thought of as a vector of key-value pairs: `Vec<(K,
|
// `0` mapping to `Cat { name: "Bob", meows: true }`
|
||||||
/// V)` (row-based) or equivalently, as two vectors of keys and values: `Vec<K>`
|
// * `x[0]name=Bob&x[0]meows=true`as a `HashMap<usize, Cat>` parses just as
|
||||||
/// and `Vec<V>` (column-based). The implication is that indexing into a
|
// above.
|
||||||
/// specific key or value requires _two_ indexes: the first to determine whether
|
// * `x[0]=Bob&x[0]=Sally&x[1]=Craig`as a `HashMap<usize, Vec<String>>`
|
||||||
/// a key or value is being indexed to, and the second to determine _which_ key
|
// just as `{ 0 => vec!["Bob", "Sally"], 1 => vec!["Craig"] }`.
|
||||||
/// 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.
|
// 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>`
|
||||||
/// The parser proceeds as follows:
|
// and `Vec<V>` (column-based). The implication is that indexing into a
|
||||||
///
|
// specific key or value requires _two_ indexes: the first to determine whether
|
||||||
/// 1. **Initialization.** The context stores parsing options, a vector of
|
// a key or value is being indexed to, and the second to determine _which_ key
|
||||||
/// `key_contexts: Vec<K::Context>`, a vector of `value_contexts:
|
// or value. The push-parser for maps thus optionally accepts two indexes for a
|
||||||
/// Vec<V::Context>`, a `mapping` from a string index to an integer index
|
// single key to allow piece-by-piece build-up of arbitrary keys and values.
|
||||||
/// into the `contexts`, and a vector of `errors`.
|
//
|
||||||
/// 2. **Push.** An index is required; an error is emitted and `push` returns
|
// The parser proceeds as follows:
|
||||||
/// 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.
|
// 1. **Initialization.** The context stores parsing options, a vector of
|
||||||
/// The key is pushed as the value to `K` and the remaining field as the
|
// `key_contexts: Vec<K::Context>`, a vector of `value_contexts:
|
||||||
/// value to `V`. The key and value are finalized; if both succeed, the key
|
// Vec<V::Context>`, a `mapping` from a string index to an integer index
|
||||||
/// and value are stored in `keys` and `values`; otherwise the error(s) is
|
// into the `contexts`, and a vector of `errors`.
|
||||||
/// stored in `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
|
||||||
/// If the first keys contains _two_ indices, the first must starts with
|
// contains _one_ index, a new `K::Context` and `V::Context` are created.
|
||||||
/// `k` or `v`, while the `second` is arbitrary. `mapping` is indexed by
|
// The key is pushed as the value to `K` and the remaining field as the
|
||||||
/// `second`; the integer is retrieved. If none exists, new contexts are
|
// value to `V`. The key and value are finalized; if both succeed, the key
|
||||||
/// created an added to `{key,value}_contexts`, and their index is mapped
|
// and value are stored in `keys` and `values`; otherwise the error(s) is
|
||||||
/// to `second` in `mapping`. If the first index is `k`, the field,
|
// stored in `errors`.
|
||||||
/// 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`.
|
// If the first keys contains _two_ indices, the first must starts with
|
||||||
/// 3. **Finalization.** Every context is finalized; errors and `Ok` values
|
// `k` or `v`, while the `second` is arbitrary. `mapping` is indexed by
|
||||||
/// are collected. TODO: FINISH. Split this into two: one for single-index,
|
// `second`; the integer is retrieved. If none exists, new contexts are
|
||||||
/// another for two-indices.
|
// 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 field;
|
||||||
mod options;
|
mod options;
|
||||||
|
@ -450,6 +460,7 @@ pub mod error;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
/// Type alias for `Result` with an error type of [`Errors`].
|
||||||
pub type Result<'v, T> = std::result::Result<T, Errors<'v>>;
|
pub type Result<'v, T> = std::result::Result<T, Errors<'v>>;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|
|
@ -66,7 +66,7 @@ impl Name {
|
||||||
type Item = &'v Key;
|
type Item = &'v Key;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.0.is_terminal() {
|
if self.0.exhausted() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ impl Name {
|
||||||
type Item = &'v Name;
|
type Item = &'v Name;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.0.is_terminal() {
|
if self.0.exhausted() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -476,16 +476,55 @@ impl<'v> NameView<'v> {
|
||||||
|
|
||||||
/// Shifts the current key once to the right.
|
/// Shifts the current key once to the right.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use rocket::form::name::NameView;
|
/// 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");
|
/// assert_eq!(view.key().unwrap(), "a");
|
||||||
///
|
///
|
||||||
/// view.shift();
|
/// view.shift();
|
||||||
/// assert_eq!(view.key().unwrap(), "b");
|
/// 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) {
|
pub fn shift(&mut self) {
|
||||||
const START_DELIMS: &'static [char] = &['.', '['];
|
const START_DELIMS: &'static [char] = &['.', '['];
|
||||||
|
@ -494,13 +533,10 @@ impl<'v> NameView<'v> {
|
||||||
let bytes = string.as_bytes();
|
let bytes = string.as_bytes();
|
||||||
let shift = match bytes.get(0) {
|
let shift = match bytes.get(0) {
|
||||||
None | Some(b'=') => 0,
|
None | Some(b'=') => 0,
|
||||||
Some(b'[') => match string[1..].find(&[']', '.'][..]) {
|
Some(b'[') => match memchr::memchr(b']', bytes) {
|
||||||
Some(j) => match string[1..].as_bytes()[j] {
|
Some(j) => j + 1,
|
||||||
b']' => j + 2,
|
|
||||||
_ => j + 1,
|
|
||||||
}
|
|
||||||
None => bytes.len(),
|
None => bytes.len(),
|
||||||
}
|
},
|
||||||
Some(b'.') => match string[1..].find(START_DELIMS) {
|
Some(b'.') => match string[1..].find(START_DELIMS) {
|
||||||
Some(j) => j + 1,
|
Some(j) => j + 1,
|
||||||
None => bytes.len(),
|
None => bytes.len(),
|
||||||
|
@ -569,6 +605,7 @@ impl<'v> NameView<'v> {
|
||||||
let key = match view.as_bytes().get(0) {
|
let key = match view.as_bytes().get(0) {
|
||||||
Some(b'.') => &view[1..],
|
Some(b'.') => &view[1..],
|
||||||
Some(b'[') if view.ends_with(']') => &view[1..view.len() - 1],
|
Some(b'[') if view.ends_with(']') => &view[1..view.len() - 1],
|
||||||
|
Some(b'[') if self.is_at_last() => &view[1..],
|
||||||
_ => view
|
_ => view
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -643,7 +680,13 @@ impl<'v> NameView<'v> {
|
||||||
self.name
|
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()
|
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)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
|
/// Whether parsing should be strict (no extra parameters) or not.
|
||||||
pub strict: bool,
|
pub strict: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_upper_case_globals, dead_code)]
|
#[allow(non_upper_case_globals, dead_code)]
|
||||||
impl Options {
|
impl Options {
|
||||||
|
/// `Options` with `strict` set to `false`.
|
||||||
pub const Lenient: Self = Options { strict: false };
|
pub const Lenient: Self = Options { strict: false };
|
||||||
|
|
||||||
|
/// `Options` with `strict` set to `true`.
|
||||||
pub const Strict: Self = Options { strict: true };
|
pub const Strict: Self = Options { strict: true };
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,6 +148,8 @@ mod raw_str_parse_tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'r, 'i> MultipartParser<'r, 'i> {
|
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>>> {
|
async fn next(&mut self) -> Option<Result<'r, Field<'r, 'i>>> {
|
||||||
if self.done {
|
if self.done {
|
||||||
return None;
|
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 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>
|
where A: PartialEq<B>
|
||||||
{
|
{
|
||||||
if a != &b {
|
if a != &b {
|
||||||
Err(Error::validation("value does not match"))?
|
Err(Error::validation("value does not match expected value"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Len {
|
/// Negative equality validator: succeeds exactly when `a` != `b`, using
|
||||||
fn len(&self) -> usize;
|
/// [`PartialEq`].
|
||||||
|
///
|
||||||
fn len_u64(&self) -> u64 {
|
/// On failure, returns a validation error with the following message:
|
||||||
self.len() as u64
|
///
|
||||||
}
|
/// ```text
|
||||||
}
|
/// value is equal to an invalid value
|
||||||
|
/// ```
|
||||||
impl Len for str {
|
///
|
||||||
fn len(&self) -> usize { self.len() }
|
/// # Example
|
||||||
}
|
///
|
||||||
|
/// ```rust
|
||||||
impl Len for String {
|
/// use rocket::form::{FromForm, FromFormField};
|
||||||
fn len(&self) -> usize { self.len() }
|
///
|
||||||
}
|
/// #[derive(FromFormField, PartialEq)]
|
||||||
|
/// enum Kind {
|
||||||
impl<T> Len for Vec<T> {
|
/// Car,
|
||||||
fn len(&self) -> usize { <Vec<T>>::len(self) }
|
/// Truck
|
||||||
}
|
/// }
|
||||||
|
///
|
||||||
impl Len for TempFile<'_> {
|
/// #[derive(FromForm)]
|
||||||
fn len(&self) -> usize { TempFile::len(self) as usize }
|
/// struct Foo<'r> {
|
||||||
|
/// #[field(validate = neq("Bob Marley"))]
|
||||||
fn len_u64(&self) -> u64 { TempFile::len(self) }
|
/// name: &'r str,
|
||||||
}
|
/// #[field(validate = neq(Kind::Car))]
|
||||||
|
/// vehicle: Kind,
|
||||||
impl<K, V> Len for std::collections::HashMap<K, V> {
|
/// #[field(validate = neq(&[5, 7, 8]))]
|
||||||
fn len(&self) -> usize { <std::collections::HashMap<K, V>>::len(self) }
|
/// numbers: Vec<usize>,
|
||||||
}
|
/// }
|
||||||
|
/// ```
|
||||||
impl<T: Len + ?Sized> Len for &T {
|
pub fn neq<'v, A, B>(a: &A, b: B) -> Result<'v, ()>
|
||||||
fn len(&self) -> usize {
|
where A: PartialEq<B>
|
||||||
<T as Len>::len(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len<'v, V, R>(value: V, range: R) -> Result<(), Errors<'v>>
|
|
||||||
where V: Len, R: RangeBounds<u64>
|
|
||||||
{
|
{
|
||||||
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() {
|
let start = match range.start_bound() {
|
||||||
Bound::Included(v) => Some(*v),
|
Bound::Included(v) => Some(V::len_into_u64(*v)),
|
||||||
Bound::Excluded(v) => Some(v.saturating_add(1)),
|
Bound::Excluded(v) => Some(V::len_into_u64(*v).saturating_add(1)),
|
||||||
Bound::Unbounded => None
|
Bound::Unbounded => None
|
||||||
};
|
};
|
||||||
|
|
||||||
let end = match range.end_bound() {
|
let end = match range.end_bound() {
|
||||||
Bound::Included(v) => Some(*v),
|
Bound::Included(v) => Some(V::len_into_u64(*v)),
|
||||||
Bound::Excluded(v) => Some(v.saturating_sub(1)),
|
Bound::Excluded(v) => Some(V::len_into_u64(*v).saturating_sub(1)),
|
||||||
Bound::Unbounded => None,
|
Bound::Unbounded => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,87 +336,42 @@ pub fn len<'v, V, R>(value: V, range: R) -> Result<(), Errors<'v>>
|
||||||
Ok(())
|
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> {
|
pub trait Contains<I> {
|
||||||
|
/// Returns `true` if `self` contains `item`.
|
||||||
fn contains(&self, item: I) -> bool;
|
fn contains(&self, item: I) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I, T: Contains<I>> Contains<I> for &T {
|
macro_rules! impl_contains {
|
||||||
fn contains(&self, item: I) -> bool {
|
([$($gen:tt)*] $T:ty [contains] $I:ty [via] $P:ty) => {
|
||||||
<T as Contains<I>>::contains(self, item)
|
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 {
|
impl_contains!([] str [contains] &str [via] str);
|
||||||
fn contains(&self, string: &str) -> bool {
|
impl_contains!([] str [contains] char [via] str);
|
||||||
<str>::contains(self, string)
|
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 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<T: PartialEq> Contains<T> for Vec<T> {
|
impl<T: PartialEq> Contains<T> for Vec<T> {
|
||||||
fn contains(&self, item: T) -> bool {
|
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> {
|
impl<I, T: Contains<I>> Contains<I> for Option<T> {
|
||||||
fn contains(&self, item: &T) -> bool {
|
fn contains(&self, item: I) -> bool {
|
||||||
<[T]>::contains(self, item)
|
self.as_ref().map(|v| v.contains(item)).unwrap_or(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains<'v, V, I>(value: V, item: I) -> Result<(), Errors<'v>>
|
impl<I, T: Contains<I>> Contains<I> for Result<'_, T> {
|
||||||
where V: for<'a> Contains<&'a I>, I: std::fmt::Debug
|
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) {
|
if !value.contains(item) {
|
||||||
Err(Error::validation(format!("must contain {:?}", item)))?
|
Err(Error::validation("value does not contain expected item"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn omits<'v, V, I>(value: V, item: I) -> Result<(), Errors<'v>>
|
/// Verbose contains validator: like `contains` but mentions `item` in the
|
||||||
where V: for<'a> Contains<&'a I>, I: std::fmt::Debug
|
/// 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) {
|
if !value.contains(item) {
|
||||||
Err(Error::validation(format!("cannot contain {:?}", item)))?
|
Err(Error::validation(format!("value must contain {:?}", item)))?
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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>
|
where V: TryInto<isize> + Copy, R: RangeBounds<isize>
|
||||||
{
|
{
|
||||||
if let Ok(v) = (*value).try_into() {
|
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))?
|
Err((start, end))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn one_of<'v, V, I>(value: V, items: &[I]) -> Result<(), Errors<'v>>
|
/// Contains one of validator: succeeds when a value contains at least one item
|
||||||
where V: for<'a> Contains<&'a I>, I: Display
|
/// 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) {
|
if value.contains(item) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let choices = items.iter()
|
let choices: Vec<Cow<'_, str>> = items
|
||||||
.map(|item| item.to_string().into())
|
.map(|item| format!("{:?}", item).into())
|
||||||
.collect::<Vec<Cow<'v, str>>>();
|
.collect();
|
||||||
|
|
||||||
Err(choices)?
|
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(file_ct) = file.content_type() {
|
||||||
if let Some(ext_ct) = ContentType::from_extension(ext) {
|
if file_ct == &r#type {
|
||||||
if file_ct == &ext_ct {
|
|
||||||
return Ok(());
|
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))?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
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:
|
Luhn validator for credit-card-like numbers, you might write:
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# #[macro_use] extern crate rocket;
|
# #[macro_use] extern crate rocket;
|
||||||
extern crate time;
|
extern crate time;
|
||||||
|
|
||||||
|
use rocket::form::{self, Error};
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
struct CreditCard<'v> {
|
struct CreditCard {
|
||||||
#[field(validate = luhn())]
|
#[field(validate = luhn(self.cvv, &self.expiration))]
|
||||||
number: &'v str,
|
number: u64,
|
||||||
# #[field(validate = luhn())]
|
|
||||||
# other: String,
|
|
||||||
#[field(validate = range(..9999))]
|
#[field(validate = range(..9999))]
|
||||||
cvv: u16,
|
cvv: u16,
|
||||||
expiration: time::Date,
|
expiration: time::Date,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn luhn<'v, S: AsRef<str>>(field: S) -> rocket::form::Result<'v, ()> {
|
fn luhn<'v>(number: &u64, cvv: u16, exp: &time::Date) -> form::Result<'v, ()> {
|
||||||
let num = field.as_ref().parse::<u64>()?;
|
# 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
|
### Defaults
|
||||||
|
|
||||||
The [`FromForm`] trait allows types to specify a default value if one isn't
|
The [`FromForm`] trait allows types to specify a default value if one isn't
|
||||||
|
|
Loading…
Reference in New Issue