From 7784cc982a9e826b0d37bdaba92793e33a7e9344 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 5 Mar 2021 18:09:12 -0800 Subject: [PATCH] Allow multiple and uncased field renamings. --- core/codegen/src/attribute/param/mod.rs | 2 +- core/codegen/src/attribute/route/parse.rs | 2 +- core/codegen/src/derive/form_field.rs | 101 +++++++++++++---- core/codegen/src/derive/from_form.rs | 67 +++++++---- core/codegen/src/derive/from_form_field.rs | 2 +- core/codegen/src/derive/uri_display.rs | 2 +- core/codegen/src/name.rs | 28 ++--- core/codegen/tests/from_form.rs | 54 +++++++++ .../tests/ui-fail-nightly/from_form.stderr | 70 ++++++++---- .../ui-fail-nightly/from_form_field.stderr | 98 +++++++++++++++++ .../tests/ui-fail-nightly/uri_display.stderr | 2 +- .../tests/ui-fail-stable/from_form.stderr | 59 +++++++--- .../ui-fail-stable/from_form_field.stderr | 104 ++++++++++++++++++ .../tests/ui-fail-stable/uri_display.stderr | 2 +- core/codegen/tests/ui-fail/from_form_field.rs | 29 +++++ site/guide/4-requests.md | 48 ++++++-- 16 files changed, 563 insertions(+), 107 deletions(-) diff --git a/core/codegen/src/attribute/param/mod.rs b/core/codegen/src/attribute/param/mod.rs index 07b6fa94..39442f4f 100644 --- a/core/codegen/src/attribute/param/mod.rs +++ b/core/codegen/src/attribute/param/mod.rs @@ -78,7 +78,7 @@ impl Parameter { impl Dynamic { // This isn't public since this `Dynamic` should always be an `Ignored`. pub fn is_wild(&self) -> bool { - self.name == "_" + &self.name == "_" } } diff --git a/core/codegen/src/attribute/route/parse.rs b/core/codegen/src/attribute/route/parse.rs index 222a362d..60dccd69 100644 --- a/core/codegen/src/attribute/route/parse.rs +++ b/core/codegen/src/attribute/route/parse.rs @@ -200,7 +200,7 @@ impl Route { .chain(query_params.iter().filter_map(|p| p.guard())) .chain(data_guard.as_ref().into_iter()); - all_other_guards.all(|g| g.name != name) + all_other_guards.all(|g| &g.name != *name) }) .enumerate() .map(|(index, (name, (ident, ty)))| Guard { diff --git a/core/codegen/src/derive/form_field.rs b/core/codegen/src/derive/form_field.rs index 05bd5cc1..e72eb90c 100644 --- a/core/codegen/src/derive/form_field.rs +++ b/core/codegen/src/derive/form_field.rs @@ -3,18 +3,19 @@ use devise::{*, ext::{TypeExt, SpanDiagnosticExt}}; use syn::visit_mut::VisitMut; use syn::visit::Visit; -use crate::proc_macro2::{Span, TokenStream, TokenTree}; +use crate::proc_macro2::{TokenStream, TokenTree}; use crate::syn_ext::IdentExt; use crate::name::Name; -pub struct FormField { - pub span: Span, - pub name: Name, +#[derive(Debug)] +pub enum FieldName { + Cased(Name), + Uncased(Name), } #[derive(FromMeta)] pub struct FieldAttr { - pub name: Option, + pub name: Option, pub validate: Option>, } @@ -24,12 +25,13 @@ impl FieldAttr { pub(crate) trait FieldExt { fn ident(&self) -> &syn::Ident; - fn field_name(&self) -> Result; + fn field_names(&self) -> Result>; + fn one_field_name(&self) -> Result; fn stripped_ty(&self) -> syn::Type; fn name_view(&self) -> Result; } -impl FromMeta for FormField { +impl FromMeta for FieldName { fn from_meta(meta: &MetaItem) -> Result { // These are used during parsing. const CONTROL_CHARS: &[char] = &['&', '=', '?', '.', '[', ']']; @@ -44,8 +46,23 @@ impl FromMeta for FormField { s.chars().all(|c| c.is_ascii_graphic() && !CONTROL_CHARS.contains(&c)) } - let name = Name::from_meta(meta)?; - if !is_valid_field_name(name.as_str()) { + let field_name = match Name::from_meta(meta) { + Ok(name) => FieldName::Cased(name), + Err(_) => { + #[derive(FromMeta)] + struct Inner { + #[meta(naked)] + uncased: Name + } + + let expr = meta.expr()?; + let item: MetaItem = syn::parse2(quote!(#expr))?; + let inner = Inner::from_meta(&item)?; + FieldName::Uncased(inner.uncased) + } + }; + + if !is_valid_field_name(field_name.as_str()) { let chars = CONTROL_CHARS.iter() .map(|c| format!("{:?}", c)) .collect::>() @@ -56,7 +73,35 @@ impl FromMeta for FormField { .help(format!("field name cannot be `isindex` or contain {}", chars))); } - Ok(FormField { span: meta.value_span(), name }) + Ok(field_name) + } +} + +impl std::ops::Deref for FieldName { + type Target = Name; + + fn deref(&self) -> &Self::Target { + match self { + FieldName::Cased(n) | FieldName::Uncased(n) => n, + } + } +} + +impl quote::ToTokens for FieldName { + fn to_tokens(&self, tokens: &mut TokenStream) { + (self as &Name).to_tokens(tokens) + } +} + +impl PartialEq for FieldName { + fn eq(&self, other: &Self) -> bool { + use FieldName::*; + + match (self, other) { + (Cased(a), Cased(b)) => a == b, + (Cased(a), Uncased(u)) | (Uncased(u), Cased(a)) => a == u.as_uncased_str(), + (Uncased(u1), Uncased(u2)) => u1.as_uncased_str() == u2.as_uncased_str(), + } } } @@ -65,22 +110,31 @@ impl FieldExt for Field<'_> { self.ident.as_ref().expect("named") } - fn field_name(&self) -> Result { - let mut fields = FieldAttr::from_attrs(FieldAttr::NAME, &self.attrs)? + fn field_names(&self) -> Result> { + let attr_names = FieldAttr::from_attrs(FieldAttr::NAME, &self.attrs)? .into_iter() - .filter_map(|attr| attr.name); + .filter_map(|attr| attr.name) + .collect::>(); - let name = fields.next() - .map(|f| f.name) - .unwrap_or_else(|| Name::from(self.ident().clone())); - - if let Some(field) = fields.next() { - return Err(field.span - .error("duplicate form field renaming") - .help("a field can only be renamed once")); + if attr_names.is_empty() { + let ident_name = Name::from(self.ident()); + return Ok(vec![FieldName::Cased(ident_name)]); } - Ok(name.to_string()) + Ok(attr_names) + } + + fn one_field_name(&self) -> Result { + let mut names = self.field_names()?.into_iter(); + let first = names.next().expect("always have >= 1 name"); + + if let Some(name) = names.next() { + return Err(name.span() + .error("unexpected second field name") + .note("only one field rename is allowed in this context")); + } + + Ok(first) } fn stripped_ty(&self) -> syn::Type { @@ -88,7 +142,8 @@ impl FieldExt for Field<'_> { } fn name_view(&self) -> Result { - let field_name = self.field_name()?; + 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)) diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index f3762660..e5e9de6d 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -2,25 +2,30 @@ use devise::{*, ext::{TypeExt, SpanDiagnosticExt, GenericsExt, Split2, quote_res use crate::exports::*; use crate::proc_macro2::TokenStream; -use crate::derive::form_field::*; +use crate::derive::form_field::{*, FieldName::*}; // F: fn(field_ty: Ty, field_context: Expr) fn fields_map(fields: Fields<'_>, map_f: F) -> Result where F: Fn(&syn::Type, &syn::Expr) -> TokenStream { - let matchers = fields.iter() - .map(|f| { - let (ident, field_name, ty) = (f.ident(), f.field_name()?, f.stripped_ty()); - let field_context = quote_spanned!(ty.span() => { - let _o = __c.__opts; - __c.#ident.get_or_insert_with(|| <#ty as #_form::FromForm<'__f>>::init(_o)) - }); + 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 _o = __c.__opts; + __c.#ident.get_or_insert_with(|| <#ty as #_form::FromForm<'__f>>::init(_o)) + }); - let field_context = syn::parse2(field_context).expect("valid expr"); - let expr = map_f(&ty, &field_context); - Ok(quote!(#field_name => { #expr })) - }) - .collect::>>()?; + 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 { + Cased(name) => quote!(#name => { #push }), + Uncased(name) => quote!(__n if __n.as_uncased() == #name => { #push }), + }); + + matchers.extend(field_matchers); + } Ok(quote! { __c.__parent = __f.name.parent(); @@ -65,15 +70,33 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { return Err(fields.span().error("at least one field is required")); } - let mut names = ::std::collections::HashMap::new(); + let mut names = vec![]; + let mut field_map = vec![]; for field in fields.iter() { - let name = field.field_name()?; - if let Some(span) = names.get(&name) { - return Err(field.span().error("duplicate form field") - .span_note(*span, "previously defined here")); - } + names.append(&mut field.field_names()?); + field_map.push((names.len(), field)); + } - names.insert(name, field.span()); + // get the field corresponding to name index `k`. + let field = |k| field_map.iter().find(|(i, _)| k < *i).expect("k < *i"); + + for (i, a) in names.iter().enumerate() { + let rest = names.iter().enumerate().skip(i + 1); + if let Some((j, b)) = rest.filter(|(_, b)| b == &a).next() { + let ((fa_i, field_a), (fb_i, field_b)) = (field(i), field(j)); + + if fa_i == fb_i { + return Err(field_a.span() + .error("field has conflicting names") + .span_note(a.span(), "this field name...") + .span_note(b.span(), "...conflicts with this name")); + } else { + return Err(b.span() + .error("field name conflicts with previous name") + .span_note(field_a.span(), "previous field with conflicting name") + .span_help(field_b.span(), "field name is part of this field")); + } + } } Ok(()) @@ -105,6 +128,10 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { quote_spanned!(ty.span() => #ident: #field_ty,) }) ) + .outer_mapper(quote! { + #[allow(unused_imports)] + use #_http::uncased::AsUncased; + }) .outer_mapper(quote!(#[rocket::async_trait])) .inner_mapper(MapperBuild::new() .try_input_map(|mapper, input| { diff --git a/core/codegen/src/derive/from_form_field.rs b/core/codegen/src/derive/from_form_field.rs index 9c153ad4..f18e58d1 100644 --- a/core/codegen/src/derive/from_form_field.rs +++ b/core/codegen/src/derive/from_form_field.rs @@ -34,7 +34,7 @@ pub fn derive_from_form_field(input: proc_macro::TokenStream) -> TokenStream { .try_enum_map(|_, data| { let variant_name_sources = data.variants() .map(|v| FieldAttr::one_from_attrs("field", &v.attrs).map(|o| { - o.map(|f| f.value).unwrap_or_else(|| v.ident.clone().into()) + o.map(|f| f.value).unwrap_or_else(|| Name::from(&v.ident)) })) .collect::>>()?; diff --git a/core/codegen/src/derive/uri_display.rs b/core/codegen/src/derive/uri_display.rs index 0849ea7f..34649749 100644 --- a/core/codegen/src/derive/uri_display.rs +++ b/core/codegen/src/derive/uri_display.rs @@ -56,7 +56,7 @@ pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream { let span = field.span().into(); let accessor = field.accessor(); let tokens = if field.ident.is_some() { - let name = field.field_name()?; + let name = field.one_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/name.rs b/core/codegen/src/name.rs index 55ec0837..c478df01 100644 --- a/core/codegen/src/name.rs +++ b/core/codegen/src/name.rs @@ -1,17 +1,18 @@ use crate::syn::{self, Ident, ext::IdentExt}; +use crate::http::uncased::UncasedStr; use crate::proc_macro2::Span; -/// A "name" read by codegen, which may or may not be a valid identifier. A -/// `Name` is typically constructed indirectly via FromMeta, or From or -/// directly from a string via `Name::new()`. +/// 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()`. /// /// Some "names" in Rocket include: /// * Dynamic parameter: `name` in `` /// * Renamed fields: `foo` in #[field(name = "foo")]. /// /// `Name` implements Hash, PartialEq, and Eq, and additionally PartialEq for -/// all types `S: AsStr`. These implementations all compare the value of -/// `name()` only. +/// all types `S: PartialEq`. These implementations all compare the value +/// of `as_str()` only. #[derive(Debug, Clone)] pub struct Name { value: String, @@ -31,6 +32,11 @@ impl Name { &self.value } + /// Like `as_str()` but into an `&UncasedStr`. + pub fn as_uncased_str(&self) -> &UncasedStr { + UncasedStr::new(self.as_str()) + } + pub fn span(&self) -> Span { self.span } @@ -50,7 +56,7 @@ impl devise::FromMeta for Name { impl quote::ToTokens for Name { fn to_tokens(&self, tokens: &mut devise::proc_macro2::TokenStream) { - self.as_str().to_tokens(tokens) + syn::LitStr::new(self.as_str(), self.span()).to_tokens(tokens) } } @@ -60,12 +66,6 @@ impl From<&Ident> for Name { } } -impl From for Name { - fn from(ident: Ident) -> Self { - Name::new(ident.unraw().to_string(), ident.span()) - } -} - impl AsRef for Name { fn as_ref(&self) -> &str { self.as_str() @@ -88,9 +88,9 @@ impl std::ops::Deref for Name { impl Eq for Name { } -impl + ?Sized> PartialEq for Name { +impl + ?Sized> PartialEq for Name { fn eq(&self, other: &S) -> bool { - self.as_str() == other.as_ref() + other == self.as_str() } } diff --git a/core/codegen/tests/from_form.rs b/core/codegen/tests/from_form.rs index 58140833..fbc2d3d2 100644 --- a/core/codegen/tests/from_form.rs +++ b/core/codegen/tests/from_form.rs @@ -227,6 +227,60 @@ fn field_renaming() { let form: Option = strict(&form_string).ok(); assert!(form.is_none()); + + #[derive(Debug, PartialEq, FromForm)] + struct MultiName<'r> { + single: usize, + #[field(name = "SomeCase")] + #[field(name = "some_case")] + some_case: &'r str, + } + + let form_string = &["single=123", "some_case=hi_im_here"].join("&"); + let form: Option = strict(&form_string).ok(); + assert_eq!(form, Some(MultiName { single: 123, some_case: "hi_im_here", })); + + let form_string = &["single=123", "SomeCase=HiImHere"].join("&"); + let form: Option = strict(&form_string).ok(); + assert_eq!(form, Some(MultiName { single: 123, some_case: "HiImHere", })); + + let form_string = &["single=123", "some_case=hi_im_here", "SomeCase=HiImHere"].join("&"); + let form: Option = strict(&form_string).ok(); + assert!(form.is_none()); + + let form_string = &["single=123", "some_case=hi_im_here", "SomeCase=HiImHere"].join("&"); + let form: Option = lenient(&form_string).ok(); + assert_eq!(form, Some(MultiName { single: 123, some_case: "hi_im_here", })); + + let form_string = &["single=123", "SomeCase=HiImHere", "some_case=hi_im_here"].join("&"); + let form: Option = lenient(&form_string).ok(); + assert_eq!(form, Some(MultiName { single: 123, some_case: "HiImHere", })); + + #[derive(Debug, PartialEq, FromForm)] + struct CaseInsensitive<'r> { + #[field(name = uncased("SomeCase"))] + #[field(name = "some_case")] + some_case: &'r str, + + #[field(name = uncased("hello"))] + hello: usize, + } + + let form_string = &["HeLLO=123", "sOMECASe=hi_im_here"].join("&"); + let form: Option = strict(&form_string).ok(); + assert_eq!(form, Some(CaseInsensitive { hello: 123, some_case: "hi_im_here", })); + + let form_string = &["hello=456", "SomeCase=HiImHere"].join("&"); + let form: Option = strict(&form_string).ok(); + assert_eq!(form, Some(CaseInsensitive { hello: 456, some_case: "HiImHere", })); + + let form_string = &["helLO=789", "some_case=hi_there"].join("&"); + let form: Option = strict(&form_string).ok(); + assert_eq!(form, Some(CaseInsensitive { hello: 789, some_case: "hi_there", })); + + let form_string = &["hello=123", "SOme_case=hi_im_here"].join("&"); + let form: Option = strict(&form_string).ok(); + assert!(form.is_none()); } #[test] diff --git a/core/codegen/tests/ui-fail-nightly/from_form.stderr b/core/codegen/tests/ui-fail-nightly/from_form.stderr index b56e77b1..abc9c793 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form.stderr @@ -77,18 +77,23 @@ 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 form field +error: field name conflicts with previous name --> $DIR/from_form.rs:33:5 | 33 | foo: usize, - | ^^^^^^^^^^ + | ^^^ | -note: previously defined here +note: previous field with conflicting name --> $DIR/from_form.rs:31:5 | 31 | / #[field(name = "foo")] 32 | | field: String, | |_________________^ +help: field name is part of this field + --> $DIR/from_form.rs:33:5 + | +33 | foo: usize, + | ^^^^^^^^^^ note: error occurred while deriving `FromForm` --> $DIR/from_form.rs:29:10 | @@ -96,19 +101,24 @@ 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 form field - --> $DIR/from_form.rs:40:5 +error: field name conflicts with previous name + --> $DIR/from_form.rs:40:20 | -40 | / #[field(name = "hello")] -41 | | other: String, - | |_________________^ +40 | #[field(name = "hello")] + | ^^^^^^^ | -note: previously defined here +note: previous field with conflicting name --> $DIR/from_form.rs:38:5 | 38 | / #[field(name = "hello")] 39 | | first: String, | |_________________^ +help: field name is part of this field + --> $DIR/from_form.rs:40:5 + | +40 | / #[field(name = "hello")] +41 | | other: String, + | |_________________^ note: error occurred while deriving `FromForm` --> $DIR/from_form.rs:36:10 | @@ -116,18 +126,23 @@ 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 form field +error: field name conflicts with previous name + --> $DIR/from_form.rs:47:20 + | +47 | #[field(name = "first")] + | ^^^^^^^ + | +note: previous field with conflicting name + --> $DIR/from_form.rs:46:5 + | +46 | first: String, + | ^^^^^^^^^^^^^ +help: field name is part of this field --> $DIR/from_form.rs:47:5 | 47 | / #[field(name = "first")] 48 | | other: String, | |_________________^ - | -note: previously defined here - --> $DIR/from_form.rs:46:5 - | -46 | first: String, - | ^^^^^^^^^^^^^ note: error occurred while deriving `FromForm` --> $DIR/from_form.rs:44:10 | @@ -200,13 +215,24 @@ 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 form field renaming +error: field has conflicting names + --> $DIR/from_form.rs:83:5 + | +83 | / #[field(name = "blah")] +84 | | #[field(name = "blah")] +85 | | my_field: String, + | |____________________^ + | +note: this field name... + --> $DIR/from_form.rs:83:20 + | +83 | #[field(name = "blah")] + | ^^^^^^ +note: ...conflicts with this name --> $DIR/from_form.rs:84:20 | 84 | #[field(name = "blah")] | ^^^^^^ - | - = help: a field can only be renamed once note: error occurred while deriving `FromForm` --> $DIR/from_form.rs:81:10 | @@ -214,7 +240,7 @@ 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: invalid value: expected string literal +error: expected list `#[attr(..)]`, found bare boolean literal --> $DIR/from_form.rs:90:20 | 90 | #[field(name = true)] @@ -227,7 +253,7 @@ 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: expected literal, found bare path "name" +error: expected expression, found bare path "name" --> $DIR/from_form.rs:96:13 | 96 | #[field(name)] @@ -240,7 +266,7 @@ 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: invalid value: expected string literal +error: expected list `#[attr(..)]`, found bare integer literal --> $DIR/from_form.rs:102:20 | 102 | #[field(name = 123)] diff --git a/core/codegen/tests/ui-fail-nightly/from_form_field.stderr b/core/codegen/tests/ui-fail-nightly/from_form_field.stderr index a912006e..133f3b32 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form_field.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form_field.stderr @@ -103,3 +103,101 @@ note: error occurred while deriving `FromFormField` 33 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = 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_field.rs:41:5 + | +41 | / #[field(name = "foo")] +42 | | #[field(name = uncased("FOO"))] +43 | | single: usize, + | |_________________^ + | +note: this field name... + --> $DIR/from_form_field.rs:41:20 + | +41 | #[field(name = "foo")] + | ^^^^^ +note: ...conflicts with this name + --> $DIR/from_form_field.rs:42:28 + | +42 | #[field(name = uncased("FOO"))] + | ^^^^^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form_field.rs:39:10 + | +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_field.rs:50:20 + | +50 | #[field(name = "foo")] + | ^^^^^ + | +note: previous field with conflicting name + --> $DIR/from_form_field.rs:48:5 + | +48 | / #[field(name = "foo")] +49 | | single: usize, + | |_________________^ +help: field name is part of this field + --> $DIR/from_form_field.rs:50:5 + | +50 | / #[field(name = "foo")] +51 | | other: usize, + | |________________^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form_field.rs:46:10 + | +46 | #[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_field.rs:58:5 + | +58 | hello_there: usize, + | ^^^^^^^^^^^ + | +note: previous field with conflicting name + --> $DIR/from_form_field.rs:56:5 + | +56 | / #[field(name = uncased("HELLO_THERE"))] +57 | | single: usize, + | |_________________^ +help: field name is part of this field + --> $DIR/from_form_field.rs:58:5 + | +58 | hello_there: usize, + | ^^^^^^^^^^^^^^^^^^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form_field.rs:54:10 + | +54 | #[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_field.rs:65:5 + | +65 | hello_there: usize, + | ^^^^^^^^^^^ + | +note: previous field with conflicting name + --> $DIR/from_form_field.rs:63:5 + | +63 | / #[field(name = "hello_there")] +64 | | single: usize, + | |_________________^ +help: field name is part of this field + --> $DIR/from_form_field.rs:65:5 + | +65 | hello_there: usize, + | ^^^^^^^^^^^^^^^^^^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form_field.rs:61:10 + | +61 | #[derive(FromForm)] + | ^^^^^^^^ + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-nightly/uri_display.stderr b/core/codegen/tests/ui-fail-nightly/uri_display.stderr index 921d47f0..bf504999 100644 --- a/core/codegen/tests/ui-fail-nightly/uri_display.stderr +++ b/core/codegen/tests/ui-fail-nightly/uri_display.stderr @@ -63,7 +63,7 @@ note: error occurred while deriving `UriDisplay` | ^^^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: invalid value: expected string literal +error: expected list `#[attr(..)]`, found bare integer literal --> $DIR/uri_display.rs:22:20 | 22 | #[field(name = 123)] diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index f7f20437..daf6ccb4 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -83,18 +83,24 @@ 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 form field +error: field name conflicts with previous name --> $DIR/from_form.rs:33:5 | 33 | foo: usize, | ^^^ -error: [note] previously defined here +error: [note] previous field with conflicting name --> $DIR/from_form.rs:31:5 | 31 | #[field(name = "foo")] | ^ +error: [help] field name is part of this field + --> $DIR/from_form.rs:33:5 + | +33 | foo: usize, + | ^^^ + error: [note] error occurred while deriving `FromForm` --> $DIR/from_form.rs:29:10 | @@ -103,18 +109,24 @@ 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 form field - --> $DIR/from_form.rs:40:5 +error: field name conflicts with previous name + --> $DIR/from_form.rs:40:20 | 40 | #[field(name = "hello")] - | ^ + | ^^^^^^^ -error: [note] previously defined here +error: [note] previous field with conflicting name --> $DIR/from_form.rs:38:5 | 38 | #[field(name = "hello")] | ^ +error: [help] field name is part of this field + --> $DIR/from_form.rs:40:5 + | +40 | #[field(name = "hello")] + | ^ + error: [note] error occurred while deriving `FromForm` --> $DIR/from_form.rs:36:10 | @@ -123,18 +135,24 @@ 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 form field - --> $DIR/from_form.rs:47:5 +error: field name conflicts with previous name + --> $DIR/from_form.rs:47:20 | 47 | #[field(name = "first")] - | ^ + | ^^^^^^^ -error: [note] previously defined here +error: [note] previous field with conflicting name --> $DIR/from_form.rs:46:5 | 46 | first: String, | ^^^^^ +error: [help] field name is part of this field + --> $DIR/from_form.rs:47:5 + | +47 | #[field(name = "first")] + | ^ + error: [note] error occurred while deriving `FromForm` --> $DIR/from_form.rs:44:10 | @@ -213,8 +231,19 @@ 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 form field renaming - --- help: a field can only be renamed once +error: field has conflicting names + --> $DIR/from_form.rs:83:5 + | +83 | #[field(name = "blah")] + | ^ + +error: [note] this field name... + --> $DIR/from_form.rs:83:20 + | +83 | #[field(name = "blah")] + | ^^^^^^ + +error: [note] ...conflicts with this name --> $DIR/from_form.rs:84:20 | 84 | #[field(name = "blah")] @@ -228,7 +257,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: invalid value: expected string literal +error: expected list `#[attr(..)]`, found bare boolean literal --> $DIR/from_form.rs:90:20 | 90 | #[field(name = true)] @@ -242,7 +271,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: expected literal, found bare path "name" +error: expected expression, found bare path "name" --> $DIR/from_form.rs:96:13 | 96 | #[field(name)] @@ -256,7 +285,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: invalid value: expected string literal +error: expected list `#[attr(..)]`, found bare integer literal --> $DIR/from_form.rs:102:20 | 102 | #[field(name = 123)] diff --git a/core/codegen/tests/ui-fail-stable/from_form_field.stderr b/core/codegen/tests/ui-fail-stable/from_form_field.stderr index 757410bf..8c0f98d0 100644 --- a/core/codegen/tests/ui-fail-stable/from_form_field.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form_field.stderr @@ -109,3 +109,107 @@ error: [note] error occurred while deriving `FromFormField` | ^^^^^^^^^^^^^ | = 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_field.rs:41:5 + | +41 | #[field(name = "foo")] + | ^ + +error: [note] this field name... + --> $DIR/from_form_field.rs:41:20 + | +41 | #[field(name = "foo")] + | ^^^^^ + +error: [note] ...conflicts with this name + --> $DIR/from_form_field.rs:42:28 + | +42 | #[field(name = uncased("FOO"))] + | ^^^^^ + +error: [note] error occurred while deriving `FromForm` + --> $DIR/from_form_field.rs:39:10 + | +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_field.rs:50:20 + | +50 | #[field(name = "foo")] + | ^^^^^ + +error: [note] previous field with conflicting name + --> $DIR/from_form_field.rs:48:5 + | +48 | #[field(name = "foo")] + | ^ + +error: [help] field name is part of this field + --> $DIR/from_form_field.rs:50:5 + | +50 | #[field(name = "foo")] + | ^ + +error: [note] error occurred while deriving `FromForm` + --> $DIR/from_form_field.rs:46:10 + | +46 | #[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_field.rs:58:5 + | +58 | hello_there: usize, + | ^^^^^^^^^^^ + +error: [note] previous field with conflicting name + --> $DIR/from_form_field.rs:56:5 + | +56 | #[field(name = uncased("HELLO_THERE"))] + | ^ + +error: [help] field name is part of this field + --> $DIR/from_form_field.rs:58:5 + | +58 | hello_there: usize, + | ^^^^^^^^^^^ + +error: [note] error occurred while deriving `FromForm` + --> $DIR/from_form_field.rs:54:10 + | +54 | #[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_field.rs:65:5 + | +65 | hello_there: usize, + | ^^^^^^^^^^^ + +error: [note] previous field with conflicting name + --> $DIR/from_form_field.rs:63:5 + | +63 | #[field(name = "hello_there")] + | ^ + +error: [help] field name is part of this field + --> $DIR/from_form_field.rs:65:5 + | +65 | hello_there: usize, + | ^^^^^^^^^^^ + +error: [note] error occurred while deriving `FromForm` + --> $DIR/from_form_field.rs:61:10 + | +61 | #[derive(FromForm)] + | ^^^^^^^^ + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-stable/uri_display.stderr b/core/codegen/tests/ui-fail-stable/uri_display.stderr index 4e3b090a..954fcd49 100644 --- a/core/codegen/tests/ui-fail-stable/uri_display.stderr +++ b/core/codegen/tests/ui-fail-stable/uri_display.stderr @@ -68,7 +68,7 @@ error: [note] error occurred while deriving `UriDisplay` | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: invalid value: expected string literal +error: expected list `#[attr(..)]`, found bare integer literal --> $DIR/uri_display.rs:22:20 | 22 | #[field(name = 123)] diff --git a/core/codegen/tests/ui-fail/from_form_field.rs b/core/codegen/tests/ui-fail/from_form_field.rs index 5a53664c..bedf84df 100644 --- a/core/codegen/tests/ui-fail/from_form_field.rs +++ b/core/codegen/tests/ui-fail/from_form_field.rs @@ -36,4 +36,33 @@ enum Bar2 { A, } +#[derive(FromForm)] +struct Renamed0 { + #[field(name = "foo")] + #[field(name = uncased("FOO"))] + single: usize, +} + +#[derive(FromForm)] +struct Renamed1 { + #[field(name = "foo")] + single: usize, + #[field(name = "foo")] + other: usize, +} + +#[derive(FromForm)] +struct Renamed2 { + #[field(name = uncased("HELLO_THERE"))] + single: usize, + hello_there: usize, +} + +#[derive(FromForm)] +struct Renamed3 { + #[field(name = "hello_there")] + single: usize, + hello_there: usize, +} + fn main() { } diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 88880ee4..00d159ea 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -764,16 +764,17 @@ By default, Rocket matches the name of an incoming form field to the name of a structure field. While this behavior is typical, it may also be desired to use different names for form fields and struct fields while still parsing as expected. You can ask Rocket to look for a different form field for a given -structure field by using the `#[field(name = "name")]` field annotation. +structure field by using one or more `#[field(name = "name")]` or `#[field(name += uncased("name")]` field annotation. The `uncased` variant case-insensitively +matches field names. As an example, say that you're writing an application that receives data from an external service. The external service `POST`s a form with a field named -`first-Name` which you'd like to write as `first_name` in Rust. Field renaming -helps: +`first-Name` which you'd like to write as `first_name` in Rust. Such a form +structure can be written as: ```rust -# #[macro_use] extern crate rocket; -# fn main() {} +# use rocket::form::FromForm; #[derive(FromForm)] struct External { @@ -782,8 +783,41 @@ struct External { } ``` -Rocket will then match the form field named `first-Name` to the structure field -named `first_name`. +If you want to accept both `firstName` case-insensitively as well as +`first_name` case-sensitively, you'll need to use two annotations: + +```rust +# use rocket::form::FromForm; + +#[derive(FromForm)] +struct External { + #[field(name = uncased("firstName"))] + #[field(name = "first_name")] + first_name: String +} +``` + +This will match any casing of `firstName` including `FirstName`, `firstname`, +`FIRSTname`, and so on, but only match exactly on `first_name`. + +If instead you wanted to match any of `first-name`, `first_name` or `firstName`, +in each instance case-insensitively, you would write: + +```rust +# use rocket::form::FromForm; + +#[derive(FromForm)] +struct External { + #[field(name = uncased("first-name"))] + #[field(name = uncased("first_name"))] + #[field(name = uncased("firstname"))] + first_name: String +} +``` + +Cased and uncased renamings can be mixed and matched, and any number of +renamings is allowed. Rocket will emit an error at compile-time if field names +conflict, preventing ambiguous parsing at runtime. ### Ad-Hoc Validation