Rocket

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit d7f6d82fe4d35ee79c424dd4164733b22f17f80a
parent b0f86dcba0629e1ab402c35e1ac2602348aad0d7
Author: Sergio Benitez <sb@sergio.bz>
Date:   Mon,  6 Aug 2018 19:58:07 -0700

Implement 'FromForm[Value]', 'Responder' proc-macro derives.

This completes the migration of custom derives to proc-macros, removing
the need for the `custom_derive` feature in consumer code. This commit
also includes documentation, unit tests, and compile UI tests for each
of the derives.

Additionally, this commit improves the existing `FromForm` and
`FromFormValue` derives. The generated code for `FromForm` now returns
an error value indicating the error condition. The `FromFormValue`
derive now accepts a `form` attribute on variants for specifying the
exact value string to match against.

Closes #590.
Closes #670.

Diffstat:
Dcore/codegen/src/decorators/derive_form.rs | 329-------------------------------------------------------------------------------
Mcore/codegen/src/decorators/mod.rs | 3---
Mcore/codegen/src/lib.rs | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Dcore/codegen/tests/compile-fail/form-field-attr.rs | 111-------------------------------------------------------------------------------
Mcore/codegen/tests/complete-decorator.rs | 20++++++++++----------
Mcore/codegen/tests/custom-content-type.rs | 2+-
Dcore/codegen/tests/derive_form.rs | 200-------------------------------------------------------------------------------
Dcore/codegen/tests/empty_form.rs | 19-------------------
Dcore/codegen/tests/form-field-rename.rs | 61-------------------------------------------------------------
Mcore/codegen/tests/segments.rs | 2+-
Mcore/codegen/tests/typed-uris.rs | 4++--
Mcore/codegen/tests/ui/typed-uris-bad-params.rs | 2+-
Mcore/codegen/tests/ui/typed-uris-invalid-syntax.rs | 2+-
Mcore/codegen_next/Cargo.toml | 14++++++++++++--
Acore/codegen_next/src/derive/from_form.rs | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/src/derive/from_form_value.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/src/derive/mod.rs | 3+++
Acore/codegen_next/src/derive/responder.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcore/codegen_next/src/ext.rs | 154-------------------------------------------------------------------------------
Acore/codegen_next/src/http_codegen.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/codegen_next/src/lib.rs | 96++++++++++++++++---------------------------------------------------------------
Dcore/codegen_next/src/parser.rs | 126-------------------------------------------------------------------------------
Dcore/codegen_next/src/spanned.rs | 29-----------------------------
Acore/codegen_next/tests/compile-test.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/from_form.rs | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/from_form_value.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/responder.rs | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/ui-fail/from_form.rs | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/ui-fail/from_form.stderr | 306+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/ui-fail/from_form_type_errors.rs | 19+++++++++++++++++++
Acore/codegen_next/tests/ui-fail/from_form_type_errors.stderr | 15+++++++++++++++
Acore/codegen_next/tests/ui-fail/from_form_value.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/ui-fail/from_form_value.stderr | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/ui-fail/responder-types.rs | 37+++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/ui-fail/responder-types.stderr | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/ui-fail/responder.stderr | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/tests/ui-fail/update-references.sh | 48++++++++++++++++++++++++++++++++++++++++++++++++
Acore/lib/src/request/form/error.rs | 21+++++++++++++++++++++
Mcore/lib/src/request/form/form.rs | 30++++++++++++------------------
Mcore/lib/src/request/form/from_form.rs | 8++++----
Mcore/lib/src/request/form/lenient.rs | 12++++++------
Mcore/lib/src/request/form/mod.rs | 4+++-
Mcore/lib/src/request/mod.rs | 2+-
Mcore/lib/tests/flash-lazy-removes-issue-466.rs | 2+-
Mcore/lib/tests/form_method-issue-45.rs | 4++--
Mcore/lib/tests/form_value_decoding-issue-82.rs | 4++--
Mcore/lib/tests/limits.rs | 4++--
Mcore/lib/tests/query-and-non-query-dont-collide.rs | 4++--
Mcore/lib/tests/responder_lifetime-issue-345.rs | 2+-
Mcore/lib/tests/route_guard.rs | 2+-
Mcore/lib/tests/strict_and_lenient_forms.rs | 4++--
Mexamples/cookies/src/main.rs | 4++--
Mexamples/form_kitchen_sink/src/main.rs | 3+--
Mexamples/form_validation/src/main.rs | 4++--
Mexamples/managed_queue/src/main.rs | 4++--
Mexamples/query_params/src/main.rs | 4++--
Mexamples/session/src/main.rs | 4++--
Mexamples/todo/src/main.rs | 4++--
58 files changed, 2176 insertions(+), 1197 deletions(-)

diff --git a/core/codegen/src/decorators/derive_form.rs b/core/codegen/src/decorators/derive_form.rs @@ -1,329 +0,0 @@ -#![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, GenericParamKind}; -use syntax::codemap::Span; -use syntax::ext::build::AstBuilder; -use syntax::ptr::P; - -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, is_valid_ident, SpanExt, GenericParamExt}; - -static ONLY_STRUCTS_ERR: &'static str = "`FromForm` can only be derived for \ - structures with named fields."; -static PRIVATE_LIFETIME: &'static str = "'rocket"; - -fn struct_lifetime(ecx: &mut ExtCtxt, item: &Annotatable, sp: Span) -> Option<String> { - match *item { - Annotatable::Item(ref item) => match item.node { - ItemKind::Struct(_, ref generics) => { - let mut lifetimes = generics.params.iter() - .filter(|p| p.is_lifetime()) - .map(|p| p.ident.to_string()); - - let lifetime = lifetimes.next(); - if lifetimes.next().is_some() { - ecx.span_err(generics.span, "cannot have more than one \ - lifetime parameter when deriving `FromForm`."); - } - - lifetime - }, - _ => ecx.span_fatal(sp, ONLY_STRUCTS_ERR) - }, - _ => ecx.span_fatal(sp, ONLY_STRUCTS_ERR) - } -} - -// 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) -) { - let struct_lifetime = struct_lifetime(ecx, annotated, span); - let (lifetime_var, trait_generics) = match struct_lifetime { - Some(ref lifetime) => (Some(lifetime.as_str()), ty::LifetimeBounds::empty()), - None => (Some(PRIVATE_LIFETIME), ty::LifetimeBounds { - lifetimes: vec![(PRIVATE_LIFETIME, vec![])], - bounds: vec![] - }) - }; - - // The error type in the derived implementation. - let error_type = ty::Ty::Literal(ty::Path::new_(vec!["rocket", "Error"], - None, vec![], ty::PathKind::Global)); - - let trait_def = TraitDef { - is_unsafe: false, - supports_unions: false, - span: span, - // We add these attribute because some `FromFormValue` implementations - // can't fail. This is indicated via the `!` type. Rust checks if a - // match is made with something of that type, and since we always emit - // an `Err` match, we'll get this lint warning. - attributes: vec![quote_attr!(ecx, #[allow(unreachable_code, unreachable_patterns)])], - path: ty::Path::new_( - vec!["rocket", "request", "FromForm"], - lifetime_var, - vec![], - ty::PathKind::Global, - ), - additional_bounds: Vec::new(), - generics: trait_generics, - methods: vec![ - MethodDef { - name: "from_form", - generics: ty::LifetimeBounds::empty(), - explicit_self: None, - args: vec![ - (ty::Ptr( - Box::new(ty::Literal(ty::Path::new_( - vec!["rocket", "request", "FormItems"], - lifetime_var, - vec![], - ty::PathKind::Global, - ))), - ty::Borrowed(None, Mutability::Mutable) - ), "it"), - (ty::Literal(ty::Path::new_local("bool")), "strict"), - ], - ret_ty: ty::Literal(ty::Path::new_( - vec!["result", "Result"], - None, - vec![ - Box::new(ty::Ty::Self_), - Box::new(error_type.clone()) - ], - ty::PathKind::Std, - )), - attributes: vec![], - is_unsafe: false, - combine_substructure: c_s(Box::new(from_form_substructure)), - unify_fieldless_variants: false, - } - ], - associated_types: vec![ - (Ident::from_str("Error"), error_type.clone()) - ], - }; - - trait_def.expand(ecx, meta_item, annotated, push); -} - -fn is_valid_field_name(name: &str) -> bool { - // The HTML5 spec (4.10.18.1) says 'isindex' is not allowed. - if name == "isindex" || name.is_empty() { - return false - } - - // We allow all visible ASCII characters except '&', '=', and '?' since we - // use those as control characters for parsing. - name.chars() - .all(|c| (c >= ' ' && c <= '~') && c != '&' && c != '=' && c != '?') -} - -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); - if !is_valid_field_name(&name) { - ecx.struct_span_err(sp, "invalid form field name") - .help("field names must be visible ASCII without '&', '=', or '?'") - .emit(); - } - - (ident, name, sp) -} - -fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substructure) -> P<Expr> { - // Check that we specified the methods to the argument correctly. - const EXPECTED_ARGS: usize = 2; - let (items_arg, strict_arg) = if substr.nonself_args.len() == EXPECTED_ARGS { - (&substr.nonself_args[0], &substr.nonself_args[1]) - } else { - let msg = format!("incorrect number of arguments in `from_form_string`: \ - expected {}, found {}", EXPECTED_ARGS, substr.nonself_args.len()); - cx.span_bug(trait_span, msg.as_str()); - }; - - debug!("arguments are: {:?}, {:?}", items_arg, strict_arg); - - // Ensure the the fields are from a 'StaticStruct' and extract them. - let fields = match *substr.fields { - StaticStruct(var_data, _) => match *var_data { - VariantData::Struct(ref fields, _) => fields, - _ => cx.span_fatal(trait_span, ONLY_STRUCTS_ERR) - }, - _ => cx.span_bug(trait_span, "impossible substructure in `from_form`") - }; - - // 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, name, span) = extract_field_ident_name(cx, field); - let stripped_ty = strip_ty_lifetimes(field.ty.clone()); - - if let Some(sp) = names.get(&name).cloned() { - 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, types, attrs: {:?}", fields_info); - let mut stmts = Vec::new(); - - // The thing to do when we wish to exit with an error. - let return_err_stmt = quote_tokens!(cx, - return Err(::rocket::Error::BadParse) - ); - - // Generate the let bindings for parameters that will be unwrapped and - // 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_info { - stmts.push(quote_stmt!(cx, - let mut $ident: ::std::option::Option<$ty> = None; - ).unwrap()); - } - - // 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, _, ref name) in &fields_info { - arms.push(quote_tokens!(cx, - $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 '{}': {:?}", - $name, __e); - $return_err_stmt - } - }; - }, - )); - } - - // The actual match statement. Iterate through all of the fields in the form - // and use the $arms generated above. - stmts.push(quote_stmt!(cx, - for (__k, __v) in $items_arg { - match __k.as_str() { - $arms - _ => { - // If we're parsing strictly, emit an error for everything - // the user hasn't asked for. Keep synced with 'preprocess'. - if $strict_arg && __k != "_method" { - println!(" => {}={} has no matching field in struct.", - __k, __v); - $return_err_stmt - } - } - }; - } - ).unwrap()); - - // This looks complicated but just generates the boolean condition checking - // that each parameter actually is Some() or has a default value. - let mut failure_conditions = vec![]; - - 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() { - println!(" => '{}' did not parse.", stringify!($ident)); - $return_err_stmt; - } - )); - } - - // 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_info { - result_fields.push(quote_tokens!(cx, - $ident: $ident.unwrap_or_else(|| - <$ty as ::rocket::request::FromFormValue>::default().unwrap() - ), - )); - } - - // The final block: check the error conditions, and if all is well, return - // the structure. - let self_ident = substr.type_ident; - let final_block = quote_block!(cx, { - $failure_conditions - - Ok($self_ident { $result_fields }) - }); - - stmts.extend(final_block.into_inner().stmts); - - debug!("Form statements:"); - for stmt in &stmts { - debug!("{:?}", stmt_to_string(stmt)); - } - - cx.expr_block(cx.block(trait_span, stmts)) -} diff --git a/core/codegen/src/decorators/mod.rs b/core/codegen/src/decorators/mod.rs @@ -1,8 +1,5 @@ mod route; mod catch; -mod derive_form; pub use self::route::*; pub use self::catch::*; -pub use self::derive_form::*; - diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs @@ -14,6 +14,16 @@ //! here is purely technical. The code generation facilities are documented //! thoroughly in the [Rocket programming guide](https://rocket.rs/guide). //! +//! ## **Table of Contents** +//! +//! 1. [Custom Attributes](#custom-attributes) +//! 2. [Custom Derives](#custom-derives) +//! * [`FromForm`](#fromform) +//! * [`FromFormValue`](#fromformvalue) +//! * [`Responder`](#responder) +//! 3. [Procedural Macros](#procedural-macros) +//! 4. [Debugging Generated Code](#debugging-codegen) +//! //! ## Custom Attributes //! //! This crate implements the following custom attributes: @@ -74,10 +84,15 @@ //! //! ## Custom Derives //! -//! This crate implements the following custom derives: +//! This crate* implements the following custom derives: //! //! * **FromForm** +//! * **FromFormValue** +//! * **Responder** //! +//! <small>* In reality, all of these custom derives are currently implemented +//! by the `rocket_codegen_next` crate. Nonetheless, they are documented +//! here.</small> //! ### `FromForm` //! //! The [`FromForm`] derive can be applied to structures with named fields: @@ -120,6 +135,154 @@ //! [`FromForm`]: /rocket/request/trait.FromForm.html //! [`FromFormValue`]: /rocket/request/trait.FromFormValue.html //! +//! ### `FromFormValue` +//! +//! The [`FromFormValue`] derive can be applied to enums with nullary +//! (zero-length) fields: +//! +//! #[derive(FromFormValue)] +//! enum MyValue { +//! First, +//! Second, +//! Third, +//! } +//! +//! The derive generates an implementation of the [`FromFormValue`] trait for +//! the decorated `enum`. The implementation returns successfully when the form +//! value matches, case insensitively, the stringified version of a variant's +//! name, returning an instance of said variant. +//! +//! As an example, for the `enum` above, the form values `"first"`, `"FIRST"`, +//! `"fiRSt"`, and so on would parse as `MyValue::First`, while `"second"` and +//! `"third"` would parse as `MyValue::Second` and `MyValue::Third`, +//! respectively. +//! +//! The `form` field attribute can be used to change the string that is compared +//! against for a given variant: +//! +//! #[derive(FromFormValue)] +//! enum MyValue { +//! First, +//! Second, +//! #[form(value = "fourth")] +//! Third, +//! } +//! +//! The attribute's grammar is: +//! +//! <pre> +//! form := 'field' '=' STRING_LIT +//! +//! STRING_LIT := any valid string literal, as defined by Rust +//! </pre> +//! +//! The attribute accepts a single string parameter of name `value` +//! corresponding to the string to use to match against for the decorated +//! variant. In the example above, the the strings `"fourth"`, `"FOUrth"` and so +//! on would parse as `MyValue::Third`. +//! +//! ## `Responder` +//! +//! The [`Responder`] derive can be applied to enums and named structs. When +//! applied to enums, variants must have at least one field. When applied to +//! structs, the struct must have at least one field. +//! +//! #[derive(Responder)] +//! enum MyResponder { +//! A(String), +//! B(OtherResponse, ContentType), +//! } +//! +//! #[derive(Responder)] +//! struct MyResponder { +//! inner: OtherResponder, +//! header: ContentType, +//! } +//! +//! The derive generates an implementation of the [`Responder`] trait for the +//! decorated enum or structure. The derive uses the _first_ field of a variant +//! or structure to generate a `Response`. As such, the type of the first field +//! must implement [`Responder`]. The remaining fields of a variant or structure +//! are set as headers in the produced [`Response`] using +//! [`Response::set_header()`]. As such, every other field (unless explicitly +//! ignored, explained next) must implement `Into<Header>`. +//! +//! Except for the first field, fields decorated with `#[response(ignore)]` are +//! ignored by the derive: +//! +//! #[derive(Responder)] +//! enum MyResponder { +//! A(String), +//! B(OtherResponse, ContentType, #[response(ignore)] Other), +//! } +//! +//! #[derive(Responder)] +//! struct MyResponder { +//! inner: InnerResponder, +//! header: ContentType, +//! #[response(ignore)] +//! other: Other, +//! } +//! +//! Decorating the first field with `#[response(ignore)]` has no effect. +//! +//! Additionally, the `response` attribute can be used on named structures and +//! enum variants to override the status and/or content-type of the [`Response`] +//! produced by the generated implementation. The `response` attribute used in +//! these positions has the following grammar: +//! +//! <pre> +//! response := parameter (',' parameter)? +//! +//! parameter := 'status' '=' STATUS +//! | 'content_type' '=' CONTENT_TYPE +//! +//! STATUS := unsigned integer >= 100 and < 600 +//! CONTENT_TYPE := string literal, as defined by Rust, identifying a valid +//! Content-Type, as defined by Rocket +//! </pre> +//! +//! It can be used as follows: +//! +//! #[derive(Responder)] +//! enum Error { +//! #[response(status = 500, content_type = "json")] +//! A(String), +//! #[response(status = 404)] +//! B(OtherResponse, ContentType), +//! } +//! +//! #[derive(Responder)] +//! #[response(status = 400)] +//! struct MyResponder { +//! inner: InnerResponder, +//! header: ContentType, +//! #[response(ignore)] +//! other: Other, +//! } +//! +//! The attribute accepts two key/value pairs: `status` and `content_type`. The +//! value of `status` must be an unsigned integer representing a valid status +//! code. The [`Response`] produced from the generated implementation will have +//! its status overriden to this value. +//! +//! The value of `content_type` must be a valid media-type in `top/sub` form or +//! `shorthand` form. Examples include: +//! +//! * `"text/html"` +//! * `"application/x-custom"` +//! * `"html"` +//! * `"json"` +//! * `"plain"` +//! * `"binary"` +//! +//! The [`Response`] produced from the generated implementation will have its +//! content-type overriden to this value. +//! +//! [`Responder`]: /rocket/response/trait.Responder.html +//! [`Response`]: /rocket/struct.Response.html +//! [`Response::set_header()`]: /rocket/struct.Response.html#method.set_header +//! //! ## Procedural Macros //! //! This crate implements the following procedural macros: @@ -265,14 +428,6 @@ macro_rules! register_decorators { ) } -macro_rules! register_derives { - ($registry:expr, $($name:expr => $func:ident),+) => ( - $($registry.register_custom_derive(Symbol::intern($name), - SyntaxExtension::MultiDecorator(Box::new(decorators::$func))); - )+ - ) -} - macro_rules! register_macros { ($reg:expr, $($n:expr => $f:ident),+) => ( $($reg.register_macro($n, macros::$f);)+ @@ -289,10 +444,6 @@ pub fn plugin_registrar(reg: &mut Registry) { "rocket_internal_uri" => uri_internal ); - register_derives!(reg, - "derive_FromForm" => from_form_derive - ); - register_decorators!(reg, "catch" => catch_decorator, "route" => route_decorator, diff --git a/core/codegen/tests/compile-fail/form-field-attr.rs b/core/codegen/tests/compile-fail/form-field-attr.rs @@ -1,111 +0,0 @@ -#![feature(plugin, decl_macro, 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, -} - -#[derive(FromForm)] -struct MyForm11 { - #[form(field = "hello&world")] - //~^ ERROR: invalid form field - first: String, -} - -#[derive(FromForm)] -struct MyForm12 { - #[form(field = "!@#$%^&*()_")] - //~^ ERROR: invalid form field - first: String, -} - -#[derive(FromForm)] -struct MyForm13 { - #[form(field = "?")] - //~^ ERROR: invalid form field - first: String, -} - -#[derive(FromForm)] -struct MyForm14 { - #[form(field = "")] - //~^ ERROR: invalid form field - first: String, -} diff --git a/core/codegen/tests/complete-decorator.rs b/core/codegen/tests/complete-decorator.rs @@ -1,8 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -#![allow(dead_code, unused_variables)] -extern crate rocket; +#[macro_use] extern crate rocket; use rocket::http::{Cookies, RawStr}; use rocket::request::Form; @@ -13,13 +12,14 @@ struct User<'a> { nickname: String, } -#[post("/<name>?<_query>", format = "application/json", data = "<user>", rank = 2)] -fn get<'r>(name: &RawStr, - _query: User<'r>, - user: Form<'r, User<'r>>, - cookies: Cookies) - -> &'static str { - "hi" +#[post("/<_name>?<_query>", format = "application/json", data = "<user>", rank = 2)] +fn get<'r>( + _name: &RawStr, + _query: User<'r>, + user: Form<'r, User<'r>>, + _cookies: Cookies +) -> String { + format!("{}:{}", user.get().name, user.get().nickname) } #[test] diff --git a/core/codegen/tests/custom-content-type.rs b/core/codegen/tests/custom-content-type.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] extern crate rocket; diff --git a/core/codegen/tests/derive_form.rs b/core/codegen/tests/derive_form.rs @@ -1,200 +0,0 @@ -#![feature(plugin, decl_macro, custom_derive)] -#![plugin(rocket_codegen)] - -extern crate rocket; - -use rocket::request::{FromForm, FromFormValue, FormItems}; -use rocket::http::RawStr; - -#[derive(Debug, PartialEq, FromForm)] -struct TodoTask { - description: String, - completed: bool -} - -// TODO: Make deriving `FromForm` for this enum possible. -#[derive(Debug, PartialEq)] -enum FormOption { - A, B, C -} - -impl<'v> FromFormValue<'v> for FormOption { - type Error = &'v str; - - fn from_form_value(v: &'v RawStr) -> Result<Self, Self::Error> { - let variant = match v.as_str() { - "a" => FormOption::A, - "b" => FormOption::B, - "c" => FormOption::C, - _ => return Err(v) - }; - - Ok(variant) - } -} - -#[derive(Debug, PartialEq, FromForm)] -struct FormInput<'r> { - checkbox: bool, - number: usize, - radio: FormOption, - password: &'r RawStr, - textarea: String, - select: FormOption, -} - -#[derive(Debug, PartialEq, FromForm)] -struct DefaultInput<'r> { - arg: Option<&'r RawStr>, -} - -#[derive(Debug, PartialEq, FromForm)] -struct ManualMethod<'r> { - _method: Option<&'r RawStr>, - done: bool -} - -#[derive(Debug, PartialEq, FromForm)] -struct UnpresentCheckbox { - checkbox: bool -} - -#[derive(Debug, PartialEq, FromForm)] -struct UnpresentCheckboxTwo<'r> { - checkbox: bool, - something: &'r RawStr -} - -#[derive(Debug, PartialEq, FromForm)] -struct FieldNamedV<'r> { - v: &'r RawStr, -} - -fn parse<'f, T: FromForm<'f>>(string: &'f str, strict: bool) -> Option<T> { - let mut items = FormItems::from(string); - let result = T::from_form(items.by_ref(), strict); - if !items.exhaust() { - panic!("Invalid form input."); - } - - result.ok() -} - -fn strict<'f, T: FromForm<'f>>(string: &'f str) -> Option<T> { - parse(string, true) -} - -fn lenient<'f, T: FromForm<'f>>(string: &'f str) -> Option<T> { - parse(string, false) -} - -#[test] -fn main() { - // Same number of arguments: simple case. - let task: Option<TodoTask> = strict("description=Hello&completed=on"); - assert_eq!(task, Some(TodoTask { - description: "Hello".to_string(), - completed: true - })); - - // Argument in string but not in form. - let task: Option<TodoTask> = strict("other=a&description=Hello&completed=on"); - assert!(task.is_none()); - - // Ensure _method isn't required. - let task: Option<TodoTask> = strict("_method=patch&description=Hello&completed=off"); - assert_eq!(task, Some(TodoTask { - description: "Hello".to_string(), - completed: false - })); - - let form_string = &[ - "password=testing", "checkbox=off", "checkbox=on", "number=10", - "checkbox=off", "textarea=", "select=a", "radio=c", - ].join("&"); - - let input: Option<FormInput> = strict(&form_string); - assert_eq!(input, Some(FormInput { - checkbox: false, - number: 10, - radio: FormOption::C, - password: "testing".into(), - textarea: "".to_string(), - select: FormOption::A, - })); - - // Argument not in string with default in form. - let default: Option<DefaultInput> = strict(""); - assert_eq!(default, Some(DefaultInput { - arg: None - })); - - // Ensure _method can be captured if desired. - let manual: Option<ManualMethod> = strict("_method=put&done=true"); - assert_eq!(manual, Some(ManualMethod { - _method: Some("put".into()), - done: true - })); - - let manual: Option<ManualMethod> = lenient("_method=put&done=true"); - assert_eq!(manual, Some(ManualMethod { - _method: Some("put".into()), - done: true - })); - - // And ignored when not present. - let manual: Option<ManualMethod> = strict("done=true"); - assert_eq!(manual, Some(ManualMethod { - _method: None, - done: true - })); - - // Check that a `bool` value that isn't in the form is marked as `false`. - let manual: Option<UnpresentCheckbox> = strict(""); - assert_eq!(manual, Some(UnpresentCheckbox { - checkbox: false - })); - - // Check that a `bool` value that isn't in the form is marked as `false`. - let manual: Option<UnpresentCheckboxTwo> = strict("something=hello"); - assert_eq!(manual, Some(UnpresentCheckboxTwo { - checkbox: false, - something: "hello".into() - })); - - // Check that a structure with one field `v` parses correctly. - let manual: Option<FieldNamedV> = strict("v=abc"); - assert_eq!(manual, Some(FieldNamedV { - v: "abc".into() - })); - - // Check that a structure with one field `v` parses correctly (lenient). - let manual: Option<FieldNamedV> = lenient("v=abc"); - assert_eq!(manual, Some(FieldNamedV { v: "abc".into() })); - - let manual: Option<FieldNamedV> = lenient("v=abc&a=123"); - assert_eq!(manual, Some(FieldNamedV { v: "abc".into() })); - - let manual: Option<FieldNamedV> = lenient("c=abcddef&v=abc&a=123"); - assert_eq!(manual, Some(FieldNamedV { v: "abc".into() })); - - // Check default values (bool) with lenient parsing. - let manual: Option<UnpresentCheckboxTwo> = lenient("something=hello"); - assert_eq!(manual, Some(UnpresentCheckboxTwo { - checkbox: false, - something: "hello".into() - })); - - let manual: Option<UnpresentCheckboxTwo> = lenient("hi=hi&something=hello"); - assert_eq!(manual, Some(UnpresentCheckboxTwo { - checkbox: false, - something: "hello".into() - })); - - // Check that a missing field doesn't parse, even leniently. - let manual: Option<FieldNamedV> = lenient("a=abc"); - assert!(manual.is_none()); - - let manual: Option<FieldNamedV> = lenient("_method=abc"); - assert!(manual.is_none()); -} diff --git a/core/codegen/tests/empty_form.rs b/core/codegen/tests/empty_form.rs @@ -1,19 +0,0 @@ -#![feature(plugin, decl_macro, custom_derive)] -#![plugin(rocket_codegen)] - -extern crate rocket; - -use rocket::request::{FromForm, FormItems}; - -#[derive(PartialEq, Debug, FromForm)] -struct Form { } - -#[test] -fn main() { - // Same number of arguments: simple case. - let task = Form::from_form(&mut FormItems::from(""), true); - assert_eq!(task, Ok(Form { })); - - let task = Form::from_form(&mut FormItems::from(""), false); - assert_eq!(task, Ok(Form { })); -} diff --git a/core/codegen/tests/form-field-rename.rs b/core/codegen/tests/form-field-rename.rs @@ -1,61 +0,0 @@ -#![feature(plugin, decl_macro, custom_derive)] -#![plugin(rocket_codegen)] - -extern crate rocket; - -use rocket::request::{FromForm, FormItems}; - -#[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, - #[form(field = "a.b")] - dot: isize, -} - -fn parse<'f, T: FromForm<'f>>(string: &'f str, strict: bool) -> Option<T> { - let mut items = FormItems::from(string); - let result = T::from_form(items.by_ref(), strict); - if !items.exhaust() { - panic!("Invalid form input."); - } - - result.ok() -} - -fn parse_strict<'f, T: FromForm<'f>>(string: &'f str) -> Option<T> { - parse(string, true) -} - -#[test] -fn main() { - let form_string = &[ - "single=100", "camelCase=helloThere", "TitleCase=HiHi", "type=-2", - "DOUBLE=bing_bong", "a.b=123", - ].join("&"); - - let form: Option<Form> = parse_strict(&form_string); - assert_eq!(form, Some(Form { - single: 100, - camel_case: "helloThere".into(), - title_case: "HiHi".into(), - field_type: -2, - double: "bing_bong".into(), - dot: 123, - })); - - let form_string = &[ - "single=100", "camel_case=helloThere", "TitleCase=HiHi", "type=-2", - "DOUBLE=bing_bong", "dot=123", - ].join("&"); - - let form: Option<Form> = parse_strict(&form_string); - assert!(form.is_none()); -} diff --git a/core/codegen/tests/segments.rs b/core/codegen/tests/segments.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] extern crate rocket; diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs @@ -1,8 +1,8 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] #![allow(dead_code, unused_variables)] -extern crate rocket; +#[macro_use] extern crate rocket; use std::fmt; use std::path::PathBuf; diff --git a/core/codegen/tests/ui/typed-uris-bad-params.rs b/core/codegen/tests/ui/typed-uris-bad-params.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] #![allow(dead_code, unused_variables)] diff --git a/core/codegen/tests/ui/typed-uris-invalid-syntax.rs b/core/codegen/tests/ui/typed-uris-invalid-syntax.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] #![allow(dead_code, unused_variables)] diff --git a/core/codegen_next/Cargo.toml b/core/codegen_next/Cargo.toml @@ -18,5 +18,15 @@ proc-macro = true [dependencies] quote = "0.6.1" -proc-macro2 = { version = "0.4.3", features = ["nightly"] } -syn = { version = "0.14.0", features = ["full", "extra-traits"] } +rocket_http = { version = "0.4.0-dev", path = "../http/" } + +[dependencies.derive_utils] +git = "https://github.com/SergioBenitez/derive-utils" +rev = "160da392" + +[dev-dependencies] +rocket = { version = "0.4.0-dev", path = "../lib" } + +[dev-dependencies.compiletest_rs] +git = "https://github.com/SergioBenitez/compiletest-rs" +branch = "regex-support" diff --git a/core/codegen_next/src/derive/from_form.rs b/core/codegen_next/src/derive/from_form.rs @@ -0,0 +1,129 @@ +use proc_macro::{Span, TokenStream}; +use derive_utils::{*, syn, ext::{TypeExt, Split3}}; + +#[derive(FromMeta)] +struct Form { + field: FormField, +} + +struct FormField { + span: Span, + name: String +} + +fn is_valid_field_name(s: &str) -> bool { + // The HTML5 spec (4.10.18.1) says 'isindex' is not allowed. + if s == "isindex" || s.is_empty() { + return false + } + + // We allow all visible ASCII characters except '&', '=', and '?' since we + // use those as control characters for parsing. + s.chars().all(|c| (c >= ' ' && c <= '~') && c != '&' && c != '=' && c != '?') +} + +impl FromMeta for FormField { + fn from_meta(meta: &syn::Meta) -> Result<Self> { + let string = <SpanWrapped<String>>::from_meta(meta)?; + if !is_valid_field_name(&string.value) { + return Err(string.value_span.error("invalid form field name")); + } + + Ok(FormField { span: string.value_span, name: string.value }) + } +} + +fn validate_struct(gen: &DeriveGenerator, data: Struct) -> Result<()> { + if data.fields().is_empty() { + return Err(gen.input.span().error("at least one field is required")); + } + + let mut names = ::std::collections::HashMap::new(); + for field in data.fields().iter() { + let id = field.ident.as_ref().expect("named field"); + let field = match Form::from_attrs("form", &field.attrs) { + Some(result) => result?.field, + None => FormField { span: Spanned::span(&id), name: id.to_string() } + }; + + if let Some(span) = names.get(&field.name) { + return Err(field.span.error("duplicate field name") + .span_note(*span, "previous definition here")); + } + + names.insert(field.name, field.span); + } + + Ok(()) +} + +pub fn derive_from_form(input: TokenStream) -> TokenStream { + let form_error = quote!(::rocket::request::FormError); + DeriveGenerator::build_for(input, "::rocket::request::FromForm<'__f>") + .generic_support(GenericSupport::Lifetime | GenericSupport::Type) + .replace_generic(0, 0) + .data_support(DataSupport::NamedStruct) + .map_type_generic(|_, ident, _| quote! { + #ident : ::rocket::request::FromFormValue<'__f> + }) + .validate_generics(|_, generics| match generics.lifetimes().count() > 1 { + true => Err(generics.span().error("only one lifetime is supported")), + false => Ok(()) + }) + .validate_struct(validate_struct) + .function(|_, inner| quote! { + type Error = ::rocket::request::FormError<'__f>; + + fn from_form( + __items: &mut ::rocket::request::FormItems<'__f>, + __strict: bool, + ) -> ::std::result::Result<Self, Self::Error> { + #inner + } + }) + .try_map_fields(move |_, fields| { + let (constructors, matchers, builders) = fields.iter().map(|field| { + let (ident, span) = (&field.ident, field.span().into()); + let default_name = ident.as_ref().expect("named").to_string(); + let name = Form::from_attrs("form", &field.attrs) + .map(|result| result.map(|form| form.field.name)) + .unwrap_or_else(|| Ok(default_name))?; + + let ty = field.ty.with_stripped_lifetimes(); + let ty = quote_spanned! { + span => <#ty as ::rocket::request::FromFormValue> + }; + + let constructor = quote_spanned!(span => let mut #ident = None;); + + let matcher = quote_spanned! { span => + #name => { #ident = Some(#ty::from_form_value(__v) + .map_err(|_| #form_error::BadValue(__k, __v))?); }, + }; + + let builder = quote_spanned! { span => + #ident: #ident.or_else(#ty::default) + .ok_or_else(|| #form_error::Missing(#name.into()))?, + }; + + Ok((constructor, matcher, builder)) + }).collect::<Result<Vec<_>>>()?.into_iter().split3(); + + Ok(quote! { + #(#constructors)* + + for (__k, __v) in __items { + match __k.as_str() { + #(#matchers)* + _ if __strict && __k != "_method" => { + return Err(#form_error::Unknown(__k, __v)); + } + _ => { /* lenient or "method"; let it pass */ } + } + } + + Ok(Self { #(#builders)* }) + }) + }) + .to_tokens() +} diff --git a/core/codegen_next/src/derive/from_form_value.rs b/core/codegen_next/src/derive/from_form_value.rs @@ -0,0 +1,53 @@ +use derive_utils::*; +use proc_macro::TokenStream; + +#[derive(FromMeta)] +struct Form { + value: String, +} + +pub fn derive_from_form_value(input: TokenStream) -> TokenStream { + DeriveGenerator::build_for(input, "::rocket::request::FromFormValue<'__v>") + .generic_support(GenericSupport::None) + .data_support(DataSupport::Enum) + .validate_enum(|generator, data| { + // This derive only works for variants that are nullary. + for variant in data.variants() { + if !variant.fields().is_empty() { + return Err(variant.span().error("variants cannot have fields")); + } + } + + // Emit a warning if the enum is empty. + if data.variants.is_empty() { + generator.input.span().warning("deriving for empty enum").emit(); + } + + Ok(()) + }) + .function(|_, inner| quote! { + type Error = &'__v ::rocket::http::RawStr; + + fn from_form_value( + value: &'__v ::rocket::http::RawStr + ) -> ::std::result::Result<Self, Self::Error> { + let uncased = value.as_uncased_str(); + #inner + ::std::result::Result::Err(value) + } + }) + .try_map_enum(null_enum_mapper) + .try_map_variant(|_, variant| { + let variant_str = Form::from_attrs("form", &variant.attrs) + .unwrap_or_else(|| Ok(Form { value: variant.ident.to_string() }))? + .value; + + let builder = variant.builder(|_| unreachable!()); + Ok(quote! { + if uncased == #variant_str { + return ::std::result::Result::Ok(#builder); + } + }) + }) + .to_tokens() +} diff --git a/core/codegen_next/src/derive/mod.rs b/core/codegen_next/src/derive/mod.rs @@ -0,0 +1,3 @@ +pub mod from_form; +pub mod from_form_value; +pub mod responder; diff --git a/core/codegen_next/src/derive/responder.rs b/core/codegen_next/src/derive/responder.rs @@ -0,0 +1,81 @@ +use quote::ToTokens; +use proc_macro::TokenStream; +use derive_utils::{*, ext::TypeExt}; +use derive_utils::proc_macro2::TokenStream as TokenStream2; + +use http_codegen::{ContentType, Status}; + +#[derive(Default, FromMeta)] +struct ItemAttr { + content_type: Option<SpanWrapped<ContentType>>, + status: Option<SpanWrapped<Status>>, +} + +#[derive(Default, FromMeta)] +struct FieldAttr { + ignore: bool, +} + +pub fn derive_responder(input: TokenStream) -> TokenStream { + DeriveGenerator::build_for(input, "::rocket::response::Responder<'__r>") + .generic_support(GenericSupport::Lifetime) + .data_support(DataSupport::Struct | DataSupport::Enum) + .replace_generic(0, 0) + .validate_generics(|_, generics| match generics.lifetimes().count() > 1 { + true => Err(generics.span().error("only one lifetime is supported")), + false => Ok(()) + }) + .validate_fields(|_, fields| match fields.is_empty() { + true => return Err(fields.span().error("need at least one field")), + false => Ok(()) + }) + .function(|_, inner| quote! { + fn respond_to( + self, + __req: &::rocket::Request + ) -> ::rocket::response::Result<'__r> { + #inner + } + }) + .try_map_fields(|_, fields| { + fn set_header_tokens<T: ToTokens + Spanned>(item: T) -> TokenStream2 { + quote_spanned!(item.span().into() => __res.set_header(#item);) + } + + let attr = ItemAttr::from_attrs("response", fields.parent_attrs()) + .unwrap_or_else(|| Ok(Default::default()))?; + + let responder = fields.iter().next().map(|f| { + let (accessor, ty) = (f.accessor(), f.ty.with_stripped_lifetimes()); + quote_spanned! { f.span().into() => + let mut __res = <#ty as ::rocket::response::Responder>::respond_to( + #accessor, __req + )?; + } + }).expect("have at least one field"); + + let mut headers = vec![]; + for field in fields.iter().skip(1) { + let attr = FieldAttr::from_attrs("response", &field.attrs) + .unwrap_or_else(|| Ok(Default::default()))?; + + if !attr.ignore { + headers.push(set_header_tokens(field.accessor())); + } + } + + let content_type = attr.content_type.map(set_header_tokens); + let status = attr.status.map(|status| { + quote_spanned!(status.span().into() => __res.set_status(#status);) + }); + + Ok(quote! { + #responder + #(#headers)* + #content_type + #status + Ok(__res) + }) + }) + .to_tokens() +} diff --git a/core/codegen_next/src/ext.rs b/core/codegen_next/src/ext.rs @@ -1,154 +0,0 @@ -use syn::*; - -pub trait MemberExt { - fn named(&self) -> Option<&Ident>; - fn unnamed(&self) -> Option<&Index>; -} - -impl MemberExt for Member { - fn named(&self) -> Option<&Ident> { - match *self { - Member::Named(ref named) => Some(named), - _ => None - } - } - - fn unnamed(&self) -> Option<&Index> { - match *self { - Member::Unnamed(ref unnamed) => Some(unnamed), - _ => None - } - } -} - -pub trait FieldsExt { - fn len(&self) -> usize; - fn is_empty(&self) -> bool; - fn named(&self) -> Option<&FieldsNamed>; - fn is_named(&self) -> bool; - fn unnamed(&self) -> Option<&FieldsUnnamed>; - fn is_unnamed(&self) -> bool; - fn is_unit(&self) -> bool; - fn nth(&self, i: usize) -> Option<&Field>; - fn find_member(&self, member: &Member) -> Option<&Field>; -} - -impl FieldsExt for Fields { - fn len(&self) -> usize { - match *self { - Fields::Named(ref fields) => fields.named.len(), - Fields::Unnamed(ref fields) => fields.unnamed.len(), - Fields::Unit => 0 - } - } - - fn is_empty(&self) -> bool { - self.len() == 0 - } - - fn named(&self) -> Option<&FieldsNamed> { - match *self { - Fields::Named(ref named) => Some(named), - _ => None - } - } - - fn is_named(&self) -> bool { - self.named().is_some() - } - - fn unnamed(&self) -> Option<&FieldsUnnamed> { - match *self { - Fields::Unnamed(ref unnamed) => Some(unnamed), - _ => None - } - } - - fn is_unnamed(&self) -> bool { - self.unnamed().is_some() - } - - fn is_unit(&self) -> bool { - match *self { - Fields::Unit => true, - _ => false - } - } - - fn nth(&self, i: usize) -> Option<&Field> { - match *self { - Fields::Named(ref fields) => fields.named.iter().nth(i), - Fields::Unnamed(ref fields) => fields.unnamed.iter().nth(i), - Fields::Unit => None - } - } - - fn find_member(&self, member: &Member) -> Option<&Field> { - if let (Some(fields), Some(ident)) = (self.named(), member.named()) { - fields.named.iter().find(|f| f.ident.as_ref().unwrap() == ident) - } else if let (Some(fields), Some(member)) = (self.unnamed(), member.unnamed()) { - fields.unnamed.iter().nth(member.index as usize) - } else { - None - } - } -} - -pub trait PathExt { - fn is(&self, global: bool, segments: &[&str]) -> bool; - fn is_local(&self, segments: &[&str]) -> bool; - fn is_global(&self, segments: &[&str]) -> bool; -} - -impl PathExt for Path { - fn is(&self, global: bool, segments: &[&str]) -> bool { - if self.global() != global || self.segments.len() != segments.len() { - return false; - } - - for (segment, wanted) in self.segments.iter().zip(segments.iter()) { - if segment.ident != wanted { - return false; - } - } - - true - } - - fn is_local(&self, segments: &[&str]) -> bool { - self.is(false, segments) - } - - fn is_global(&self, segments: &[&str]) -> bool { - self.is(true, segments) - } -} - -pub trait DataExt { - fn into_enum(self) -> Option<DataEnum>; - fn into_struct(self) -> Option<DataStruct>; - fn into_union(self) -> Option<DataUnion>; -} - -impl DataExt for Data { - fn into_enum(self) -> Option<DataEnum> { - match self { - Data::Enum(e) => Some(e), - _ => None - } - } - - fn into_struct(self) -> Option<DataStruct> { - match self { - Data::Struct(s) => Some(s), - _ => None - } - } - - fn into_union(self) -> Option<DataUnion> { - match self { - Data::Union(u) => Some(u), - _ => None - } - } -} diff --git a/core/codegen_next/src/http_codegen.rs b/core/codegen_next/src/http_codegen.rs @@ -0,0 +1,72 @@ +use syn; +use quote::ToTokens; +use proc_macro2::TokenStream as TokenStream2; +use derive_utils::{SpanWrapped, FromMeta, Result, ext::Split2}; +use rocket_http as http; + +pub struct ContentType(http::ContentType); + +pub struct Status(http::Status); + +struct MediaType(http::MediaType); + +impl FromMeta for Status { + fn from_meta(meta: &syn::Meta) -> Result<Self> { + let num = <SpanWrapped<usize>>::from_meta(meta)?; + if num.value < 100 || num.value >= 600 { + return Err(num.value_span.error("status must be in range [100, 600)")); + } + + Ok(Status(http::Status::raw(num.value as u16))) + } +} + +impl ToTokens for Status { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let (code, reason) = (self.0.code, self.0.reason); + tokens.extend(quote!(rocket::http::Status::new(#code, #reason))); + } +} + +impl FromMeta for ContentType { + fn from_meta(meta: &syn::Meta) -> Result<Self> { + let s = <SpanWrapped<String>>::from_meta(meta)?; + let parsed = http::ContentType::parse_flexible(&s.value) + .ok_or_else(|| s.value_span.error("invalid or unknown content-type"))?; + + Ok(ContentType(parsed)) + } +} + +impl ToTokens for ContentType { + fn to_tokens(&self, tokens: &mut TokenStream2) { + // Yeah, yeah. (((((i))).kn0w())) + let media_type = MediaType((self.0).clone().0); + tokens.extend(quote!(::rocket::http::ContentType(#media_type))); + } +} + +impl ToTokens for MediaType { + fn to_tokens(&self, tokens: &mut TokenStream2) { + use std::iter::repeat; + let (top, sub) = (self.0.top().as_str(), self.0.sub().as_str()); + let (keys, values) = self.0.params().split2(); + + let (http, cow) = (quote!(::rocket::http), quote!(::std::borrow::Cow)); + let (http_, http__) = (repeat(&http), repeat(&http)); + let (cow_, cow__) = (repeat(&cow), repeat(&cow)); + + // TODO: Produce less code when possible (for known media types). + tokens.extend(quote!(#http::MediaType { + source: #http::Source::None, + top: #http::Indexed::Concrete(#cow::Borrowed(#top)), + sub: #http::Indexed::Concrete(#cow::Borrowed(#sub)), + params: #http::MediaParams::Static(&[ + #(( + #http_::Indexed::Concrete(#cow_::Borrowed(#keys)), + #http__::Indexed::Concrete(#cow__::Borrowed(#values)) + )),* + ]) + })) + } +} diff --git a/core/codegen_next/src/lib.rs b/core/codegen_next/src/lib.rs @@ -1,88 +1,30 @@ -#![feature(core_intrinsics, decl_macro)] -#![feature(proc_macro_diagnostic, proc_macro_span)] -#![recursion_limit="256"] +#![feature(proc_macro_diagnostic)] +#![feature(crate_visibility_modifier)] +#![recursion_limit="128"] -extern crate syn; -extern crate proc_macro; -extern crate proc_macro2; #[macro_use] extern crate quote; +#[macro_use] extern crate derive_utils; +extern crate proc_macro; +extern crate rocket_http; -mod parser; -mod spanned; -mod ext; - -use parser::Result as PResult; -use proc_macro::{Span, TokenStream}; -use spanned::Spanned; - -use ext::*; -use syn::*; - -const NO_FIELDS_ERR: &str = "variants in `FromFormValue` derives cannot have fields"; -const NO_GENERICS: &str = "enums with generics cannot derive `FromFormValue`"; -const ONLY_ENUMS: &str = "`FromFormValue` can only be derived for enums"; -const EMPTY_ENUM_WARN: &str = "deriving `FromFormValue` for empty enum"; - -fn validate_input(input: DeriveInput) -> PResult<DataEnum> { - // This derive doesn't support generics. Error out if there are generics. - if !input.generics.params.is_empty() { - return Err(input.generics.span().error(NO_GENERICS)); - } - - // This derive only works for enums. Error out if the input is not an enum. - let input_span = input.span(); - let data = input.data.into_enum().ok_or_else(|| input_span.error(ONLY_ENUMS))?; +mod derive; +mod http_codegen; - // This derive only works for variants that are nullary. - for variant in data.variants.iter() { - if !variant.fields.is_empty() { - return Err(variant.span().error(NO_FIELDS_ERR)); - } - } +crate use derive_utils::{syn, proc_macro2}; - // Emit a warning if the enum is empty. - if data.variants.is_empty() { - Span::call_site().warning(EMPTY_ENUM_WARN).emit(); - } +use proc_macro::TokenStream; - Ok(data) +#[proc_macro_derive(FromFormValue, attributes(form))] +pub fn derive_from_form_value(input: TokenStream) -> TokenStream { + derive::from_form_value::derive_from_form_value(input) } -fn real_derive_from_form_value(input: TokenStream) -> PResult<TokenStream> { - // Parse the input `TokenStream` as a `syn::DeriveInput`, an AST. - let input: DeriveInput = syn::parse(input).map_err(|e| { - Span::call_site().error(format!("error: failed to parse input: {:?}", e)) - })?; - - // Validate the enum. - let name = input.ident.clone(); - let enum_data = validate_input(input)?; - - // Create iterators over the identifers as idents and as strings. - let variant_strs = enum_data.variants.iter().map(|v| v.ident.to_string()); - let variant_idents = enum_data.variants.iter().map(|v| &v.ident); - let names = ::std::iter::repeat(&name); - - // Generate the implementation. - Ok(quote! { - impl<'v> ::rocket::request::FromFormValue<'v> for #name { - type Error = &'v ::rocket::http::RawStr; - - fn from_form_value(v: &'v ::rocket::http::RawStr) -> ::std::result::Result<Self, Self::Error> { - #(if v.as_uncased_str() == #variant_strs { - return ::std::result::Result::Ok(#names::#variant_idents); - })* - - ::std::result::Result::Err(v) - } - } - }.into()) +#[proc_macro_derive(FromForm, attributes(form))] +pub fn derive_from_form(input: TokenStream) -> TokenStream { + derive::from_form::derive_from_form(input) } -#[proc_macro_derive(FromFormValue)] -pub fn derive_from_form_value(input: TokenStream) -> TokenStream { - real_derive_from_form_value(input).unwrap_or_else(|diag| { - diag.emit(); - TokenStream::new() - }) +#[proc_macro_derive(Responder, attributes(response))] +pub fn derive_responder(input: TokenStream) -> TokenStream { + derive::responder::derive_responder(input) } diff --git a/core/codegen_next/src/parser.rs b/core/codegen_next/src/parser.rs @@ -1,126 +0,0 @@ -#![allow(dead_code)] - -use syn::token; -use syn::synom::Synom; -use syn::buffer::{Cursor, TokenBuffer}; - -use proc_macro::{TokenStream, Span, Diagnostic}; - -pub use proc_macro2::Delimiter; - -pub type Result<T> = ::std::result::Result<T, Diagnostic>; - -#[derive(Copy, Clone)] -pub enum Seperator { - Comma, - Pipe, - Semi, -} - -pub struct Parser { - buffer: Box<TokenBuffer>, - cursor: Cursor<'static>, -} - -impl Parser { - pub fn new(tokens: TokenStream) -> Parser { - let buffer = Box::new(TokenBuffer::new(tokens)); - // Our `Parser` is self-referential. We cast a pointer to the heap - // allocation as `&'static` to allow the storage of the reference - // along-side the allocation. This is safe as long as `buffer` is never - // dropped while `self` lives, `buffer` is never mutated, and an - // instance or reference to `cursor` is never allowed to escape. These - // properties can be confirmed with a cursory look over the method - // signatures and implementations of `Parser`. - let cursor = unsafe { - let buffer: &'static TokenBuffer = ::std::mem::transmute(&*buffer); - buffer.begin() - }; - - Parser { buffer, cursor } - } - - pub fn current_span(&self) -> Span { - self.cursor.token_tree() - .map(|_| self.cursor.span().unstable()) - .unwrap_or_else(Span::call_site) - } - - pub fn parse<T: Synom>(&mut self) -> Result<T> { - let (val, cursor) = T::parse(self.cursor) - .map_err(|e| { - let expected = match T::description() { - Some(desc) => desc, - // We're just grabbing the type's name here. This is totally - // unnecessary. There's nothing potentially memory-unsafe - // about this. It's simply unsafe because it's an intrinsic. - None => unsafe { ::std::intrinsics::type_name::<T>() } - }; - - self.current_span().error(format!("{}: expected {}", e, expected)) - })?; - - self.cursor = cursor; - Ok(val) - } - - pub fn eat<T: Synom>(&mut self) -> bool { - self.parse::<T>().is_ok() - } - - pub fn parse_group<F, T>(&mut self, delim: Delimiter, f: F) -> Result<T> - where F: FnOnce(&mut Parser) -> Result<T> - { - if let Some((group_cursor, _, next_cursor)) = self.cursor.group(delim) { - self.cursor = group_cursor; - let result = f(self); - self.cursor = next_cursor; - result - } else { - let expected = match delim { - Delimiter::Brace => "curly braced group", - Delimiter::Bracket => "square bracketed group", - Delimiter::Parenthesis => "parenthesized group", - Delimiter::None => "invisible group" - }; - - Err(self.current_span() - .error(format!("parse error: expected {}", expected))) - } - } - - pub fn parse_sep<F, T>(&mut self, sep: Seperator, mut f: F) -> Result<Vec<T>> - where F: FnMut(&mut Parser) -> Result<T> - { - let mut output = vec![]; - while !self.is_eof() { - output.push(f(self)?); - let have_sep = match sep { - Seperator::Comma => self.eat::<token::Comma>(), - Seperator::Pipe => self.eat::<token::Or>(), - Seperator::Semi => self.eat::<token::Semi>(), - }; - - if !have_sep { - break; - } - } - - Ok(output) - } - - pub fn eof(&self) -> Result<()> { - if !self.cursor.eof() { - let diag = self.current_span() - .error("trailing characters; expected eof"); - - return Err(diag); - } - - Ok(()) - } - - fn is_eof(&self) -> bool { - self.eof().is_ok() - } -} diff --git a/core/codegen_next/src/spanned.rs b/core/codegen_next/src/spanned.rs @@ -1,29 +0,0 @@ -use proc_macro::Span; - -use quote::ToTokens; - -pub trait Spanned { - fn span(&self) -> Span; -} - -// FIXME: Remove this once proc_macro's stabilize. -impl<T: ToTokens> Spanned for T { - fn span(&self) -> Span { - let token_stream = self.into_token_stream(); - let mut iter = token_stream.into_iter(); - let mut span = match iter.next() { - Some(tt) => tt.span().unstable(), - None => { - return Span::call_site(); - } - }; - - for tt in iter { - if let Some(joined) = span.join(tt.span().unstable()) { - span = joined; - } - } - - span - } -} diff --git a/core/codegen_next/tests/compile-test.rs b/core/codegen_next/tests/compile-test.rs @@ -0,0 +1,105 @@ +extern crate compiletest_rs as compiletest; + +use std::path::{Path, PathBuf}; +use std::{io, fs::Metadata, time::SystemTime}; + +#[derive(Copy, Clone)] +enum Kind { + Dynamic, Static +} + +impl Kind { + fn extension(self) -> &'static str { + match self { + #[cfg(windows)] Kind::Dynamic => ".dll", + #[cfg(all(unix, target_os = "macos"))] Kind::Dynamic => ".dylib", + #[cfg(all(unix, not(target_os = "macos")))] Kind::Dynamic => ".so", + Kind::Static => ".rlib" + } + } + + fn prefix(self) -> &'static str { + #[cfg(windows)] { "" } + #[cfg(not(windows))] { "lib" } + } +} + +fn target_path() -> PathBuf { + #[cfg(debug_assertions)] const ENVIRONMENT: &str = "debug"; + #[cfg(not(debug_assertions))] const ENVIRONMENT: &str = "release"; + + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent().unwrap().parent().unwrap() + .join("target") + .join(ENVIRONMENT) +} + +fn link_flag(flag: &str, lib: &str, rel_path: &[&str]) -> String { + let mut path = target_path(); + for component in rel_path { + path = path.join(component); + } + + format!("{} {}={}", flag, lib, path.display()) +} + +fn best_time_for(metadata: &Metadata) -> SystemTime { + metadata.created() + .or_else(|_| metadata.modified()) + .or_else(|_| metadata.accessed()) + .unwrap_or_else(|_| SystemTime::now()) +} + +fn extern_dep(name: &str, kind: Kind) -> io::Result<String> { + let deps_root = target_path().join("deps"); + let dep_name = format!("{}{}", kind.prefix(), name); + + let mut dep_path: Option<PathBuf> = None; + for entry in deps_root.read_dir().expect("read_dir call failed") { + let entry = match entry { + Ok(entry) => entry, + Err(_) => continue + }; + + let filename = entry.file_name(); + let filename = filename.to_string_lossy(); + let lib_name = filename.split('.').next().unwrap().split('-').next().unwrap(); + + if lib_name == dep_name && filename.ends_with(kind.extension()) { + if let Some(ref mut existing) = dep_path { + if best_time_for(&entry.metadata()?) > best_time_for(&existing.metadata()?) { + *existing = entry.path().into(); + } + } else { + dep_path = Some(entry.path().into()); + } + } + } + + let dep = dep_path.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?; + let filename = dep.file_name().ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?; + Ok(link_flag("--extern", name, &["deps", &filename.to_string_lossy()])) +} + +fn run_mode(mode: &'static str, path: &'static str) { + let mut config = compiletest::Config::default(); + config.mode = mode.parse().expect("invalid mode"); + config.src_base = format!("tests/{}", path).into(); + config.clean_rmeta(); + + config.target_rustcflags = Some([ + link_flag("-L", "crate", &[]), + link_flag("-L", "dependency", &["deps"]), + extern_dep("rocket_codegen_next", Kind::Dynamic).expect("find codegen dep"), + extern_dep("rocket_http", Kind::Static).expect("find http dep"), + extern_dep("rocket", Kind::Static).expect("find core dep"), + ].join(" ")); + + compiletest::run_tests(&config); +} + +#[test] +fn compile_test() { + run_mode("ui", "ui-fail"); + run_mode("compile-fail", "ui-fail"); +} diff --git a/core/codegen_next/tests/from_form.rs b/core/codegen_next/tests/from_form.rs @@ -0,0 +1,319 @@ +#[macro_use] extern crate rocket; + +use rocket::request::{FromForm, FormItems, FormError}; +use rocket::http::RawStr; + +fn parse<'f, T>(string: &'f str, strict: bool) -> Result<T, FormError<'f>> + where T: FromForm<'f, Error = FormError<'f>> +{ + let mut items = FormItems::from(string); + let result = T::from_form(items.by_ref(), strict); + if !items.exhaust() { + panic!("Invalid form input."); + } + + result +} + +fn strict<'f, T>(string: &'f str) -> Result<T, FormError<'f>> + where T: FromForm<'f, Error = FormError<'f>> +{ + parse(string, true) +} + +fn lenient<'f, T>(string: &'f str) -> Result<T, FormError<'f>> + where T: FromForm<'f, Error = FormError<'f>> +{ + parse(string, false) +} + +#[derive(Debug, PartialEq, FromForm)] +struct TodoTask { + description: String, + completed: bool +} + +#[test] +fn simple() { + // Same number of arguments: simple case. + let task: Option<TodoTask> = strict("description=Hello&completed=on").ok(); + assert_eq!(task, Some(TodoTask { + description: "Hello".to_string(), + completed: true + })); + + // Argument in string but not in form. + let task: Option<TodoTask> = strict("other=a&description=Hello&completed=on").ok(); + assert!(task.is_none()); + + // Ensure _method isn't required. + let task: Option<TodoTask> = strict("_method=patch&description=Hello&completed=off").ok(); + assert_eq!(task, Some(TodoTask { + description: "Hello".to_string(), + completed: false + })); +} + +#[derive(Debug, PartialEq, FromFormValue)] +enum FormOption { + A, B, C +} + +#[derive(Debug, PartialEq, FromForm)] +struct FormInput<'r> { + checkbox: bool, + number: usize, + radio: FormOption, + password: &'r RawStr, + textarea: String, + select: FormOption, +} + +#[derive(Debug, PartialEq, FromForm)] +struct DefaultInput<'r> { + arg: Option<&'r RawStr>, +} + +#[derive(Debug, PartialEq, FromForm)] +struct ManualMethod<'r> { + _method: Option<&'r RawStr>, + done: bool +} + +#[derive(Debug, PartialEq, FromForm)] +struct UnpresentCheckbox { + checkbox: bool +} + +#[derive(Debug, PartialEq, FromForm)] +struct UnpresentCheckboxTwo<'r> { + checkbox: bool, + something: &'r RawStr +} + +#[derive(Debug, PartialEq, FromForm)] +struct FieldNamedV<'r> { + v: &'r RawStr, +} + +#[test] +fn base_conditions() { + let form_string = &[ + "password=testing", "checkbox=off", "checkbox=on", "number=10", + "checkbox=off", "textarea=", "select=a", "radio=c", + ].join("&"); + + let input: Option<FormInput> = strict(&form_string).ok(); + assert_eq!(input, Some(FormInput { + checkbox: false, + number: 10, + radio: FormOption::C, + password: "testing".into(), + textarea: "".to_string(), + select: FormOption::A, + })); + + // Argument not in string with default in form. + let default: Option<DefaultInput> = strict("").ok(); + assert_eq!(default, Some(DefaultInput { + arg: None + })); + + // Ensure _method can be captured if desired. + let manual: Option<ManualMethod> = strict("_method=put&done=true").ok(); + assert_eq!(manual, Some(ManualMethod { + _method: Some("put".into()), + done: true + })); + + let manual: Option<ManualMethod> = lenient("_method=put&done=true").ok(); + assert_eq!(manual, Some(ManualMethod { + _method: Some("put".into()), + done: true + })); + + // And ignored when not present. + let manual: Option<ManualMethod> = strict("done=true").ok(); + assert_eq!(manual, Some(ManualMethod { + _method: None, + done: true + })); + + // Check that a `bool` value that isn't in the form is marked as `false`. + let manual: Option<UnpresentCheckbox> = strict("").ok(); + assert_eq!(manual, Some(UnpresentCheckbox { + checkbox: false + })); + + // Check that a `bool` value that isn't in the form is marked as `false`. + let manual: Option<UnpresentCheckboxTwo> = strict("something=hello").ok(); + assert_eq!(manual, Some(UnpresentCheckboxTwo { + checkbox: false, + something: "hello".into() + })); + + // Check that a structure with one field `v` parses correctly. + let manual: Option<FieldNamedV> = strict("v=abc").ok(); + assert_eq!(manual, Some(FieldNamedV { + v: "abc".into() + })); + +} + +#[test] +fn lenient_parsing() { + // Check that a structure with one field `v` parses correctly (lenient). + let manual: Option<FieldNamedV> = lenient("v=abc").ok(); + assert_eq!(manual, Some(FieldNamedV { v: "abc".into() })); + + let manual: Option<FieldNamedV> = lenient("v=abc&a=123").ok(); + assert_eq!(manual, Some(FieldNamedV { v: "abc".into() })); + + let manual: Option<FieldNamedV> = lenient("c=abcddef&v=abc&a=123").ok(); + assert_eq!(manual, Some(FieldNamedV { v: "abc".into() })); + + // Check default values (bool) with lenient parsing. + let manual: Option<UnpresentCheckboxTwo> = lenient("something=hello").ok(); + assert_eq!(manual, Some(UnpresentCheckboxTwo { + checkbox: false, + something: "hello".into() + })); + + let manual: Option<UnpresentCheckboxTwo> = lenient("hi=hi&something=hello").ok(); + assert_eq!(manual, Some(UnpresentCheckboxTwo { + checkbox: false, + something: "hello".into() + })); + + // Check that a missing field doesn't parse, even leniently. + let manual: Option<FieldNamedV> = lenient("a=abc").ok(); + assert!(manual.is_none()); + + let manual: Option<FieldNamedV> = lenient("_method=abc").ok(); + assert!(manual.is_none()); +} + +#[derive(Debug, PartialEq, FromForm)] +struct RenamedForm { + 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, + #[form(field = "a.b")] + dot: isize, + #[form(field = "some space")] + some_space: String, +} + +#[test] +fn field_renaming() { + let form_string = &[ + "single=100", "camelCase=helloThere", "TitleCase=HiHi", "type=-2", + "DOUBLE=bing_bong", "a.b=123", "some space=okay" + ].join("&"); + + let form: Option<RenamedForm> = strict(&form_string).ok(); + assert_eq!(form, Some(RenamedForm { + single: 100, + camel_case: "helloThere".into(), + title_case: "HiHi".into(), + field_type: -2, + double: "bing_bong".into(), + dot: 123, + some_space: "okay".into(), + })); + + let form_string = &[ + "single=100", "camel_case=helloThere", "TitleCase=HiHi", "type=-2", + "DOUBLE=bing_bong", "dot=123", "some_space=okay" + ].join("&"); + + let form: Option<RenamedForm> = strict(&form_string).ok(); + assert!(form.is_none()); +} + +#[derive(FromForm, Debug, PartialEq)] +struct YetOneMore<'f, T> { + string: &'f RawStr, + other: T, +} + +#[derive(FromForm, Debug, PartialEq)] +struct Oops<A, B, C> { + base: String, + a: A, + b: B, + c: C, +} + +#[test] +fn generics() { + let form_string = &[ + "string=hello", "other=00128" + ].join("&"); + + let form: Option<YetOneMore<usize>> = strict(&form_string).ok(); + assert_eq!(form, Some(YetOneMore { + string: "hello".into(), + other: 128, + })); + + let form: Option<YetOneMore<u8>> = strict(&form_string).ok(); + assert_eq!(form, Some(YetOneMore { + string: "hello".into(), + other: 128, + })); + + let form: Option<YetOneMore<i8>> = strict(&form_string).ok(); + assert!(form.is_none()); + + let form_string = &[ + "base=just%20a%20test", "a=hey%20there", "b=a", "c=811", + ].join("&"); + + let form: Option<Oops<&RawStr, FormOption, usize>> = strict(&form_string).ok(); + assert_eq!(form, Some(Oops { + base: "just a test".into(), + a: "hey%20there".into(), + b: FormOption::A, + c: 811, + })); +} + +#[derive(Debug, PartialEq, FromForm)] +struct WhoopsForm { + complete: bool, + other: usize, +} + +#[test] +fn form_errors() { + let form: Result<WhoopsForm, _> = strict("complete=true&other=781"); + assert_eq!(form, Ok(WhoopsForm { complete: true, other: 781 })); + + let form: Result<WhoopsForm, _> = strict("complete=true&other=unknown"); + assert_eq!(form, Err(FormError::BadValue("other".into(), "unknown".into()))); + + let form: Result<WhoopsForm, _> = strict("complete=unknown&other=unknown"); + assert_eq!(form, Err(FormError::BadValue("complete".into(), "unknown".into()))); + + let form: Result<WhoopsForm, _> = strict("complete=true&other=1&extra=foo"); + assert_eq!(form, Err(FormError::Unknown("extra".into(), "foo".into()))); + + // Bad values take highest precedence. + let form: Result<WhoopsForm, _> = strict("complete=unknown&unknown=foo"); + assert_eq!(form, Err(FormError::BadValue("complete".into(), "unknown".into()))); + + // Then unknown key/values for strict parses. + let form: Result<WhoopsForm, _> = strict("complete=true&unknown=foo"); + assert_eq!(form, Err(FormError::Unknown("unknown".into(), "foo".into()))); + + // Finally, missing. + let form: Result<WhoopsForm, _> = strict("complete=true"); + assert_eq!(form, Err(FormError::Missing("other".into()))); +} diff --git a/core/codegen_next/tests/from_form_value.rs b/core/codegen_next/tests/from_form_value.rs @@ -0,0 +1,69 @@ +#[macro_use] extern crate rocket; + +use rocket::request::FromFormValue; + +macro_rules! assert_parse { + ($($string:expr),* => $item:ident :: $variant:ident) => ($( + match $item::from_form_value($string.into()) { + Ok($item::$variant) => { /* okay */ }, + Ok(item) => panic!("Failed to parse {} as {:?}. Got {:?} instead.", + $string, $item::$variant, item), + Err(e) => panic!("Failed to parse {} as {}: {:?}", + $string, stringify!($item), e), + + } + )*) +} + +macro_rules! assert_no_parse { + ($($string:expr),* => $item:ident) => ($( + match $item::from_form_value($string.into()) { + Err(_) => { /* okay */ }, + Ok(item) => panic!("Unexpectedly parsed {} as {:?}", $string, item) + } + )*) +} + +#[test] +fn from_form_value_simple() { + #[derive(Debug, FromFormValue)] + enum Foo { A, B, C, } + + assert_parse!("a", "A" => Foo::A); + assert_parse!("b", "B" => Foo::B); + assert_parse!("c", "C" => Foo::C); +} + +#[test] +fn from_form_value_weirder() { + #[allow(non_camel_case_types)] + #[derive(Debug, FromFormValue)] + enum Foo { Ab_Cd, OtherA } + + assert_parse!("ab_cd", "ab_CD", "Ab_CD" => Foo::Ab_Cd); + assert_parse!("othera", "OTHERA", "otherA", "OtherA" => Foo::OtherA); +} + +#[test] +fn from_form_value_no_parse() { + #[derive(Debug, FromFormValue)] + enum Foo { A, B, C, } + + assert_no_parse!("abc", "ab", "bc", "ca" => Foo); + assert_no_parse!("b ", "a ", "c ", "a b" => Foo); +} + +#[test] +fn from_form_value_renames() { + #[derive(Debug, FromFormValue)] + enum Foo { + #[form(value = "foo")] + Bar, + #[form(value = ":book")] + Book + } + + assert_parse!("foo", "FOO", "FoO" => Foo::Bar); + assert_parse!(":book", ":BOOK", ":bOOk", ":booK" => Foo::Book); + assert_no_parse!("book", "bar" => Foo); +} diff --git a/core/codegen_next/tests/responder.rs b/core/codegen_next/tests/responder.rs @@ -0,0 +1,113 @@ +#![feature(attr_literals)] + +#[macro_use] extern crate rocket; + +use rocket::local::Client; +use rocket::response::Responder; +use rocket::http::{Status, ContentType, Cookie}; + +#[derive(Responder)] +pub enum Foo<'r> { + First(String), + #[response(status = 500)] + Second(Vec<u8>), + #[response(status = 404, content_type = "html")] + Third { + responder: &'r str, + ct: ::rocket::http::ContentType, + }, + #[response(status = 105)] + Fourth { + string: &'r str, + ct: ::rocket::http::ContentType, + }, +} + +#[test] +fn responder_foo() { + let client = Client::new(rocket::ignite()).expect("valid rocket"); + let local_req = client.get("/"); + let req = local_req.inner(); + + let mut response = Foo::First("hello".into()) + .respond_to(req) + .expect("response okay"); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::Plain)); + assert_eq!(response.body_string(), Some("hello".into())); + + let mut response = Foo::Second("just a test".into()) + .respond_to(req) + .expect("response okay"); + + assert_eq!(response.status(), Status::InternalServerError); + assert_eq!(response.content_type(), Some(ContentType::Binary)); + assert_eq!(response.body_string(), Some("just a test".into())); + + let mut response = Foo::Third { responder: "well, hi", ct: ContentType::JSON } + .respond_to(req) + .expect("response okay"); + + assert_eq!(response.status(), Status::NotFound); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + assert_eq!(response.body_string(), Some("well, hi".into())); + + let mut response = Foo::Fourth { string: "goodbye", ct: ContentType::JSON } + .respond_to(req) + .expect("response okay"); + + assert_eq!(response.status(), Status::raw(105)); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.body_string(), Some("goodbye".into())); +} + +#[derive(Responder)] +#[response(content_type = "plain")] +pub struct Bar<'r> { + responder: Foo<'r>, + other: ContentType, + third: Cookie<'static>, + #[response(ignore)] + _yet_another: String, +} + +#[test] +fn responder_bar() { + let client = Client::new(rocket::ignite()).expect("valid rocket"); + let local_req = client.get("/"); + let req = local_req.inner(); + + let mut response = Bar { + responder: Foo::Second("foo foo".into()), + other: ContentType::HTML, + third: Cookie::new("cookie", "here!"), + _yet_another: "uh..hi?".into() + }.respond_to(req).expect("response okay"); + + assert_eq!(response.status(), Status::InternalServerError); + assert_eq!(response.content_type(), Some(ContentType::Plain)); + assert_eq!(response.body_string(), Some("foo foo".into())); + assert_eq!(response.headers().get_one("Set-Cookie"), Some("cookie=here!")); +} + +#[derive(Responder)] +#[response(content_type = "application/x-custom")] +pub struct Baz { + responder: &'static str, +} + +#[test] +fn responder_baz() { + let client = Client::new(rocket::ignite()).expect("valid rocket"); + let local_req = client.get("/"); + let req = local_req.inner(); + + let mut response = Baz { responder: "just a custom" } + .respond_to(req) + .expect("response okay"); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::new("application", "x-custom"))); + assert_eq!(response.body_string(), Some("just a custom".into())); +} diff --git a/core/codegen_next/tests/ui-fail/from_form.rs b/core/codegen_next/tests/ui-fail/from_form.rs @@ -0,0 +1,164 @@ +#[macro_use] extern crate rocket; + +#[derive(FromForm)] +enum Thing { } +//~^ ERROR not supported + +#[derive(FromForm)] +struct Foo1; +//~^ ERROR not supported + +#[derive(FromForm)] +struct Foo2 { } +//~^ ERROR one field is required + +#[derive(FromForm)] +struct Foo3(usize); +//~^ ERROR not supported + +#[derive(FromForm)] +struct NextTodoTask<'f, 'a> { +//~^ ERROR only one lifetime + description: String, + raw_description: &'f RawStr, + other: &'a RawStr, + completed: bool, +} + +#[derive(FromForm)] +struct BadName1 { + #[form(field = "isindex")] + //~^ ERROR invalid form field name + field: String, +} + +#[derive(FromForm)] +struct Demo2 { + #[form(field = "foo")] + field: String, + foo: usize, + //~^ ERROR duplicate field +} + +#[derive(FromForm)] +struct MyForm9 { + #[form(field = "hello")] + first: String, + #[form(field = "hello")] + //~^ ERROR duplicate field + other: String, +} + +#[derive(FromForm)] +struct MyForm10 { + first: String, + #[form(field = "first")] + //~^ ERROR duplicate field + other: String, +} + +#[derive(FromForm)] +struct MyForm { + #[form(field = "blah", field = "bloo")] + //~^ ERROR duplicate + my_field: String, +} + +#[derive(FromForm)] +struct MyForm1 { + #[form] + //~^ ERROR malformed attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm2 { + #[form("blah")] + //~^ ERROR unexpected literal + my_field: String, +} + +#[derive(FromForm)] +struct MyForm3 { + #[form(123)] + //~^ ERROR unexpected literal + my_field: String, +} + +#[derive(FromForm)] +struct MyForm4 { + #[form(beep = "bop")] + //~^ ERROR unexpected attribute parameter + my_field: String, +} + +#[derive(FromForm)] +struct MyForm5 { + #[form(field = "blah")] + #[form(field = "bleh")] + //~^ ERROR duplicate + my_field: String, +} + +#[derive(FromForm)] +struct MyForm6 { + #[form(field = true)] + //~^ ERROR invalid value: expected string + my_field: String, +} + +#[derive(FromForm)] +struct MyForm7 { + #[form(field)] + //~^ ERROR malformed parameter + my_field: String, +} + +#[derive(FromForm)] +struct MyForm8 { + #[form(field = 123)] + //~^ ERROR invalid value: expected string + my_field: String, +} + +#[derive(FromForm)] +struct MyForm11 { + #[form(field = "hello&world")] + //~^ ERROR invalid form field name + first: String, +} + +#[derive(FromForm)] +struct MyForm12 { + #[form(field = "!@#$%^&*()_")] + //~^ ERROR invalid form field name + first: String, +} + +#[derive(FromForm)] +struct MyForm13 { + #[form(field = "?")] + //~^ ERROR invalid form field name + first: String, +} + +#[derive(FromForm)] +struct MyForm14 { + #[form(field = "")] + //~^ ERROR invalid form field name + first: String, +} + +#[derive(FromForm)] +struct BadName2 { + #[form(field = "a&b")] + //~^ ERROR invalid form field name + field: String, +} + +#[derive(FromForm)] +struct BadName3 { + #[form(field = "a=")] + //~^ ERROR invalid form field name + field: String, +} diff --git a/core/codegen_next/tests/ui-fail/from_form.stderr b/core/codegen_next/tests/ui-fail/from_form.stderr @@ -0,0 +1,306 @@ +error: enums are not supported + --> $DIR/from_form.rs:4:1 + | +4 | enum Thing { } + | ^^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:3:10 + | +3 | #[derive(FromForm)] + | ^^^^^^^^ + +error: tuple structs are not supported + --> $DIR/from_form.rs:8:1 + | +8 | struct Foo1; + | ^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:7:10 + | +7 | #[derive(FromForm)] + | ^^^^^^^^ + +error: at least one field is required + --> $DIR/from_form.rs:12:1 + | +12 | struct Foo2 { } + | ^^^^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:11:10 + | +11 | #[derive(FromForm)] + | ^^^^^^^^ + +error: tuple structs are not supported + --> $DIR/from_form.rs:16:1 + | +16 | struct Foo3(usize); + | ^^^^^^^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:15:10 + | +15 | #[derive(FromForm)] + | ^^^^^^^^ + +error: only one lifetime is supported + --> $DIR/from_form.rs:20:20 + | +20 | struct NextTodoTask<'f, 'a> { + | ^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:19:10 + | +19 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid form field name + --> $DIR/from_form.rs:30:20 + | +30 | #[form(field = "isindex")] + | ^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:28:10 + | +28 | #[derive(FromForm)] + | ^^^^^^^^ + +error: duplicate field name + --> $DIR/from_form.rs:39:5 + | +39 | foo: usize, + | ^^^ + | +note: previous definition here + --> $DIR/from_form.rs:37:20 + | +37 | #[form(field = "foo")] + | ^^^^^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:35:10 + | +35 | #[derive(FromForm)] + | ^^^^^^^^ + +error: duplicate field name + --> $DIR/from_form.rs:47:20 + | +47 | #[form(field = "hello")] + | ^^^^^^^ + | +note: previous definition here + --> $DIR/from_form.rs:45:20 + | +45 | #[form(field = "hello")] + | ^^^^^^^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:43:10 + | +43 | #[derive(FromForm)] + | ^^^^^^^^ + +error: duplicate field name + --> $DIR/from_form.rs:55:20 + | +55 | #[form(field = "first")] + | ^^^^^^^ + | +note: previous definition here + --> $DIR/from_form.rs:54:5 + | +54 | first: String, + | ^^^^^ +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:52:10 + | +52 | #[derive(FromForm)] + | ^^^^^^^^ + +error: duplicate attribute parameter: field + --> $DIR/from_form.rs:62:28 + | +62 | #[form(field = "blah", field = "bloo")] + | ^^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:60:10 + | +60 | #[derive(FromForm)] + | ^^^^^^^^ + +error: malformed attribute + --> $DIR/from_form.rs:69:7 + | +69 | #[form] + | ^^^^ + | + = help: expected syntax: #[attr(key = value, ..)] +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:67:10 + | +67 | #[derive(FromForm)] + | ^^^^^^^^ + +error: unexpected literal + --> $DIR/from_form.rs:76:12 + | +76 | #[form("blah")] + | ^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:74:10 + | +74 | #[derive(FromForm)] + | ^^^^^^^^ + +error: unexpected literal + --> $DIR/from_form.rs:83:12 + | +83 | #[form(123)] + | ^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:81:10 + | +81 | #[derive(FromForm)] + | ^^^^^^^^ + +error: unexpected attribute parameter: beep + --> $DIR/from_form.rs:90:12 + | +90 | #[form(beep = "bop")] + | ^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:88:10 + | +88 | #[derive(FromForm)] + | ^^^^^^^^ + +error: duplicate invocation of `form` attribute + --> $DIR/from_form.rs:98:5 + | +98 | #[form(field = "bleh")] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:95:10 + | +95 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid value: expected string + --> $DIR/from_form.rs:105:20 + | +105 | #[form(field = true)] + | ^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:103:10 + | +103 | #[derive(FromForm)] + | ^^^^^^^^ + +error: malformed parameter: expected key/value pair + --> $DIR/from_form.rs:112:12 + | +112 | #[form(field)] + | ^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:110:10 + | +110 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid value: expected string + --> $DIR/from_form.rs:119:20 + | +119 | #[form(field = 123)] + | ^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:117:10 + | +117 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid form field name + --> $DIR/from_form.rs:126:20 + | +126 | #[form(field = "hello&world")] + | ^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:124:10 + | +124 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid form field name + --> $DIR/from_form.rs:133:20 + | +133 | #[form(field = "!@#$%^&*()_")] + | ^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:131:10 + | +131 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid form field name + --> $DIR/from_form.rs:140:20 + | +140 | #[form(field = "?")] + | ^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:138:10 + | +138 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid form field name + --> $DIR/from_form.rs:147:20 + | +147 | #[form(field = "")] + | ^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:145:10 + | +145 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid form field name + --> $DIR/from_form.rs:154:20 + | +154 | #[form(field = "a&b")] + | ^^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:152:10 + | +152 | #[derive(FromForm)] + | ^^^^^^^^ + +error: invalid form field name + --> $DIR/from_form.rs:161:20 + | +161 | #[form(field = "a=")] + | ^^^^ + | +note: error occurred while deriving `FromForm` + --> $DIR/from_form.rs:159:10 + | +159 | #[derive(FromForm)] + | ^^^^^^^^ + +error: aborting due to 24 previous errors + diff --git a/core/codegen_next/tests/ui-fail/from_form_type_errors.rs b/core/codegen_next/tests/ui-fail/from_form_type_errors.rs @@ -0,0 +1,19 @@ +#[macro_use] extern crate rocket; + +struct Unknown; + +#[derive(FromForm)] +struct BadType3 { + field: Unknown, + //~^ rocket::request::FromFormValue +} + +struct Foo<T>(T); + +#[derive(FromForm)] +struct Other { + field: Foo<usize>, + //~^ rocket::request::FromFormValue +} + +fn main() { } diff --git a/core/codegen_next/tests/ui-fail/from_form_type_errors.stderr b/core/codegen_next/tests/ui-fail/from_form_type_errors.stderr @@ -0,0 +1,15 @@ +error[E0277]: the trait bound `Unknown: rocket::request::FromFormValue<'_>` is not satisfied + --> $DIR/from_form_type_errors.rs:7:5 + | +7 | field: Unknown, + | ^^^^^^^^^^^^^^ the trait `rocket::request::FromFormValue<'_>` is not implemented for `Unknown` + +error[E0277]: the trait bound `Foo<usize>: rocket::request::FromFormValue<'_>` is not satisfied + --> $DIR/from_form_type_errors.rs:15:5 + | +15 | field: Foo<usize>, + | ^^^^^^^^^^^^^^^^^ the trait `rocket::request::FromFormValue<'_>` is not implemented for `Foo<usize>` + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0277`. diff --git a/core/codegen_next/tests/ui-fail/from_form_value.rs b/core/codegen_next/tests/ui-fail/from_form_value.rs @@ -0,0 +1,45 @@ +#[macro_use] extern crate rocket; + +#[derive(FromFormValue)] +struct Foo1; +//~^ ERROR not supported + +#[derive(FromFormValue)] +struct Foo2(usize); +//~^ ERROR not supported + +#[derive(FromFormValue)] +struct Foo3 { +//~^ ERROR not supported + foo: usize, +} + +#[derive(FromFormValue)] +enum Foo4 { + A(usize), + //~^ ERROR cannot have fields +} + +#[derive(FromFormValue)] +enum Foo5 { } +//~^ WARNING empty enum + +#[derive(FromFormValue)] +enum Foo6<T> { +//~^ ERROR type generics are not supported + A(T), +} + +#[derive(FromFormValue)] +enum Bar1 { + #[form(value = 123)] + //~^ ERROR invalid value: expected string + A, +} + +#[derive(FromFormValue)] +enum Bar2 { + #[form(value)] + //~^ ERROR malformed parameter + A, +} diff --git a/core/codegen_next/tests/ui-fail/from_form_value.stderr b/core/codegen_next/tests/ui-fail/from_form_value.stderr @@ -0,0 +1,95 @@ +error: tuple structs are not supported + --> $DIR/from_form_value.rs:4:1 + | +4 | struct Foo1; + | ^^^^^^^^^^^^ + | +note: error occurred while deriving `FromFormValue` + --> $DIR/from_form_value.rs:3:10 + | +3 | #[derive(FromFormValue)] + | ^^^^^^^^^^^^^ + +error: tuple structs are not supported + --> $DIR/from_form_value.rs:8:1 + | +8 | struct Foo2(usize); + | ^^^^^^^^^^^^^^^^^^^ + | +note: error occurred while deriving `FromFormValue` + --> $DIR/from_form_value.rs:7:10 + | +7 | #[derive(FromFormValue)] + | ^^^^^^^^^^^^^ + +error: named structs are not supported + --> $DIR/from_form_value.rs:12:1 + | +12 | / struct Foo3 { +13 | | //~^ ERROR not supported +14 | | foo: usize, +15 | | } + | |_^ + | +note: error occurred while deriving `FromFormValue` + --> $DIR/from_form_value.rs:11:10 + | +11 | #[derive(FromFormValue)] + | ^^^^^^^^^^^^^ + +error: variants cannot have fields + --> $DIR/from_form_value.rs:19:5 + | +19 | A(usize), + | ^^^^^^^^ + | +note: error occurred while deriving `FromFormValue` + --> $DIR/from_form_value.rs:17:10 + | +17 | #[derive(FromFormValue)] + | ^^^^^^^^^^^^^ + +warning: deriving for empty enum + --> $DIR/from_form_value.rs:24:1 + | +24 | enum Foo5 { } + | ^^^^^^^^^^^^^ + +error: type generics are not supported + --> $DIR/from_form_value.rs:28:11 + | +28 | enum Foo6<T> { + | ^ + | +note: error occurred while deriving `FromFormValue` + --> $DIR/from_form_value.rs:27:10 + | +27 | #[derive(FromFormValue)] + | ^^^^^^^^^^^^^ + +error: invalid value: expected string + --> $DIR/from_form_value.rs:35:20 + | +35 | #[form(value = 123)] + | ^^^ + | +note: error occurred while deriving `FromFormValue` + --> $DIR/from_form_value.rs:33:10 + | +33 | #[derive(FromFormValue)] + | ^^^^^^^^^^^^^ + +error: malformed parameter: expected key/value pair + --> $DIR/from_form_value.rs:42:12 + | +42 | #[form(value)] + | ^^^^^ + | +note: error occurred while deriving `FromFormValue` + --> $DIR/from_form_value.rs:40:10 + | +40 | #[derive(FromFormValue)] + | ^^^^^^^^^^^^^ + +error: aborting due to 7 previous errors + diff --git a/core/codegen_next/tests/ui-fail/responder-types.rs b/core/codegen_next/tests/ui-fail/responder-types.rs @@ -0,0 +1,37 @@ +// normalize-stderr-test: "<(.*) as (.*)>" -> "$1 as $$TRAIT" +// normalize-stderr-test: "and \d+ others" -> "and $$N others" + +#![feature(attr_literals)] + +#[macro_use] extern crate rocket; + +#[derive(Responder)] +struct Thing1 { + thing: u8, + //~^ ERROR Responder +} + +#[derive(Responder)] +struct Thing2 { + thing: String, + other: u8, + //~^ ERROR Header +} + +#[derive(Responder)] +struct Thing3 { + thing: u8, + //~^ ERROR Responder + other: u8, + //~^ ERROR Header +} + +#[derive(Responder)] +struct Thing4 { + thing: String, + other: ::rocket::http::ContentType, + then: String, + //~^ ERROR Header +} + +fn main() { } diff --git a/core/codegen_next/tests/ui-fail/responder-types.stderr b/core/codegen_next/tests/ui-fail/responder-types.stderr @@ -0,0 +1,61 @@ +error[E0277]: the trait bound `u8: rocket::response::Responder<'_>` is not satisfied + --> $DIR/responder-types.rs:10:5 + | +10 | thing: u8, + | ^^^^^^^^^ the trait `rocket::response::Responder<'_>` is not implemented for `u8` + | + = note: required by `rocket::response::Responder::respond_to` + +error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<u8>` is not satisfied + --> $DIR/responder-types.rs:17:5 + | +17 | other: u8, + | ^^^^^^^^^ the trait `std::convert::From<u8>` is not implemented for `rocket::http::Header<'_>` + | + = help: the following implementations were found: + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + and $N others + = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8` + +error[E0277]: the trait bound `u8: rocket::response::Responder<'_>` is not satisfied + --> $DIR/responder-types.rs:23:5 + | +23 | thing: u8, + | ^^^^^^^^^ the trait `rocket::response::Responder<'_>` is not implemented for `u8` + | + = note: required by `rocket::response::Responder::respond_to` + +error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<u8>` is not satisfied + --> $DIR/responder-types.rs:25:5 + | +25 | other: u8, + | ^^^^^^^^^ the trait `std::convert::From<u8>` is not implemented for `rocket::http::Header<'_>` + | + = help: the following implementations were found: + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + and $N others + = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8` + +error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<std::string::String>` is not satisfied + --> $DIR/responder-types.rs:33:5 + | +33 | then: String, + | ^^^^^^^^^^^^ the trait `std::convert::From<std::string::String>` is not implemented for `rocket::http::Header<'_>` + | + = help: the following implementations were found: + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + rocket::http::Header<'static> as $TRAIT + and $N others + = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `std::string::String` + +error: aborting due to 5 previous errors + +For more information about this error, try `rustc --explain E0277`. diff --git a/core/codegen_next/tests/ui-fail/responder.stderr b/core/codegen_next/tests/ui-fail/responder.stderr @@ -0,0 +1,158 @@ +error: need at least one field + --> $DIR/responder.rs:6:1 + | +6 | struct Thing1; + | ^^^^^^^^^^^^^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:5:10 + | +5 | #[derive(Responder)] + | ^^^^^^^^^ + +error: need at least one field + --> $DIR/responder.rs:10:14 + | +10 | struct Thing2(); + | ^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:9:10 + | +9 | #[derive(Responder)] + | ^^^^^^^^^ + +error: need at least one field + --> $DIR/responder.rs:15:5 + | +15 | Bark, + | ^^^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:13:10 + | +13 | #[derive(Responder)] + | ^^^^^^^^^ + +error: only one lifetime is supported + --> $DIR/responder.rs:20:14 + | +20 | struct Thing4<'a, 'b>(&'a str, &'b str); + | ^^^^^^^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:19:10 + | +19 | #[derive(Responder)] + | ^^^^^^^^^ + +error: type generics are not supported + --> $DIR/responder.rs:24:15 + | +24 | struct Thing5<T>(T); + | ^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:23:10 + | +23 | #[derive(Responder)] + | ^^^^^^^^^ + +error: type generics are not supported + --> $DIR/responder.rs:28:23 + | +28 | struct Thing6<'a, 'b, T>(&'a str, &'b str, T); + | ^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:27:10 + | +27 | #[derive(Responder)] + | ^^^^^^^^^ + +error: invalid or unknown content-type + --> $DIR/responder.rs:33:31 + | +33 | #[response(content_type = "")] + | ^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:31:10 + | +31 | #[derive(Responder)] + | ^^^^^^^^^ + +error: invalid or unknown content-type + --> $DIR/responder.rs:40:31 + | +40 | #[response(content_type = "idk")] + | ^^^^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:38:10 + | +38 | #[derive(Responder)] + | ^^^^^^^^^ + +error: invalid value: expected string + --> $DIR/responder.rs:47:31 + | +47 | #[response(content_type = 100)] + | ^^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:45:10 + | +45 | #[derive(Responder)] + | ^^^^^^^^^ + +error: status must be in range [100, 600) + --> $DIR/responder.rs:54:25 + | +54 | #[response(status = 8)] + | ^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:52:10 + | +52 | #[derive(Responder)] + | ^^^^^^^^^ + +error: invalid value: expected unsigned integer + --> $DIR/responder.rs:61:25 + | +61 | #[response(status = "404")] + | ^^^^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:59:10 + | +59 | #[derive(Responder)] + | ^^^^^^^^^ + +error: invalid value: expected unsigned integer + --> $DIR/responder.rs:68:25 + | +68 | #[response(status = "404", content_type = "html")] + | ^^^^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:66:10 + | +66 | #[derive(Responder)] + | ^^^^^^^^^ + +error: invalid value: expected string + --> $DIR/responder.rs:75:45 + | +75 | #[response(status = 404, content_type = 120)] + | ^^^ + | +note: error occurred while deriving `Responder` + --> $DIR/responder.rs:73:10 + | +73 | #[derive(Responder)] + | ^^^^^^^^^ + +error: aborting due to 13 previous errors + diff --git a/core/codegen_next/tests/ui-fail/update-references.sh b/core/codegen_next/tests/ui-fail/update-references.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# +# Copyright 2015 The Rust Project Developers. See the COPYRIGHT +# file at the top-level directory of this distribution and at +# http://rust-lang.org/COPYRIGHT. +# +# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +# A script to update the references for particular tests. The idea is +# that you do a run, which will generate files in the build directory +# containing the (normalized) actual output of the compiler. This +# script will then copy that output and replace the "expected output" +# files. You can then commit the changes. +# +# If you find yourself manually editing a foo.stderr file, you're +# doing it wrong. + +if [[ "$1" == "--help" || "$1" == "-h" || "$1" == "" || "$2" == "" ]]; then + echo "usage: $0 <build-directory> <relative-path-to-rs-files>" + echo "" + echo "For example:" + echo " $0 ../../../build/x86_64-apple-darwin/test/ui *.rs */*.rs" +fi + +MYDIR=$(dirname $0) + +BUILD_DIR="$1" +shift + +shopt -s nullglob + +while [[ "$1" != "" ]]; do + for EXT in "stderr" "stdout" "fixed"; do + for OUT_NAME in $BUILD_DIR/${1%.rs}.*$EXT; do + OUT_DIR=`dirname "$1"` + OUT_BASE=`basename "$OUT_NAME"` + if ! (diff $OUT_NAME $MYDIR/$OUT_DIR/$OUT_BASE >& /dev/null); then + echo updating $MYDIR/$OUT_DIR/$OUT_BASE + cp $OUT_NAME $MYDIR/$OUT_DIR + fi + done + done + shift +done diff --git a/core/lib/src/request/form/error.rs b/core/lib/src/request/form/error.rs @@ -0,0 +1,21 @@ +use http::RawStr; + +/// Error returned by the [`FromForm`] derive on form parsing errors. +/// +/// If multiple errors occur while parsing a form, the first error in the +/// following precedence, from highest to lowest, is returned: +/// +/// * `BadValue` or `Unknown` in incoming form string field order +/// * `Missing` in lexical field order +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum FormError<'f> { + /// The field named `.0` with value `.1` failed to parse or validate. + BadValue(&'f RawStr, &'f RawStr), + /// The parse was strict and the field named `.0` with value `.1` appeared + /// in the incoming form string but was unexpected. + /// + /// This error cannot occur when parsing is lenient. + Unknown(&'f RawStr, &'f RawStr), + /// The field named `.0` was expected but is missing in the incoming form. + Missing(&'f RawStr), +} diff --git a/core/lib/src/request/form/form.rs b/core/lib/src/request/form/form.rs @@ -49,10 +49,10 @@ use request::form::{FromForm, FormItems}; /// The simplest data structure with a reference into form data looks like this: /// /// ```rust -/// # #![feature(plugin, decl_macro, custom_derive)] +/// # #![feature(plugin, decl_macro)] /// # #![allow(deprecated, dead_code, unused_attributes)] /// # #![plugin(rocket_codegen)] -/// # extern crate rocket; +/// # #[macro_use] extern crate rocket; /// # use rocket::http::RawStr; /// #[derive(FromForm)] /// struct UserInput<'f> { @@ -65,10 +65,10 @@ use request::form::{FromForm, FormItems}; /// a string. A handler for this type can be written as: /// /// ```rust -/// # #![feature(plugin, decl_macro, custom_derive)] +/// # #![feature(plugin, decl_macro)] /// # #![allow(deprecated, unused_attributes)] /// # #![plugin(rocket_codegen)] -/// # extern crate rocket; +/// # #[macro_use] extern crate rocket; /// # use rocket::request::Form; /// # use rocket::http::RawStr; /// # #[derive(FromForm)] @@ -91,24 +91,18 @@ use request::form::{FromForm, FormItems}; /// The owned analog of the `UserInput` type above is: /// /// ```rust -/// # #![feature(plugin, decl_macro, custom_derive)] -/// # #![allow(deprecated, dead_code, unused_attributes)] -/// # #![plugin(rocket_codegen)] -/// # extern crate rocket; -/// #[derive(FromForm)] /// struct OwnedUserInput { /// value: String /// } -/// # fn main() { } /// ``` /// /// The handler is written similarly: /// /// ```rust -/// # #![feature(plugin, decl_macro, custom_derive)] +/// # #![feature(plugin, decl_macro)] /// # #![allow(deprecated, unused_attributes)] /// # #![plugin(rocket_codegen)] -/// # extern crate rocket; +/// # #[macro_use] extern crate rocket; /// # use rocket::request::Form; /// # #[derive(FromForm)] /// # struct OwnedUserInput { @@ -170,9 +164,9 @@ impl<'f, T: FromForm<'f> + 'f> Form<'f, T> { /// # Example /// /// ```rust - /// # #![feature(plugin, decl_macro, custom_derive)] + /// # #![feature(plugin, decl_macro)] /// # #![plugin(rocket_codegen)] - /// # extern crate rocket; + /// # #[macro_use] extern crate rocket; /// use rocket::request::Form; /// /// #[derive(FromForm)] @@ -198,9 +192,9 @@ impl<'f, T: FromForm<'f> + 'f> Form<'f, T> { /// # Example /// /// ```rust - /// # #![feature(plugin, decl_macro, custom_derive)] + /// # #![feature(plugin, decl_macro)] /// # #![plugin(rocket_codegen)] - /// # extern crate rocket; + /// # #[macro_use] extern crate rocket; /// use rocket::request::Form; /// /// #[derive(FromForm)] @@ -269,9 +263,9 @@ impl<'f, T: FromForm<'f> + 'static> Form<'f, T> { /// # Example /// /// ```rust - /// # #![feature(plugin, decl_macro, custom_derive)] + /// # #![feature(plugin, decl_macro)] /// # #![plugin(rocket_codegen)] - /// # extern crate rocket; + /// # #[macro_use] extern crate rocket; /// use rocket::request::Form; /// /// #[derive(FromForm)] diff --git a/core/lib/src/request/form/from_form.rs b/core/lib/src/request/form/from_form.rs @@ -14,11 +14,11 @@ use request::FormItems; /// validation. /// /// ```rust -/// #![feature(plugin, decl_macro, custom_derive)] +/// #![feature(plugin, decl_macro)] /// #![plugin(rocket_codegen)] /// # #![allow(deprecated, dead_code, unused_attributes)] /// -/// extern crate rocket; +/// #[macro_use] extern crate rocket; /// /// #[derive(FromForm)] /// struct TodoTask { @@ -34,10 +34,10 @@ use request::FormItems; /// data via the `data` parameter and `Form` type. /// /// ```rust -/// # #![feature(plugin, decl_macro, custom_derive)] +/// # #![feature(plugin, decl_macro)] /// # #![allow(deprecated, dead_code, unused_attributes)] /// # #![plugin(rocket_codegen)] -/// # extern crate rocket; +/// # #[macro_use] extern crate rocket; /// # use rocket::request::Form; /// # #[derive(FromForm)] /// # struct TodoTask { description: String, completed: bool } diff --git a/core/lib/src/request/form/lenient.rs b/core/lib/src/request/form/lenient.rs @@ -56,9 +56,9 @@ impl<'f, T: FromForm<'f> + 'f> LenientForm<'f, T> { /// # Example /// /// ```rust - /// # #![feature(plugin, decl_macro, custom_derive)] + /// # #![feature(plugin, decl_macro)] /// # #![plugin(rocket_codegen)] - /// # extern crate rocket; + /// # #[macro_use] extern crate rocket; /// use rocket::request::LenientForm; /// /// #[derive(FromForm)] @@ -84,9 +84,9 @@ impl<'f, T: FromForm<'f> + 'f> LenientForm<'f, T> { /// # Example /// /// ```rust - /// # #![feature(plugin, decl_macro, custom_derive)] + /// # #![feature(plugin, decl_macro)] /// # #![plugin(rocket_codegen)] - /// # extern crate rocket; + /// # #[macro_use] extern crate rocket; /// use rocket::request::LenientForm; /// /// #[derive(FromForm)] @@ -114,9 +114,9 @@ impl<'f, T: FromForm<'f> + 'static> LenientForm<'f, T> { /// # Example /// /// ```rust - /// # #![feature(plugin, decl_macro, custom_derive)] + /// # #![feature(plugin, decl_macro)] /// # #![plugin(rocket_codegen)] - /// # extern crate rocket; + /// # #[macro_use] extern crate rocket; /// use rocket::request::LenientForm; /// /// #[derive(FromForm)] diff --git a/core/lib/src/request/form/mod.rs b/core/lib/src/request/form/mod.rs @@ -19,14 +19,16 @@ mod form_items; mod from_form; mod from_form_value; -mod form; mod lenient; +mod error; +mod form; pub use self::form_items::FormItems; pub use self::from_form::FromForm; pub use self::from_form_value::FromFormValue; pub use self::form::Form; pub use self::lenient::LenientForm; +pub use self::error::FormError; use std::cmp; use std::io::Read; diff --git a/core/lib/src/request/mod.rs b/core/lib/src/request/mod.rs @@ -12,7 +12,7 @@ mod tests; pub use self::request::Request; pub use self::from_request::{FromRequest, Outcome}; pub use self::param::{FromParam, FromSegments, SegmentError}; -pub use self::form::{Form, LenientForm, FromForm, FromFormValue, FormItems}; +pub use self::form::{Form, FormError, LenientForm, FromForm, FromFormValue, FormItems}; pub use self::state::State; #[doc(inline)] diff --git a/core/lib/tests/flash-lazy-removes-issue-466.rs b/core/lib/tests/flash-lazy-removes-issue-466.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] extern crate rocket; diff --git a/core/lib/tests/form_method-issue-45.rs b/core/lib/tests/form_method-issue-45.rs @@ -1,7 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; use rocket::request::Form; diff --git a/core/lib/tests/form_value_decoding-issue-82.rs b/core/lib/tests/form_value_decoding-issue-82.rs @@ -1,7 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; use rocket::request::Form; diff --git a/core/lib/tests/limits.rs b/core/lib/tests/limits.rs @@ -1,7 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; use rocket::request::Form; diff --git a/core/lib/tests/query-and-non-query-dont-collide.rs b/core/lib/tests/query-and-non-query-dont-collide.rs @@ -1,7 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; #[derive(FromForm)] struct Query { diff --git a/core/lib/tests/responder_lifetime-issue-345.rs b/core/lib/tests/responder_lifetime-issue-345.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] #![allow(dead_code)] // This test is only here so that we can ensure it compiles. diff --git a/core/lib/tests/route_guard.rs b/core/lib/tests/route_guard.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] extern crate rocket; diff --git a/core/lib/tests/strict_and_lenient_forms.rs b/core/lib/tests/strict_and_lenient_forms.rs @@ -1,7 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; use rocket::request::{Form, LenientForm}; use rocket::http::RawStr; diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs @@ -1,8 +1,8 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] extern crate rocket_contrib; -extern crate rocket; +#[macro_use] extern crate rocket; #[cfg(test)] mod tests; diff --git a/examples/form_kitchen_sink/src/main.rs b/examples/form_kitchen_sink/src/main.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] #[macro_use] extern crate rocket; @@ -11,7 +11,6 @@ use rocket::http::RawStr; #[cfg(test)] mod tests; -// TODO: Make deriving `FromForm` for this enum possible. #[derive(Debug, FromFormValue)] enum FormOption { A, B, C diff --git a/examples/form_validation/src/main.rs b/examples/form_validation/src/main.rs @@ -1,7 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; mod files; #[cfg(test)] mod tests; diff --git a/examples/managed_queue/src/main.rs b/examples/managed_queue/src/main.rs @@ -1,8 +1,8 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] extern crate crossbeam; -extern crate rocket; +#[macro_use] extern crate rocket; #[cfg(test)] mod tests; diff --git a/examples/query_params/src/main.rs b/examples/query_params/src/main.rs @@ -1,7 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; #[cfg(test)] mod tests; diff --git a/examples/session/src/main.rs b/examples/session/src/main.rs @@ -1,8 +1,8 @@ -#![feature(plugin, decl_macro, custom_derive)] +#![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] extern crate rocket_contrib; -extern crate rocket; +#[macro_use] extern crate rocket; #[cfg(test)] mod tests; diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs @@ -1,7 +1,7 @@ -#![feature(plugin, decl_macro, custom_derive, const_fn)] +#![feature(plugin, decl_macro, const_fn)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; #[macro_use] extern crate diesel; #[macro_use] extern crate serde_derive; extern crate rocket_contrib;