Rocket

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

commit 7624aaf3e494080c74b312d10fb8d7702a5cb064
parent cee2f4439f9e8735f6c19af73d79d9354a9cd68a
Author: jeb <jeb@jebrosen.com>
Date:   Wed, 19 Sep 2018 21:47:58 -0700

Reimplement 'uri!' as a proc-macro.

Diffstat:
Mcore/codegen/src/lib.rs | 12------------
Dcore/codegen/src/macros/mod.rs | 14--------------
Dcore/codegen/src/macros/uri.rs | 214-------------------------------------------------------------------------------
Mcore/codegen/src/parser/mod.rs | 2--
Dcore/codegen/src/parser/uri_macro.rs | 264-------------------------------------------------------------------------------
Mcore/codegen/src/utils/mod.rs | 27---------------------------
Mcore/codegen/tests/typed-uris.rs | 2+-
Mcore/codegen/tests/ui/typed-uri-bad-type.rs | 4++--
Mcore/codegen/tests/ui/typed-uris-bad-params.rs | 4++--
Mcore/codegen/tests/ui/typed-uris-bad-params.stderr | 4++--
Mcore/codegen/tests/ui/typed-uris-invalid-syntax.rs | 4++--
Mcore/codegen_next/Cargo.toml | 1+
Mcore/codegen_next/src/bang/mod.rs | 15+++++++++++++++
Acore/codegen_next/src/bang/uri.rs | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/codegen_next/src/bang/uri_parsing.rs | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/codegen_next/src/lib.rs | 13++++++++++++-
16 files changed, 493 insertions(+), 543 deletions(-)

diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs @@ -399,7 +399,6 @@ extern crate indexmap; #[macro_use] mod utils; mod parser; -mod macros; mod decorators; use rustc_plugin::Registry; @@ -424,20 +423,9 @@ macro_rules! register_decorators { ) } -macro_rules! register_macros { - ($reg:expr, $($n:expr => $f:ident),+) => ( - $($reg.register_macro($n, macros::$f);)+ - ) -} - /// Compiler hook for Rust to register plugins. #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { - register_macros!(reg, - "uri" => uri, - "rocket_internal_uri" => uri_internal - ); - register_decorators!(reg, "route" => route_decorator, "get" => get_decorator, diff --git a/core/codegen/src/macros/mod.rs b/core/codegen/src/macros/mod.rs @@ -1,14 +0,0 @@ -mod uri; - -use utils::IdentExt; - -use syntax::ast::Path; - -pub use self::uri::{uri, uri_internal}; - -#[inline] -pub fn prefix_path(prefix: &str, path: &mut Path) { - let last = path.segments.len() - 1; - let last_seg = &mut path.segments[last]; - last_seg.ident = last_seg.ident.prepend(prefix); -} diff --git a/core/codegen/src/macros/uri.rs b/core/codegen/src/macros/uri.rs @@ -1,214 +0,0 @@ -use std::fmt::Display; - -use syntax::source_map::Span; -use syntax::ext::base::{DummyResult, ExtCtxt, MacEager, MacResult}; -use syntax::tokenstream::{TokenStream, TokenTree}; -use syntax::ast::{self, Expr, GenericArg, MacDelimiter, Ident}; -use syntax::symbol::Symbol; -use syntax::parse::PResult; -use syntax::ext::build::AstBuilder; -use syntax::ptr::P; - -use URI_INFO_MACRO_PREFIX; -use super::prefix_path; -use utils::{IdentExt, split_idents, ExprExt, option_as_expr}; -use parser::{UriParams, InternalUriParams, Validation}; - -use rocket_http::uri::Origin; -use rocket_http::ext::IntoOwned; - -// What gets called when `uri!` is invoked. This just invokes the internal URI -// macro which calls the `uri_internal` function below. -pub fn uri( - ecx: &mut ExtCtxt, - sp: Span, - args: &[TokenTree], -) -> Box<MacResult + 'static> { - // Generate the path to the internal macro. - let mut parser = ecx.new_parser_from_tts(args); - let (_, mut path) = try_parse!(sp, UriParams::parse_prelude(ecx, &mut parser)); - prefix_path(URI_INFO_MACRO_PREFIX, &mut path); - - // It's incredibly important we use `sp` as the Span for the generated code - // so that errors from the `internal` call show up on the user's code. - let expr = parser.mk_mac_expr(sp, - ast::Mac_ { - path, - delim: MacDelimiter::Parenthesis, - tts: args.to_vec().into_iter().collect::<TokenStream>().into(), - }, - ::syntax::ThinVec::new(), - ); - - MacEager::expr(expr) -} - -fn extract_exprs<'a>( - ecx: &ExtCtxt<'a>, - internal: &InternalUriParams, -) -> PResult<'a, Vec<P<ast::Expr>>> { - let route_name = &internal.uri_params.route_path; - match internal.validate() { - Validation::Ok(exprs) => Ok(exprs), - Validation::Unnamed(expected, actual) => { - let mut diag = ecx.struct_span_err(internal.uri_params.args_span(), - &format!("`{}` route uri expects {} but {} supplied", - route_name, p!(expected, "parameter"), p!(actual, "was"))); - - if expected > 0 { - let ps = p!("parameter", expected); - diag.note(&format!("expected {}: {}", ps, internal.fn_args_str())); - } - - Err(diag) - } - Validation::Named(missing, extra, dup) => { - let e = &format!("invalid parameters for `{}` route uri", route_name); - let mut diag = ecx.struct_span_err(internal.uri_params.args_span(), e); - diag.note(&format!("uri parameters are: {}", internal.fn_args_str())); - - fn join<S: Display, T: Iterator<Item = S>>(iter: T) -> (&'static str, String) { - let items: Vec<_> = iter.map(|i| format!("`{}`", i)).collect(); - (p!("parameter", items.len()), items.join(", ")) - } - - if !extra.is_empty() { - let (ps, msg) = join(extra.iter().map(|id| id.node)); - let spans: Vec<_> = extra.iter().map(|ident| ident.span).collect(); - diag.span_help(spans, &format!("unknown {}: {}", ps, msg)); - } - - if !dup.is_empty() { - let (ps, msg) = join(dup.iter().map(|id| id.node)); - let spans: Vec<_> = dup.iter().map(|ident| ident.span).collect(); - diag.span_help(spans, &format!("duplicate {}: {}", ps, msg)); - } - - if !missing.is_empty() { - let (ps, msg) = join(missing.iter()); - diag.help(&format!("missing {}: {}", ps, msg)); - } - - Err(diag) - } - } -} - -// Validates the mount path and the URI and returns a single Origin URI with -// both paths concatinated. Validation should always succeed since this macro -// can only be called if the route attribute succeed, which implies that the -// route URI was valid. -fn extract_origin<'a>( - ecx: &ExtCtxt<'a>, - internal: &InternalUriParams, -) -> PResult<'a, Origin<'static>> { - let base_uri = match internal.uri_params.mount_point { - Some(base) => Origin::parse(&base.node) - .map_err(|_| ecx.struct_span_err(base.span, "invalid path URI"))? - .into_owned(), - None => Origin::dummy() - }; - - Origin::parse_route(&format!("{}/{}", base_uri, internal.uri.node)) - .map(|o| o.to_normalized().into_owned()) - .map_err(|_| ecx.struct_span_err(internal.uri.span, "invalid route URI")) -} - -fn explode<I>(ecx: &ExtCtxt, route_str: &str, items: I) -> P<Expr> - where I: Iterator<Item = (ast::Ident, P<ast::Ty>, P<Expr>)> -{ - // Generate the statements to typecheck each parameter. - // Building <$T as ::rocket::http::uri::FromUriParam<_>>::from_uri_param($e). - let mut let_bindings = vec![]; - let mut fmt_exprs = vec![]; - for (mut ident, ty, expr) in items { - let (span, mut expr) = (expr.span, expr.clone()); - ident.span = span; - - // path for call: <T as FromUriParam<_>>::from_uri_param - let idents = split_idents("rocket::http::uri::FromUriParam"); - let generics = vec![GenericArg::Type(ecx.ty(span, ast::TyKind::Infer))]; - let trait_path = ecx.path_all(span, true, idents, generics, vec![]); - let method = Ident::new(Symbol::intern("from_uri_param"), span); - let (qself, path) = ecx.qpath(ty.clone(), trait_path, method); - - // replace &expr with [let tmp = expr; &tmp] so that borrows of - // temporary expressions live at least as long as the call to - // `from_uri_param`. Otherwise, exprs like &S { .. } won't compile. - let cloned_expr = expr.clone().into_inner(); - if let ast::ExprKind::AddrOf(_, inner) = cloned_expr.node { - // Only reassign temporary expressions, not locations. - if !inner.is_location() { - let tmp_ident = ident.append("_tmp"); - let tmp_stmt = ecx.stmt_let(span, false, tmp_ident, inner); - let_bindings.push(tmp_stmt); - expr = ecx.expr_ident(span, tmp_ident); - } - } - - // generating: let $ident = path($expr); - let path_expr = ecx.expr_qpath(span, qself, path); - let call = ecx.expr_call(span, path_expr, vec![expr]); - let stmt = ecx.stmt_let(span, false, ident, call); - debug!("Emitting URI typecheck statement: {:?}", stmt); - let_bindings.push(stmt); - - // generating: arg tokens for format string - let mut tokens = quote_tokens!(ecx, &$ident as &::rocket::http::uri::UriDisplay,); - tokens.iter_mut().for_each(|tree| tree.set_span(span)); - fmt_exprs.push(tokens); - } - - // Convert all of the '<...>' into '{}'. - let mut inside = false; - let fmt_string: String = route_str.chars().filter_map(|c| { - Some(match c { - '<' => { inside = true; '{' } - '>' => { inside = false; '}' } - _ if !inside => c, - _ => return None - }) - }).collect(); - - // Don't allocate if there are no formatting expressions. - if fmt_exprs.is_empty() { - quote_expr!(ecx, $fmt_string.into()) - } else { - quote_expr!(ecx, { $let_bindings format!($fmt_string, $fmt_exprs).into() }) - } -} - -#[allow(unused_imports)] -pub fn uri_internal( - ecx: &mut ExtCtxt, - sp: Span, - tt: &[TokenTree], -) -> Box<MacResult + 'static> { - // Parse the internal invocation and the user's URI param expressions. - let mut parser = ecx.new_parser_from_tts(tt); - let internal = try_parse!(sp, InternalUriParams::parse(ecx, &mut parser)); - let exprs = try_parse!(sp, extract_exprs(ecx, &internal)); - let origin = try_parse!(sp, extract_origin(ecx, &internal)); - - // Determine how many parameters there are in the URI path. - let path_param_count = origin.path().matches('<').count(); - - // Create an iterator over the `ident`, `ty`, and `expr` triple. - let mut arguments = internal.fn_args - .into_iter() - .zip(exprs.into_iter()) - .map(|((ident, ty), expr)| (ident, ty, expr)); - - // Generate an expression for both the path and query. - let path = explode(ecx, origin.path(), arguments.by_ref().take(path_param_count)); - let query = option_as_expr(ecx, &origin.query().map(|q| explode(ecx, q, arguments))); - - // Generate the final `Origin` expression. - let expr = quote_expr!(ecx, ::rocket::http::uri::Origin::new::< - ::std::borrow::Cow<'static, str>, - ::std::borrow::Cow<'static, str>, - >($path, $query)); - - debug!("Emitting URI expression: {:?}", expr); - MacEager::expr(expr) -} diff --git a/core/codegen/src/parser/mod.rs b/core/codegen/src/parser/mod.rs @@ -3,10 +3,8 @@ mod route; mod param; mod function; mod uri; -mod uri_macro; pub use self::keyvalue::KVSpanned; pub use self::route::RouteParams; pub use self::param::Param; pub use self::function::Function; -pub use self::uri_macro::{Args, InternalUriParams, UriParams, Validation}; diff --git a/core/codegen/src/parser/uri_macro.rs b/core/codegen/src/parser/uri_macro.rs @@ -1,264 +0,0 @@ -use utils::{self, ParserExt, SpanExt}; - -use syntax::source_map::{Spanned, Span}; -use syntax::ext::base::ExtCtxt; -use syntax::symbol::LocalInternedString; -use syntax::ast::{self, Expr, Name, Ident, Path}; -use syntax::parse::{SeqSep, PResult}; -use syntax::parse::token::{DelimToken, Token}; -use syntax::parse::parser::{Parser, PathStyle}; -use syntax::print::pprust::ty_to_string; -use syntax::ptr::P; - -use indexmap::IndexMap; - -#[derive(Debug)] -enum Arg { - Unnamed(P<Expr>), - Named(Spanned<Ident>, P<Expr>), -} - -#[derive(Debug)] -pub enum Args { - Unnamed(Vec<P<Expr>>), - Named(Vec<(Spanned<Ident>, P<Expr>)>), -} - -// For an invocation that looks like: -// uri!("/mount/point", this::route: e1, e2, e3); -// ^-------------| ^----------| ^---------| -// uri_params.mount_point | uri_params.arguments -// uri_params.route_path -// -#[derive(Debug)] -pub struct UriParams { - pub mount_point: Option<Spanned<LocalInternedString>>, - pub route_path: Path, - pub arguments: Option<Spanned<Args>>, -} - -// `fn_args` are the URI arguments (excluding guards) from the original route's -// handler in the order they were declared in the URI (`<first>/<second>`). -// `uri` is the full URI used in the origin route's attribute -#[derive(Debug)] -pub struct InternalUriParams { - pub uri: Spanned<String>, - pub fn_args: Vec<(ast::Ident, P<ast::Ty>)>, - pub uri_params: UriParams, -} - -impl Arg { - fn is_named(&self) -> bool { - match *self { - Arg::Named(..) => true, - Arg::Unnamed(_) => false, - } - } - - fn unnamed(self) -> P<Expr> { - match self { - Arg::Unnamed(expr) => expr, - _ => panic!("Called Arg::unnamed() on an Arg::named!"), - } - } - - fn named(self) -> (Spanned<Ident>, P<Expr>) { - match self { - Arg::Named(ident, expr) => (ident, expr), - _ => panic!("Called Arg::named() on an Arg::Unnamed!"), - } - } -} - -impl UriParams { - // Parses the mount point, if any, and route identifier. - pub fn parse_prelude<'a>( - ecx: &'a ExtCtxt, - parser: &mut Parser<'a> - ) -> PResult<'a, (Option<Spanned<LocalInternedString>>, Path)> { - if parser.token == Token::Eof { - return Err(ecx.struct_span_err(ecx.call_site(), - "call to `uri!` cannot be empty")); - } - - // Parse the mount point and suffixing ',', if any. - let mount_point = match parser.parse_optional_str() { - Some((symbol, _, _)) => { - let string = symbol.as_str(); - let span = parser.prev_span; - if string.contains('<') || !string.starts_with('/') { - let mut diag = ecx.struct_span_err(span, "invalid mount point"); - diag.help("mount points must be static, absolute URIs: `/example`"); - return Err(diag); - } - - parser.expect(&Token::Comma)?; - Some(span.wrap(string)) - } - None => None, - }; - - // Parse the route identifier, which must always exist. - let route_path = parser.parse_path(PathStyle::Mod)?; - Ok((mount_point, route_path)) - } - - /// The Span to use when referring to all of the arguments. - pub fn args_span(&self) -> Span { - match self.arguments { - Some(ref args) => args.span, - None => self.route_path.span - } - } - - pub fn parse<'a>( - ecx: &'a ExtCtxt, - parser: &mut Parser<'a> - ) -> PResult<'a, UriParams> { - // Parse the mount point and suffixing ',', if any. - let (mount_point, route_path) = Self::parse_prelude(ecx, parser)?; - - // If there are no arguments, finish early. - if !parser.eat(&Token::Colon) { - parser.expect(&Token::Eof)?; - let arguments = None; - return Ok(UriParams { mount_point, route_path, arguments, }); - } - - // Parse arguments. - let mut args_span = parser.span; - let comma = SeqSep::trailing_allowed(Token::Comma); - let arguments = parser.parse_seq_to_end(&Token::Eof, comma, |parser| { - let has_key = parser.look_ahead(1, |token| *token == Token::Eq); - - if has_key { - let inner_ident = parser.parse_ident()?; - let ident = parser.prev_span.wrap(inner_ident); - parser.expect(&Token::Eq)?; - - let expr = parser.parse_expr()?; - Ok(Arg::Named(ident, expr)) - } else { - let expr = parser.parse_expr()?; - Ok(Arg::Unnamed(expr)) - } - })?; - - // Set the end of the args_span to be the end of the args. - args_span = args_span.with_hi(parser.prev_span.hi()); - - // A 'colon' was used but there are no arguments. - if arguments.is_empty() { - return Err(ecx.struct_span_err(parser.prev_span, - "expected argument list after `:`")); - } - - // Ensure that both types of arguments were not used at once. - let (mut homogeneous_args, mut prev_named) = (true, None); - for arg in &arguments { - match prev_named { - Some(prev_named) => homogeneous_args = prev_named == arg.is_named(), - None => prev_named = Some(arg.is_named()), - } - } - - if !homogeneous_args { - return Err(ecx.struct_span_err(args_span, - "named and unnamed parameters cannot be mixed")); - } - - // Create the `Args` enum, which properly types one-kind-of-argument-ness. - let args = if prev_named.unwrap() { - Args::Named(arguments.into_iter().map(|arg| arg.named()).collect()) - } else { - Args::Unnamed(arguments.into_iter().map(|arg| arg.unnamed()).collect()) - }; - - let arguments = Some(args_span.wrap(args)); - Ok(UriParams { mount_point, route_path, arguments, }) - } -} - -pub enum Validation { - // Number expected, what we actually got. - Unnamed(usize, usize), - // (Missing, Extra, Duplicate) - Named(Vec<Name>, Vec<Spanned<Ident>>, Vec<Spanned<Ident>>), - // Everything is okay. - Ok(Vec<P<Expr>>) -} - -impl InternalUriParams { - pub fn parse<'a>( - ecx: &'a ExtCtxt, - parser: &mut Parser<'a>, - ) -> PResult<'a, InternalUriParams> { - let uri_str = parser.parse_str_lit().map(|(s, _)| s.as_str().to_string())?; - let uri = parser.prev_span.wrap(uri_str); - parser.expect(&Token::Comma)?; - - let start = Token::OpenDelim(DelimToken::Paren); - let end = Token::CloseDelim(DelimToken::Paren); - let comma = SeqSep::trailing_allowed(Token::Comma); - let fn_args = parser - .parse_seq(&start, &end, comma, |parser| { - let param = parser.parse_ident_inc_pat()?; - parser.expect(&Token::Colon)?; - let ty = utils::strip_ty_lifetimes(parser.parse_ty()?); - Ok((param, ty)) - })? - .node; - - parser.expect(&Token::Comma)?; - let uri_params = UriParams::parse(ecx, parser)?; - Ok(InternalUriParams { uri, fn_args, uri_params, }) - } - - pub fn fn_args_str(&self) -> String { - self.fn_args.iter() - .map(|&(ident, ref ty)| format!("{}: {}", ident.name, ty_to_string(&ty))) - .collect::<Vec<_>>() - .join(", ") - } - - pub fn validate(&self) -> Validation { - let unnamed = |args: &Vec<P<Expr>>| -> Validation { - let (expected, actual) = (self.fn_args.len(), args.len()); - if expected != actual { Validation::Unnamed(expected, actual) } - else { Validation::Ok(args.clone()) } - }; - - match self.uri_params.arguments { - None => unnamed(&vec![]), - Some(Spanned { node: Args::Unnamed(ref args), .. }) => unnamed(args), - Some(Spanned { node: Args::Named(ref args), .. }) => { - let mut params: IndexMap<Name, Option<P<Expr>>> = self.fn_args.iter() - .map(|&(ident, _)| (ident.name, None)) - .collect(); - - let (mut extra, mut dup) = (vec![], vec![]); - for &(ident, ref expr) in args { - match params.get_mut(&ident.node.name) { - Some(ref entry) if entry.is_some() => dup.push(ident), - Some(entry) => *entry = Some(expr.clone()), - None => extra.push(ident), - } - } - - let (mut missing, mut exprs) = (vec![], vec![]); - for (name, expr) in params { - match expr { - Some(expr) => exprs.push(expr), - None => missing.push(name) - } - } - - if (extra.len() + dup.len() + missing.len()) == 0 { - Validation::Ok(exprs) - } else { - Validation::Named(missing, extra, dup) - } - } - } - } -} diff --git a/core/codegen/src/utils/mod.rs b/core/codegen/src/utils/mod.rs @@ -141,10 +141,6 @@ pub fn is_valid_ident<S: AsRef<str>>(s: S) -> bool { true } -pub fn split_idents(path: &str) -> Vec<Ident> { - path.split("::").map(|segment| Ident::from_str(segment)).collect() -} - macro_rules! quote_enum { ($ecx:expr, $var:expr => $(::$from_root:ident)+ -> $(::$to_root:ident)+ { $($variant:ident),+ ; $($extra:pat => $result:expr),* }) => ({ @@ -163,26 +159,3 @@ macro_rules! quote_enum { } }) } - -macro_rules! try_parse { - ($sp:expr, $parse:expr) => ( - match $parse { - Ok(v) => v, - Err(mut e) => { e.emit(); return DummyResult::expr($sp); } - } - ) -} - -macro_rules! p { - ("parameter", $num:expr) => ( - if $num == 1 { "parameter" } else { "parameters" } - ); - - ($num:expr, "was") => ( - if $num == 1 { "1 was".into() } else { format!("{} were", $num) } - ); - - ($num:expr, "parameter") => ( - if $num == 1 { "1 parameter".into() } else { format!("{} parameters", $num) } - ) -} diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs @@ -1,4 +1,4 @@ -#![feature(plugin, decl_macro)] +#![feature(plugin, decl_macro, proc_macro_non_items)] #![plugin(rocket_codegen)] #![allow(dead_code, unused_variables)] diff --git a/core/codegen/tests/ui/typed-uri-bad-type.rs b/core/codegen/tests/ui/typed-uri-bad-type.rs @@ -1,8 +1,8 @@ -#![feature(plugin, decl_macro)] +#![feature(plugin, decl_macro, proc_macro_non_items)] #![plugin(rocket_codegen)] #![allow(dead_code, unused_variables)] -extern crate rocket; +#[macro_use] extern crate rocket; use rocket::http::RawStr; use rocket::request::FromParam; diff --git a/core/codegen/tests/ui/typed-uris-bad-params.rs b/core/codegen/tests/ui/typed-uris-bad-params.rs @@ -1,8 +1,8 @@ -#![feature(plugin, decl_macro)] +#![feature(plugin, decl_macro, proc_macro_non_items)] #![plugin(rocket_codegen)] #![allow(dead_code, unused_variables)] -extern crate rocket; +#[macro_use] extern crate rocket; use std::fmt; diff --git a/core/codegen/tests/ui/typed-uris-bad-params.stderr b/core/codegen/tests/ui/typed-uris-bad-params.stderr @@ -18,7 +18,7 @@ error: `simple` route uri expects 1 parameter but 2 were supplied --> $DIR/typed-uris-bad-params.rs:20:18 | 20 | uri!(simple: "Hello", 23, ); - | ^^^^^^^^^^^^ + | ^^^^^^^^^^^ | = note: expected parameter: id: i32 @@ -73,7 +73,7 @@ error: invalid parameters for `simple` route uri --> $DIR/typed-uris-bad-params.rs:26:18 | 26 | uri!(simple: id = 100, id = 100, ); - | ^^^^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: duplicate parameter: `id` diff --git a/core/codegen/tests/ui/typed-uris-invalid-syntax.rs b/core/codegen/tests/ui/typed-uris-invalid-syntax.rs @@ -1,8 +1,8 @@ -#![feature(plugin, decl_macro)] +#![feature(plugin, decl_macro, proc_macro_non_items)] #![plugin(rocket_codegen)] #![allow(dead_code, unused_variables)] -extern crate rocket; +#[macro_use] extern crate rocket; #[post("/<id>/<name>")] fn simple(id: i32, name: String) -> &'static str { "" } diff --git a/core/codegen_next/Cargo.toml b/core/codegen_next/Cargo.toml @@ -17,6 +17,7 @@ publish = false proc-macro = true [dependencies] +indexmap = "1.0" quote = "0.6.1" rocket_http = { version = "0.4.0-dev", path = "../http/" } diff --git a/core/codegen_next/src/bang/mod.rs b/core/codegen_next/src/bang/mod.rs @@ -5,6 +5,9 @@ use derive_utils::{syn, Spanned, Result}; use self::syn::{Path, punctuated::Punctuated, parse::Parser, token::Comma}; use syn_ext::{IdentExt, syn_to_diag}; +mod uri; +mod uri_parsing; + fn _prefixed_vec(prefix: &str, input: TokenStream, ty: &TokenStream2) -> Result<TokenStream2> { // Parse a comma-separated list of paths. let mut paths = <Punctuated<Path, Comma>>::parse_terminated @@ -46,3 +49,15 @@ pub static CATCH_STRUCT_PREFIX: &str = "static_rocket_catch_info_for_"; pub fn catchers_macro(input: TokenStream) -> TokenStream { prefixed_vec(CATCH_STRUCT_PREFIX, input, quote!(::rocket::Catcher)) } + +pub fn uri_macro(input: TokenStream) -> TokenStream { + uri::_uri_macro(input) + .map_err(|diag| diag.emit()) + .unwrap_or_else(|_| quote!(()).into()) +} + +pub fn uri_internal_macro(input: TokenStream) -> TokenStream { + uri::_uri_internal_macro(input) + .map_err(|diag| diag.emit()) + .unwrap_or_else(|_| quote!(()).into()) +} diff --git a/core/codegen_next/src/bang/uri.rs b/core/codegen_next/src/bang/uri.rs @@ -0,0 +1,186 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use std::fmt::Display; + +use derive_utils::{syn, Result}; +use quote::ToTokens; +use syn_ext::{IdentExt, syn_to_diag}; + +use self::syn::{Expr, Ident, Type}; +use self::syn::spanned::Spanned as SynSpanned; +use super::uri_parsing::*; + +use rocket_http::uri::Origin; +use rocket_http::ext::IntoOwned; + +const URI_INFO_MACRO_PREFIX: &str = "rocket_uri_for_"; + +crate fn _uri_macro(input: TokenStream) -> Result<TokenStream> { + let args: TokenStream2 = input.clone().into(); + + let params = match syn::parse::<UriParams>(input) { + Ok(p) => p, + Err(e) => return Err(syn_to_diag(e)), + }; + let mut path = params.route_path; + { + let mut last_seg = path.segments.last_mut().expect("last path segment"); + last_seg.value_mut().ident = last_seg.value().ident.prepend(URI_INFO_MACRO_PREFIX); + } + + // It's incredibly important we use this span as the Span for the generated + // code so that errors from the `internal` call show up on the user's code. + Ok(quote_spanned!(args.span().into() => { + #path!(#args) + }).into()) +} + +macro_rules! p { + ("parameter", $num:expr) => ( + if $num == 1 { "parameter" } else { "parameters" } + ); + + ($num:expr, "was") => ( + if $num == 1 { "1 was".into() } else { format!("{} were", $num) } + ); + + ($num:expr, "parameter") => ( + if $num == 1 { "1 parameter".into() } else { format!("{} parameters", $num) } + ) +} + +fn extract_exprs(internal: &InternalUriParams) -> Result<Vec<Expr>> { + let route_name = &internal.uri_params.route_path; + match internal.validate() { + Validation::Ok(exprs) => Ok(exprs), + Validation::Unnamed(expected, actual) => { + let mut diag = internal.uri_params.args_span().error( + format!("`{}` route uri expects {} but {} supplied", + route_name.clone().into_token_stream(), p!(expected, "parameter"), p!(actual, "was"))); + + if expected > 0 { + let ps = p!("parameter", expected); + diag = diag.note(format!("expected {}: {}", ps, internal.fn_args_str())); + } + + Err(diag) + } + Validation::Named(missing, extra, dup) => { + let e = format!("invalid parameters for `{}` route uri", route_name.clone().into_token_stream()); + let mut diag = internal.uri_params.args_span().error(e); + diag = diag.note(format!("uri parameters are: {}", internal.fn_args_str())); + + fn join<S: Display, T: Iterator<Item = S>>(iter: T) -> (&'static str, String) { + let items: Vec<_> = iter.map(|i| format!("`{}`", i)).collect(); + (p!("parameter", items.len()), items.join(", ")) + } + + if !extra.is_empty() { + let (ps, msg) = join(extra.iter()); + let spans: Vec<_> = extra.iter().map(|ident| ident.span().unstable()).collect(); + diag = diag.span_help(spans, format!("unknown {}: {}", ps, msg)); + } + + if !dup.is_empty() { + let (ps, msg) = join(dup.iter()); + let spans: Vec<_> = dup.iter().map(|ident| ident.span().unstable()).collect(); + diag = diag.span_help(spans, format!("duplicate {}: {}", ps, msg)); + } + + if !missing.is_empty() { + let (ps, msg) = join(missing.iter()); + diag = diag.help(format!("missing {}: {}", ps, msg)); + } + + Err(diag) + } + } +} + +// Validates the mount path and the URI and returns a single Origin URI with +// both paths concatinated. Validation should always succeed since this macro +// can only be called if the route attribute succeed, which implies that the +// route URI was valid. +fn extract_origin(internal: &InternalUriParams) -> Result<Origin<'static>> { + let base_uri = match internal.uri_params.mount_point { + Some(ref base) => Origin::parse(&base.value()) + .map_err(|_| base.span().unstable().error("invalid path URI"))? + .into_owned(), + None => Origin::dummy() + }; + + Origin::parse_route(&format!("{}/{}", base_uri, internal.uri)) + .map(|o| o.to_normalized().into_owned()) + .map_err(|_| internal.uri.span().unstable().error("invalid route URI")) +} + +fn explode<I>(route_str: &str, items: I) -> TokenStream2 + where I: Iterator<Item = (Ident, Type, Expr)> +{ + // Generate the statements to typecheck each parameter. + // Building <$T as ::rocket::http::uri::FromUriParam<_>>::from_uri_param($e). + let mut let_bindings = vec![]; + let mut fmt_exprs = vec![]; + + for (mut ident, ty, expr) in items { + let (span, mut expr) = (expr.span(), expr.clone()); + ident.set_span(span); + let ident_tmp = ident.prepend("tmp"); + + let_bindings.push(quote_spanned!(span => + let #ident_tmp = #expr; let #ident = <#ty as ::rocket::http::uri::FromUriParam<_>>::from_uri_param(#ident_tmp); + )); + + // generating: arg tokens for format string + fmt_exprs.push(quote_spanned!(span => { &#ident as &::rocket::http::uri::UriDisplay })); + } + + // Convert all of the '<...>' into '{}'. + let mut inside = false; + let fmt_string: String = route_str.chars().filter_map(|c| { + Some(match c { + '<' => { inside = true; '{' } + '>' => { inside = false; '}' } + _ if !inside => c, + _ => return None + }) + }).collect(); + + // Don't allocate if there are no formatting expressions. + if fmt_exprs.is_empty() { + quote!({ #fmt_string.into() }) + } else { + quote!({ #(#let_bindings)* format!(#fmt_string, #(#fmt_exprs),*).into() }) + } +} + +crate fn _uri_internal_macro(input: TokenStream) -> Result<TokenStream> { + // Parse the internal invocation and the user's URI param expressions. + let internal = syn::parse::<InternalUriParams>(input).map_err(syn_to_diag)?; + let exprs = extract_exprs(&internal)?; + let origin = extract_origin(&internal)?; + + // Determine how many parameters there are in the URI path. + let path_param_count = origin.path().matches('<').count(); + + // Create an iterator over the `ident`, `ty`, and `expr` triple. + let mut arguments = internal.fn_args + .into_iter() + .zip(exprs.into_iter()) + .map(|(FnArg { ident, ty }, expr)| (ident, ty, expr)); + + // Generate an expression for both the path and query. + let path = explode(origin.path(), arguments.by_ref().take(path_param_count)); + let query = if let Some(expr) = origin.query().map(|q| explode(q, arguments)) { + quote!({ Some(#expr) }) + } else { + quote!({ None }) + }; + + Ok(quote!({ + ::rocket::http::uri::Origin::new::< + ::std::borrow::Cow<'static, str>, + ::std::borrow::Cow<'static, str>, + >(#path, #query) + }).into()) +} diff --git a/core/codegen_next/src/bang/uri_parsing.rs b/core/codegen_next/src/bang/uri_parsing.rs @@ -0,0 +1,270 @@ +use proc_macro::Span; + +use derive_utils::syn; +use derive_utils::ext::TypeExt; +use quote::ToTokens; + +use self::syn::{Expr, Ident, LitStr, Path, Token, Type}; +use self::syn::spanned::Spanned as SynSpanned; +use self::syn::parse::{self, Parse, ParseStream}; +use self::syn::punctuated::Punctuated; + +use indexmap::IndexMap; + +#[derive(Debug)] +enum Arg { + Unnamed(Expr), + Named(Ident, Expr), +} + +#[derive(Debug)] +pub enum Args { + Unnamed(Vec<Expr>), + Named(Vec<(Ident, Expr)>), +} + +// For an invocation that looks like: +// uri!("/mount/point", this::route: e1, e2, e3); +// ^-------------| ^----------| ^---------| +// uri_params.mount_point | uri_params.arguments +// uri_params.route_path +// +#[derive(Debug)] +pub struct UriParams { + pub mount_point: Option<LitStr>, + pub route_path: Path, + pub arguments: Option<Args>, +} + +#[derive(Debug)] +pub struct FnArg { + pub ident: Ident, + pub ty: Type, +} + +pub enum Validation { + // Number expected, what we actually got. + Unnamed(usize, usize), + // (Missing, Extra, Duplicate) + Named(Vec<Ident>, Vec<Ident>, Vec<Ident>), + // Everything is okay. + Ok(Vec<Expr>) +} + +// `fn_args` are the URI arguments (excluding guards) from the original route's +// handler in the order they were declared in the URI (`<first>/<second>`). +// `uri` is the full URI used in the origin route's attribute +#[derive(Debug)] +pub struct InternalUriParams { + pub uri: String, + pub fn_args: Vec<FnArg>, + pub uri_params: UriParams, +} + +impl Arg { + fn is_named(&self) -> bool { + match *self { + Arg::Named(..) => true, + Arg::Unnamed(_) => false, + } + } + + fn unnamed(self) -> Expr { + match self { + Arg::Unnamed(expr) => expr, + _ => panic!("Called Arg::unnamed() on an Arg::named!"), + } + } + + fn named(self) -> (Ident, Expr) { + match self { + Arg::Named(ident, expr) => (ident, expr), + _ => panic!("Called Arg::named() on an Arg::Unnamed!"), + } + } +} + +impl UriParams { + /// The Span to use when referring to all of the arguments. + pub fn args_span(&self) -> Span { + match self.arguments { + Some(ref args) => { + let (first, last) = match args { + Args::Unnamed(ref exprs) => { + ( + exprs.first().unwrap().span().unstable(), + exprs.last().unwrap().span().unstable() + ) + }, + Args::Named(ref pairs) => { + ( + pairs.first().unwrap().0.span().unstable(), + pairs.last().unwrap().1.span().unstable() + ) + }, + }; + first.join(last).expect("join spans") + }, + None => self.route_path.span().unstable(), + } + } +} + +impl Parse for Arg { + fn parse(input: ParseStream) -> parse::Result<Self> { + let has_key = input.peek2(Token![=]); + if has_key { + let ident = input.parse::<Ident>()?; + input.parse::<Token![=]>()?; + let expr = input.parse::<Expr>()?; + Ok(Arg::Named(ident, expr)) + } else { + let expr = input.parse::<Expr>()?; + Ok(Arg::Unnamed(expr)) + } + } +} + +impl Parse for UriParams { + // Parses the mount point, if any, route identifier, and arguments. + fn parse(input: ParseStream) -> parse::Result<Self> { + if input.is_empty() { + return Err(input.error("call to `uri!` cannot be empty")); + } + + // Parse the mount point and suffixing ',', if any. + let mount_point = if input.peek(LitStr) { + let string = input.parse::<LitStr>()?; + let value = string.value(); + if value.contains('<') || !value.starts_with('/') { + return Err(parse::Error::new(string.span(), "invalid mount point; mount points must be static, absolute URIs: `/example`")); + } + input.parse::<Token![,]>()?; + Some(string) + } else { + None + }; + + // Parse the route identifier, which must always exist. + let route_path = input.parse::<Path>()?; + + // If there are no arguments, finish early. + if !input.peek(Token![:]) { + let arguments = None; + return Ok(Self { mount_point, route_path, arguments }); + } + + let colon = input.parse::<Token![:]>()?; + + // Parse arguments + let args_start = input.cursor(); + let arguments: Punctuated<Arg, Token![,]> = input.parse_terminated(Arg::parse)?; + + // A 'colon' was used but there are no arguments. + if arguments.is_empty() { + return Err(parse::Error::new(colon.span(), "expected argument list after `:`")); + } + + // Ensure that both types of arguments were not used at once. + let (mut homogeneous_args, mut prev_named) = (true, None); + for arg in &arguments { + match prev_named { + Some(prev_named) => homogeneous_args = prev_named == arg.is_named(), + None => prev_named = Some(arg.is_named()), + } + } + + if !homogeneous_args { + // TODO: This error isn't showing up with the right span. + return Err(parse::Error::new(args_start.token_stream().span(), "named and unnamed parameters cannot be mixed")); + } + + // Create the `Args` enum, which properly types one-kind-of-argument-ness. + let args = if prev_named.unwrap() { + Args::Named(arguments.into_iter().map(|arg| arg.named()).collect()) + } else { + Args::Unnamed(arguments.into_iter().map(|arg| arg.unnamed()).collect()) + }; + + let arguments = Some(args); + Ok(Self { mount_point, route_path, arguments }) + } +} + +impl Parse for FnArg { + fn parse(input: ParseStream) -> parse::Result<FnArg> { + let ident = input.parse::<Ident>()?; + input.parse::<Token![:]>()?; + let mut ty = input.parse::<Type>()?; + ty.strip_lifetimes(); + Ok(FnArg { ident, ty }) + } +} + +impl Parse for InternalUriParams { + fn parse(input: ParseStream) -> parse::Result<InternalUriParams> { + let uri = input.parse::<LitStr>()?.value(); + //let uri = parser.prev_span.wrap(uri_str); + input.parse::<Token![,]>()?; + + let content; + syn::parenthesized!(content in input); + let fn_args: Punctuated<FnArg, Token![,]> = content.parse_terminated(FnArg::parse)?; + let fn_args = fn_args.into_iter().collect(); + + input.parse::<Token![,]>()?; + let uri_params = input.parse::<UriParams>()?; + Ok(InternalUriParams { uri, fn_args, uri_params }) + } +} + +impl InternalUriParams { + pub fn fn_args_str(&self) -> String { + self.fn_args.iter() + .map(|&FnArg { ref ident, ref ty }| format!("{}: {}", ident, ty.clone().into_token_stream().to_string().trim())) + .collect::<Vec<_>>() + .join(", ") + } + + pub fn validate(&self) -> Validation { + let unnamed = |args: &Vec<Expr>| -> Validation { + let (expected, actual) = (self.fn_args.len(), args.len()); + if expected != actual { Validation::Unnamed(expected, actual) } + else { Validation::Ok(args.clone()) } + }; + + match self.uri_params.arguments { + None => unnamed(&vec![]), + Some(Args::Unnamed(ref args)) => unnamed(args), + Some(Args::Named(ref args)) => { + let mut params: IndexMap<Ident, Option<Expr>> = self.fn_args.iter() + .map(|&FnArg { ref ident, .. }| (ident.clone(), None)) + .collect(); + + let (mut extra, mut dup) = (vec![], vec![]); + for &(ref ident, ref expr) in args { + match params.get_mut(ident) { + Some(ref entry) if entry.is_some() => dup.push(ident.clone()), + Some(entry) => *entry = Some(expr.clone()), + None => extra.push(ident.clone()), + } + } + + let (mut missing, mut exprs) = (vec![], vec![]); + for (name, expr) in params { + match expr { + Some(expr) => exprs.push(expr), + None => missing.push(name) + } + } + + if (extra.len() + dup.len() + missing.len()) == 0 { + Validation::Ok(exprs) + } else { + Validation::Named(missing, extra, dup) + } + } + } + } +} + diff --git a/core/codegen_next/src/lib.rs b/core/codegen_next/src/lib.rs @@ -1,9 +1,10 @@ -#![feature(proc_macro_diagnostic)] +#![feature(proc_macro_diagnostic, proc_macro_span)] #![feature(crate_visibility_modifier)] #![recursion_limit="128"] #[macro_use] extern crate quote; #[macro_use] extern crate derive_utils; +extern crate indexmap; extern crate proc_macro; extern crate rocket_http; @@ -46,3 +47,13 @@ pub fn routes(input: TokenStream) -> TokenStream { pub fn catchers(input: TokenStream) -> TokenStream { bang::catchers_macro(input) } + +#[proc_macro] +pub fn uri(input: TokenStream) -> TokenStream { + bang::uri_macro(input) +} + +#[proc_macro] +pub fn rocket_internal_uri(input: TokenStream) -> TokenStream { + bang::uri_internal_macro(input) +}