diff --git a/Cargo.toml b/Cargo.toml index 1c1b91df..78be987e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ codegen-units = 4 members = [ "lib/", "codegen/", + "codegen_next/", "contrib/", "examples/cookies", "examples/errors", diff --git a/README.md b/README.md index 49892109..174bcbcf 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ expressibility, and speed. Here's an example of a complete Rocket application: #![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; #[get("//")] fn hello(name: String, age: u8) -> String { diff --git a/codegen_next/Cargo.toml b/codegen_next/Cargo.toml new file mode 100644 index 00000000..ba7979e7 --- /dev/null +++ b/codegen_next/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rocket_codegen_next" +version = "0.4.0-dev" +authors = ["Sergio Benitez "] +description = "Procedural macros for the Rocket web framework." +documentation = "https://api.rocket.rs/rocket_codegen/" +homepage = "https://rocket.rs" +repository = "https://github.com/SergioBenitez/Rocket" +readme = "../README.md" +keywords = ["rocket", "web", "framework", "code", "generation"] +license = "MIT/Apache-2.0" + +[lib] +proc-macro = true + +[dependencies] +quote = "0.5.1" +proc-macro2 = { version = "0.3.6", features = ["nightly"] } +syn = { version = "0.13.1", features = ["full", "extra-traits"] } diff --git a/codegen_next/src/codegen_ext.rs b/codegen_next/src/codegen_ext.rs new file mode 100644 index 00000000..ee4f67ad --- /dev/null +++ b/codegen_next/src/codegen_ext.rs @@ -0,0 +1,101 @@ +use syn::*; +use ext::*; +use quote::Tokens; +use spanned::Spanned; + +use FieldMember; + +pub trait CodegenFieldsExt { + fn surround(&self, tokens: Tokens) -> Tokens; + fn ignore_tokens(&self) -> Tokens; + fn id_match_tokens(&self) -> Tokens; +} + +pub fn field_to_ident(i: usize, field: &Field) -> Ident { + let name = match field.ident { + Some(id) => format!("_{}", id), + None => format!("_{}", i) + }; + + Ident::new(&name, field.span().into()) +} + +pub fn field_to_match((i, field): (usize, &Field)) -> Tokens { + let ident = field_to_ident(i, field); + match field.ident { + Some(id) => quote!(#id: #ident), + None => quote!(#ident) + } +} + +impl CodegenFieldsExt for Fields { + fn surround(&self, tokens: Tokens) -> Tokens { + match *self { + Fields::Named(..) => quote!({ #tokens }), + Fields::Unnamed(..) => quote!(( #tokens )), + Fields::Unit => quote!() + } + } + + fn ignore_tokens(&self) -> Tokens { + self.surround(quote!(..)) + } + + fn id_match_tokens(&self) -> Tokens { + let idents = self.iter() + .enumerate() + .map(field_to_match); + + self.surround(quote!(#(#idents),*)) + } +} + +pub trait TokensExt { + fn tokens(&self) -> Tokens; +} + +impl<'f> TokensExt for FieldMember<'f> { + fn tokens(&self) -> Tokens { + let index = self.member.unnamed().map(|i| i.index).unwrap_or(0); + let ident = field_to_ident(index as usize, &self.field); + quote!(#ident) + } +} + +// use rocket::http::{ContentType, MediaType, Status}; + +// impl TokensExt for ContentType { +// fn tokens(&self) -> Tokens { +// let mt_tokens = self.0.tokens(); +// quote!(rocket::http::ContentType(#mt_tokens)) +// } +// } + +// impl TokensExt for MediaType { +// fn tokens(&self) -> Tokens { +// let (top, sub) = (self.top().as_str(), self.sub().as_str()); +// let (keys, values) = (self.params().map(|(k, _)| k), self.params().map(|(_, v)| v)); +// quote!(rocket::http::MediaType { +// source: rocket::http::Source::None, +// top: rocket::http::IndexedStr::Concrete( +// std::borrow::Cow::Borrowed(#top) +// ), +// sub: rocket::http::IndexedStr::Concrete( +// std::borrow::Cow::Borrowed(#sub) +// ), +// params: rocket::http::MediaParams::Static(&[ +// #(( +// rocket::http::IndexedStr::Concrete(std::borrow::Cow::Borrowed(#keys)), +// rocket::http::IndexedStr::Concrete(std::borrow::Cow::Borrowed(#values)) +// )),* +// ]) +// }) +// } +// } + +// impl TokensExt for Status { +// fn tokens(&self) -> Tokens { +// let (code, reason) = (self.code, self.reason); +// quote!(rocket::http::Status { code: #code, reason: #reason }) +// } +// } diff --git a/codegen_next/src/ext.rs b/codegen_next/src/ext.rs new file mode 100644 index 00000000..59d5e621 --- /dev/null +++ b/codegen_next/src/ext.rs @@ -0,0 +1,169 @@ +use syn::*; +use FieldMember; +use spanned::Spanned; + +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(crate) 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>; + fn to_field_members<'f>(&'f self) -> Box> + 'f>; +} + +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 to_field_members<'f>(&'f self) -> Box> + 'f> { + Box::new(self.iter().enumerate().map(|(index, field)| { + if let Some(ident) = field.ident { + FieldMember { field, member: Member::Named(ident) } + } else { + let index = Index { index: index as u32, span: field.span().into() }; + let member = Member::Unnamed(index); + FieldMember { field, member } + } + })) + } + + 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; + fn into_struct(self) -> Option; + fn into_union(self) -> Option; +} + +impl DataExt for Data { + fn into_enum(self) -> Option { + match self { + Data::Enum(e) => Some(e), + _ => None + } + } + + fn into_struct(self) -> Option { + match self { + Data::Struct(s) => Some(s), + _ => None + } + } + + fn into_union(self) -> Option { + match self { + Data::Union(u) => Some(u), + _ => None + } + } +} diff --git a/codegen_next/src/lib.rs b/codegen_next/src/lib.rs new file mode 100644 index 00000000..f9d18f89 --- /dev/null +++ b/codegen_next/src/lib.rs @@ -0,0 +1,103 @@ +#![feature(proc_macro, core_intrinsics, decl_macro)] +#![recursion_limit="256"] + +extern crate syn; +extern crate proc_macro; +extern crate proc_macro2; +#[macro_use] extern crate quote; + +mod parser; +mod spanned; +mod ext; +mod codegen_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"; + +#[derive(Debug, Clone)] +pub(crate) struct FieldMember<'f> { + field: &'f Field, + member: Member +} + +fn validate_input(input: DeriveInput) -> PResult { + // 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))?; + + // 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)); + } + } + + // Emit a warning if the enum is empty. + if data.variants.is_empty() { + Span::call_site().warning(EMPTY_ENUM_WARN).emit(); + } + + Ok(data) +} + +fn real_derive_from_form_value(input: TokenStream) -> PResult { + // 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; + 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.as_ref() as &str); + let variant_idents = enum_data.variants.iter().map(|v| v.ident); + let names = ::std::iter::repeat(name); + + // Generate the implementation. + Ok(quote! { + mod scope { + extern crate std; + extern crate rocket; + + use self::std::prelude::v1::*; + use self::rocket::request::FromFormValue; + use self::rocket::http::RawStr; + + impl<'v> FromFormValue<'v> for #name { + type Error = &'v RawStr; + + fn from_form_value(v: &'v RawStr) -> Result { + #(if v.as_uncased_str() == #variant_strs { + return Ok(#names::#variant_idents); + })* + + Err(v) + } + } + } + }.into()) +} + +#[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::empty() + }) +} diff --git a/codegen_next/src/parser.rs b/codegen_next/src/parser.rs new file mode 100644 index 00000000..c09f69d2 --- /dev/null +++ b/codegen_next/src/parser.rs @@ -0,0 +1,118 @@ +#![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 = ::std::result::Result; + +pub enum Seperator { + Comma, + Pipe, + Semi, +} + +pub struct Parser { + buffer: Box, + cursor: Cursor<'static>, +} + +impl Parser { + pub fn new(tokens: TokenStream) -> Parser { + let buffer = Box::new(TokenBuffer::new(tokens.into())); + let cursor = unsafe { + let buffer: &'static TokenBuffer = ::std::mem::transmute(&*buffer); + buffer.begin() + }; + + Parser { + buffer: buffer, + cursor: 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(&mut self) -> Result { + let (val, cursor) = T::parse(self.cursor) + .map_err(|e| { + let expected = match T::description() { + Some(desc) => desc, + None => unsafe { ::std::intrinsics::type_name::() } + }; + + self.current_span().error(format!("{}: expected {}", e, expected)) + })?; + + self.cursor = cursor; + Ok(val) + } + + pub fn eat(&mut self) -> bool { + self.parse::().is_ok() + } + + pub fn parse_group(&mut self, delim: Delimiter, f: F) -> Result + where F: FnOnce(&mut Parser) -> Result + { + 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(&mut self, sep: Seperator, mut f: F) -> Result> + where F: FnMut(&mut Parser) -> Result + { + let mut output = vec![]; + while !self.is_eof() { + output.push(f(self)?); + let have_sep = match sep { + Seperator::Comma => self.eat::(), + Seperator::Pipe => self.eat::(), + Seperator::Semi => self.eat::(), + }; + + 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/codegen_next/src/spanned.rs b/codegen_next/src/spanned.rs new file mode 100644 index 00000000..35a35b09 --- /dev/null +++ b/codegen_next/src/spanned.rs @@ -0,0 +1,32 @@ +use proc_macro::Span; + +use proc_macro2::TokenStream; +use quote::{Tokens, ToTokens}; + +pub trait Spanned { + fn span(&self) -> Span; +} + +// FIXME: Remove this once proc_macro's stabilize. +impl Spanned for T { + fn span(&self) -> Span { + let mut tokens = Tokens::new(); + self.to_tokens(&mut tokens); + let token_stream = TokenStream::from(tokens); + 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/examples/form_kitchen_sink/src/main.rs b/examples/form_kitchen_sink/src/main.rs index c05eea91..1239de44 100644 --- a/examples/form_kitchen_sink/src/main.rs +++ b/examples/form_kitchen_sink/src/main.rs @@ -1,37 +1,22 @@ #![feature(plugin, decl_macro, custom_derive)] #![plugin(rocket_codegen)] -extern crate rocket; +#[macro_use] extern crate rocket; use std::io; -use rocket::request::{Form, FromFormValue}; +use rocket::request::Form; use rocket::response::NamedFile; use rocket::http::RawStr; #[cfg(test)] mod tests; // TODO: Make deriving `FromForm` for this enum possible. -#[derive(Debug)] +#[derive(Debug, FromFormValue)] enum FormOption { A, B, C } -impl<'v> FromFormValue<'v> for FormOption { - type Error = &'v RawStr; - - fn from_form_value(v: &'v RawStr) -> Result { - let variant = match v.as_str() { - "a" => FormOption::A, - "b" => FormOption::B, - "c" => FormOption::C, - _ => return Err(v) - }; - - Ok(variant) - } -} - #[derive(Debug, FromForm)] struct FormInput<'r> { checkbox: bool, diff --git a/examples/form_kitchen_sink/src/tests.rs b/examples/form_kitchen_sink/src/tests.rs index 4274d629..9be834de 100644 --- a/examples/form_kitchen_sink/src/tests.rs +++ b/examples/form_kitchen_sink/src/tests.rs @@ -131,9 +131,9 @@ fn check_semantically_invalid_forms() { form_vals[1] = "NaN"; assert_invalid_form(&client, &mut form_vals); - form_vals[2] = "A"; + form_vals[2] = "A?"; assert_invalid_form(&client, &mut form_vals); - form_vals[2] = "B"; + form_vals[2] = " B"; assert_invalid_form(&client, &mut form_vals); form_vals[2] = "d"; assert_invalid_form(&client, &mut form_vals); @@ -143,7 +143,7 @@ fn check_semantically_invalid_forms() { assert_invalid_form(&client, &mut form_vals); // password and textarea are always valid, so we skip them - form_vals[5] = "A"; + form_vals[5] = "A."; assert_invalid_form(&client, &mut form_vals); form_vals[5] = "b "; assert_invalid_form(&client, &mut form_vals); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b27b3597..750def7c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -18,6 +18,7 @@ categories = ["web-programming::http-server"] tls = ["rustls", "hyper-sync-rustls"] [dependencies] +rocket_codegen_next = { version = "0.4.0-dev", path = "../codegen_next" } yansi = "0.4" log = "0.4" percent-encoding = "1" @@ -32,10 +33,14 @@ pear = { git = "http://github.com/SergioBenitez/pear" } pear_codegen = "0.0" rustls = { version = "0.12.0", optional = true } hyper = { version = "0.10.13", default-features = false } -hyper-sync-rustls = { version = "=0.3.0-rc.2", features = ["server"], optional = true } indexmap = "1.0" isatty = "0.1" +[dependencies.hyper-sync-rustls] +version = "=0.3.0-rc.2" +features = ["server"] +optional = true + [dependencies.cookie] git = "https://github.com/alexcrichton/cookie-rs" rev = "8579b4b" diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 0f476c34..2beb44a5 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -96,6 +96,9 @@ //! module documentation](/rocket/local) and the [testing chapter of the //! guide](https://rocket.rs/guide/testing/#testing) include detailed examples. +#[allow(unused_imports)] #[macro_use] extern crate rocket_codegen_next; +#[doc(hidden)] pub use rocket_codegen_next::*; + #[macro_use] extern crate log; #[macro_use] extern crate pear; #[cfg(feature = "tls")] extern crate rustls;