mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-24 07:53:00 +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-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" }
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
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::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();
|
||||||
|
Loading…
Reference in New Issue
Block a user