From 22708e78a9043c16038d24d5edb7941b4241891e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 11:09:42 -0800 Subject: [PATCH] added proc-macros for route registration --- Cargo.toml | 2 + actix-files/src/lib.rs | 10 +-- actix-web-codegen/Cargo.toml | 15 +++++ actix-web-codegen/src/lib.rs | 115 ++++++++++++++++++++++++++++++++ actix-web-codegen/src/server.rs | 31 +++++++++ examples/basic.rs | 13 ++-- src/handler.rs | 48 ++++++------- src/lib.rs | 18 +++++ src/route.rs | 11 +-- 9 files changed, 221 insertions(+), 42 deletions(-) create mode 100644 actix-web-codegen/Cargo.toml create mode 100644 actix-web-codegen/src/lib.rs create mode 100644 actix-web-codegen/src/server.rs diff --git a/Cargo.toml b/Cargo.toml index 7b2e6c99a8c..f9e2266ea10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ ".", "actix-files", "actix-session", + "actix-web-codegen", ] [package.metadata.docs.rs] @@ -63,6 +64,7 @@ actix-codec = "0.1.0" #actix-service = "0.3.2" #actix-utils = "0.3.1" actix-rt = "0.2.0" +actix-web-codegen = { path="actix-web-codegen" } actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index c6b52f049da..c08cae9c2fa 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -318,9 +318,7 @@ where } } -impl NewService> - for Files -{ +impl NewService> for Files { type Response = ServiceResponse; type Error = (); type Service = FilesService; @@ -730,8 +728,7 @@ mod tests { #[test] fn test_named_file_content_range_headers() { let mut srv = test::init_service( - App::new() - .service(Files::new("/test", ".").index_file("tests/test.binary")), + App::new().service(Files::new("/test", ".").index_file("tests/test.binary")), ); // Valid range header @@ -770,8 +767,7 @@ mod tests { #[test] fn test_named_file_content_length_headers() { let mut srv = test::init_service( - App::new() - .service(Files::new("test", ".").index_file("tests/test.binary")), + App::new().service(Files::new("test", ".").index_file("tests/test.binary")), ); // Valid range header diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml new file mode 100644 index 00000000000..24ed36b772b --- /dev/null +++ b/actix-web-codegen/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "actix-web-codegen" +description = "Actix web codegen macros" +version = "0.1.0" +authors = ["Nikolay Kim "] +license = "MIT/Apache-2.0" +edition = "2018" +workspace = ".." + +[lib] +proc-macro = true + +[dependencies] +quote = "0.6" +syn = { version = "0.15", features = ["full", "parsing"] } diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs new file mode 100644 index 00000000000..1052c82a729 --- /dev/null +++ b/actix-web-codegen/src/lib.rs @@ -0,0 +1,115 @@ +#![recursion_limit = "512"] + +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +/// #[get("path")] attribute +#[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::AppConfig

) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::get().to(#name)), config); + } + } + }) + .into() +} + +/// #[post("path")] attribute +#[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: #[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::AppConfig

) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::post().to(#name)), config); + } + } + }) + .into() +} + +/// #[put("path")] attribute +#[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: #[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::AppConfig

) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::put().to(#name)), config); + } + } + }) + .into() +} diff --git a/actix-web-codegen/src/server.rs b/actix-web-codegen/src/server.rs new file mode 100644 index 00000000000..43e663f3aaf --- /dev/null +++ b/actix-web-codegen/src/server.rs @@ -0,0 +1,31 @@ +use std::collections::HashSet; +use std::env; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn; + +/// Thrift mux server impl +pub struct Server {} + +impl Server { + fn new() -> Server { + Server {} + } + + /// generate servers + pub fn generate(input: TokenStream) { + let mut srv = Server::new(); + let ast: syn::ItemFn = syn::parse2(input).unwrap(); + println!("T: {:?}", ast.ident); + + // quote! { + // #ast + + // #(#servers)* + // } + } +} diff --git a/examples/basic.rs b/examples/basic.rs index 5fd862d49a6..39633f5233e 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,9 +1,9 @@ use futures::IntoFuture; -use actix_web::{ - http::Method, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, -}; +use actix_web::macros::get; +use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +#[get("/resource1/index.html")] fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); "Hello world!\r\n" @@ -14,6 +14,7 @@ fn index_async(req: HttpRequest) -> impl IntoFuture &'static str { "Hello world!\r\n" } @@ -27,7 +28,8 @@ fn main() -> std::io::Result<()> { App::new() .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) - .service(web::resource("/resource1/index.html").route(web::get().to(index))) + .service(index) + .service(no_params) .service( web::resource("/resource2/index.html") .middleware( @@ -36,10 +38,9 @@ fn main() -> std::io::Result<()> { .default_resource(|r| { r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) }) - .route(web::method(Method::GET).to_async(index_async)), + .route(web::get().to_async(index_async)), ) .service(web::resource("/test1.html").to(|| "Test\r\n")) - .service(web::resource("/").to(no_params)) }) .bind("127.0.0.1:8080")? .workers(1) diff --git a/src/handler.rs b/src/handler.rs index 442dc60d80d..435d9a8bb18 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -31,7 +31,7 @@ where } #[doc(hidden)] -pub struct Handle +pub struct Handler where F: Factory, R: Responder, @@ -40,19 +40,19 @@ where _t: PhantomData<(T, R)>, } -impl Handle +impl Handler where F: Factory, R: Responder, { pub fn new(hnd: F) -> Self { - Handle { + Handler { hnd, _t: PhantomData, } } } -impl NewService<(T, HttpRequest)> for Handle +impl NewService<(T, HttpRequest)> for Handler where F: Factory, R: Responder + 'static, @@ -60,11 +60,11 @@ where type Response = ServiceResponse; type Error = Void; type InitError = (); - type Service = HandleService; + type Service = HandlerService; type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(HandleService { + ok(HandlerService { hnd: self.hnd.clone(), _t: PhantomData, }) @@ -72,7 +72,7 @@ where } #[doc(hidden)] -pub struct HandleService +pub struct HandlerService where F: Factory, R: Responder + 'static, @@ -81,14 +81,14 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for HandleService +impl Service<(T, HttpRequest)> for HandlerService where F: Factory, R: Responder + 'static, { type Response = ServiceResponse; type Error = Void; - type Future = HandleServiceResponse<::Future>; + type Future = HandlerServiceResponse<::Future>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) @@ -96,19 +96,19 @@ where fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { let fut = self.hnd.call(param).respond_to(&req).into_future(); - HandleServiceResponse { + HandlerServiceResponse { fut, req: Some(req), } } } -pub struct HandleServiceResponse { +pub struct HandlerServiceResponse { fut: T, req: Option, } -impl Future for HandleServiceResponse +impl Future for HandlerServiceResponse where T: Future, T::Error: Into, @@ -157,7 +157,7 @@ where } #[doc(hidden)] -pub struct AsyncHandle +pub struct AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -168,7 +168,7 @@ where _t: PhantomData<(T, R)>, } -impl AsyncHandle +impl AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -176,13 +176,13 @@ where R::Error: Into, { pub fn new(hnd: F) -> Self { - AsyncHandle { + AsyncHandler { hnd, _t: PhantomData, } } } -impl NewService<(T, HttpRequest)> for AsyncHandle +impl NewService<(T, HttpRequest)> for AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -192,11 +192,11 @@ where type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = AsyncHandleService; + type Service = AsyncHandlerService; type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(AsyncHandleService { + ok(AsyncHandlerService { hnd: self.hnd.clone(), _t: PhantomData, }) @@ -204,7 +204,7 @@ where } #[doc(hidden)] -pub struct AsyncHandleService +pub struct AsyncHandlerService where F: AsyncFactory, R: IntoFuture, @@ -215,7 +215,7 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for AsyncHandleService +impl Service<(T, HttpRequest)> for AsyncHandlerService where F: AsyncFactory, R: IntoFuture, @@ -224,14 +224,14 @@ where { type Response = ServiceResponse; type Error = (); - type Future = AsyncHandleServiceResponse; + type Future = AsyncHandlerServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { - AsyncHandleServiceResponse { + AsyncHandlerServiceResponse { fut: self.hnd.call(param).into_future(), req: Some(req), } @@ -239,12 +239,12 @@ where } #[doc(hidden)] -pub struct AsyncHandleServiceResponse { +pub struct AsyncHandlerServiceResponse { fut: T, req: Option, } -impl Future for AsyncHandleServiceResponse +impl Future for AsyncHandlerServiceResponse where T: Future, T::Item: Into, diff --git a/src/lib.rs b/src/lib.rs index fd1b21f3533..dd60c7b843a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,24 @@ mod service; mod state; pub mod test; +/// Attribute macros for route registration +/// +/// ```rust +/// use actix_web::{macros, App, HttpResponse}; +/// +/// #[macros::get("/index.html")] +/// fn index() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// +/// fn main() { +/// let app = App::new().service(index); +/// } +/// ``` +pub mod macros { + pub use actix_web_codegen::{get, post, put}; +} + // re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; diff --git a/src/route.rs b/src/route.rs index b611164e963..9538dfd2c30 100644 --- a/src/route.rs +++ b/src/route.rs @@ -8,7 +8,7 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::extract::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::guard::{self, Guard}; -use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, Handle}; +use crate::handler::{AsyncFactory, AsyncHandler, Extract, Factory, Handler}; use crate::responder::Responder; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::HttpResponse; @@ -50,8 +50,9 @@ impl Route

{ let config_ref = Rc::new(RefCell::new(None)); Route { service: Box::new(RouteNewService::new( - Extract::new(config_ref.clone()) - .and_then(Handle::new(HttpResponse::NotFound).map_err(|_| panic!())), + Extract::new(config_ref.clone()).and_then( + Handler::new(HttpResponse::NotFound).map_err(|_| panic!()), + ), )), guards: Rc::new(Vec::new()), config: ConfigStorage::default(), @@ -272,7 +273,7 @@ impl Route

{ T::Config::store_default(&mut self.config); self.service = Box::new(RouteNewService::new( Extract::new(self.config_ref.clone()) - .and_then(Handle::new(handler).map_err(|_| panic!())), + .and_then(Handler::new(handler).map_err(|_| panic!())), )); self } @@ -314,7 +315,7 @@ impl Route

{ { self.service = Box::new(RouteNewService::new( Extract::new(self.config_ref.clone()) - .and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + .and_then(AsyncHandler::new(handler).map_err(|_| panic!())), )); self }