mirror of
https://github.com/rwf2/Rocket.git
synced 2025-01-17 23:19:06 +00:00
Allow field defaults in 'FromForm' derive.
This commit is contained in:
parent
559320d155
commit
ebb9f3cfdd
@ -28,3 +28,4 @@ glob = "0.3"
|
||||
rocket = { version = "0.5.0-dev", path = "../lib", features = ["json"] }
|
||||
version_check = "0.9"
|
||||
trybuild = "1.0"
|
||||
time = "0.2.11"
|
||||
|
@ -17,6 +17,7 @@ pub enum FieldName {
|
||||
pub struct FieldAttr {
|
||||
pub name: Option<FieldName>,
|
||||
pub validate: Option<SpanWrapped<syn::Expr>>,
|
||||
pub default: Option<SpanWrapped<syn::Expr>>,
|
||||
}
|
||||
|
||||
impl FieldAttr {
|
||||
@ -325,7 +326,46 @@ pub fn validators<'v>(
|
||||
})).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>(
|
||||
|
@ -230,16 +230,23 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream {
|
||||
.try_field_map(|_, f| {
|
||||
let (ident, ty, name_view) = (f.ident(), f.stripped_ty(), f.name_view()?);
|
||||
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;
|
||||
Ok(quote_spanned! { ty.span() => {
|
||||
let _name = #name_view;
|
||||
let _opts = __c.__opts;
|
||||
__c.#ident
|
||||
.map(<#ty as #_form::FromForm<'__f>>::finalize)
|
||||
.unwrap_or_else(|| {
|
||||
<#ty as #_form::FromForm<'__f>>::default(_opts)
|
||||
.ok_or_else(|| #_form::ErrorKind::Missing.into())
|
||||
})
|
||||
.unwrap_or_else(|| #default)
|
||||
.and_then(|#ident| {
|
||||
let mut _es = #_form::Errors::new();
|
||||
#(if let #_err(_e) = #validator { _es.extend(_e); })*
|
||||
|
@ -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>))
|
||||
.support(Support::Enum)
|
||||
.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| {
|
||||
if !fields.is_empty() {
|
||||
return Err(fields.span().error("variants cannot have fields"));
|
||||
|
@ -556,8 +556,10 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream {
|
||||
/// #[field(name = "renamed_field")]
|
||||
/// #[field(name = uncased("RenamedField"))]
|
||||
/// other: &'r str,
|
||||
/// #[field(validate = range(1..))]
|
||||
/// #[field(validate = range(1..), default = 3)]
|
||||
/// 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:
|
||||
///
|
||||
/// ```text
|
||||
/// field := name? validate*
|
||||
/// field := name? default? validate*
|
||||
///
|
||||
/// name := 'name' '=' name_val
|
||||
/// name_val := '"' FIELD_NAME '"'
|
||||
/// | 'uncased(' '"' FIELD_NAME '"' ')
|
||||
///
|
||||
/// default := 'default' '=' EXPR
|
||||
///
|
||||
/// validate := 'validate' '=' EXPR
|
||||
///
|
||||
/// FIELD_NAME := valid field name, according to the HTML5 spec
|
||||
|
@ -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>> {
|
||||
Form::<Strict<T>>::parse(string).map(|s| s.into_inner())
|
||||
@ -612,3 +614,242 @@ fn test_multipart() {
|
||||
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
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
|
||||
--> $DIR/from_form.rs:150:24
|
||||
|
|
||||
@ -403,3 +416,17 @@ error[E0308]: mismatched types
|
||||
|
|
||||
162 | #[field(validate = ext("hello"))]
|
||||
| ^^^^^^^ 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`
|
||||
|
@ -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)
|
||||
|
||||
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
|
||||
--> $DIR/from_form.rs:150:24
|
||||
|
|
||||
@ -429,3 +443,17 @@ error[E0308]: mismatched types
|
||||
|
|
||||
162 | #[field(validate = ext("hello"))]
|
||||
| ^^^^^^^ 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`
|
||||
|
@ -163,4 +163,16 @@ struct Validate3 {
|
||||
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() { }
|
||||
|
@ -56,6 +56,23 @@ use crate::form::prelude::*;
|
||||
/// can access fields of `T` transparently through a `Form<T>`, as seen above
|
||||
/// 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
|
||||
///
|
||||
/// The total amount of data accepted by the `Form` data guard is limited by the
|
||||
|
@ -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
|
||||
`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
|
||||
|
||||
Rocket's form support allows your application to express _any_ structure with
|
||||
|
Loading…
Reference in New Issue
Block a user