From 008605bec7a5ca6ca3e87a9c6630a59930a09858 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 30 Sep 2016 01:25:07 -0700 Subject: [PATCH] This commit changes parsing traits and documents some of the core library: * All From* trait methods are now named like the trait. * All From* traits have an associated Error type. * Document all of the `form` module. * Add codegen tests for auto-derived forms. * The param parsing traits now live under Request. --- README.md | 5 +- codegen/src/decorators/derive_form.rs | 26 ++-- codegen/tests/run-pass/derive_form.rs | 84 +++++++++++++ examples/extended_validation/src/main.rs | 6 +- examples/form_kitchen_sink/src/main.rs | 2 +- examples/query_params/src/main.rs | 2 +- lib/src/form.rs | 145 ++++++++++++++++++++--- lib/src/lib.rs | 16 ++- lib/src/logger.rs | 5 +- lib/src/request/mod.rs | 17 +-- lib/src/{ => request}/param.rs | 29 +++-- lib/src/request/request.rs | 6 +- 12 files changed, 281 insertions(+), 62 deletions(-) create mode 100644 codegen/tests/run-pass/derive_form.rs rename lib/src/{ => request}/param.rs (61%) diff --git a/README.md b/README.md index f3e42e0b..ab9764ef 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,10 @@ Contributions are absolutely, positively welcome and encouraged! Contributions come in many forms. You could: 1. Submit a feature request or bug report as an [issue](https://github.com/SergioBenitez/Rocket/issues). - 2. Comment on [issues that require + 2. Ask for improved documentation as an [issue](https://github.com/SergioBenitez/Rocket/issues). + 3. Comment on [issues that require feedback](https://github.com/SergioBenitez/Rocket/issues?q=is%3Aissue+is%3Aopen+label%3A%22feedback+wanted%22). - 3. Contribute code via [pull requests](https://github.com/SergioBenitez/Rocket/pulls). + 4. Contribute code via [pull requests](https://github.com/SergioBenitez/Rocket/pulls). We aim to keep Rocket's code quality at the highest level. This means that any code you contribute must be: diff --git a/codegen/src/decorators/derive_form.rs b/codegen/src/decorators/derive_form.rs index a41bf938..b3f093f7 100644 --- a/codegen/src/decorators/derive_form.rs +++ b/codegen/src/decorators/derive_form.rs @@ -2,6 +2,7 @@ use syntax::ext::base::{Annotatable, ExtCtxt}; use syntax::print::pprust::{stmt_to_string}; +use syntax::parse::token::{str_to_ident}; use syntax::ast::{ItemKind, Expr, MetaItem, Mutability, VariantData}; use syntax::codemap::Span; use syntax::ext::build::AstBuilder; @@ -58,6 +59,9 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem, }) }; + // The error type in the derived implementation. + let error_type = ty::Ty::Literal(ty::Path::new(vec!["rocket", "Error"])); + let trait_def = TraitDef { is_unsafe: false, supports_unions: false, @@ -88,9 +92,7 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem, lifetime: None, params: vec![ Box::new(ty::Ty::Self_), - Box::new(ty::Ty::Literal( - ty::Path::new(vec!["rocket", "Error"]) - )), + Box::new(error_type.clone()) ], global: true, } @@ -101,7 +103,9 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem, unify_fieldless_variants: false, } ], - associated_types: vec![], + associated_types: vec![ + (str_to_ident("Error"), error_type.clone()) + ], }; trait_def.expand(ecx, meta_item, annotated, push); @@ -164,12 +168,14 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct let ident_string = ident.to_string(); let id_str = ident_string.as_str(); arms.push(quote_tokens!(cx, - $id_str => $ident = match ::rocket::form::FromFormValue::parse(v) { - Ok(v) => Some(v), - Err(e) => { - println!("\tError parsing form value '{}': {:?}", $id_str, e); - $return_err_stmt - } + $id_str => { + $ident = match ::rocket::form::FromFormValue::from_form_value(v) { + Ok(v) => Some(v), + Err(e) => { + println!("\tError parsing form val '{}': {:?}", $id_str, e); + $return_err_stmt + } + }; }, )); } diff --git a/codegen/tests/run-pass/derive_form.rs b/codegen/tests/run-pass/derive_form.rs new file mode 100644 index 00000000..f2d0434a --- /dev/null +++ b/codegen/tests/run-pass/derive_form.rs @@ -0,0 +1,84 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +use rocket::form::FromForm; + +#[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 +} + +use rocket::form::FromFormValue; + +impl<'v> FromFormValue<'v> for FormOption { + type Error = &'v str; + + fn from_form_value(v: &'v str) -> Result { + let variant = match v { + "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 str, + textarea: String, + select: FormOption, +} + +#[derive(Debug, PartialEq, FromForm)] +struct DefaultInput<'r> { + arg: Option<&'r str>, +} + +fn main() { + // Same number of arguments: simple case. + let task = TodoTask::from_form_string("description=Hello&completed=on"); + assert_eq!(task, Ok(TodoTask { + description: "Hello".to_string(), + completed: true + })); + + // Argument in string but not in form. + let task = TodoTask::from_form_string("other=a&description=Hello&completed=on"); + assert!(task.is_err()); + + let form_string = &[ + "password=testing", "checkbox=off", "checkbox=on", "number=10", + "checkbox=off", "textarea=", "select=a", "radio=c", + ].join("&"); + + let input = FormInput::from_form_string(&form_string); + assert_eq!(input, Ok(FormInput { + checkbox: false, + number: 10, + radio: FormOption::C, + password: "testing", + textarea: "".to_string(), + select: FormOption::A, + })); + + // Argument not in string with default in form. + let default = DefaultInput::from_form_string(""); + assert_eq!(default, Ok(DefaultInput { + arg: None + })); +} diff --git a/examples/extended_validation/src/main.rs b/examples/extended_validation/src/main.rs index 4621aca6..b2f1079c 100644 --- a/examples/extended_validation/src/main.rs +++ b/examples/extended_validation/src/main.rs @@ -25,7 +25,7 @@ struct UserLogin<'r> { impl<'v> FromFormValue<'v> for StrongPassword<'v> { type Error = &'static str; - fn parse(v: &'v str) -> Result { + fn from_form_value(v: &'v str) -> Result { if v.len() < 8 { Err("Too short!") } else { @@ -37,8 +37,8 @@ impl<'v> FromFormValue<'v> for StrongPassword<'v> { impl<'v> FromFormValue<'v> for AdultAge { type Error = &'static str; - fn parse(v: &'v str) -> Result { - let age = match isize::parse(v) { + fn from_form_value(v: &'v str) -> Result { + let age = match isize::from_form_value(v) { Ok(v) => v, Err(_) => return Err("Age value is not a number."), }; diff --git a/examples/form_kitchen_sink/src/main.rs b/examples/form_kitchen_sink/src/main.rs index 495955c8..d0a7ce9f 100644 --- a/examples/form_kitchen_sink/src/main.rs +++ b/examples/form_kitchen_sink/src/main.rs @@ -17,7 +17,7 @@ enum FormOption { impl<'v> FromFormValue<'v> for FormOption { type Error = &'v str; - fn parse(v: &'v str) -> Result { + fn from_form_value(v: &'v str) -> Result { let variant = match v { "a" => FormOption::A, "b" => FormOption::B, diff --git a/examples/query_params/src/main.rs b/examples/query_params/src/main.rs index ee25dc15..85a0a42c 100644 --- a/examples/query_params/src/main.rs +++ b/examples/query_params/src/main.rs @@ -3,7 +3,7 @@ extern crate rocket; -use rocket::{Rocket, Error}; +use rocket::Rocket; #[derive(FromForm)] struct Person<'r> { diff --git a/lib/src/form.rs b/lib/src/form.rs index ec6d24e8..0c985a49 100644 --- a/lib/src/form.rs +++ b/lib/src/form.rs @@ -1,25 +1,111 @@ +//! Types and traits to handle form processing. +//! +//! In general, you will deal with forms in Rocket via the `form` parameter in +//! routes: +//! +//! ```rust,ignore +//! #[post("/", form = )] +//! fn form_submit(my_form: MyType) -> ... +//! ``` +//! +//! Form parameter types must implement the [FromForm](trait.FromForm.html) +//! trait, which is automatically derivable. Automatically deriving `FromForm` +//! for a structure requires that all of its fields implement +//! [FromFormValue](trait.FormFormValue.html). See the +//! [codegen](/rocket_codegen/) documentation or the [forms guide](/guide/forms) +//! for more information on forms and on deriving `FromForm`. + +use url; +use error::Error; use std::str::FromStr; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr}; -use url; - -use error::Error; +/// Trait to create instance of some type from an HTTP form; used by code +/// generation for `form` route parameters. +/// +/// This trait can be automatically derived via the +/// [rocket_codegen](/rocket_codegen) plugin: +/// +/// ```rust,ignore +/// #![feature(plugin, custom_derive)] +/// #![plugin(rocket_codegen)] +/// +/// extern crate rocket; +/// +/// #[derive(FromForm)] +/// struct TodoTask { +/// description: String, +/// completed: bool +/// } +/// ``` +/// +/// When deriving `FromForm`, every field in the structure must implement +/// [FromFormValue](trait.FromFormValue.html). If you implement `FormForm` +/// yourself, use the [FormItems](struct.FormItems.html) iterator to iterate +/// through the form key/value pairs. pub trait FromForm<'f>: Sized { - fn from_form_string(s: &'f str) -> Result; + /// The associated error which can be returned from parsing. + type Error; + + /// Parses an instance of `Self` from a raw HTTP form + /// (`application/x-www-form-urlencoded data`) or returns an `Error` if one + /// cannot be parsed. + fn from_form_string(form_string: &'f str) -> Result; } -// This implementation should only be ued during debugging! -#[doc(hidden)] +/// This implementation should only be used during debugging! impl<'f> FromForm<'f> for &'f str { + type Error = Error; fn from_form_string(s: &'f str) -> Result { Ok(s) } } +/// Trait to create instance of some type from a form value; expected from field +/// types in structs deriving `FromForm`. +/// +/// # Examples +/// +/// This trait is generally implemented when verifying form inputs. For example, +/// if you'd like to verify that some user is over some age in a form, then you +/// might define a new type and implement `FromFormValue` as follows: +/// +/// ```rust +/// use rocket::form::FromFormValue; +/// use rocket::Error; +/// +/// struct AdultAge(usize); +/// +/// impl<'v> FromFormValue<'v> for AdultAge { +/// type Error = &'v str; +/// +/// fn from_form_value(form_value: &'v str) -> Result { +/// match usize::from_form_value(form_value) { +/// Ok(age) if age >= 21 => Ok(AdultAge(age)), +/// _ => Err(form_value), +/// } +/// } +/// } +/// ``` +/// +/// This type can then be used in a `FromForm` struct as follows: +/// +/// ```rust,ignore +/// #[derive(FromForm)] +/// struct User { +/// age: AdultAge, +/// ... +/// } +/// ``` pub trait FromFormValue<'v>: Sized { + /// The associated error which can be returned from parsing. It is a good + /// idea to have the return type be or contain an `&'v str` so that the + /// unparseable string can be examined after a bad parse. type Error; - fn parse(v: &'v str) -> Result; + /// Parses an instance of `Self` from an HTTP form field value or returns an + /// `Error` if one cannot be parsed. + fn from_form_value(form_value: &'v str) -> Result; /// Returns a default value to be used when the form field does not exist. /// If this returns None, then the field is required. Otherwise, this should @@ -32,7 +118,7 @@ pub trait FromFormValue<'v>: Sized { impl<'v> FromFormValue<'v> for &'v str { type Error = Error; - fn parse(v: &'v str) -> Result { + fn from_form_value(v: &'v str) -> Result { Ok(v) } } @@ -41,7 +127,7 @@ impl<'v> FromFormValue<'v> for String { type Error = &'v str; // This actually parses the value according to the standard. - fn parse(v: &'v str) -> Result { + fn from_form_value(v: &'v str) -> Result { let decoder = url::percent_encoding::percent_decode(v.as_bytes()); let res = decoder.decode_utf8().map_err(|_| v).map(|s| s.into_owned()); match res { @@ -64,7 +150,7 @@ impl<'v> FromFormValue<'v> for String { impl<'v> FromFormValue<'v> for bool { type Error = &'v str; - fn parse(v: &'v str) -> Result { + fn from_form_value(v: &'v str) -> Result { match v { "on" | "true" => Ok(true), "off" | "false" => Ok(false), @@ -77,7 +163,7 @@ macro_rules! impl_with_fromstr { ($($T:ident),+) => ($( impl<'v> FromFormValue<'v> for $T { type Error = &'v str; - fn parse(v: &'v str) -> Result { + fn from_form_value(v: &'v str) -> Result { $T::from_str(v).map_err(|_| v) } } @@ -90,8 +176,8 @@ impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64, impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option { type Error = Error; - fn parse(v: &'v str) -> Result { - match T::parse(v) { + fn from_form_value(v: &'v str) -> Result { + match T::from_form_value(v) { Ok(v) => Ok(Some(v)), Err(_) => Ok(None) } @@ -106,14 +192,43 @@ impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option { impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result { type Error = Error; - fn parse(v: &'v str) -> Result { - match T::parse(v) { + fn from_form_value(v: &'v str) -> Result { + match T::from_form_value(v) { ok@Ok(_) => Ok(ok), e@Err(_) => Ok(e) } } } +/// Iterator over the key/value pairs of a given HTTP form string. You'll likely +/// want to use this if you're implementing [FromForm](trait.FromForm.html) +/// manually, for whatever reason, by iterating over the items in `form_string`. +/// +/// # Examples +/// +/// `FormItems` can be used directly as an iterator: +/// +/// ```rust +/// use rocket::form::FormItems; +/// +/// // prints "greeting = hello" then "username = jake" +/// let form_string = "greeting=hello&username=jake"; +/// for (key, value) in FormItems(form_string) { +/// println!("{} = {}", key, value); +/// } +/// ``` +/// +/// This is the same example as above, but the iterator is used explicitly. +/// +/// ```rust +/// use rocket::form::FormItems; +/// +/// let form_string = "greeting=hello&username=jake"; +/// let mut items = FormItems(form_string); +/// assert_eq!(items.next(), Some(("greeting", "hello"))); +/// assert_eq!(items.next(), Some(("username", "jake"))); +/// assert_eq!(items.next(), None); +/// ``` pub struct FormItems<'f>(pub &'f str); impl<'f> Iterator for FormItems<'f> { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index e28fd619..a0f421e3 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -13,7 +13,7 @@ //! chapter](https://rocket.rs/guide/getting_started) of the guide. //! //! You may also be interested in looking at the [contrib API -//! documentation](../rocket_contrib), which contains JSON and templaring +//! documentation](../rocket_contrib), which contains JSON and templating //! support. extern crate term_painter; @@ -22,6 +22,7 @@ extern crate url; extern crate mime; #[macro_use] extern crate log; +#[doc(hidden)] #[macro_use] pub mod logger; pub mod form; @@ -32,30 +33,33 @@ pub mod content_type; mod method; mod error; -mod param; mod router; mod rocket; mod codegen; mod catcher; -#[doc(hidden)] +/// Defines the types for request and error handlers. pub mod handler { use super::{Request, Response, Error}; + /// The type of a request handler. pub type Handler = for<'r> fn(&'r Request<'r>) -> Response<'r>; + + /// The type of an error handler. pub type ErrorHandler = for<'r> fn(error: Error, &'r Request<'r>) -> Response<'r>; } -#[doc(hidden)] -pub use logger::{RocketLogger, LoggingLevel}; pub use content_type::ContentType; pub use codegen::{StaticRouteInfo, StaticCatchInfo}; pub use request::Request; pub use method::Method; +#[doc(inline)] pub use response::{Response, Responder}; pub use error::Error; -pub use param::{FromParam, FromSegments}; pub use router::{Router, Route}; pub use catcher::Catcher; pub use rocket::Rocket; +#[doc(inline)] pub use handler::{Handler, ErrorHandler}; +#[doc(inline)] +pub use logger::LoggingLevel; diff --git a/lib/src/logger.rs b/lib/src/logger.rs index 678f2a3c..b5f8b3cc 100644 --- a/lib/src/logger.rs +++ b/lib/src/logger.rs @@ -1,9 +1,12 @@ +//! Rocket's logging infrastructure. + use log::{self, Log, LogLevel, LogRecord, LogMetadata}; use term_painter::Color::*; use term_painter::ToStyle; -pub struct RocketLogger(LoggingLevel); +struct RocketLogger(LoggingLevel); +/// Defines the different levels for log messages. #[derive(PartialEq)] pub enum LoggingLevel { /// Only shows errors and warning. diff --git a/lib/src/request/mod.rs b/lib/src/request/mod.rs index ecbe0f6e..a024a943 100644 --- a/lib/src/request/mod.rs +++ b/lib/src/request/mod.rs @@ -1,17 +1,18 @@ +//! Types and traits that deal with request parsing and handling. + mod request; +mod param; mod from_request; pub use self::request::Request; pub use self::from_request::FromRequest; - -#[doc(hidden)] -pub use hyper::server::Request as HyperRequest; -#[doc(hidden)] -pub use hyper::header::Headers as HyperHeaders; -#[doc(hidden)] -pub use hyper::header::Cookie as HyperCookie; - +pub use self::param::{FromParam, FromSegments}; pub use hyper::header::CookiePair as Cookie; +// Unexported Hyper types. +#[doc(hidden)] pub use hyper::server::Request as HyperRequest; +#[doc(hidden)] pub use hyper::header::Headers as HyperHeaders; +#[doc(hidden)] pub use hyper::header::Cookie as HyperCookie; + use hyper::header::CookieJar; pub type Cookies = CookieJar<'static>; diff --git a/lib/src/param.rs b/lib/src/request/param.rs similarity index 61% rename from lib/src/param.rs rename to lib/src/request/param.rs index dfda6cfc..d58bc0ef 100644 --- a/lib/src/param.rs +++ b/lib/src/request/param.rs @@ -1,34 +1,36 @@ use std::str::FromStr; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr}; use std::path::PathBuf; -use router::Segments; +use router::Segments; use url; -use error::Error; - pub trait FromParam<'a>: Sized { - fn from_param(param: &'a str) -> Result; + type Error; + fn from_param(param: &'a str) -> Result; } impl<'a> FromParam<'a> for &'a str { - fn from_param(param: &'a str) -> Result<&'a str, Error> { + type Error = (); + fn from_param(param: &'a str) -> Result<&'a str, Self::Error> { Ok(param) } } impl<'a> FromParam<'a> for String { - fn from_param(p: &'a str) -> Result { + type Error = &'a str; + fn from_param(p: &'a str) -> Result { let decoder = url::percent_encoding::percent_decode(p.as_bytes()); - decoder.decode_utf8().map_err(|_| Error::BadParse).map(|s| s.into_owned()) + decoder.decode_utf8().map_err(|_| p).map(|s| s.into_owned()) } } macro_rules! impl_with_fromstr { ($($T:ident),+) => ($( impl<'a> FromParam<'a> for $T { - fn from_param(param: &'a str) -> Result { - $T::from_str(param).map_err(|_| Error::BadParse) + type Error = &'a str; + fn from_param(param: &'a str) -> Result { + $T::from_str(param).map_err(|_| param) } } )+) @@ -39,17 +41,20 @@ impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64, SocketAddr); pub trait FromSegments<'a>: Sized { - fn from_segments(segments: Segments<'a>) -> Result; + type Error; + fn from_segments(segments: Segments<'a>) -> Result; } impl<'a> FromSegments<'a> for Segments<'a> { - fn from_segments(segments: Segments<'a>) -> Result, Error> { + type Error = (); + fn from_segments(segments: Segments<'a>) -> Result, ()> { Ok(segments) } } impl<'a> FromSegments<'a> for PathBuf { - fn from_segments(segments: Segments<'a>) -> Result { + type Error = (); + fn from_segments(segments: Segments<'a>) -> Result { Ok(segments.collect()) } } diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index f19a8c17..134522aa 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -6,7 +6,7 @@ use term_painter::Color::*; use term_painter::ToStyle; use error::Error; -use param::{FromParam, FromSegments}; +use super::{FromParam, FromSegments}; use method::Method; use content_type::ContentType; @@ -36,7 +36,7 @@ impl<'a> Request<'a> { debug!("{} is >= param count {}", n, params.as_ref().unwrap().len()); Err(Error::NoKey) } else { - T::from_param(params.as_ref().unwrap()[n]) + T::from_param(params.as_ref().unwrap()[n]).map_err(|_| Error::BadParse) } } @@ -55,7 +55,7 @@ impl<'a> Request<'a> { // but the std lib doesn't implement it for Skip. let mut segments = self.uri.segments(); for _ in segments.by_ref().take(i) { /* do nothing */ } - T::from_segments(segments) + T::from_segments(segments).map_err(|_| Error::BadParse) } }