diff --git a/core/codegen/src/derive/form_field.rs b/core/codegen/src/derive/form_field.rs index 004d86ee..92d9fbbb 100644 --- a/core/codegen/src/derive/form_field.rs +++ b/core/codegen/src/derive/form_field.rs @@ -26,11 +26,13 @@ impl FieldAttr { } pub(crate) trait FieldExt { - fn ident(&self) -> &syn::Ident; + fn ident(&self) -> Option<&syn::Ident>; + fn member(&self) -> syn::Member; + fn context_ident(&self) -> syn::Ident; fn field_names(&self) -> Result>; - fn first_field_name(&self) -> Result; + fn first_field_name(&self) -> Result>; fn stripped_ty(&self) -> syn::Type; - fn name_view(&self) -> Result; + fn name_buf_opt(&self) -> Result; } #[derive(FromMeta)] @@ -49,13 +51,13 @@ pub(crate) trait VariantExt { impl VariantExt for Variant<'_> { fn first_form_field_value(&self) -> Result { - let first = VariantAttr::from_attrs(VariantAttr::NAME, &self.attrs)? + let value = VariantAttr::from_attrs(VariantAttr::NAME, &self.attrs)? .into_iter() - .next(); + .next() + .map(|attr| FieldName::Uncased(attr.value)) + .unwrap_or_else(|| FieldName::Uncased(Name::from(&self.ident))); - Ok(first.map_or_else( - || FieldName::Uncased(Name::from(&self.ident)), - |attr| FieldName::Uncased(attr.value))) + Ok(value) } fn form_field_values(&self) -> Result> { @@ -147,10 +149,27 @@ impl PartialEq for FieldName { } impl FieldExt for Field<'_> { - fn ident(&self) -> &syn::Ident { - self.ident.as_ref().expect("named") + fn ident(&self) -> Option<&syn::Ident> { + self.ident.as_ref() } + fn member(&self) -> syn::Member { + match self.ident().cloned() { + Some(ident) => syn::Member::Named(ident), + None => syn::Member::Unnamed(syn::Index { + index: self.index as u32, + span: self.ty.span() + }) + } + } + + fn context_ident(&self) -> syn::Ident { + self.ident() + .map(|i| i.clone()) + .unwrap_or_else(|| syn::Ident::new("__form_field", self.span())) + } + + // With named existentials, this could return an `impl Iterator`... fn field_names(&self) -> Result> { let attr_names = FieldAttr::from_attrs(FieldAttr::NAME, &self.attrs)? .into_iter() @@ -158,31 +177,29 @@ impl FieldExt for Field<'_> { .collect::>(); if attr_names.is_empty() { - let ident_name = Name::from(self.ident()); - return Ok(vec![FieldName::Cased(ident_name)]); + if let Some(ident) = self.ident() { + return Ok(vec![FieldName::Cased(Name::from(ident))]); + } } Ok(attr_names) } - fn first_field_name(&self) -> Result { - let mut names = self.field_names()?.into_iter(); - Ok(names.next().expect("always have >= 1 name")) + fn first_field_name(&self) -> Result> { + Ok(self.field_names()?.into_iter().next()) } fn stripped_ty(&self) -> syn::Type { self.ty.with_stripped_lifetimes() } - fn name_view(&self) -> Result { - let field_names = self.field_names()?; - let field_name = field_names.first().expect("always have name"); - define_spanned_export!(self.span() => _form); - let name_view = quote_spanned! { self.span() => - #_form::NameBuf::from((__c.__parent, #field_name)) - }; + fn name_buf_opt(&self) -> Result { + let (span, field_names) = (self.span(), self.field_names()?); + define_spanned_export!(span => _form); - Ok(syn::parse2(name_view).unwrap()) + Ok(field_names.first() + .map(|name| quote_spanned!(span => Some(#_form::NameBuf::from((__c.__parent, #name))))) + .unwrap_or_else(|| quote_spanned!(span => None::<#_form::NameBuf>))) } } @@ -278,7 +295,7 @@ impl VisitMut for ValidationMutator<'_> { 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; + let new_expr = self.field; *i = syn::parse_quote!(#new_expr); } } @@ -291,43 +308,34 @@ impl VisitMut for ValidationMutator<'_> { pub fn validators<'v>( field: Field<'v>, parent: &'v syn::Ident, // field ident (if local) or form ident (if !local) - local: bool, // whether to emit local or global (w/self) validations + local: bool, // whether to emit local (true) or global (w/self) validations ) -> Result + 'v> { - let exprs = FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)? + Ok(FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)? .into_iter() + .chain(FieldAttr::from_attrs(FieldAttr::NAME, field.parent.attrs())?) .filter_map(|a| a.validate) .map(move |expr| { let mut members = RecordMemberAccesses(vec![]); members.visit_expr(&expr); - let field_ident = field.ident(); - let is_local_validation = members.0.iter() - .all(|member| match member { - syn::Member::Named(i) => i == field_ident, - _ => false - }); - + let field_member = field.member(); + let is_local_validation = members.0.iter().all(|m| m == &field_member); (expr, is_local_validation) }) .filter(move |(_, is_local)| *is_local == local) .map(move |(mut expr, _)| { - let field_span = field.ident().span() - .join(field.ty.span()) - .unwrap_or(field.ty.span()); - - let field = &field.ident().clone().with_span(field_span); + let ty_span = field.ty.span(); + let field = &field.context_ident().with_span(ty_span); let mut v = ValidationMutator { parent, local, field, visited: false }; v.visit_expr_mut(&mut expr); - let span = expr.key_span.unwrap_or(field_span); + let span = expr.key_span.unwrap_or(ty_span); define_spanned_export!(span => _form); syn::parse2(quote_spanned!(span => { let __result: #_form::Result<'_, ()> = #expr; __result })).unwrap() - }); - - Ok(exprs) + })) } /// Take an $expr in `default = $expr` and turn it into a `Some($expr.into())`. @@ -348,13 +356,17 @@ fn default_expr(expr: &syn::Expr) -> TokenStream { } pub fn default<'v>(field: Field<'v>) -> Result> { - let attrs = FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)?; + let field_attrs = FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)?; + let parent_attrs = FieldAttr::from_attrs(FieldAttr::NAME, field.parent.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); + let mut expr = field_attrs.iter() + .chain(parent_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() + let mut expr_with = field_attrs.iter() + .chain(parent_attrs.iter()) .filter_map(|a| a.default_with.as_ref()) .map(|e| e.to_token_stream()); @@ -380,7 +392,13 @@ pub fn default<'v>(field: Field<'v>) -> Result> { }, (Some(e), None) | (None, Some(e)) => { Ok(Some(quote_spanned!(e.span() => { - let __default: Option<#ty> = #e; + let __default: Option<#ty>; + if __opts.strict { + __default = None; + } else { + __default = #e; + } + __default }))) }, diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index 6afbf656..f27c906d 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -1,8 +1,13 @@ -use devise::{*, ext::{TypeExt, SpanDiagnosticExt, GenericsExt, Split2, quote_respanned}}; use proc_macro2::TokenStream; +use devise::ext::{TypeExt, SpanDiagnosticExt, GenericsExt, quote_respanned}; +use syn::parse::Parser; +use devise::*; use crate::exports::*; use crate::derive::form_field::{*, FieldName::*}; +use crate::syn_ext::{GenericsExt as _, TypeExt as _}; + +type WherePredicates = syn::punctuated::Punctuated; // F: fn(field_ty: Ty, field_context: Expr) fn fields_map(fields: Fields<'_>, map_f: F) -> Result @@ -10,21 +15,26 @@ fn fields_map(fields: Fields<'_>, map_f: F) -> Result { let mut matchers = vec![]; for field in fields.iter() { - let (ident, ty) = (field.ident(), field.stripped_ty()); - let field_context = quote_spanned!(ty.span() => { + let (ident, ty) = (field.context_ident(), field.stripped_ty()); + let field_context: syn::Expr = syn::parse2(quote_spanned!(ty.span() => { let __o = __c.__opts; - __c.#ident.get_or_insert_with(|| <#ty as #_form::FromForm<'__f>>::init(__o)) - }); + __c.#ident.get_or_insert_with(|| <#ty as #_form::FromForm<'r>>::init(__o)) + })).expect("form context expression"); - let field_names = field.field_names()?; - let field_context = syn::parse2(field_context).expect("valid expr"); let push = map_f(&ty, &field_context); - let field_matchers = field_names.iter().map(|f| match f { + if fields.are_unnamed() { + // If we have unnamed fields, then we have exactly one by virtue of + // the earlier validation. Push directly to it and return. + return Ok(quote_spanned!(ident.span() => + __c.__parent = __f.name.parent(); + #push + )); + } + + matchers.extend(field.field_names()?.into_iter().map(|f| match f { Cased(name) => quote!(#name => { #push }), Uncased(name) => quote!(__n if __n.as_uncased() == #name => { #push }), - }); - - matchers.extend(field_matchers); + })); } Ok(quote! { @@ -38,28 +48,44 @@ fn fields_map(fields: Fields<'_>, map_f: F) -> Result }) } -fn context_type(input: Input<'_>) -> (TokenStream, Option) { +fn generic_bounds_tokens(input: Input<'_>) -> Result { + MapperBuild::new() + .try_enum_map(|m, e| mapper::enum_null(m, e)) + .try_fields_map(|_, fields| { + let generic_idents = fields.parent.input().generics().type_idents(); + + let bounds = fields.iter() + .filter(|f| !f.ty.is_concrete(&generic_idents)) + .map(|f| f.ty.with_replaced_lifetimes(syn::Lifetime::new("'r", f.ty.span()))) + .map(|ty| quote_spanned!(ty.span() => #ty: #_form::FromForm<'r>)); + + Ok(quote!(#(#bounds),*)) + }) + .map_input(input) +} + +fn generic_bounds(input: Input<'_>) -> Result { + Ok(WherePredicates::parse_terminated.parse2(generic_bounds_tokens(input)?)?) +} + +fn context_type(input: Input<'_>) -> Result<(TokenStream, syn::Generics)> { let mut gen = input.generics().clone(); - let lifetime = syn::parse_quote!('__f); + let lifetime = syn::parse_quote!('r); if !gen.replace_lifetime(0, &lifetime) { gen.insert_lifetime(syn::LifetimeDef::new(lifetime.clone())); } - let span = input.ident().span(); - gen.add_type_bound(syn::parse_quote!(#_form::FromForm<#lifetime>)); - gen.add_type_bound(syn::TypeParamBound::from(lifetime)); - let (_, ty_gen, where_clause) = gen.split_for_impl(); - (quote_spanned!(span => FromFormGeneratedContext #ty_gen), where_clause.cloned()) + gen.add_where_predicates(generic_bounds(input)?); + let ty = quote_spanned!(input.ident().span() => FromFormGeneratedContext); + Ok((ty, gen)) } - pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { - 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) + DeriveGenerator::build_for(input, quote!(impl<'r> #_form::FromForm<'r>)) + .support(Support::Struct | Support::Lifetime | Support::Type) .replace_generic(0, 0) - .type_bound(quote!(#_form::FromForm<'__f> + '__f)) + .type_bound_mapper(MapperBuild::new().try_input_map(|_, i| generic_bounds_tokens(i))) .validator(ValidatorBuild::new() .input_validate(|_, i| match i.generics().lifetimes().enumerate().last() { Some((i, lt)) if i >= 1 => Err(lt.span().error("only one lifetime is supported")), @@ -68,9 +94,9 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { .fields_validate(|_, fields| { if fields.is_empty() { return Err(fields.span().error("at least one field is required")); - } - - if let Some(d) = first_duplicate(fields.iter(), |f| f.field_names())? { + } else if fields.are_unnamed() && fields.count() != 1 { + return Err(fields.span().error("tuple struct must have exactly one field")); + } else if let Some(d) = first_duplicate(fields.iter(), |f| f.field_names())? { let (field_a_i, field_a, name_a) = d.0; let (field_b_i, field_b, name_b) = d.1; @@ -90,25 +116,29 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { ) .outer_mapper(MapperBuild::new() .try_input_map(|mapper, input| { - let (ctxt_ty, where_clause) = context_type(input); + let vis = input.vis(); + let (ctxt_ty, gen) = context_type(input)?; + let (impl_gen, _, where_clause) = gen.split_for_impl(); let output = mapper::input_default(mapper, input)?; - Ok(quote! { + Ok(quote_spanned! { input.span() => /// Rocket generated FormForm context. #[doc(hidden)] - pub struct #ctxt_ty #where_clause { + #[allow(private_in_public)] + #vis struct #ctxt_ty #impl_gen #where_clause { __opts: #_form::Options, - __errors: #_form::Errors<'__f>, - __parent: #_Option<&'__f #_form::Name>, + __errors: #_form::Errors<'r>, + __parent: #_Option<&'r #_form::Name>, #output } }) }) .try_fields_map(|m, f| mapper::fields_null(m, f)) .field_map(|_, field| { - let (ident, mut ty) = (field.ident(), field.stripped_ty()); - ty.replace_lifetimes(syn::parse_quote!('__f)); + let ident = field.context_ident(); + let mut ty = field.stripped_ty(); + ty.replace_lifetimes(syn::parse_quote!('r)); let field_ty = quote_respanned!(ty.span() => - #_Option<<#ty as #_form::FromForm<'__f>>::Context> + #_Option<<#ty as #_form::FromForm<'r>>::Context> ); quote_spanned!(ty.span() => #ident: #field_ty,) @@ -118,13 +148,15 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { #[allow(unused_imports)] use #_http::uncased::AsUncased; }) + .outer_mapper(quote!(#[allow(private_in_public)])) .outer_mapper(quote!(#[rocket::async_trait])) .inner_mapper(MapperBuild::new() .try_input_map(|mapper, input| { - let (ctxt_ty, _) = context_type(input); + let (ctxt_ty, gen) = context_type(input)?; + let (_, ty_gen, _) = gen.split_for_impl(); let output = mapper::input_default(mapper, input)?; Ok(quote! { - type Context = #ctxt_ty; + type Context = #ctxt_ty #ty_gen; fn init(__opts: #_form::Options) -> Self::Context { Self::Context { @@ -138,47 +170,45 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { }) .try_fields_map(|m, f| mapper::fields_null(m, f)) .field_map(|_, field| { - let ident = field.ident.as_ref().expect("named"); + let ident = field.context_ident(); let ty = field.ty.with_stripped_lifetimes(); - quote_spanned!(ty.span() => - #ident: #_None, - // #ident: <#ty as #_form::FromForm<'__f>>::init(__opts), - ) + quote_spanned!(ty.span() => #ident: #_None,) }) ) .inner_mapper(MapperBuild::new() .with_output(|_, output| quote! { - fn push_value(__c: &mut Self::Context, __f: #_form::ValueField<'__f>) { + fn push_value(__c: &mut Self::Context, __f: #_form::ValueField<'r>) { #output } }) .try_fields_map(|_, f| fields_map(f, |ty, ctxt| quote_spanned!(ty.span() => { - <#ty as #_form::FromForm<'__f>>::push_value(#ctxt, __f.shift()); + <#ty as #_form::FromForm<'r>>::push_value(#ctxt, __f.shift()); }))) ) .inner_mapper(MapperBuild::new() .try_input_map(|mapper, input| { - let (ctxt_ty, _) = context_type(input); + let (ctxt_ty, gen) = context_type(input)?; + let (_, ty_gen, _) = gen.split_for_impl(); let output = mapper::input_default(mapper, input)?; Ok(quote! { async fn push_data( - __c: &mut #ctxt_ty, - __f: #_form::DataField<'__f, '_> + __c: &mut #ctxt_ty #ty_gen, + __f: #_form::DataField<'r, '_> ) { #output } }) }) // Without the `let _fut`, we get a wild lifetime error. It don't - // make no sense, Rust async/await, it don't make no sense. + // make no sense, Rust async/await: it don't make no sense. .try_fields_map(|_, f| fields_map(f, |ty, ctxt| quote_spanned!(ty.span() => { - let _fut = <#ty as #_form::FromForm<'__f>>::push_data(#ctxt, __f.shift()); + let _fut = <#ty as #_form::FromForm<'r>>::push_data(#ctxt, __f.shift()); _fut.await; }))) ) .inner_mapper(MapperBuild::new() .with_output(|_, output| quote! { - fn finalize(mut __c: Self::Context) -> #_Result> { + fn finalize(mut __c: Self::Context) -> #_Result> { #[allow(unused_imports)] use #_form::validate::*; @@ -186,21 +216,24 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { } }) .try_fields_map(|mapper, fields| { + // This validates the attributes so we can `unwrap()` later. let finalize_field = fields.iter() .map(|f| mapper.map_field(f)) .collect::>>()?; - let ident: Vec<_> = fields.iter() - .map(|f| f.ident().clone()) - .collect(); - let o = syn::Ident::new("__o", fields.span()); let (_ok, _some, _err, _none) = (_Ok, _Some, _Err, _None); - let (name_view, validate) = fields.iter() - .map(|f| (f.name_view().unwrap(), validators(f, &o, false).unwrap())) - .map(|(nv, vs)| vs.map(move |v| (nv.clone(), v))) - .flatten() - .split2(); + let validate = fields.iter().flat_map(|f| validators(f, &o, false).unwrap()); + let name_buf_opt = fields.iter().map(|f| f.name_buf_opt().unwrap()); + + let ident: Vec<_> = fields.iter() + .map(|f| f.context_ident()) + .collect(); + + let builder = fields.builder(|f| { + let ident = f.context_ident(); + quote!(#ident.unwrap()) + }); Ok(quote_spanned! { fields.span() => #(let #ident = match #finalize_field { @@ -212,11 +245,14 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { return #_Err(__c.__errors); } - let #o = Self { #(#ident: #ident.unwrap()),* }; + let #o = #builder; #( if let #_err(__e) = #validate { - __c.__errors.extend(__e.with_name(#name_view)); + __c.__errors.extend(match #name_buf_opt { + Some(__name) => __e.with_name(__name), + None => __e + }); } )* @@ -228,28 +264,32 @@ 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 (ident, ty) = (f.context_ident(), f.stripped_ty()); let validator = validators(f, &ident, true)?; + let name_buf_opt = f.name_buf_opt()?; let default = default(f)? .unwrap_or_else(|| quote_spanned!(ty.span() => { - <#ty as #_form::FromForm<'__f>>::default(__opts) + <#ty as #_form::FromForm<'r>>::default(__opts) })); let _err = _Err; Ok(quote_spanned! { ty.span() => { - let __name = #name_view; let __opts = __c.__opts; + let __name = #name_buf_opt; __c.#ident .map_or_else( || #default.ok_or_else(|| #_form::ErrorKind::Missing.into()), - <#ty as #_form::FromForm<'__f>>::finalize + <#ty as #_form::FromForm<'r>>::finalize ) .and_then(|#ident| { let mut __es = #_form::Errors::new(); #(if let #_err(__e) = #validator { __es.extend(__e); })* __es.is_empty().then(|| #ident).ok_or(__es) }) - .map_err(|__e| __e.with_name(__name)) + .map_err(|__e| match __name { + Some(__name) => __e.with_name(__name), + None => __e, + }) .map_err(|__e| __e.is_empty() .then(|| #_form::ErrorKind::Unknown.into()) .unwrap_or(__e)) diff --git a/core/codegen/src/derive/uri_display.rs b/core/codegen/src/derive/uri_display.rs index 7dbe5e28..87e5ca5a 100644 --- a/core/codegen/src/derive/uri_display.rs +++ b/core/codegen/src/derive/uri_display.rs @@ -12,7 +12,6 @@ const ONLY_ONE_UNNAMED: &str = "tuple structs or variants must have exactly one const EXACTLY_ONE_FIELD: &str = "struct must have exactly one field"; pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream { - const URI_DISPLAY: StaticTokens = quote_static!(#_fmt::UriDisplay<#_fmt::Query>); const FORMATTER: StaticTokens = quote_static!(#_fmt::Formatter<#_fmt::Query>); @@ -65,8 +64,7 @@ pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream { .try_field_map(|_, field| { let span = field.span().into(); let accessor = field.accessor(); - let tokens = if field.ident.is_some() { - let name = field.first_field_name()?; + let tokens = if let Some(name) = field.first_field_name()? { quote_spanned!(span => f.write_named_value(#name, &#accessor)?;) } else { quote_spanned!(span => f.write_value(&#accessor)?;) diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 99b8ca99..e0810c94 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -547,10 +547,12 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// Derive for the [`FromForm`] trait. /// -/// The [`FromForm`] derive can be applied to structures with named fields: +/// The [`FromForm`] derive can be applied to structures with named or unnamed +/// fields: /// /// ```rust -/// # #[macro_use] extern crate rocket; +/// use rocket::form::FromForm; +/// /// #[derive(FromForm)] /// struct MyStruct<'r> { /// field: usize, @@ -562,19 +564,36 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// #[field(default = None)] /// is_nice: bool, /// } +/// +/// #[derive(FromForm)] +/// #[field(validate = len(6..))] +/// #[field(validate = neq("password"))] +/// struct Password<'r>(&'r str); /// ``` /// -/// Each field's type is required to implement [`FromForm`]. +/// Each field type is required to implement [`FromForm`]. /// -/// The derive generates an implementation of the [`FromForm`] trait. The -/// 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 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 generates an implementation of the [`FromForm`] trait. /// -/// The derive accepts one field attribute: `field`, with the following syntax: +/// **Named Fields** +/// +/// If the structure has named fields, the 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 +/// fields parse successfully or return a default. Errors are collected into a +/// [`form::Errors`] and returned if non-empty after parsing all fields. +/// +/// **Unnamed Fields** +/// +/// If the structure is a tuple struct, it must have exactly one field. The +/// implementation parses a form exactly when the internal field parses a form +/// _and_ any `#[field]` validations succeed. +/// +/// ## Syntax +/// +/// The derive accepts one field attribute: `field`, and one container +/// attribute, `form`, with the following syntax: /// /// ```text /// field := name? default? validate* @@ -592,8 +611,9 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// EXPR := valid expression, as defined by Rust /// ``` /// -/// 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: +/// `#[field]` can be applied any number of times on a field. `default` and +/// `default_with` are mutually exclusive: at most _one_ of `default` or +/// `default_with` can be present per field. /// /// ```rust /// # #[macro_use] extern crate rocket; @@ -610,54 +630,118 @@ pub fn derive_from_form_field(input: TokenStream) -> TokenStream { /// } /// ``` /// -/// **`name`** -/// -/// A `name` attribute changes the name to match against when parsing the form -/// field. The value is either an exact string to match against (`"foo"`), or -/// `uncased("foo")`, which causes the match to be case-insensitive but -/// case-preserving. When more than one `name` attribute is applied, the field -/// will match against _any_ of the names. -/// -/// **`validate = expr`** -/// -/// 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. +/// For tuples structs, the `field` attribute can be applied to the structure +/// itself: /// /// ```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, -/// } +/// #[field(default = 42, validate = eq(42))] +/// struct Meaning(usize); /// ``` /// +/// ## Field Attribute Parameters +/// +/// * **`name`** +/// +/// A `name` attribute changes the name to match against when parsing the +/// form field. The value is either an exact string to match against +/// (`"foo"`), or `uncased("foo")`, which causes the match to be +/// case-insensitive but case-preserving. When more than one `name` +/// attribute is applied, the field will match against _any_ of the names. +/// +/// * **`validate = expr`** +/// +/// 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 -#[proc_macro_derive(FromForm, attributes(field))] +/// +/// # Generics +/// +/// The derive accepts any number of type generics and at most one lifetime +/// generic. If a type generic is present, the generated implementation will +/// require a bound of `FromForm<'r>` for the field type containing the generic. +/// For example, for a struct `struct Foo(Json)`, the bound `Json: +/// FromForm<'r>` will be added to the generated implementation. +/// +/// ```rust +/// use rocket::form::FromForm; +/// use rocket::serde::json::Json; +/// +/// // The bounds `A: FromForm<'r>`, `B: FromForm<'r>` will be required. +/// #[derive(FromForm)] +/// struct FancyForm { +/// first: A, +/// second: B, +/// }; +/// +/// // The bound `Json: FromForm<'r>` will be required. +/// #[derive(FromForm)] +/// struct JsonToken { +/// token: Json, +/// id: usize, +/// } +/// ``` +/// +/// If a lifetime generic is present, it is replaced with `'r` in the +/// generated implementation `impl FromForm<'r>`: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// // Generates `impl<'r> FromForm<'r> for MyWrapper<'r>`. +/// #[derive(FromForm)] +/// struct MyWrapper<'a>(&'a str); +/// ``` +/// +/// Both type generics and one lifetime generic may be used: +/// +/// ```rust +/// use rocket::form::{self, FromForm}; +/// +/// // The bound `form::Result<'r, T>: FromForm<'r>` will be required. +/// #[derive(FromForm)] +/// struct SomeResult<'o, T>(form::Result<'o, T>); +/// ``` +/// +/// The special bounds on `Json` and `Result` are required due to incomplete and +/// incorrect support for lifetime generics in `async` blocks in Rust. See +/// [rust-lang/#64552](https://github.com/rust-lang/rust/issues/64552) for +/// further details. +#[proc_macro_derive(FromForm, attributes(form, field))] pub fn derive_from_form(input: TokenStream) -> TokenStream { emit!(derive::from_form::derive_from_form(input)) } diff --git a/core/codegen/src/name.rs b/core/codegen/src/name.rs index d4902429..c3a2521b 100644 --- a/core/codegen/src/name.rs +++ b/core/codegen/src/name.rs @@ -5,7 +5,7 @@ use proc_macro2::{Span, TokenStream}; /// A "name" read by codegen, which may or may not be an identifier. A `Name` is /// typically constructed indirectly via FromMeta, or From or directly -/// from a string via `Name::new()`. +/// from a string via `Name::new()`. A name is tokenized as a string. /// /// Some "names" in Rocket include: /// * Dynamic parameter: `name` in `` diff --git a/core/codegen/tests/from_form.rs b/core/codegen/tests/from_form.rs index fec1d9c3..19a70362 100644 --- a/core/codegen/tests/from_form.rs +++ b/core/codegen/tests/from_form.rs @@ -2,7 +2,11 @@ use std::net::{IpAddr, SocketAddr}; use std::collections::{BTreeMap, HashMap}; use pretty_assertions::assert_eq; +use rocket::UriDisplayQuery; +use rocket::http::uri::fmt::{UriDisplay, Query}; use rocket::form::{self, Form, Strict, FromForm, FromFormField, Errors}; +use rocket::form::error::{ErrorKind, Entity}; +use rocket::serde::json::Json; fn strict<'f, T: FromForm<'f>>(string: &'f str) -> Result> { Form::>::parse(string).map(|s| s.into_inner()) @@ -12,7 +16,7 @@ fn lenient<'f, T: FromForm<'f>>(string: &'f str) -> Result> { Form::::parse(string) } -fn strict_encoded(string: &'static str) -> Result> +fn strict_encoded(string: &str) -> Result> where for<'a> T: FromForm<'a> { Form::>::parse_encoded(string.into()).map(|s| s.into_inner()) @@ -331,8 +335,6 @@ fn generics() { #[test] fn form_errors() { - use rocket::form::error::{ErrorKind, Entity}; - #[derive(Debug, PartialEq, FromForm)] struct WhoopsForm { complete: bool, @@ -671,16 +673,19 @@ fn test_defaults() { fn test_hashmap() -> HashMap<&'static str, &'static str> { let mut map = HashMap::new(); map.insert("key", "value"); + map.insert("one-more", "good-value"); map } - fn test_btreemap() -> BTreeMap<&'static str, &'static str> { + fn test_btreemap() -> BTreeMap, &'static str> { let mut map = BTreeMap::new(); - map.insert("key", "value"); + map.insert(vec![], "empty"); + map.insert(vec![1, 2], "one-and-two"); + map.insert(vec![3, 7, 9], "prime"); map } - #[derive(FromForm, PartialEq, Debug)] + #[derive(FromForm, UriDisplayQuery, PartialEq, Debug)] struct FormWithDefaults<'a> { field2: i128, field5: bool, @@ -708,7 +713,7 @@ fn test_defaults() { #[field(default = test_hashmap())] hashmap: HashMap<&'a str, &'a str>, #[field(default = test_btreemap())] - btreemap: BTreeMap<&'a str, &'a str>, + btreemap: BTreeMap, &'a str>, #[field(default_with = Some(false))] boolean: bool, #[field(default_with = (|| Some(777))())] @@ -789,15 +794,39 @@ fn test_defaults() { })); // And that strict parsing still works. - let form4: Option = strict(&form_string).ok(); - assert_eq!(form4, Some(FormWithDefaults { - field1: 101, - field2: 102, - field3: true, - field4: false, - field5: true, - ..form3.unwrap() + let form = form3.unwrap(); + let form_string = format!("{}", &form as &dyn UriDisplay); + let form4: form::Result<'_, FormWithDefaults> = strict(&form_string); + assert_eq!(form4, Ok(form)); + + #[derive(FromForm, UriDisplayQuery, PartialEq, Debug)] + struct OwnedFormWithDefaults { + #[field(default = { + let mut map = BTreeMap::new(); + map.insert(vec![], "empty!/!? neat".into()); + map.insert(vec![1, 2], "one/ and+two".into()); + map.insert(vec![3, 7, 9], "prime numbers".into()); + map + })] + btreemap: BTreeMap, String>, + } + + // And that strict parsing still works even when encoded. + let form5: Option = lenient("").ok(); + assert_eq!(form5, Some(OwnedFormWithDefaults { + btreemap: { + let mut map = BTreeMap::new(); + map.insert(vec![3, 7, 9], "prime numbers".into()); + map.insert(vec![1, 2], "one/ and+two".into()); + map.insert(vec![], "empty!/!? neat".into()); + map + } })); + + let form = form5.unwrap(); + let form_string = format!("{}", &form as &dyn UriDisplay); + let form6: form::Result<'_, OwnedFormWithDefaults> = strict_encoded(&form_string); + assert_eq!(form6, Ok(form)); } #[test] @@ -842,3 +871,65 @@ fn test_lazy_default() { missing3: 42 })); } + +#[derive(Debug, PartialEq, FromForm, UriDisplayQuery)] +#[field(validate = len(3..), default = "some default hello")] +struct Token<'r>(&'r str); + +#[derive(Debug, PartialEq, FromForm, UriDisplayQuery)] +#[field(validate = try_with(|s| s.parse::()), default = "123456")] +struct TokenOwned(String); + +#[test] +fn wrapper_works() { + let form: Option = lenient("").ok(); + assert_eq!(form, Some(Token("some default hello"))); + + let form: Option = lenient("").ok(); + assert_eq!(form, Some(TokenOwned("123456".into()))); + + let errors = strict::("").unwrap_err(); + assert!(errors.iter().any(|e| matches!(e.kind, ErrorKind::Missing))); + + let form: Option = lenient("=hi there").ok(); + assert_eq!(form, Some(Token("hi there"))); + + let form: Option = strict_encoded("=2318").ok(); + assert_eq!(form, Some(TokenOwned("2318".into()))); + + let errors = lenient::("=hi").unwrap_err(); + assert!(errors.iter().any(|e| matches!(e.kind, ErrorKind::InvalidLength { .. }))); + + let errors = lenient::("=hellothere").unwrap_err(); + assert!(errors.iter().any(|e| matches!(e.kind, ErrorKind::Validation { .. }))); +} + +#[derive(Debug, PartialEq, FromForm, UriDisplayQuery)] +struct JsonToken(Json); + +#[test] +fn json_wrapper_works() { + let form: JsonToken = lenient("=\"hello\"").unwrap(); + assert_eq!(form, JsonToken(Json("hello".into()))); + + let form: JsonToken = lenient("=10").unwrap(); + assert_eq!(form, JsonToken(Json(10))); + + let form: JsonToken<()> = lenient("=null").unwrap(); + assert_eq!(form, JsonToken(Json(()))); + + let form: JsonToken> = lenient("=[1, 4, 3, 9]").unwrap(); + assert_eq!(form, JsonToken(Json(vec![1, 4, 3, 9]))); + + let string = String::from("=\"foo bar\""); + let form: JsonToken<&str> = lenient(&string).unwrap(); + assert_eq!(form, JsonToken(Json("foo bar"))); +} + +// FIXME: https://github.com/rust-lang/rust/issues/86706 +#[allow(private_in_public)] +struct Q(T); + +// This is here to ensure we don't warn, which we can't test with trybuild. +#[derive(FromForm)] +pub struct JsonTokenBad(Q); diff --git a/core/codegen/tests/ui-fail-nightly/from_form.stderr b/core/codegen/tests/ui-fail-nightly/from_form.stderr index 5402f737..cca1e706 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form.stderr @@ -11,7 +11,7 @@ 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: tuple structs are not supported +error: at least one field is required --> $DIR/from_form.rs:7:1 | 7 | struct Foo1; @@ -37,24 +37,11 @@ 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: tuple structs are not supported - --> $DIR/from_form.rs:13:1 +error: tuple struct must have exactly one field + --> $DIR/from_form.rs:16:12 | -13 | struct Foo3(usize); - | ^^^^^^^^^^^^^^^^^^^ - | -note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:12:10 - | -12 | #[derive(FromForm)] - | ^^^^^^^^ - = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: only one lifetime is supported - --> $DIR/from_form.rs:16:25 - | -16 | struct NextTodoTask<'f, 'a> { - | ^^ +16 | struct Foo4(usize, usize, usize); + | ^^^^^^^^^^^^^^^^^^^^^ | note: error occurred while deriving `FromForm` --> $DIR/from_form.rs:15:10 @@ -63,381 +50,394 @@ 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: invalid form field name - --> $DIR/from_form.rs:25:20 +error: only one lifetime is supported + --> $DIR/from_form.rs:19:25 | -25 | #[field(name = "isindex")] +19 | struct NextTodoTask<'f, 'a> { + | ^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:18:10 + | +18 | #[derive(FromForm)] + | ^^^^^^^^ + = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: invalid form field name + --> $DIR/from_form.rs:28:20 + | +28 | #[field(name = "isindex")] | ^^^^^^^^^ | = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:23:10 + --> $DIR/from_form.rs:26:10 | -23 | #[derive(FromForm)] +26 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: field name conflicts with previous name - --> $DIR/from_form.rs:33:5 + --> $DIR/from_form.rs:36:5 | -33 | foo: usize, +36 | foo: usize, | ^^^ | help: declared in this field - --> $DIR/from_form.rs:33:5 + --> $DIR/from_form.rs:36:5 | -33 | foo: usize, +36 | foo: usize, | ^^^^^^^^^^ note: previous field with conflicting name - --> $DIR/from_form.rs:31:5 + --> $DIR/from_form.rs:34:5 | -31 | / #[field(name = "foo")] -32 | | field: String, +34 | / #[field(name = "foo")] +35 | | field: String, | |_________________^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:29:10 + --> $DIR/from_form.rs:32:10 | -29 | #[derive(FromForm)] +32 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: field name conflicts with previous name - --> $DIR/from_form.rs:40:20 + --> $DIR/from_form.rs:43:20 | -40 | #[field(name = "hello")] +43 | #[field(name = "hello")] | ^^^^^^^ | help: declared in this field - --> $DIR/from_form.rs:40:5 + --> $DIR/from_form.rs:43:5 | -40 | / #[field(name = "hello")] -41 | | other: String, +43 | / #[field(name = "hello")] +44 | | other: String, | |_________________^ note: previous field with conflicting name - --> $DIR/from_form.rs:38:5 + --> $DIR/from_form.rs:41:5 | -38 | / #[field(name = "hello")] -39 | | first: String, +41 | / #[field(name = "hello")] +42 | | first: String, | |_________________^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:36:10 + --> $DIR/from_form.rs:39:10 | -36 | #[derive(FromForm)] +39 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: field name conflicts with previous name - --> $DIR/from_form.rs:47:20 + --> $DIR/from_form.rs:50:20 | -47 | #[field(name = "first")] +50 | #[field(name = "first")] | ^^^^^^^ | help: declared in this field - --> $DIR/from_form.rs:47:5 + --> $DIR/from_form.rs:50:5 | -47 | / #[field(name = "first")] -48 | | other: String, +50 | / #[field(name = "first")] +51 | | other: String, | |_________________^ note: previous field with conflicting name - --> $DIR/from_form.rs:46:5 + --> $DIR/from_form.rs:49:5 | -46 | first: String, +49 | first: String, | ^^^^^^^^^^^^^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:44:10 + --> $DIR/from_form.rs:47:10 | -44 | #[derive(FromForm)] +47 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected attribute parameter: `field` - --> $DIR/from_form.rs:53:28 + --> $DIR/from_form.rs:56:28 | -53 | #[field(name = "blah", field = "bloo")] +56 | #[field(name = "blah", field = "bloo")] | ^^^^^^^^^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:51:10 + --> $DIR/from_form.rs:54:10 | -51 | #[derive(FromForm)] +54 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected list `#[field(..)]`, found bare path "field" - --> $DIR/from_form.rs:59:7 + --> $DIR/from_form.rs:62:7 | -59 | #[field] +62 | #[field] | ^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:57:10 + --> $DIR/from_form.rs:60:10 | -57 | #[derive(FromForm)] +60 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected key/value `key = value` - --> $DIR/from_form.rs:65:13 + --> $DIR/from_form.rs:68:13 | -65 | #[field("blah")] +68 | #[field("blah")] | ^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:63:10 + --> $DIR/from_form.rs:66:10 | -63 | #[derive(FromForm)] +66 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected key/value `key = value` - --> $DIR/from_form.rs:71:13 + --> $DIR/from_form.rs:74:13 | -71 | #[field(123)] +74 | #[field(123)] | ^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:69:10 + --> $DIR/from_form.rs:72:10 | -69 | #[derive(FromForm)] +72 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected attribute parameter: `beep` - --> $DIR/from_form.rs:77:13 + --> $DIR/from_form.rs:80:13 | -77 | #[field(beep = "bop")] +80 | #[field(beep = "bop")] | ^^^^^^^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:75:10 + --> $DIR/from_form.rs:78:10 | -75 | #[derive(FromForm)] +78 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: field has conflicting names - --> $DIR/from_form.rs:83:5 + --> $DIR/from_form.rs:86:5 | -83 | / #[field(name = "blah")] -84 | | #[field(name = "blah")] -85 | | my_field: String, +86 | / #[field(name = "blah")] +87 | | #[field(name = "blah")] +88 | | my_field: String, | |____________________^ | note: this field name... - --> $DIR/from_form.rs:83:20 + --> $DIR/from_form.rs:86:20 | -83 | #[field(name = "blah")] +86 | #[field(name = "blah")] | ^^^^^^ note: ...conflicts with this field name - --> $DIR/from_form.rs:84:20 + --> $DIR/from_form.rs:87:20 | -84 | #[field(name = "blah")] +87 | #[field(name = "blah")] | ^^^^^^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:81:10 + --> $DIR/from_form.rs:84:10 | -81 | #[derive(FromForm)] +84 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected list `#[attr(..)]`, found bare boolean literal - --> $DIR/from_form.rs:90:20 + --> $DIR/from_form.rs:93:20 | -90 | #[field(name = true)] +93 | #[field(name = true)] | ^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:88:10 + --> $DIR/from_form.rs:91:10 | -88 | #[derive(FromForm)] +91 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected expression, found bare path "name" - --> $DIR/from_form.rs:96:13 + --> $DIR/from_form.rs:99:13 | -96 | #[field(name)] +99 | #[field(name)] | ^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:94:10 + --> $DIR/from_form.rs:97:10 | -94 | #[derive(FromForm)] +97 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected list `#[attr(..)]`, found bare integer literal - --> $DIR/from_form.rs:102:20 + --> $DIR/from_form.rs:105:20 | -102 | #[field(name = 123)] +105 | #[field(name = 123)] | ^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:100:10 + --> $DIR/from_form.rs:103:10 | -100 | #[derive(FromForm)] +103 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:108:20 + --> $DIR/from_form.rs:111:20 | -108 | #[field(name = "hello&world")] +111 | #[field(name = "hello&world")] | ^^^^^^^^^^^^^ | = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:106:10 + --> $DIR/from_form.rs:109:10 | -106 | #[derive(FromForm)] +109 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:114:20 + --> $DIR/from_form.rs:117:20 | -114 | #[field(name = "!@#$%^&*()_")] +117 | #[field(name = "!@#$%^&*()_")] | ^^^^^^^^^^^^^ | = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:112:10 + --> $DIR/from_form.rs:115:10 | -112 | #[derive(FromForm)] +115 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:120:20 + --> $DIR/from_form.rs:123:20 | -120 | #[field(name = "?")] +123 | #[field(name = "?")] | ^^^ | = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:118:10 + --> $DIR/from_form.rs:121:10 | -118 | #[derive(FromForm)] +121 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:126:20 + --> $DIR/from_form.rs:129:20 | -126 | #[field(name = "")] +129 | #[field(name = "")] | ^^ | = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:124:10 + --> $DIR/from_form.rs:127:10 | -124 | #[derive(FromForm)] +127 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:132:20 + --> $DIR/from_form.rs:135:20 | -132 | #[field(name = "a&b")] +135 | #[field(name = "a&b")] | ^^^^^ | = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:130:10 + --> $DIR/from_form.rs:133:10 | -130 | #[derive(FromForm)] +133 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:138:20 + --> $DIR/from_form.rs:141:20 | -138 | #[field(name = "a=")] +141 | #[field(name = "a=")] | ^^^^ | = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:136:10 + --> $DIR/from_form.rs:139:10 | -136 | #[derive(FromForm)] +139 | #[derive(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 + --> $DIR/from_form.rs:177:26 | -174 | #[field(default = 1, default = 2)] +177 | #[field(default = 1, default = 2)] | ^^^^^^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:172:10 + --> $DIR/from_form.rs:175:10 | -172 | #[derive(FromForm)] +175 | #[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 field expression - --> $DIR/from_form.rs:181:23 + --> $DIR/from_form.rs:184:23 | -181 | #[field(default = 2)] +184 | #[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 + --> $DIR/from_form.rs:181:10 | -178 | #[derive(FromForm)] +181 | #[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 + --> $DIR/from_form.rs:190:23 | -187 | #[field(default = 1, default_with = None)] +190 | #[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 + --> $DIR/from_form.rs:190:41 | -187 | #[field(default = 1, default_with = None)] +190 | #[field(default = 1, default_with = None)] | ^^^^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:185:10 + --> $DIR/from_form.rs:188:10 | -185 | #[derive(FromForm)] +188 | #[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 + --> $DIR/from_form.rs:197:23 | -194 | #[field(default = 1)] +197 | #[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 + --> $DIR/from_form.rs:196:28 | -193 | #[field(default_with = None)] +196 | #[field(default_with = None)] | ^^^^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:191:10 + --> $DIR/from_form.rs:194:10 | -191 | #[derive(FromForm)] +194 | #[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 + --> $DIR/from_form.rs:153:24 | -150 | #[field(validate = unknown())] +153 | #[field(validate = unknown())] | ^^^^^^^ not found in this scope error[E0308]: mismatched types - --> $DIR/from_form.rs:144:24 + --> $DIR/from_form.rs:147:24 | -144 | #[field(validate = 123)] +147 | #[field(validate = 123)] | -------- ^^^ expected enum `Result`, found integer | | | expected due to this @@ -446,33 +446,33 @@ error[E0308]: mismatched types found type `{integer}` error[E0308]: mismatched types - --> $DIR/from_form.rs:157:5 + --> $DIR/from_form.rs:160:12 | -157 | first: String, - | ^^^^^^^^^^^^^ expected enum `TempFile`, found struct `std::string::String` +160 | first: String, + | ^^^^^^ expected enum `TempFile`, found struct `std::string::String` | = note: expected reference `&TempFile<'_>` found reference `&std::string::String` error[E0308]: mismatched types - --> $DIR/from_form.rs:163:5 + --> $DIR/from_form.rs:166:12 | -163 | first: String, - | ^^^^^^^^^^^^^ expected enum `TempFile`, found struct `std::string::String` +166 | first: String, + | ^^^^^^ expected enum `TempFile`, found struct `std::string::String` | = note: expected reference `&TempFile<'_>` found reference `&std::string::String` error[E0308]: mismatched types - --> $DIR/from_form.rs:162:28 + --> $DIR/from_form.rs:165:28 | -162 | #[field(validate = ext("hello"))] +165 | #[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 + --> $DIR/from_form.rs:171:23 | -168 | #[field(default = "no conversion")] +171 | #[field(default = "no conversion")] | ^^^^^^^^^^^^^^^ the trait `From<&str>` is not implemented for `i32` | = help: the following implementations were found: @@ -484,14 +484,14 @@ error[E0277]: the trait bound `i32: From<&str>` is not satisfied = note: required because of the requirements on the impl of `Into` for `&str` error[E0308]: mismatched types - --> $DIR/from_form.rs:200:33 + --> $DIR/from_form.rs:203:33 | -200 | #[field(default_with = Some("hi"))] +203 | #[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()))] +203 | #[field(default_with = Some("hi".to_string()))] | ^^^^^^^^^^^^^^^^ -200 | #[field(default_with = Some("hi".to_string()))] +203 | #[field(default_with = Some("hi".to_string()))] | ^^^^^^^^^^^^^^^^ diff --git a/core/codegen/tests/ui-fail-nightly/from_form_type_errors.stderr b/core/codegen/tests/ui-fail-nightly/from_form_type_errors.stderr index d1ce628f..4b5d39fb 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form_type_errors.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form_type_errors.stderr @@ -4,7 +4,7 @@ error[E0277]: the trait bound `Unknown: FromFormField<'_>` is not satisfied 7 | field: Unknown, | ^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Unknown` | - = note: required because of the requirements on the impl of `FromForm<'__f>` for `Unknown` + = note: required because of the requirements on the impl of `FromForm<'r>` for `Unknown` error[E0277]: the trait bound `Foo: FromFormField<'_>` is not satisfied --> $DIR/from_form_type_errors.rs:14:12 @@ -12,4 +12,4 @@ error[E0277]: the trait bound `Foo: FromFormField<'_>` is not satisfied 14 | field: Foo, | ^^^^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Foo` | - = note: required because of the requirements on the impl of `FromForm<'__f>` for `Foo` + = note: required because of the requirements on the impl of `FromForm<'r>` for `Foo` diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index 7794ca5b..1dd53327 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -12,7 +12,7 @@ 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: tuple structs are not supported +error: at least one field is required --> $DIR/from_form.rs:7:1 | 7 | struct Foo1; @@ -40,25 +40,11 @@ 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: tuple structs are not supported - --> $DIR/from_form.rs:13:1 +error: tuple struct must have exactly one field + --> $DIR/from_form.rs:16:12 | -13 | struct Foo3(usize); - | ^^^^^^ - -error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:12:10 - | -12 | #[derive(FromForm)] - | ^^^^^^^^ - | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) - -error: only one lifetime is supported - --> $DIR/from_form.rs:16:25 - | -16 | struct NextTodoTask<'f, 'a> { - | ^^ +16 | struct Foo4(usize, usize, usize); + | ^^^^^^^^^^^^^^^^^^^^^ error: [note] error occurred while deriving `FromForm` --> $DIR/from_form.rs:15:10 @@ -68,408 +54,422 @@ 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: only one lifetime is supported + --> $DIR/from_form.rs:19:25 + | +19 | struct NextTodoTask<'f, 'a> { + | ^^ + +error: [note] error occurred while deriving `FromForm` + --> $DIR/from_form.rs:18:10 + | +18 | #[derive(FromForm)] + | ^^^^^^^^ + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + error: invalid form field name --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' - --> $DIR/from_form.rs:25:20 + --> $DIR/from_form.rs:28:20 | -25 | #[field(name = "isindex")] +28 | #[field(name = "isindex")] | ^^^^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:23:10 + --> $DIR/from_form.rs:26:10 | -23 | #[derive(FromForm)] +26 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: field name conflicts with previous name - --> $DIR/from_form.rs:33:5 + --> $DIR/from_form.rs:36:5 | -33 | foo: usize, +36 | foo: usize, | ^^^ error: [help] declared in this field - --> $DIR/from_form.rs:33:5 + --> $DIR/from_form.rs:36:5 | -33 | foo: usize, +36 | foo: usize, | ^^^ error: [note] previous field with conflicting name - --> $DIR/from_form.rs:31:5 + --> $DIR/from_form.rs:34:5 | -31 | #[field(name = "foo")] +34 | #[field(name = "foo")] | ^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:29:10 + --> $DIR/from_form.rs:32:10 | -29 | #[derive(FromForm)] +32 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: field name conflicts with previous name - --> $DIR/from_form.rs:40:20 + --> $DIR/from_form.rs:43:20 | -40 | #[field(name = "hello")] +43 | #[field(name = "hello")] | ^^^^^^^ error: [help] declared in this field - --> $DIR/from_form.rs:40:5 + --> $DIR/from_form.rs:43:5 | -40 | #[field(name = "hello")] +43 | #[field(name = "hello")] | ^ error: [note] previous field with conflicting name - --> $DIR/from_form.rs:38:5 + --> $DIR/from_form.rs:41:5 | -38 | #[field(name = "hello")] +41 | #[field(name = "hello")] | ^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:36:10 + --> $DIR/from_form.rs:39:10 | -36 | #[derive(FromForm)] +39 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: field name conflicts with previous name - --> $DIR/from_form.rs:47:20 + --> $DIR/from_form.rs:50:20 | -47 | #[field(name = "first")] +50 | #[field(name = "first")] | ^^^^^^^ error: [help] declared in this field - --> $DIR/from_form.rs:47:5 + --> $DIR/from_form.rs:50:5 | -47 | #[field(name = "first")] +50 | #[field(name = "first")] | ^ error: [note] previous field with conflicting name - --> $DIR/from_form.rs:46:5 + --> $DIR/from_form.rs:49:5 | -46 | first: String, +49 | first: String, | ^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:44:10 + --> $DIR/from_form.rs:47:10 | -44 | #[derive(FromForm)] +47 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected attribute parameter: `field` - --> $DIR/from_form.rs:53:28 + --> $DIR/from_form.rs:56:28 | -53 | #[field(name = "blah", field = "bloo")] +56 | #[field(name = "blah", field = "bloo")] | ^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:51:10 + --> $DIR/from_form.rs:54:10 | -51 | #[derive(FromForm)] +54 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: expected list `#[field(..)]`, found bare path "field" - --> $DIR/from_form.rs:59:7 + --> $DIR/from_form.rs:62:7 | -59 | #[field] +62 | #[field] | ^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:57:10 + --> $DIR/from_form.rs:60:10 | -57 | #[derive(FromForm)] +60 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: expected key/value `key = value` - --> $DIR/from_form.rs:65:13 + --> $DIR/from_form.rs:68:13 | -65 | #[field("blah")] +68 | #[field("blah")] | ^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:63:10 + --> $DIR/from_form.rs:66:10 | -63 | #[derive(FromForm)] +66 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: expected key/value `key = value` - --> $DIR/from_form.rs:71:13 + --> $DIR/from_form.rs:74:13 | -71 | #[field(123)] +74 | #[field(123)] | ^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:69:10 + --> $DIR/from_form.rs:72:10 | -69 | #[derive(FromForm)] +72 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected attribute parameter: `beep` - --> $DIR/from_form.rs:77:13 + --> $DIR/from_form.rs:80:13 | -77 | #[field(beep = "bop")] +80 | #[field(beep = "bop")] | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:75:10 + --> $DIR/from_form.rs:78:10 | -75 | #[derive(FromForm)] +78 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: field has conflicting names - --> $DIR/from_form.rs:83:5 + --> $DIR/from_form.rs:86:5 | -83 | #[field(name = "blah")] +86 | #[field(name = "blah")] | ^ error: [note] this field name... - --> $DIR/from_form.rs:83:20 + --> $DIR/from_form.rs:86:20 | -83 | #[field(name = "blah")] +86 | #[field(name = "blah")] | ^^^^^^ error: [note] ...conflicts with this field name - --> $DIR/from_form.rs:84:20 + --> $DIR/from_form.rs:87:20 | -84 | #[field(name = "blah")] +87 | #[field(name = "blah")] | ^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:81:10 + --> $DIR/from_form.rs:84:10 | -81 | #[derive(FromForm)] +84 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: expected list `#[attr(..)]`, found bare boolean literal - --> $DIR/from_form.rs:90:20 + --> $DIR/from_form.rs:93:20 | -90 | #[field(name = true)] +93 | #[field(name = true)] | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:88:10 + --> $DIR/from_form.rs:91:10 | -88 | #[derive(FromForm)] +91 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: expected expression, found bare path "name" - --> $DIR/from_form.rs:96:13 + --> $DIR/from_form.rs:99:13 | -96 | #[field(name)] +99 | #[field(name)] | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:94:10 + --> $DIR/from_form.rs:97:10 | -94 | #[derive(FromForm)] +97 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: expected list `#[attr(..)]`, found bare integer literal - --> $DIR/from_form.rs:102:20 + --> $DIR/from_form.rs:105:20 | -102 | #[field(name = 123)] +105 | #[field(name = 123)] | ^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:100:10 + --> $DIR/from_form.rs:103:10 | -100 | #[derive(FromForm)] +103 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' - --> $DIR/from_form.rs:108:20 + --> $DIR/from_form.rs:111:20 | -108 | #[field(name = "hello&world")] +111 | #[field(name = "hello&world")] | ^^^^^^^^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:106:10 + --> $DIR/from_form.rs:109:10 | -106 | #[derive(FromForm)] +109 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' - --> $DIR/from_form.rs:114:20 + --> $DIR/from_form.rs:117:20 | -114 | #[field(name = "!@#$%^&*()_")] +117 | #[field(name = "!@#$%^&*()_")] | ^^^^^^^^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:112:10 + --> $DIR/from_form.rs:115:10 | -112 | #[derive(FromForm)] +115 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' - --> $DIR/from_form.rs:120:20 + --> $DIR/from_form.rs:123:20 | -120 | #[field(name = "?")] +123 | #[field(name = "?")] | ^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:118:10 + --> $DIR/from_form.rs:121:10 | -118 | #[derive(FromForm)] +121 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' - --> $DIR/from_form.rs:126:20 + --> $DIR/from_form.rs:129:20 | -126 | #[field(name = "")] +129 | #[field(name = "")] | ^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:124:10 + --> $DIR/from_form.rs:127:10 | -124 | #[derive(FromForm)] +127 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' - --> $DIR/from_form.rs:132:20 + --> $DIR/from_form.rs:135:20 | -132 | #[field(name = "a&b")] +135 | #[field(name = "a&b")] | ^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:130:10 + --> $DIR/from_form.rs:133:10 | -130 | #[derive(FromForm)] +133 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' - --> $DIR/from_form.rs:138:20 + --> $DIR/from_form.rs:141:20 | -138 | #[field(name = "a=")] +141 | #[field(name = "a=")] | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:136:10 + --> $DIR/from_form.rs:139:10 | -136 | #[derive(FromForm)] +139 | #[derive(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 + --> $DIR/from_form.rs:177:26 | -174 | #[field(default = 1, default = 2)] +177 | #[field(default = 1, default = 2)] | ^^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:172:10 + --> $DIR/from_form.rs:175:10 | -172 | #[derive(FromForm)] +175 | #[derive(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 + --> $DIR/from_form.rs:184:23 | -181 | #[field(default = 2)] +184 | #[field(default = 2)] | ^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:178:10 + --> $DIR/from_form.rs:181:10 | -178 | #[derive(FromForm)] +181 | #[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 + --> $DIR/from_form.rs:190:23 | -187 | #[field(default = 1, default_with = None)] +190 | #[field(default = 1, default_with = None)] | ^ error: [note] other default expression is here - --> $DIR/from_form.rs:187:41 + --> $DIR/from_form.rs:190:41 | -187 | #[field(default = 1, default_with = None)] +190 | #[field(default = 1, default_with = None)] | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:185:10 + --> $DIR/from_form.rs:188:10 | -185 | #[derive(FromForm)] +188 | #[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 + --> $DIR/from_form.rs:197:23 | -194 | #[field(default = 1)] +197 | #[field(default = 1)] | ^ error: [note] other default expression is here - --> $DIR/from_form.rs:193:28 + --> $DIR/from_form.rs:196:28 | -193 | #[field(default_with = None)] +196 | #[field(default_with = None)] | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:191:10 + --> $DIR/from_form.rs:194:10 | -191 | #[derive(FromForm)] +194 | #[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 + --> $DIR/from_form.rs:153:24 | -150 | #[field(validate = unknown())] +153 | #[field(validate = unknown())] | ^^^^^^^ not found in this scope error[E0308]: mismatched types - --> $DIR/from_form.rs:144:24 + --> $DIR/from_form.rs:147:24 | -144 | #[field(validate = 123)] +147 | #[field(validate = 123)] | -------- ^^^ expected enum `Result`, found integer | | | expected due to this @@ -478,33 +478,33 @@ error[E0308]: mismatched types found type `{integer}` error[E0308]: mismatched types - --> $DIR/from_form.rs:157:12 + --> $DIR/from_form.rs:160:12 | -157 | first: String, +160 | first: String, | ^^^^^^ expected enum `TempFile`, found struct `std::string::String` | = note: expected reference `&TempFile<'_>` found reference `&std::string::String` error[E0308]: mismatched types - --> $DIR/from_form.rs:163:12 + --> $DIR/from_form.rs:166:12 | -163 | first: String, +166 | first: String, | ^^^^^^ expected enum `TempFile`, found struct `std::string::String` | = note: expected reference `&TempFile<'_>` found reference `&std::string::String` error[E0308]: mismatched types - --> $DIR/from_form.rs:162:28 + --> $DIR/from_form.rs:165:28 | -162 | #[field(validate = ext("hello"))] +165 | #[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 + --> $DIR/from_form.rs:171:23 | -168 | #[field(default = "no conversion")] +171 | #[field(default = "no conversion")] | ^^^^^^^^^^^^^^^ the trait `From<&str>` is not implemented for `i32` | = help: the following implementations were found: @@ -516,14 +516,14 @@ error[E0277]: the trait bound `i32: From<&str>` is not satisfied = note: required because of the requirements on the impl of `Into` for `&str` error[E0308]: mismatched types - --> $DIR/from_form.rs:200:33 + --> $DIR/from_form.rs:203:33 | -200 | #[field(default_with = Some("hi"))] +203 | #[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()))] +203 | #[field(default_with = Some("hi".to_string()))] | ^^^^^^^^^^^^^^^^ -200 | #[field(default_with = Some("hi".to_string()))] +203 | #[field(default_with = Some("hi".to_string()))] | ^^^^^^^^^^^^^^^^ diff --git a/core/codegen/tests/ui-fail-stable/from_form_type_errors.stderr b/core/codegen/tests/ui-fail-stable/from_form_type_errors.stderr index dd5bd6a8..27a490b9 100644 --- a/core/codegen/tests/ui-fail-stable/from_form_type_errors.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form_type_errors.stderr @@ -4,7 +4,7 @@ error[E0277]: the trait bound `Unknown: FromFormField<'_>` is not satisfied 7 | field: Unknown, | ^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Unknown` | - = note: required because of the requirements on the impl of `FromForm<'__f>` for `Unknown` + = note: required because of the requirements on the impl of `FromForm<'r>` for `Unknown` error[E0277]: the trait bound `Foo: FromFormField<'_>` is not satisfied --> $DIR/from_form_type_errors.rs:14:12 @@ -12,4 +12,4 @@ error[E0277]: the trait bound `Foo: FromFormField<'_>` is not satisfied 14 | field: Foo, | ^^^ the trait `FromFormField<'_>` is not implemented for `Foo` | - = note: required because of the requirements on the impl of `FromForm<'__f>` for `Foo` + = note: required because of the requirements on the impl of `FromForm<'r>` for `Foo` diff --git a/core/codegen/tests/ui-fail.rs b/core/codegen/tests/ui-fail.rs index 1cd5552a..666ec6fe 100644 --- a/core/codegen/tests/ui-fail.rs +++ b/core/codegen/tests/ui-fail.rs @@ -5,6 +5,10 @@ fn ui() { _ => "ui-fail-stable" }; + let glob = std::env::args().last() + .map(|arg| format!("*{}*.rs", arg)) + .unwrap_or_else(|| "*.rs".into()); + let t = trybuild::TestCases::new(); - t.compile_fail(format!("tests/{}/*.rs", path)); + t.compile_fail(format!("tests/{}/{}", path, glob)); } diff --git a/core/codegen/tests/ui-fail/from_form.rs b/core/codegen/tests/ui-fail/from_form.rs index 94bc63ee..2389f3c6 100644 --- a/core/codegen/tests/ui-fail/from_form.rs +++ b/core/codegen/tests/ui-fail/from_form.rs @@ -1,4 +1,4 @@ -#[macro_use] extern crate rocket; +use rocket::form::FromForm; #[derive(FromForm)] enum Thing { } @@ -12,6 +12,9 @@ struct Foo2 { } #[derive(FromForm)] struct Foo3(usize); +#[derive(FromForm)] +struct Foo4(usize, usize, usize); + #[derive(FromForm)] struct NextTodoTask<'f, 'a> { description: String, @@ -201,4 +204,16 @@ struct Default5 { no_conversion_from_with: String, } +#[derive(FromForm)] // NO ERROR +struct Another { + _foo: T, + _bar: T, +} + +#[derive(FromForm)] // NO ERROR +struct AnotherOne { // NO ERROR + _foo: T, + _bar: T, +} + fn main() { } diff --git a/core/lib/src/form/validate.rs b/core/lib/src/form/validate.rs index 0c8b7ef9..38ebfe37 100644 --- a/core/lib/src/form/validate.rs +++ b/core/lib/src/form/validate.rs @@ -841,3 +841,41 @@ pub fn with<'v, V, F, M>(value: V, f: F, msg: M) -> Result<'v, ()> Ok(()) } + +/// _Try_ With validator: succeeds when an arbitrary function or closure does. +/// +/// Along with [`with`], this is the most generic validator. It succeeds +/// excactly when `f` returns `Ok` and fails otherwise. +/// +/// On failure, returns a validation error with the message in the `Err` +/// variant converted into a string. +/// +/// # Example +/// +/// Assuming `Token` has a `from_str` method: +/// +/// ```rust +/// # use rocket::form::FromForm; +/// # impl FromStr for Token<'_> { +/// # type Err = &'static str; +/// # fn from_str(s: &str) -> Result { todo!() } +/// # } +/// use std::str::FromStr; +/// +/// #[derive(FromForm)] +/// #[field(validate = try_with(|s| Token::from_str(s)))] +/// struct Token<'r>(&'r str); +/// +/// #[derive(FromForm)] +/// #[field(validate = try_with(|s| s.parse::()))] +/// struct Token2<'r>(&'r str); +/// ``` +pub fn try_with<'v, V, F, T, E>(value: V, f: F) -> Result<'v, ()> + where F: FnOnce(V) -> std::result::Result, + E: std::fmt::Display +{ + match f(value) { + Ok(_) => Ok(()), + Err(e) => Err(Error::validation(e.to_string()).into()) + } +}