Allow field defaults in 'FromForm' derive.

This commit is contained in:
ThouCheese 2021-03-27 02:38:17 +01:00 committed by Sergio Benitez
parent 559320d155
commit ebb9f3cfdd
11 changed files with 427 additions and 9 deletions

View File

@ -28,3 +28,4 @@ glob = "0.3"
rocket = { version = "0.5.0-dev", path = "../lib", features = ["json"] } rocket = { version = "0.5.0-dev", path = "../lib", features = ["json"] }
version_check = "0.9" version_check = "0.9"
trybuild = "1.0" trybuild = "1.0"
time = "0.2.11"

View File

@ -17,6 +17,7 @@ pub enum FieldName {
pub struct FieldAttr { pub struct FieldAttr {
pub name: Option<FieldName>, pub name: Option<FieldName>,
pub validate: Option<SpanWrapped<syn::Expr>>, pub validate: Option<SpanWrapped<syn::Expr>>,
pub default: Option<SpanWrapped<syn::Expr>>,
} }
impl FieldAttr { impl FieldAttr {
@ -325,7 +326,46 @@ pub fn validators<'v>(
})).unwrap() })).unwrap()
}); });
Ok(exprs) Ok(exprs)
}
pub fn default<'v>(field: Field<'v>) -> Result<Option<syn::Expr>> {
let mut exprs = FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)?
.into_iter()
.filter_map(|a| a.default)
.map(move |expr| {
use syn::{Expr, Lit, ExprLit};
// As a result of calling `#expr.into()`, type inference fails for
// two common expressions: integer literals and the bare `None`. As
// a result, we cheat: if the syntax matches either of these two
// conditions, we provide the field type as a hint.
let is_int_lit = matches!(*expr, Expr::Lit(ExprLit { lit: Lit::Int(_), .. }));
let is_none = matches!(*expr, Expr::Path(ref e) if e.path.is_ident("None"));
let ty = field.stripped_ty();
let ty_hint = (is_int_lit || is_none)
.then(|| quote!(#ty))
.unwrap_or_else(|| quote!(_));
let opt_expr = if is_none {
quote_spanned!(expr.span => None)
} else if is_int_lit {
quote_spanned!(expr.span => Some(#expr))
} else {
quote_spanned!(expr.span => Some({ #expr }.into()))
};
syn::parse2(quote_spanned!(expr.span => {
let __default: Option<#ty_hint> = #opt_expr;
__default
})).unwrap()
});
let first: Option<syn::Expr> = exprs.next();
if let Some(expr) = exprs.next() {
return Err(expr.span()
.error("duplicate `default` form field attribute")
.help("form fields can have at most one `default`"));
}
Ok(first)
} }
pub fn first_duplicate<K: Spanned, V: PartialEq + Spanned>( pub fn first_duplicate<K: Spanned, V: PartialEq + Spanned>(

View File

@ -230,16 +230,23 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream {
.try_field_map(|_, f| { .try_field_map(|_, f| {
let (ident, ty, name_view) = (f.ident(), f.stripped_ty(), f.name_view()?); let (ident, ty, name_view) = (f.ident(), f.stripped_ty(), f.name_view()?);
let validator = validators(f, &ident, true)?; let validator = validators(f, &ident, true)?;
let user_default = default(f)?;
let default = user_default
.map(|expr| quote_spanned!(ty.span() =>
#expr.or_else(|| <#ty as #_form::FromForm<'__f>>::default(_opts))
.ok_or_else(|| #_form::ErrorKind::Missing.into())
))
.unwrap_or_else(|| quote_spanned!(ty.span() =>
<#ty as #_form::FromForm<'__f>>::default(_opts)
.ok_or_else(|| #_form::ErrorKind::Missing.into())
));
let _err = _Err; let _err = _Err;
Ok(quote_spanned! { ty.span() => { Ok(quote_spanned! { ty.span() => {
let _name = #name_view; let _name = #name_view;
let _opts = __c.__opts; let _opts = __c.__opts;
__c.#ident __c.#ident
.map(<#ty as #_form::FromForm<'__f>>::finalize) .map(<#ty as #_form::FromForm<'__f>>::finalize)
.unwrap_or_else(|| { .unwrap_or_else(|| #default)
<#ty as #_form::FromForm<'__f>>::default(_opts)
.ok_or_else(|| #_form::ErrorKind::Missing.into())
})
.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); })*

View File

@ -8,7 +8,7 @@ pub fn derive_from_form_field(input: proc_macro::TokenStream) -> TokenStream {
DeriveGenerator::build_for(input, quote!(impl<'__v> #_form::FromFormField<'__v>)) DeriveGenerator::build_for(input, quote!(impl<'__v> #_form::FromFormField<'__v>))
.support(Support::Enum) .support(Support::Enum)
.validator(ValidatorBuild::new() .validator(ValidatorBuild::new()
// We only accepts C-like enums with at least one variant. // We only accept C-like enums with at least one variant.
.fields_validate(|_, fields| { .fields_validate(|_, fields| {
if !fields.is_empty() { if !fields.is_empty() {
return Err(fields.span().error("variants cannot have fields")); return Err(fields.span().error("variants cannot have fields"));

View File

@ -556,8 +556,10 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream {
/// #[field(name = "renamed_field")] /// #[field(name = "renamed_field")]
/// #[field(name = uncased("RenamedField"))] /// #[field(name = uncased("RenamedField"))]
/// other: &'r str, /// other: &'r str,
/// #[field(validate = range(1..))] /// #[field(validate = range(1..), default = 3)]
/// r#type: usize, /// r#type: usize,
/// #[field(default = true)]
/// is_nice: bool,
/// } /// }
/// ``` /// ```
/// ///
@ -574,12 +576,14 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream {
/// The derive accepts one field attribute: `field`, with the following syntax: /// The derive accepts one field attribute: `field`, with the following syntax:
/// ///
/// ```text /// ```text
/// field := name? validate* /// field := name? default? validate*
/// ///
/// name := 'name' '=' name_val /// name := 'name' '=' name_val
/// name_val := '"' FIELD_NAME '"' /// name_val := '"' FIELD_NAME '"'
/// | 'uncased(' '"' FIELD_NAME '"' ') /// | 'uncased(' '"' FIELD_NAME '"' ')
/// ///
/// default := 'default' '=' EXPR
///
/// validate := 'validate' '=' EXPR /// validate := 'validate' '=' EXPR
/// ///
/// FIELD_NAME := valid field name, according to the HTML5 spec /// FIELD_NAME := valid field name, according to the HTML5 spec

View File

@ -1,4 +1,6 @@
use rocket::form::{Form, Strict, FromForm, FromFormField, Errors}; use std::{collections::{BTreeMap, HashMap}, net::{IpAddr, SocketAddr}, num::NonZeroI32};
use rocket::form::{self, Form, Strict, FromForm, FromFormField, Errors};
fn strict<'f, T: FromForm<'f>>(string: &'f str) -> Result<T, Errors<'f>> { fn strict<'f, T: FromForm<'f>>(string: &'f str) -> Result<T, Errors<'f>> {
Form::<Strict<T>>::parse(string).map(|s| s.into_inner()) Form::<Strict<T>>::parse(string).map(|s| s.into_inner())
@ -612,3 +614,242 @@ fn test_multipart() {
assert!(response.status().class().is_success()); assert!(response.status().class().is_success());
} }
fn test_hashmap() -> HashMap<&'static str, &'static str> {
let mut map = HashMap::new();
map.insert("key", "value");
map
}
fn test_btreemap() -> BTreeMap<&'static str, &'static str> {
let mut map = BTreeMap::new();
map.insert("key", "value");
map
}
mod default_macro_tests {
#![allow(dead_code)]
#[derive(rocket::FromForm)]
struct Form1 {
#[field(default = 10)]
field: i8,
}
#[derive(rocket::FromForm)]
struct Form2 {
#[field(default = 10)]
field: u8,
}
#[derive(rocket::FromForm)]
struct Form3 {
#[field(default = 10)]
field: usize,
}
#[derive(rocket::FromForm)]
struct Form4 {
#[field(default = 10)]
field: usize,
}
#[derive(rocket::FromForm)]
struct Form5 {
#[field(default = None)]
field: usize,
}
#[derive(rocket::FromForm)]
struct Form6 {
#[field(default = None)]
field: usize,
}
#[derive(rocket::FromForm)]
struct Form7 {
#[field(default = None)]
field: String,
}
#[derive(rocket::FromForm)]
struct Form8<'a> {
#[field(default = None)]
field: &'a str,
}
// #[derive(rocket::FromForm)]
// struct Form9<'a> {
// #[field(default = None)]
// field: Cow<'a, str>,
// }
#[derive(rocket::FromForm)]
struct Form10 {
#[field(default = "string")]
field: String,
}
#[derive(rocket::FromForm)]
struct Form11<'a> {
#[field(default = "string")]
field: &'a str,
}
// #[derive(rocket::FromForm)]
// struct Form12 {
// #[field(default = "string")]
// field: Cow<'static, str>,
// }
// #[derive(rocket::FromForm)]
// struct Form13<'a> {
// #[field(default = "string".to_string())]
// field: Cow<'a, str>,
// }
#[derive(rocket::FromForm)]
struct Form14 {
#[field(default = 'a')]
field: String,
}
#[derive(rocket::FromForm)]
struct Form15 {
#[field(default = 10 + 2)]
field: u8,
}
#[derive(rocket::FromForm)]
struct Form16 {
#[field(default = 10 + 2)]
field: i8,
}
#[derive(rocket::FromForm)]
struct Form17 {
#[field(default = 10 + 2)]
field: i64,
}
#[derive(rocket::FromForm)]
struct Form18 {
#[field(default = 10 + 2usize)]
field: usize,
}
#[derive(rocket::FromForm)]
struct Form19 {
#[field(default = 10 + 2usize)]
field: usize,
}
}
#[test]
fn test_defaults() {
#[derive(FromForm, PartialEq, Debug)]
struct FormWithDefaults<'a> {
#[field(default = 100)]
field1: usize,
field2: i128,
#[field(default = true)]
field3: bool,
#[field(default = false)]
field4: bool,
field5: bool,
#[field(default = Some(true))]
opt: Option<bool>,
#[field(default = Ok("hello".to_string()))]
res: form::Result<'a, String>,
#[field(default = vec![1, 2, 3])]
vec_num: Vec<usize>,
#[field(default = vec!["wow", "a", "string", "nice"])]
vec_str: Vec<&'a str>,
#[field(default = test_hashmap())]
hashmap: HashMap<&'a str, &'a str>,
#[field(default = test_btreemap())]
btreemap: BTreeMap<&'a str, &'a str>,
#[field(default = false)]
boolean: bool,
#[field(default = 3)]
unsigned: usize,
#[field(default = NonZeroI32::new(3).unwrap())]
nonzero: NonZeroI32,
#[field(default = 3.0)]
float: f64,
#[field(default = "wow")]
str_ref: &'a str,
#[field(default = "wowie")]
string: String,
#[field(default = [192u8, 168, 1, 0])]
ip: IpAddr,
#[field(default = ([192u8, 168, 1, 0], 20))]
addr: SocketAddr,
#[field(default = time::date!(2021-05-27))]
date: time::Date,
#[field(default = time::time!(01:15:00))]
time: time::Time,
#[field(default = time::PrimitiveDateTime::new(
time::date!(2021-05-27),
time::time!(01:15:00),
))]
datetime: time::PrimitiveDateTime,
}
let form_string = &["field1=101", "field2=102"].join("&");
let form1: Option<FormWithDefaults> = lenient(&form_string).ok();
assert_eq!(form1, Some(FormWithDefaults {
field1: 101,
field2: 102,
field3: true,
field4: false,
field5: false,
opt: Some(true),
res: Ok("hello".to_string()),
vec_num: vec![1, 2, 3],
vec_str: vec!["wow", "a", "string", "nice"],
hashmap: test_hashmap(),
btreemap: test_btreemap(),
boolean: false,
unsigned: 3,
nonzero: NonZeroI32::new(3).unwrap(),
float: 3.0,
str_ref: "wow",
string: "wowie".to_string(),
ip: [192u8, 168, 1, 0].into(),
addr: ([192u8, 168, 1, 0], 20).into(),
date: time::date!(2021-05-27),
time: time::time!(01:15:00),
datetime: time::PrimitiveDateTime::new(time::date!(2021-05-27), time::time!(01:15:00)),
}));
let form2: Option<FormWithDefaults> = strict(&form_string).ok();
assert!(form2.is_none());
let form_string = &[
"field1=101",
"field2=102",
"field3=true",
"field5=true"
].join("&");
let form3: Option<FormWithDefaults> = lenient(&form_string).ok();
assert_eq!(form3, Some(FormWithDefaults {
field1: 101,
field2: 102,
field3: true,
field4: false,
field5: true,
..form1.unwrap() // cannot fail due to assertio and keeps test more concise
}));
let form4: Option<FormWithDefaults> = strict(&form_string).ok();
assert_eq!(form4, Some(FormWithDefaults {
field1: 101,
field2: 102,
field3: true,
field4: false,
field5: true,
..form3.unwrap() // cannot fail due to assertio and keeps test more concise
}));
}

View File

@ -363,6 +363,19 @@ note: error occurred while deriving `FromForm`
| ^^^^^^^^ | ^^^^^^^^
= note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info)
error: duplicate attribute parameter: default
--> $DIR/from_form.rs:174:26
|
174 | #[field(default = 1, default = 2)]
| ^^^^^^^^^^^
|
note: error occurred while deriving `FromForm`
--> $DIR/from_form.rs:172:10
|
172 | #[derive(FromForm)]
| ^^^^^^^^
= note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0425]: cannot find function `unknown` in this scope error[E0425]: cannot find function `unknown` in this scope
--> $DIR/from_form.rs:150:24 --> $DIR/from_form.rs:150:24
| |
@ -403,3 +416,17 @@ error[E0308]: mismatched types
| |
162 | #[field(validate = ext("hello"))] 162 | #[field(validate = ext("hello"))]
| ^^^^^^^ expected struct `ContentType`, found `&str` | ^^^^^^^ expected struct `ContentType`, found `&str`
error[E0277]: the trait bound `i32: From<&str>` is not satisfied
--> $DIR/from_form.rs:168:23
|
168 | #[field(default = "no conversion")]
| ^^^^^^^^^^^^^^^ the trait `From<&str>` is not implemented for `i32`
|
= help: the following implementations were found:
<i32 as From<NonZeroI32>>
<i32 as From<bool>>
<i32 as From<i16>>
<i32 as From<i8>>
and 5 others
= note: required because of the requirements on the impl of `Into<i32>` for `&str`

View File

@ -389,6 +389,20 @@ error: [note] error occurred while deriving `FromForm`
| |
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: duplicate attribute parameter: default
--> $DIR/from_form.rs:174:26
|
174 | #[field(default = 1, default = 2)]
| ^^^^^^^
error: [note] error occurred while deriving `FromForm`
--> $DIR/from_form.rs:172:10
|
172 | #[derive(FromForm)]
| ^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0425]: cannot find function `unknown` in this scope error[E0425]: cannot find function `unknown` in this scope
--> $DIR/from_form.rs:150:24 --> $DIR/from_form.rs:150:24
| |
@ -429,3 +443,17 @@ error[E0308]: mismatched types
| |
162 | #[field(validate = ext("hello"))] 162 | #[field(validate = ext("hello"))]
| ^^^^^^^ expected struct `ContentType`, found `&str` | ^^^^^^^ expected struct `ContentType`, found `&str`
error[E0277]: the trait bound `i32: From<&str>` is not satisfied
--> $DIR/from_form.rs:168:23
|
168 | #[field(default = "no conversion")]
| ^^^^^^^^^^^^^^^ the trait `From<&str>` is not implemented for `i32`
|
= help: the following implementations were found:
<i32 as From<NonZeroI32>>
<i32 as From<bool>>
<i32 as From<i16>>
<i32 as From<i8>>
and 5 others
= note: required because of the requirements on the impl of `Into<i32>` for `&str`

View File

@ -163,4 +163,16 @@ struct Validate3 {
first: String, first: String,
} }
#[derive(FromForm)]
struct Default0 {
#[field(default = "no conversion")]
first: i32,
}
#[derive(FromForm)]
struct Default1 {
#[field(default = 1, default = 2)]
double_default: i32,
}
fn main() { } fn main() { }

View File

@ -56,6 +56,23 @@ use crate::form::prelude::*;
/// can access fields of `T` transparently through a `Form<T>`, as seen above /// can access fields of `T` transparently through a `Form<T>`, as seen above
/// with `user_input.value`. /// with `user_input.value`.
/// ///
/// ### Defaults
/// When deriving a `FromForm` implementation it is possible to provide a
/// default that is then used when no value is provided. For a form field of
/// type `T`, the provided default can be any expression of type `U` such that
/// `U` implements `Into<T>`.
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::form::Form;
///
/// #[derive(FromForm)]
/// struct UserInput<'r> {
/// #[field(default = "anonymous")]
/// value: &'r str
/// }
/// ```
///
/// ## Data Limits /// ## Data Limits
/// ///
/// The total amount of data accepted by the `Form` data guard is limited by the /// The total amount of data accepted by the `Form` data guard is limited by the

View File

@ -956,6 +956,47 @@ 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 it is validated prior to those fields that do. For `CreditCard`, `cvv` and
`expiration` will be validated prior to `number`. `expiration` will be validated prior to `number`.
### Defaults
The [`FromForm`] trait allows types to specify a default value if one isn't
provided in a submitted form. This includes types such as `bool`, useful for
checkboxes, and `Option<T>`. Additionally, `FromForm` is implemented for
`Result<T, Errors<'_>>` where the error value is [`Errors<'_>`]. All of these
types can be used just like any other form field:
```rust
# use rocket::form::FromForm;
use rocket::form::Errors;
#[derive(FromForm)]
struct MyForm<'v> {
maybe_string: Option<String>,
ok_or_error: Result<Vec<String>, Errors<'v>>,
here: bool,
}
# rocket_guide_tests::assert_form_parses_ok!(MyForm, "");
```
Additionally, this default may be overridden when usng the `FromForm` derive
macro. The default specified in the macro is applied both in `Lenient` and
`Strict` mode. Furthermore, in `Lenient` mode, it takes precendence over the
default specified by the `FromForm` trait implementation of the field.
```rust
# use rocket::form::FromForm;
#[derive(FromForm)]
struct MyForm {
#[field(default = "hello")]
greet: String, // <String as FromForm> does not provide a default, so we set one here
#[field(default = true)]
is_friendly: bool, // The default for bool is `false`, but we override it here
}
```
[`Errors<'_>`]: @api/rocket/form/struct.Errors.html
### Collections ### Collections
Rocket's form support allows your application to express _any_ structure with Rocket's form support allows your application to express _any_ structure with