1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-23 23:51:06 +01:00

Expand codegen to allow specify guards and async

This commit is contained in:
Douman 2019-03-31 10:23:15 +03:00
parent c22a3a71f2
commit 046b7a1425
4 changed files with 266 additions and 99 deletions

View File

@ -18,4 +18,5 @@ syn = { version = "0.15", features = ["full", "parsing"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "1.0.0-alpha.2" } actix-web = { version = "1.0.0-alpha.2" }
actix-http = { version = "0.1.0-alpha.2", features=["ssl"] } actix-http = { version = "0.1.0-alpha.2", features=["ssl"] }
actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] }
futures = { version = "0.1" }

View File

@ -1,118 +1,94 @@
#![recursion_limit = "512"] #![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<Item=HttpResponse, Error=actix_web::Error> {
//! future::ok(HttpResponse::Ok().finish())
//! }
//! ```
extern crate proc_macro; extern crate proc_macro;
mod route;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input; 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] #[proc_macro_attribute]
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
if args.is_empty() { let gen = route::Args::new(&args, input, route::GuardType::Get);
panic!("invalid server definition, expected: #[get(\"some path\")]"); gen.generate()
}
// 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<P: 'static> actix_web::dev::HttpServiceFactory<P> for #name {
fn register(self, config: &mut actix_web::dev::ServiceConfig<P>) {
#ast
actix_web::dev::HttpServiceFactory::register(
actix_web::Resource::new(#path)
.guard(actix_web::guard::Get())
.to(#name), config);
}
}
})
.into()
} }
/// #[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] #[proc_macro_attribute]
pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
if args.is_empty() { let gen = route::Args::new(&args, input, route::GuardType::Post);
panic!("invalid server definition, expected: #[post(\"some path\")]"); gen.generate()
}
// 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<P: 'static> actix_web::dev::HttpServiceFactory<P> for #name {
fn register(self, config: &mut actix_web::dev::ServiceConfig<P>) {
#ast
actix_web::dev::HttpServiceFactory::register(
actix_web::Resource::new(#path)
.guard(actix_web::guard::Post())
.to(#name), config);
}
}
})
.into()
} }
/// #[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] #[proc_macro_attribute]
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
if args.is_empty() { let gen = route::Args::new(&args, input, route::GuardType::Put);
panic!("invalid server definition, expected: #[put(\"some path\")]"); gen.generate()
} }
// path /// Creates route handler with `DELETE` method guard.
let path = match args[0] { ///
syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { /// Syntax: `#[delete("path"[, attributes])]`
let fname = quote!(#fname).to_string(); ///
fname.as_str()[1..fname.len() - 1].to_owned() /// Attributes are the same as in [get](attr.get.html)
} #[proc_macro_attribute]
_ => panic!("resource path"), 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);
let ast: syn::ItemFn = syn::parse(input).unwrap(); gen.generate()
let name = ast.ident.clone();
(quote! {
#[allow(non_camel_case_types)]
struct #name;
impl<P: 'static> actix_web::dev::HttpServiceFactory<P> for #name {
fn register(self, config: &mut actix_web::dev::ServiceConfig<P>) {
#ast
actix_web::dev::HttpServiceFactory::register(
actix_web::Resource::new(#path)
.guard(actix_web::guard::Put())
.to(#name), config);
}
}
})
.into()
} }

View File

@ -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<String>,
}
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<P: 'static> actix_web::dev::HttpServiceFactory<P> for {name} {{
fn register(self, config: &mut actix_web::dev::ServiceConfig<P>) {{
{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<syn::NestedMeta>, 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)
}
}
}

View File

@ -1,15 +1,46 @@
use actix_http::HttpService; use actix_http::HttpService;
use actix_http_test::TestServer; 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")] #[get("/test")]
fn test() -> impl Responder { fn test() -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
} }
#[get("/test")]
fn auto_async() -> impl Future<Item=HttpResponse, Error=actix_web::Error> {
future::ok(HttpResponse::Ok().finish())
}
#[get("/test")]
fn auto_sync() -> impl Future<Item=HttpResponse, Error=actix_web::Error> {
future::ok(HttpResponse::Ok().finish())
}
#[test] #[test]
fn test_body() { fn test_body() {
let mut srv = TestServer::new(|| HttpService::new(App::new().service(test))); 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 request = srv.request(http::Method::GET, srv.url("/test"));
let response = srv.block_on(request.send()).unwrap(); let response = srv.block_on(request.send()).unwrap();