diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index 99d570b7..7c549591 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -26,6 +26,7 @@ glob = "0.3" [dev-dependencies] rocket = { version = "0.5.0-dev", path = "../lib", features = ["json"] } +pretty_assertions = "0.7" 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 27001b09..6e9fb1f1 100644 --- a/core/codegen/src/derive/form_field.rs +++ b/core/codegen/src/derive/form_field.rs @@ -1,8 +1,8 @@ use devise::{*, ext::{TypeExt, SpanDiagnosticExt}}; -use syn::visit_mut::VisitMut; -use syn::visit::Visit; +use syn::{visit_mut::VisitMut, visit::Visit}; use proc_macro2::{TokenStream, TokenTree, Span}; +use quote::{ToTokens, TokenStreamExt}; use crate::syn_ext::IdentExt; use crate::name::Name; @@ -17,7 +17,8 @@ pub enum FieldName { pub struct FieldAttr { pub name: Option, pub validate: Option>, - pub default: Option>, + pub default: Option, + pub default_with: Option, } impl FieldAttr { @@ -127,7 +128,7 @@ impl std::ops::Deref for FieldName { } } -impl quote::ToTokens for FieldName { +impl ToTokens for FieldName { fn to_tokens(&self, tokens: &mut TokenStream) { (self as &Name).to_tokens(tokens) } @@ -208,7 +209,6 @@ struct ValidationMutator<'a> { impl ValidationMutator<'_> { fn visit_token_stream(&mut self, tt: TokenStream) -> TokenStream { - use quote::{ToTokens, TokenStreamExt}; use TokenTree::*; let mut iter = tt.into_iter(); @@ -329,43 +329,62 @@ pub fn validators<'v>( 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() - }); +/// Take an $expr in `default = $expr` and turn it into a `Some($expr.into())`. +/// +/// As a result of calling `into()`, type inference fails for two common +/// expressions: integer literals and the bare `None`. As a result, we cheat: if +/// the expr matches either condition, we pass them through unchanged. +fn default_expr(expr: &syn::Expr) -> TokenStream { + use syn::{Expr, Lit, ExprLit}; - 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`")); + if matches!(expr, Expr::Path(e) if e.path.is_ident("None")) { + quote!(#expr) + } else if matches!(expr, Expr::Lit(ExprLit { lit: Lit::Int(_), .. })) { + quote_spanned!(expr.span() => Some(#expr)) + } else { + quote_spanned!(expr.span() => Some({ #expr }.into())) + } +} + +pub fn default<'v>(field: Field<'v>) -> Result> { + let attrs = FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)?; + + // Expressions in `default = `, except for `None`, are wrapped in `Some()`. + let mut expr = attrs.iter().filter_map(|a| a.default.as_ref()).map(default_expr); + + // Expressions in `default_with` are passed through directly. + let mut expr_with = attrs.iter() + .filter_map(|a| a.default_with.as_ref()) + .map(|e| e.to_token_stream()); + + // Pull the first `default` and `default_with` expressions. + let (default, default_with) = (expr.next(), expr_with.next()); + + // If there are any more of either, emit an error. + if let (Some(e), _) | (_, Some(e)) = (expr.next(), expr_with.next()) { + return Err(e.span() + .error("duplicate default field expression") + .help("at most one `default` or `default_with` is allowed")); } - Ok(first) + // Emit the final expression of type `Option<#ty>` unless both `default` and + // `default_with` were provided in which case we error. + let ty = field.stripped_ty(); + match (default, default_with) { + (Some(e1), Some(e2)) => { + return Err(e1.span() + .error("duplicate default expressions") + .help("only one of `default` or `default_with` must be used") + .span_note(e2.span(), "other default expression is here")); + }, + (Some(e), None) | (None, Some(e)) => { + Ok(Some(quote_spanned!(e.span() => { + let __default: Option<#ty> = #e; + __default + }))) + }, + (None, None) => Ok(None) + } } pub fn first_duplicate( diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index f844ff79..c0ef555f 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -230,36 +230,33 @@ 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 default = default(f)? + .unwrap_or_else(|| quote_spanned!(ty.span() => { + <#ty as #_form::FromForm<'__f>>::default(__opts) + })); + let _err = _Err; Ok(quote_spanned! { ty.span() => { - let _name = #name_view; - let _opts = __c.__opts; + let __name = #name_view; + let __opts = __c.__opts; __c.#ident .map(<#ty as #_form::FromForm<'__f>>::finalize) - .unwrap_or_else(|| #default) + .unwrap_or_else(|| { + #default.ok_or_else(|| #_form::ErrorKind::Missing.into()) + }) .and_then(|#ident| { - let mut _es = #_form::Errors::new(); - #(if let #_err(_e) = #validator { _es.extend(_e); })* + let mut __es = #_form::Errors::new(); + #(if let #_err(__e) = #validator { __es.extend(__e); })* - match _es.is_empty() { + match __es.is_empty() { true => #_Ok(#ident), - false => #_Err(_es) + false => #_Err(__es) } }) - .map_err(|_e| _e.with_name(_name)) - .map_err(|_e| match _e.is_empty() { + .map_err(|__e| __e.with_name(__name)) + .map_err(|__e| match __e.is_empty() { true => #_form::ErrorKind::Unknown.into(), - false => _e, + false => __e, }) }}) }) diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 65747427..296bd85b 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -549,7 +549,6 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # /// #[derive(FromForm)] /// struct MyStruct<'r> { /// field: usize, @@ -558,7 +557,7 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// other: &'r str, /// #[field(validate = range(1..), default = 3)] /// r#type: usize, -/// #[field(default = true)] +/// #[field(default = None)] /// is_nice: bool, /// } /// ``` @@ -569,8 +568,8 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// implementation parses a form whose field names match the field names of the /// structure on which the derive was applied. Each field's value is parsed with /// the [`FromForm`] implementation of the field's type. The `FromForm` -/// implementation succeeds only when all of the field parses succeed or return -/// a default. Errors are collected into a [`form::Errors`] and return if +/// implementation succeeds only when all fields parse successfully or return a +/// default. Errors are collected into a [`form::Errors`] and returned if /// non-empty after parsing all fields. /// /// The derive accepts one field attribute: `field`, with the following syntax: @@ -578,26 +577,28 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// ```text /// field := name? default? validate* /// -/// name := 'name' '=' name_val +/// name := 'name' '=' name_val ','? /// name_val := '"' FIELD_NAME '"' /// | 'uncased(' '"' FIELD_NAME '"' ') /// -/// default := 'default' '=' EXPR +/// default := 'default' '=' EXPR ','? +/// | 'default_with' '=' EXPR ','? /// -/// validate := 'validate' '=' EXPR +/// validate := 'validate' '=' EXPR ','? /// /// FIELD_NAME := valid field name, according to the HTML5 spec /// EXPR := valid expression, as defined by Rust /// ``` /// -/// The attribute can be applied any number of times on a field. When applied, -/// the attribute looks as follows: +/// The attribute can be applied any number of times on a field as long as at +/// most _one_ of `default` or `default_with` is present per field: /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # /// #[derive(FromForm)] /// struct MyStruct { +/// #[field(name = uncased("number"))] +/// #[field(default = 42)] /// field: usize, /// #[field(name = "renamed_field")] /// #[field(name = uncased("anotherName"))] @@ -615,12 +616,42 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// case-preserving. When more than one `name` attribute is applied, the field /// will match against _any_ of the names. /// -/// **`validate`** +/// **`validate = expr`** /// -/// The validation expression will be run if the field type parses successfully. -/// The expression must return a value of type `Result<(), form::Errors>`. On -/// `Err`, the errors are added to the thus-far collected errors. If more than -/// one `validate` attribute is applied, _all_ validations are run. +/// The validation `expr` is run if the field type parses successfully. The +/// expression must return a value of type `Result<(), form::Errors>`. On `Err`, +/// the errors are added to the thus-far collected errors. If more than one +/// `validate` attribute is applied, _all_ validations are run. +/// +/// **`default = expr`** +/// +/// If `expr` is not literally `None`, the parameter sets the default value of +/// the field to be `expr.into()`. If `expr` _is_ `None`, the parameter _unsets_ +/// the default value of the field, if any. The expression is only evaluated if +/// the attributed field is missing in the incoming form. +/// +/// Except when `expr` is `None`, `expr` must be of type `T: Into` where `F` +/// is the field's type. +/// +/// **`default_with = expr`** +/// +/// The parameter sets the default value of the field to be exactly `expr` which +/// must be of type `Option` where `F` is the field's type. If the expression +/// evaluates to `None`, there is no default. Otherwise the value wrapped in +/// `Some` is used. The expression is only evaluated if the attributed field is +/// missing in the incoming form. +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use std::num::NonZeroUsize; +/// +/// #[derive(FromForm)] +/// struct MyForm { +/// // `NonZeroUsize::new()` return an `Option`. +/// #[field(default_with = NonZeroUsize::new(42))] +/// num: NonZeroUsize, +/// } +/// ``` /// /// [`FromForm`]: rocket::form::FromForm /// [`form::Errors`]: rocket::form::Errors diff --git a/core/codegen/tests/from_form.rs b/core/codegen/tests/from_form.rs index 118ad0e3..d9e458a1 100644 --- a/core/codegen/tests/from_form.rs +++ b/core/codegen/tests/from_form.rs @@ -1,4 +1,6 @@ -use std::{collections::{BTreeMap, HashMap}, net::{IpAddr, SocketAddr}, num::NonZeroI32}; +use std::net::{IpAddr, SocketAddr}; +use std::collections::{BTreeMap, HashMap}; +use pretty_assertions::assert_eq; use rocket::form::{self, Form, Strict, FromForm, FromFormField, Errors}; @@ -615,153 +617,90 @@ 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 { +#[test] +fn test_default_removed() { + #[derive(FromForm, PartialEq, Debug)] + struct FormNoDefault { #[field(default = None)] - field: usize, - } - - #[derive(rocket::FromForm)] - struct Form6 { + field1: bool, #[field(default = None)] - field: usize, - } - - #[derive(rocket::FromForm)] - struct Form7 { + field2: Option, #[field(default = None)] - field: String, + field3: Option>, + #[field(default_with = None)] + field4: bool, } - #[derive(rocket::FromForm)] - struct Form8<'a> { - #[field(default = None)] - field: &'a str, - } + let form_string = &["field1=false", "field2=10", "field3=23", "field4"].join("&"); + let form1: Option = lenient(&form_string).ok(); + assert_eq!(form1, Some(FormNoDefault { + field1: false, + field2: Some(10), + field3: Some(Some(23)), + field4: true, + })); - // #[derive(rocket::FromForm)] - // struct Form9<'a> { - // #[field(default = None)] - // field: Cow<'a, str>, - // } + let form_string = &["field1=true", "field2=10", "field3=23", "field4"].join("&"); + let form1: Option = lenient(&form_string).ok(); + assert_eq!(form1, Some(FormNoDefault { + field1: true, + field2: Some(10), + field3: Some(Some(23)), + field4: true, + })); - #[derive(rocket::FromForm)] - struct Form10 { - #[field(default = "string")] - field: String, - } + // Field 1 missing. + let form_string = &["field2=20", "field3=10", "field4"].join("&"); + assert!(lenient::(&form_string).is_err()); - #[derive(rocket::FromForm)] - struct Form11<'a> { - #[field(default = "string")] - field: &'a str, - } + // Field 2 missing. + let form_string = &["field1=true", "field3=10", "field4"].join("&"); + assert!(lenient::(&form_string).is_err()); - // #[derive(rocket::FromForm)] - // struct Form12 { - // #[field(default = "string")] - // field: Cow<'static, str>, - // } + // Field 3 missing. + let form_string = &["field1=true", "field2=10", "field4=false"].join("&"); + assert!(lenient::(&form_string).is_err()); - // #[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, - } + // Field 4 missing. + let form_string = &["field1=true", "field2=10", "field3=23"].join("&"); + assert!(lenient::(&form_string).is_err()); } #[test] fn test_defaults() { + 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 + } + #[derive(FromForm, PartialEq, Debug)] struct FormWithDefaults<'a> { + field2: i128, + field5: bool, + #[field(default = 100)] field1: usize, - field2: i128, - #[field(default = true)] field3: bool, #[field(default = false)] field4: bool, - field5: bool, - + #[field(default = 254 + 1)] + field6: u8, #[field(default = Some(true))] - opt: Option, - #[field(default = Ok("hello".to_string()))] + opt1: Option, + #[field(default = false)] + opt2: Option, + #[field(default = Ok("hello".into()))] res: form::Result<'a, String>, + #[field(default = Ok("hello"))] + res2: form::Result<'a, &'a str>, #[field(default = vec![1, 2, 3])] vec_num: Vec, #[field(default = vec!["wow", "a", "string", "nice"])] @@ -770,12 +709,14 @@ fn test_defaults() { hashmap: HashMap<&'a str, &'a str>, #[field(default = test_btreemap())] btreemap: BTreeMap<&'a str, &'a str>, - #[field(default = false)] + #[field(default_with = Some(false))] boolean: bool, - #[field(default = 3)] + #[field(default_with = (|| Some(777))())] unsigned: usize, - #[field(default = NonZeroI32::new(3).unwrap())] - nonzero: NonZeroI32, + #[field(default = std::num::NonZeroI32::new(3).unwrap())] + nonzero: std::num::NonZeroI32, + #[field(default_with = std::num::NonZeroI32::new(9001))] + nonzero2: std::num::NonZeroI32, #[field(default = 3.0)] float: f64, #[field(default = "wow")] @@ -797,24 +738,31 @@ fn test_defaults() { datetime: time::PrimitiveDateTime, } - let form_string = &["field1=101", "field2=102"].join("&"); + // `field2` has no default. + assert!(lenient::("").is_err()); + // every other field should. + let form_string = &["field2=102"].join("&"); let form1: Option = lenient(&form_string).ok(); assert_eq!(form1, Some(FormWithDefaults { - field1: 101, + field1: 100, field2: 102, field3: true, field4: false, field5: false, - opt: Some(true), - res: Ok("hello".to_string()), + field6: 255, + opt1: Some(true), + opt2: Some(false), + res: Ok("hello".into()), + res2: Ok("hello"), 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(), + unsigned: 777, + nonzero: std::num::NonZeroI32::new(3).unwrap(), + nonzero2: std::num::NonZeroI32::new(9001).unwrap(), float: 3.0, str_ref: "wow", string: "wowie".to_string(), @@ -824,16 +772,12 @@ fn test_defaults() { 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("&"); - + // Ensure actual form field values take precendence. + 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, @@ -841,8 +785,10 @@ fn test_defaults() { field3: true, field4: false, field5: true, - ..form1.unwrap() // cannot fail due to assertio and keeps test more concise + ..form1.unwrap() })); + + // And that strict parsing still works. let form4: Option = strict(&form_string).ok(); assert_eq!(form4, Some(FormWithDefaults { field1: 101, @@ -850,6 +796,49 @@ fn test_defaults() { field3: true, field4: false, field5: true, - ..form3.unwrap() // cannot fail due to assertio and keeps test more concise + ..form3.unwrap() + })); +} + +#[test] +fn test_lazy_default() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + static VAL: AtomicUsize = AtomicUsize::new(40); + + fn f() -> usize { + VAL.fetch_add(1, Ordering::Relaxed) + } + + fn opt_f() -> Option { + Some(f()) + } + + #[derive(Debug, PartialEq, FromForm)] + struct MyForm { + #[field(default = f())] + missing0: usize, + #[field(default = VAL.load(Ordering::Relaxed))] + missing1: usize, + #[field(default_with = opt_f())] + missing2: usize, + #[field(default_with = opt_f())] + a: usize, + #[field(default = f())] + b: usize, + #[field(default = VAL.load(Ordering::Relaxed))] + missing3: usize, + } + + // Ensure actual form field values take precendence. + let form_string = &["a=100", "b=300"].join("&"); + let form3: Option = lenient(&form_string).ok(); + assert_eq!(form3, Some(MyForm { + missing0: 40, + missing1: 41, + missing2: 41, + a: 100, + b: 300, + missing3: 42 })); } diff --git a/core/codegen/tests/ui-fail-nightly/from_form.stderr b/core/codegen/tests/ui-fail-nightly/from_form.stderr index 59ae7a8e..5402f737 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form.stderr @@ -376,6 +376,58 @@ 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 default field expression + --> $DIR/from_form.rs:181:23 + | +181 | #[field(default = 2)] + | ^ + | + = help: at most one `default` or `default_with` is allowed +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:178:10 + | +178 | #[derive(FromForm)] + | ^^^^^^^^ + = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: duplicate default expressions + --> $DIR/from_form.rs:187:23 + | +187 | #[field(default = 1, default_with = None)] + | ^ + | + = help: only one of `default` or `default_with` must be used +note: other default expression is here + --> $DIR/from_form.rs:187:41 + | +187 | #[field(default = 1, default_with = None)] + | ^^^^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:185:10 + | +185 | #[derive(FromForm)] + | ^^^^^^^^ + = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: duplicate default expressions + --> $DIR/from_form.rs:194:23 + | +194 | #[field(default = 1)] + | ^ + | + = help: only one of `default` or `default_with` must be used +note: other default expression is here + --> $DIR/from_form.rs:193:28 + | +193 | #[field(default_with = None)] + | ^^^^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:191:10 + | +191 | #[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 | @@ -430,3 +482,16 @@ error[E0277]: the trait bound `i32: From<&str>` is not satisfied > and 5 others = note: required because of the requirements on the impl of `Into` for `&str` + +error[E0308]: mismatched types + --> $DIR/from_form.rs:200:33 + | +200 | #[field(default_with = Some("hi"))] + | ^^^^ expected struct `std::string::String`, found `&str` + | +help: try using a conversion method + | +200 | #[field(default_with = Some("hi".to_string()))] + | ^^^^^^^^^^^^^^^^ +200 | #[field(default_with = Some("hi".to_string()))] + | ^^^^^^^^^^^^^^^^ diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index e49b4df4..7794ca5b 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -403,6 +403,63 @@ 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 default field expression + --- help: at most one `default` or `default_with` is allowed + --> $DIR/from_form.rs:181:23 + | +181 | #[field(default = 2)] + | ^ + +error: [note] error occurred while deriving `FromForm` + --> $DIR/from_form.rs:178:10 + | +178 | #[derive(FromForm)] + | ^^^^^^^^ + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: duplicate default expressions + --- help: only one of `default` or `default_with` must be used + --> $DIR/from_form.rs:187:23 + | +187 | #[field(default = 1, default_with = None)] + | ^ + +error: [note] other default expression is here + --> $DIR/from_form.rs:187:41 + | +187 | #[field(default = 1, default_with = None)] + | ^^^^ + +error: [note] error occurred while deriving `FromForm` + --> $DIR/from_form.rs:185:10 + | +185 | #[derive(FromForm)] + | ^^^^^^^^ + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: duplicate default expressions + --- help: only one of `default` or `default_with` must be used + --> $DIR/from_form.rs:194:23 + | +194 | #[field(default = 1)] + | ^ + +error: [note] other default expression is here + --> $DIR/from_form.rs:193:28 + | +193 | #[field(default_with = None)] + | ^^^^ + +error: [note] error occurred while deriving `FromForm` + --> $DIR/from_form.rs:191:10 + | +191 | #[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 | @@ -457,3 +514,16 @@ error[E0277]: the trait bound `i32: From<&str>` is not satisfied > and 5 others = note: required because of the requirements on the impl of `Into` for `&str` + +error[E0308]: mismatched types + --> $DIR/from_form.rs:200:33 + | +200 | #[field(default_with = Some("hi"))] + | ^^^^ expected struct `std::string::String`, found `&str` + | +help: try using a conversion method + | +200 | #[field(default_with = Some("hi".to_string()))] + | ^^^^^^^^^^^^^^^^ +200 | #[field(default_with = Some("hi".to_string()))] + | ^^^^^^^^^^^^^^^^ diff --git a/core/codegen/tests/ui-fail/from_form.rs b/core/codegen/tests/ui-fail/from_form.rs index 680bc6d7..94bc63ee 100644 --- a/core/codegen/tests/ui-fail/from_form.rs +++ b/core/codegen/tests/ui-fail/from_form.rs @@ -172,7 +172,33 @@ struct Default0 { #[derive(FromForm)] struct Default1 { #[field(default = 1, default = 2)] - double_default: i32, + double_default: usize, +} + +#[derive(FromForm)] +struct Default2 { + #[field(default = 1)] + #[field(default = 2)] + double_default: usize, +} + +#[derive(FromForm)] +struct Default3 { + #[field(default = 1, default_with = None)] + double_default: usize, +} + +#[derive(FromForm)] +struct Default4 { + #[field(default_with = None)] + #[field(default = 1)] + double_default: usize, +} + +#[derive(FromForm)] +struct Default5 { + #[field(default_with = Some("hi"))] + no_conversion_from_with: String, } fn main() { } diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index 856d9be0..25367e83 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -56,23 +56,6 @@ 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 c25ac72b..c943d5b0 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -810,8 +810,35 @@ struct MyForm<'v> { # rocket_guide_tests::assert_form_parses_ok!(MyForm, ""); ``` +The default can be overridden or unset using the `#[field(default = expr)` field +attribute. If `expr` is not literally `None`, the parameter sets the default +value of the field to be `expr.into()`. If `expr` _is_ `None`, the parameter +_unsets_ the default value of the field, if any. + +```rust +# use rocket::form::FromForm; + +#[derive(FromForm)] +struct MyForm { + // Set the default value to be `"hello"`. + // + // Note how an `&str` is automatically converted into a `String`. + #[field(default = "hello")] + greeting: String, + // Remove the default value of `false`, requiring all parses of `MyForm` + // to contain an `is_friendly` field. + #[field(default = None)] + is_friendly: bool, +} +``` + +See the [`FromForm` derive] documentation for full details on the `default` +attribute parameter as well documentation on the more expressive `default_with` +parameter option. + [`Errors<'_>`]: @api/rocket/form/struct.Errors.html [`form::Result`]: @api/rocket/form/type.Result.html +[`FromForm` derive]: @api/rocket/derive.FromForm.html ### Field Renaming @@ -956,47 +983,6 @@ 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