diff --git a/codegen/src/decorators/derive_form.rs b/codegen/src/decorators/derive_form.rs index 0c0408e7..3b551265 100644 --- a/codegen/src/decorators/derive_form.rs +++ b/codegen/src/decorators/derive_form.rs @@ -1,10 +1,12 @@ #![allow(unused_imports)] // FIXME: Why is this coming from quote_tokens? use std::mem::transmute; +use std::collections::HashMap; use syntax::ext::base::{Annotatable, ExtCtxt}; use syntax::print::pprust::{stmt_to_string}; use syntax::ast::{ItemKind, Expr, MetaItem, Mutability, VariantData, Ident}; +use syntax::ast::StructField; use syntax::codemap::Span; use syntax::ext::build::AstBuilder; use syntax::ptr::P; @@ -13,7 +15,7 @@ use syntax_ext::deriving::generic::MethodDef; use syntax_ext::deriving::generic::{StaticStruct, Substructure, TraitDef, ty}; use syntax_ext::deriving::generic::combine_substructure as c_s; -use utils::strip_ty_lifetimes; +use utils::{strip_ty_lifetimes, SpanExt}; static ONLY_STRUCTS_ERR: &'static str = "`FromForm` can only be derived for \ structures with named fields."; @@ -49,6 +51,55 @@ fn get_struct_lifetime(ecx: &mut ExtCtxt, item: &Annotatable, span: Span) } } +pub fn extract_field_ident_name(ecx: &ExtCtxt, struct_field: &StructField) + -> (Ident, String, Span) { + let ident = match struct_field.ident { + Some(ident) => ident, + None => ecx.span_fatal(struct_field.span, ONLY_STRUCTS_ERR) + }; + + let field_attrs: Vec<_> = struct_field.attrs.iter() + .filter(|attr| attr.check_name("form")) + .collect(); + + let default = |ident: Ident| (ident, ident.to_string(), struct_field.span); + if field_attrs.len() == 0 { + return default(ident); + } else if field_attrs.len() > 1 { + ecx.span_err(struct_field.span, "only a single #[form(..)] \ + attribute can be applied to a given struct field at a time"); + return default(ident); + } + + let field_attr = field_attrs[0]; + ::syntax::attr::mark_known(&field_attr); + if !field_attr.meta_item_list().map_or(false, |l| l.len() == 1) { + ecx.struct_span_err(field_attr.span, "incorrect use of attribute") + .help(r#"the `form` attribute must have the form: #[form(field = "..")]"#) + .emit(); + return default(ident); + } + + let inner_item = &field_attr.meta_item_list().unwrap()[0]; + if !inner_item.check_name("field") { + ecx.struct_span_err(inner_item.span, "invalid `form` attribute contents") + .help(r#"only the 'field' key is supported: #[form(field = "..")]"#) + .emit(); + return default(ident); + } + + if !inner_item.is_value_str() { + ecx.struct_span_err(inner_item.span, "invalid `field` in attribute") + .help(r#"the `form` attribute must have the form: #[form(field = "..")]"#) + .emit(); + return default(ident); + } + + let name = inner_item.value_str().unwrap().as_str().to_string(); + let sp = inner_item.span.shorten_upto(name.len() + 2); + (ident, name, sp) +} + // TODO: Use proper logging to emit the error messages. pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem, annotated: &Annotatable, push: &mut FnMut(Annotatable)) { @@ -144,19 +195,25 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct _ => cx.span_bug(trait_span, "impossible substructure in `from_form`") }; - // Create a vector of (ident, type) pairs, one for each field in struct. - let mut fields_and_types = vec![]; + // Vec of (ident: Ident, type: Ty, name: String), one for each field. + let mut names = HashMap::new(); + let mut fields_info = vec![]; for field in fields { - let ident = match field.ident { - Some(ident) => ident, - None => cx.span_fatal(trait_span, ONLY_STRUCTS_ERR) - }; - + let (ident, name, span) = extract_field_ident_name(cx, field); let stripped_ty = strip_ty_lifetimes(field.ty.clone()); - fields_and_types.push((ident, stripped_ty)); + + if let Some(sp) = names.get(&name).map(|sp| *sp) { + cx.struct_span_err(span, "field with duplicate name") + .span_note(sp, "original was declared here") + .emit(); + } else { + names.insert(name.clone(), span); + } + + fields_info.push((ident, stripped_ty, name)); } - debug!("Fields and types: {:?}", fields_and_types); + debug!("Fields, types, attrs: {:?}", fields_info); let mut stmts = Vec::new(); // The thing to do when we wish to exit with an error. @@ -168,7 +225,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // placed into the final struct. They start out as `None` and are changed // to Some when a parse completes, or some default value if the parse was // unsuccessful and default() returns Some. - for &(ref ident, ref ty) in &fields_and_types { + for &(ref ident, ref ty, _) in &fields_info { stmts.push(quote_stmt!(cx, let mut $ident: ::std::option::Option<$ty> = None; ).unwrap()); @@ -177,17 +234,15 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // Generating an arm for each struct field. This matches against the key and // tries to parse the value according to the type. let mut arms = vec![]; - for &(ref ident, _) in &fields_and_types { - let ident_string = ident.to_string(); - let id_str = ident_string.as_str(); + for &(ref ident, _, ref name) in &fields_info { arms.push(quote_tokens!(cx, - $id_str => { + $name => { let r = ::rocket::http::RawStr::from_str(v); $ident = match ::rocket::request::FromFormValue::from_form_value(r) { Ok(v) => Some(v), Err(e) => { println!(" => Error parsing form val '{}': {:?}", - $id_str, e); + $name, e); $return_err_stmt } }; @@ -219,7 +274,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // that each parameter actually is Some() or has a default value. let mut failure_conditions = vec![]; - for &(ref ident, ref ty) in (&fields_and_types).iter() { + for &(ref ident, ref ty, _) in (&fields_info).iter() { failure_conditions.push(quote_tokens!(cx, if $ident.is_none() && <$ty as ::rocket::request::FromFormValue>::default().is_none() { @@ -232,7 +287,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // The fields of the struct, which are just the let bindings declared above // or the default value. let mut result_fields = vec![]; - for &(ref ident, ref ty) in &fields_and_types { + for &(ref ident, ref ty, _) in &fields_info { result_fields.push(quote_tokens!(cx, $ident: $ident.unwrap_or_else(|| <$ty as ::rocket::request::FromFormValue>::default().unwrap() diff --git a/codegen/src/utils/span_ext.rs b/codegen/src/utils/span_ext.rs index f6ea4631..932e69ba 100644 --- a/codegen/src/utils/span_ext.rs +++ b/codegen/src/utils/span_ext.rs @@ -1,8 +1,6 @@ use syntax::codemap::{Span, BytePos}; pub trait SpanExt { - fn shorten_to(self, to_length: usize) -> Span; - /// Trim the span on the left and right by `length`. fn trim(self, length: u32) -> Span; @@ -11,6 +9,12 @@ pub trait SpanExt { /// Trim the span on the right by `length`. fn trim_right(self, length: usize) -> Span; + + // Trim from the right so that the span is `length` in size. + fn shorten_to(self, to_length: usize) -> Span; + + // Trim from the left so that the span is `length` in size. + fn shorten_upto(self, length: usize) -> Span; } impl SpanExt for Span { @@ -29,6 +33,11 @@ impl SpanExt for Span { self } + fn shorten_upto(mut self, length: usize) -> Span { + self.lo = self.hi - BytePos(length as u32); + self + } + fn trim(mut self, length: u32) -> Span { self.lo = self.lo + BytePos(length); self.hi = self.hi - BytePos(length); diff --git a/codegen/tests/compile-fail/form-field-attr.rs b/codegen/tests/compile-fail/form-field-attr.rs new file mode 100644 index 00000000..5c2352ac --- /dev/null +++ b/codegen/tests/compile-fail/form-field-attr.rs @@ -0,0 +1,83 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen)] + +#[derive(FromForm)] +struct MyForm { + #[form(field = "blah", field = "bloo")] + //~^ ERROR: incorrect use of attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm1 { + #[form] + //~^ ERROR: incorrect use of attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm2 { + #[form("blah")] + //~^ ERROR: invalid `form` attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm3 { + #[form(123)] + //~^ ERROR: invalid `form` attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm4 { + #[form(beep = "bop")] + //~^ ERROR: invalid `form` attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm5 { + #[form(field = "blah")] + #[form(field = "blah")] + my_field: String, + //~^ ERROR: only a single +} + +#[derive(FromForm)] +struct MyForm6 { + #[form(field = true)] + //~^ ERROR: invalid `field` in attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm7 { + #[form(field)] + //~^ ERROR: invalid `field` in attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm8 { + #[form(field = 123)] + //~^ ERROR: invalid `field` in attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm9 { + #[form(field = "hello")] + first: String, + #[form(field = "hello")] + //~^ ERROR: field with duplicate name + other: String, +} + +#[derive(FromForm)] +struct MyForm10 { + first: String, + #[form(field = "first")] + //~^ ERROR: field with duplicate name + other: String, +} diff --git a/codegen/tests/run-pass/form-field-rename.rs b/codegen/tests/run-pass/form-field-rename.rs new file mode 100644 index 00000000..674b6e24 --- /dev/null +++ b/codegen/tests/run-pass/form-field-rename.rs @@ -0,0 +1,54 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +use rocket::request::{FromForm, FromFormValue, FormItems}; +use rocket::http::RawStr; + +#[derive(Debug, PartialEq, FromForm)] +struct Form { + single: usize, + #[form(field = "camelCase")] + camel_case: String, + #[form(field = "TitleCase")] + title_case: String, + #[form(field = "type")] + field_type: isize, + #[form(field = "DOUBLE")] + double: String, +} + +fn parse<'f, T: FromForm<'f>>(string: &'f str) -> Option { + let mut items = FormItems::from(string); + let result = T::from_form_items(items.by_ref()); + if !items.exhaust() { + panic!("Invalid form input."); + } + + result.ok() +} + +fn main() { + let form_string = &[ + "single=100", "camelCase=helloThere", "TitleCase=HiHi", "type=-2", + "DOUBLE=bing_bong" + ].join("&"); + + let form: Option
= parse(&form_string); + assert_eq!(form, Some(Form { + single: 100, + camel_case: "helloThere".into(), + title_case: "HiHi".into(), + field_type: -2, + double: "bing_bong".into() + })); + + let form_string = &[ + "single=100", "camel_case=helloThere", "TitleCase=HiHi", "type=-2", + "DOUBLE=bing_bong" + ].join("&"); + + let form: Option = parse(&form_string); + assert!(form.is_none()); +} diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs index f4f114dc..73776061 100644 --- a/examples/cookies/src/main.rs +++ b/examples/cookies/src/main.rs @@ -1,4 +1,4 @@ -#![feature(plugin, custom_derive, custom_attribute)] +#![feature(plugin, custom_derive)] #![plugin(rocket_codegen)] extern crate rocket_contrib; diff --git a/examples/form_kitchen_sink/src/main.rs b/examples/form_kitchen_sink/src/main.rs index 9563b696..8583578c 100644 --- a/examples/form_kitchen_sink/src/main.rs +++ b/examples/form_kitchen_sink/src/main.rs @@ -33,9 +33,11 @@ impl<'v> FromFormValue<'v> for FormOption { struct FormInput { checkbox: bool, number: usize, + #[form(field = "type")] radio: FormOption, password: String, - textarea: String, + #[form(field = "textarea")] + text_area: String, select: FormOption, } diff --git a/examples/form_kitchen_sink/static/index.html b/examples/form_kitchen_sink/static/index.html index 8749f995..7fc87cf9 100644 --- a/examples/form_kitchen_sink/static/index.html +++ b/examples/form_kitchen_sink/static/index.html @@ -14,15 +14,15 @@

-