From 046b7a142595af3714c2b8221a6eec69361e46fc Mon Sep 17 00:00:00 2001 From: Douman Date: Sun, 31 Mar 2019 10:23:15 +0300 Subject: [PATCH] Expand codegen to allow specify guards and async --- actix-web-codegen/Cargo.toml | 3 +- actix-web-codegen/src/lib.rs | 170 +++++++++++--------------- actix-web-codegen/src/route.rs | 159 ++++++++++++++++++++++++ actix-web-codegen/tests/test_macro.rs | 33 ++++- 4 files changed, 266 insertions(+), 99 deletions(-) create mode 100644 actix-web-codegen/src/route.rs diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index da2640760..4d8c0910e 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -18,4 +18,5 @@ syn = { version = "0.15", features = ["full", "parsing"] } [dev-dependencies] actix-web = { version = "1.0.0-alpha.2" } actix-http = { version = "0.1.0-alpha.2", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } \ No newline at end of file +actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } +futures = { version = "0.1" } diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 16123930a..70cde90e4 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,118 +1,94 @@ #![recursion_limit = "512"] +//! Actix-web codegen module +//! +//! Generators for routes and scopes +//! +//! ## Route +//! +//! Macros: +//! +//! - [get](attr.get.html) +//! - [post](attr.post.html) +//! - [put](attr.put.html) +//! - [delete](attr.delete.html) +//! +//! ### Attributes: +//! +//! - `"path"` - Raw literal string with path for which to register handle. Mandatory. +//! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` +//! +//! ## Notes +//! +//! Function name can be specified as any expression that is going to be accessible to the generate +//! code (e.g `my_guard` or `my_module::my_guard`) +//! +//! ## Example: +//! +//! ```rust +//! use actix_web::HttpResponse; +//! use actix_web_codegen::get; +//! use futures::{future, Future}; +//! +//! #[get("/test")] +//! fn async_test() -> impl Future { +//! future::ok(HttpResponse::Ok().finish()) +//! } +//! ``` extern crate proc_macro; +mod route; + use proc_macro::TokenStream; -use quote::quote; use syn::parse_macro_input; -/// #[get("path")] attribute +/// Creates route handler with `GET` method guard. +/// +/// Syntax: `#[get("path"[, attributes])]` +/// +/// ## Attributes: +/// +/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. +/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` #[proc_macro_attribute] pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - if args.is_empty() { - panic!("invalid server definition, expected: #[get(\"some path\")]"); - } - - // path - let path = match args[0] { - syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { - let fname = quote!(#fname).to_string(); - fname.as_str()[1..fname.len() - 1].to_owned() - } - _ => panic!("resource path"), - }; - - let ast: syn::ItemFn = syn::parse(input).unwrap(); - let name = ast.ident.clone(); - - (quote! { - #[allow(non_camel_case_types)] - struct #name; - - impl actix_web::dev::HttpServiceFactory

for #name { - fn register(self, config: &mut actix_web::dev::ServiceConfig

) { - #ast - actix_web::dev::HttpServiceFactory::register( - actix_web::Resource::new(#path) - .guard(actix_web::guard::Get()) - .to(#name), config); - } - } - }) - .into() + let gen = route::Args::new(&args, input, route::GuardType::Get); + gen.generate() } -/// #[post("path")] attribute +/// Creates route handler with `POST` method guard. +/// +/// Syntax: `#[post("path"[, attributes])]` +/// +/// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - if args.is_empty() { - panic!("invalid server definition, expected: #[post(\"some path\")]"); - } - - // path - let path = match args[0] { - syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { - let fname = quote!(#fname).to_string(); - fname.as_str()[1..fname.len() - 1].to_owned() - } - _ => panic!("resource path"), - }; - - let ast: syn::ItemFn = syn::parse(input).unwrap(); - let name = ast.ident.clone(); - - (quote! { - #[allow(non_camel_case_types)] - struct #name; - - impl actix_web::dev::HttpServiceFactory

for #name { - fn register(self, config: &mut actix_web::dev::ServiceConfig

) { - #ast - actix_web::dev::HttpServiceFactory::register( - actix_web::Resource::new(#path) - .guard(actix_web::guard::Post()) - .to(#name), config); - } - } - }) - .into() + let gen = route::Args::new(&args, input, route::GuardType::Post); + gen.generate() } -/// #[put("path")] attribute +/// Creates route handler with `PUT` method guard. +/// +/// Syntax: `#[put("path"[, attributes])]` +/// +/// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - if args.is_empty() { - panic!("invalid server definition, expected: #[put(\"some path\")]"); - } - - // path - let path = match args[0] { - syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { - let fname = quote!(#fname).to_string(); - fname.as_str()[1..fname.len() - 1].to_owned() - } - _ => panic!("resource path"), - }; - - let ast: syn::ItemFn = syn::parse(input).unwrap(); - let name = ast.ident.clone(); - - (quote! { - #[allow(non_camel_case_types)] - struct #name; - - impl actix_web::dev::HttpServiceFactory

for #name { - fn register(self, config: &mut actix_web::dev::ServiceConfig

) { - #ast - actix_web::dev::HttpServiceFactory::register( - actix_web::Resource::new(#path) - .guard(actix_web::guard::Put()) - .to(#name), config); - } - } - }) - .into() + let gen = route::Args::new(&args, input, route::GuardType::Put); + gen.generate() +} + +/// Creates route handler with `DELETE` method guard. +/// +/// Syntax: `#[delete("path"[, attributes])]` +/// +/// Attributes are the same as in [get](attr.get.html) +#[proc_macro_attribute] +pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as syn::AttributeArgs); + let gen = route::Args::new(&args, input, route::GuardType::Delete); + gen.generate() } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs new file mode 100644 index 000000000..588debf80 --- /dev/null +++ b/actix-web-codegen/src/route.rs @@ -0,0 +1,159 @@ +extern crate proc_macro; + +use std::fmt; + +use proc_macro::TokenStream; +use quote::{quote}; + +enum ResourceType { + Async, + Sync, +} + +impl fmt::Display for ResourceType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &ResourceType::Async => write!(f, "to_async"), + &ResourceType::Sync => write!(f, "to"), + } + } +} + +#[derive(PartialEq)] +pub enum GuardType { + Get, + Post, + Put, + Delete, +} + +impl fmt::Display for GuardType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &GuardType::Get => write!(f, "Get"), + &GuardType::Post => write!(f, "Post"), + &GuardType::Put => write!(f, "Put"), + &GuardType::Delete => write!(f, "Delete"), + } + } +} + +pub struct Args { + name: syn::Ident, + path: String, + ast: syn::ItemFn, + resource_type: ResourceType, + pub guard: GuardType, + pub extra_guards: Vec, +} + +impl fmt::Display for Args { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let ast = &self.ast; + let guards = format!(".guard(actix_web::guard::{}())", self.guard); + let guards = self.extra_guards.iter().fold(guards, |acc, val| format!("{}.guard(actix_web::guard::fn_guard({}))", acc, val)); + + write!(f, " +#[allow(non_camel_case_types)] +pub struct {name}; + +impl actix_web::dev::HttpServiceFactory

for {name} {{ + fn register(self, config: &mut actix_web::dev::ServiceConfig

) {{ + {ast} + + let resource = actix_web::Resource::new(\"{path}\"){guards}.{to}({name}); + + actix_web::dev::HttpServiceFactory::register(resource, config) + }} +}}", name=self.name, ast=quote!(#ast), path=self.path, guards=guards, to=self.resource_type) + } +} + +fn guess_resource_type(typ: &syn::Type) -> ResourceType { + let mut guess = ResourceType::Sync; + + match typ { + syn::Type::ImplTrait(typ) => for bound in typ.bounds.iter() { + match bound { + syn::TypeParamBound::Trait(bound) => { + for bound in bound.path.segments.iter() { + if bound.ident == "Future" { + guess = ResourceType::Async; + break; + } else if bound.ident == "Responder" { + guess = ResourceType::Sync; + break; + } + } + }, + _ => (), + } + }, + _ => (), + } + + guess + +} + +impl Args { + pub fn new(args: &Vec, input: TokenStream, guard: GuardType) -> Self { + if args.is_empty() { + panic!("invalid server definition, expected: #[{}(\"some path\")]", guard); + } + + let ast: syn::ItemFn = syn::parse(input).expect("Parse input as function"); + let name = ast.ident.clone(); + + let mut extra_guards = Vec::new(); + let mut path = None; + for arg in args { + match arg { + syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { + if path.is_some() { + panic!("Multiple paths specified! Should be only one!") + } + let fname = quote!(#fname).to_string(); + path = Some(fname.as_str()[1..fname.len() - 1].to_owned()) + }, + syn::NestedMeta::Meta(syn::Meta::NameValue(ident)) => match ident.ident.to_string().to_lowercase().as_str() { + "guard" => match ident.lit { + syn::Lit::Str(ref text) => extra_guards.push(text.value()), + _ => panic!("Attribute guard expects literal string!"), + }, + attr => panic!("Unknown attribute key is specified: {}. Allowed: guard", attr) + }, + attr => panic!("Unknown attribute{:?}", attr) + } + } + + let resource_type = if ast.asyncness.is_some() { + ResourceType::Async + } else { + match ast.decl.output { + syn::ReturnType::Default => panic!("Function {} has no return type. Cannot be used as handler"), + syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()), + } + }; + + let path = path.unwrap(); + + Self { + name, + path, + ast, + resource_type, + guard, + extra_guards, + } + } + + pub fn generate(&self) -> TokenStream { + let text = self.to_string(); + + match text.parse() { + Ok(res) => res, + Err(error) => panic!("Error: {:?}\nGenerated code: {}", error, text) + } + } +} diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 8bf2c88be..b028f0123 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -1,15 +1,46 @@ use actix_http::HttpService; use actix_http_test::TestServer; -use actix_web::{get, http, App, HttpResponse, Responder}; +use actix_web_codegen::get; +use actix_web::{http, App, HttpResponse, Responder}; +use futures::{Future, future}; +//fn guard_head(head: &actix_web::dev::RequestHead) -> bool { +// true +//} + +//#[get("/test", guard="guard_head")] #[get("/test")] fn test() -> impl Responder { HttpResponse::Ok() } +#[get("/test")] +fn auto_async() -> impl Future { + future::ok(HttpResponse::Ok().finish()) +} + +#[get("/test")] +fn auto_sync() -> impl Future { + future::ok(HttpResponse::Ok().finish()) +} + + #[test] fn test_body() { let mut srv = TestServer::new(|| HttpService::new(App::new().service(test))); + let request = srv.request(http::Method::GET, srv.url("/test")); + let response = srv.block_on(request.send()).unwrap(); + assert!(response.status().is_success()); + + let mut srv = TestServer::new(|| HttpService::new(App::new().service(auto_sync))); + let request = srv.request(http::Method::GET, srv.url("/test")); + let response = srv.block_on(request.send()).unwrap(); + assert!(response.status().is_success()); +} + +#[test] +fn test_auto_async() { + let mut srv = TestServer::new(|| HttpService::new(App::new().service(auto_async))); let request = srv.request(http::Method::GET, srv.url("/test")); let response = srv.block_on(request.send()).unwrap();