From ebb9f3cfddc809502cee0a34a0b62112eda8b016 Mon Sep 17 00:00:00 2001 From: ThouCheese Date: Sat, 27 Mar 2021 02:38:17 +0100 Subject: [PATCH] Allow field defaults in 'FromForm' derive. --- core/codegen/Cargo.toml | 1 + core/codegen/src/derive/form_field.rs | 42 ++- core/codegen/src/derive/from_form.rs | 15 +- core/codegen/src/derive/from_form_field.rs | 2 +- core/codegen/src/lib.rs | 8 +- core/codegen/tests/from_form.rs | 243 +++++++++++++++++- .../tests/ui-fail-nightly/from_form.stderr | 27 ++ .../tests/ui-fail-stable/from_form.stderr | 28 ++ core/codegen/tests/ui-fail/from_form.rs | 12 + core/lib/src/form/form.rs | 17 ++ site/guide/4-requests.md | 41 +++ 11 files changed, 427 insertions(+), 9 deletions(-) diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index d06594bd..99d570b7 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -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" diff --git a/core/codegen/src/derive/form_field.rs b/core/codegen/src/derive/form_field.rs index 9382785e..27001b09 100644 --- a/core/codegen/src/derive/form_field.rs +++ b/core/codegen/src/derive/form_field.rs @@ -17,6 +17,7 @@ pub enum FieldName { pub struct FieldAttr { pub name: Option, pub validate: Option>, + pub default: Option>, } impl FieldAttr { @@ -325,7 +326,46 @@ pub fn validators<'v>( })).unwrap() }); - Ok(exprs) + Ok(exprs) +} + +pub fn default<'v>(field: Field<'v>) -> Result> { + 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 = 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( diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index f6b37c57..f844ff79 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -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); })* diff --git a/core/codegen/src/derive/from_form_field.rs b/core/codegen/src/derive/from_form_field.rs index a2bcba4e..7c03f5b7 100644 --- a/core/codegen/src/derive/from_form_field.rs +++ b/core/codegen/src/derive/from_form_field.rs @@ -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")); diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 86b0ec36..65747427 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -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 diff --git a/core/codegen/tests/from_form.rs b/core/codegen/tests/from_form.rs index cdda26ed..118ad0e3 100644 --- a/core/codegen/tests/from_form.rs +++ b/core/codegen/tests/from_form.rs @@ -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> { Form::>::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, + #[field(default = Ok("hello".to_string()))] + res: form::Result<'a, String>, + #[field(default = vec![1, 2, 3])] + vec_num: Vec, + #[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 = 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 = strict(&form_string).ok(); + assert!(form2.is_none()); + + let form_string = &[ + "field1=101", + "field2=102", + "field3=true", + "field5=true" + ].join("&"); + + let form3: Option = 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 = 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 + })); +} diff --git a/core/codegen/tests/ui-fail-nightly/from_form.stderr b/core/codegen/tests/ui-fail-nightly/from_form.stderr index 0d2c058e..59ae7a8e 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form.stderr @@ -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: + > + > + > + > + and 5 others + = note: required because of the requirements on the impl of `Into` for `&str` diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index 0724589d..e49b4df4 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -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: + > + > + > + > + and 5 others + = note: required because of the requirements on the impl of `Into` for `&str` diff --git a/core/codegen/tests/ui-fail/from_form.rs b/core/codegen/tests/ui-fail/from_form.rs index 7e8b5515..680bc6d7 100644 --- a/core/codegen/tests/ui-fail/from_form.rs +++ b/core/codegen/tests/ui-fail/from_form.rs @@ -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() { } diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index 25367e83..856d9be0 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -56,6 +56,23 @@ use crate::form::prelude::*; /// can access fields of `T` transparently through a `Form`, 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`. +/// +/// ```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 diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 36e2dad5..c25ac72b 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -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`. Additionally, `FromForm` is implemented for +`Result>` 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, + ok_or_error: Result, 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, // 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