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:
parent
c22a3a71f2
commit
046b7a1425
@ -19,3 +19,4 @@ syn = { version = "0.15", features = ["full", "parsing"] }
|
||||
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"] }
|
||||
futures = { version = "0.1" }
|
||||
|
@ -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<Item=HttpResponse, Error=actix_web::Error> {
|
||||
//! 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<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()
|
||||
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<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()
|
||||
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<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()
|
||||
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()
|
||||
}
|
||||
|
159
actix-web-codegen/src/route.rs
Normal file
159
actix-web-codegen/src/route.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<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]
|
||||
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();
|
||||
|
Loading…
Reference in New Issue
Block a user