diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 921321759..5d7a2b46b 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -254,10 +254,10 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { /// /// # Example /// +/// scope("/test") /// ```rust /// use actix_web_codegen::{scope}; /// -/// #[scope("/test")] /// const mod_inner: () = { /// use actix_web::{HttpResponse, HttpRequest, Responder, put, head, connect, options, trace, patch, delete, web }; /// use actix_web::web::Json; @@ -320,12 +320,7 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { ///}; /// ``` /// -/// # Note -/// -/// Internally the macro generates struct with name of scope (e.g. `mod_inner`). -/// And creates public module as `_scope`. -/// #[proc_macro_attribute] pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream { - route::ScopeArgs::new(args, input).generate() + route::with_scope(args, input) } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 097329cf7..f5b17ed6c 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -1,10 +1,11 @@ -use std::{collections::HashSet, fmt}; +use std::{collections::HashSet}; use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token}; +use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token, ItemFn}; +use syn::Item::Fn; // todo: cleanup #[derive(Debug)] pub struct RouteArgs { @@ -331,14 +332,14 @@ pub struct Route { args: Vec, /// AST of the handler function being annotated. - ast: syn::ItemFn, + ast: ItemFn, /// The doc comment attributes to copy to generated struct, if any. doc_attributes: Vec, } impl Route { - pub fn new(args: RouteArgs, ast: syn::ItemFn, method: Option) -> syn::Result { + pub fn new(args: RouteArgs, ast: ItemFn, method: Option) -> syn::Result { let name = ast.sig.ident.clone(); // Try and pull out the doc comments so that we can reapply them to the generated struct. @@ -374,7 +375,7 @@ impl Route { }) } - fn multiple(args: Vec, ast: syn::ItemFn) -> syn::Result { + fn multiple(args: Vec, ast: ItemFn) -> syn::Result { let name = ast.sig.ident.clone(); // Try and pull out the doc comments so that we can reapply them to the generated struct. @@ -483,7 +484,7 @@ pub(crate) fn with_method( Err(err) => return input_and_compile_error(input, err), }; - let ast = match syn::parse::(input.clone()) { + let ast = match syn::parse::(input.clone()) { Ok(ast) => ast, // on parse error, make IDEs happy; see fn docs Err(err) => return input_and_compile_error(input, err), @@ -497,7 +498,7 @@ pub(crate) fn with_method( } pub(crate) fn with_methods(input: TokenStream) -> TokenStream { - let mut ast = match syn::parse::(input.clone()) { + let mut ast = match syn::parse::(input.clone()) { Ok(ast) => ast, // on parse error, make IDEs happy; see fn docs Err(err) => return input_and_compile_error(input, err), @@ -555,142 +556,72 @@ fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStrea item } -/// Implements scope proc macro -/// +// todo: test with enums in module... +// todo: test with multiple get/post methods... +// todo: test with doc strings in attributes... +// todo: clean up the nested tree of death, tell it about the input_and_compile_error convenience function... +// todo: add in the group functions for convenient handling of group... +// todo: see if can cleanup the Attribute with a clone plus one field change... +pub fn with_scope(args: TokenStream, input: TokenStream) -> TokenStream { + // get the path of the scope argument + if args.is_empty() { + return input_and_compile_error( + args, syn::Error::new(proc_macro2::Span::call_site(), "invalid server definition, expected: #[scope(\"some path\")]")); + } + let scope_path = syn::parse::(args.clone()).map_err(|_| syn::Error::new( + proc_macro2::Span::call_site(), + "Scope macro needs a path to be specified.", + )); -struct ScopeItems { - handlers: Vec, -} - -impl ScopeItems { - pub fn from_items(items: &[syn::Item]) -> Self { - let mut handlers = Vec::new(); - - for item in items { - match item { - syn::Item::Fn(ref fun) => { - for attr in fun.attrs.iter() { - for bound in attr.path().segments.iter() { - if bound.ident == "get" - || bound.ident == "post" - || bound.ident == "put" - || bound.ident == "head" - || bound.ident == "connect" - || bound.ident == "options" - || bound.ident == "trace" - || bound.ident == "patch" - || bound.ident == "delete" - { - handlers.push(format!("{}", fun.sig.ident)); - break; + // modify the attribute of functions with method attributes in the module + let mut ast: syn::ItemMod = syn::parse(input.clone()).expect("expecting module for macro scope attribute"); + match scope_path { + Ok(scope_path) => { // scope_path : LitStr + if let Some((_, ref mut all_items)) = ast.content { + for item in all_items.iter_mut() { + if let Fn(fun) = item { + let mut new_attrs = Vec::new(); + for attr in fun.attrs.iter() { + if let Ok(_method) = MethodType::from_path(attr.path()) { + if let Ok(route_args) = attr.parse_args::() { + if let syn::Meta::List(meta_list) = attr.clone().meta { + let modified_path = format!("{}{}", scope_path.value(), route_args.path.value()); + let modified_tokens = quote! { + #modified_path + }; + let new_attr = syn::Attribute { + pound_token: attr.pound_token, + style: attr.style, + bracket_token: attr.bracket_token, + meta: syn::Meta::List(syn::MetaList { + path: meta_list.path, + delimiter: meta_list.delimiter, + tokens: modified_tokens, + }) + }; + new_attrs.push(new_attr); + } + else { + new_attrs.push(attr.clone()); + } + } + else { + new_attrs.push(attr.clone()); + } + } + else { + new_attrs.push(attr.clone()); } } - } - } - _ => continue, - } - } - - Self { handlers } - } -} - -pub struct ScopeArgs { - ast: syn::ItemConst, - name: syn::Ident, - path: String, - scope_items: ScopeItems, -} - -impl ScopeArgs { - pub fn new(args: TokenStream, input: TokenStream) -> Self { - if args.is_empty() { - panic!("invalid server definition, expected: #[scope(\"some path\")]"); - } - - let ast: syn::ItemConst = syn::parse(input).expect("Parse input as module"); - //TODO: we should change it to mod once supported on stable - //let ast: syn::ItemMod = syn::parse(input).expect("Parse input as module"); - let name = ast.ident.clone(); - - let mut items = Vec::new(); - match ast.expr.as_ref() { - syn::Expr::Block(expr) => { - for item in expr.block.stmts.iter() { - match item { - syn::Stmt::Item(ref item) => items.push(item.clone()), - _ => continue, + fun.attrs = new_attrs; } } } - _ => panic!("Scope should containt only code block"), + }, + Err(err) => { + return input_and_compile_error(args, err); } + }; - let scope_items = ScopeItems::from_items(&items); - - let mut path = None; - if let Ok(parsed) = syn::parse::(args) { - path = Some(parsed.value()); - } - - let path = path.expect("Scope's path is not specified!"); - - Self { - ast, - name, - path, - scope_items, - } - } - - pub fn generate(&self) -> TokenStream { - let text = self.to_string(); - - match text.parse() { - Ok(res) => res, - Err(error) => panic!("Error: {:?}\nGenerated code: {}", error, text), - } - } -} - -impl fmt::Display for ScopeArgs { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let ast = &self.ast; - - let module_name = format!("{}_scope", self.name); - let module_name = syn::Ident::new(&module_name, ast.ident.span()); - let ast = match ast.expr.as_ref() { - syn::Expr::Block(expr) => quote!(pub mod #module_name #expr), - _ => panic!("Unexpect non-block ast in scope macro"), - }; - - writeln!(f, "{}\n", ast)?; - writeln!(f, "#[allow(non_camel_case_types)]")?; - writeln!(f, "struct {};\n", self.name)?; - writeln!( - f, - "impl actix_web::dev::HttpServiceFactory for {} {{", - self.name - )?; - writeln!( - f, - " fn register(self, __config: &mut actix_web::dev::AppService) {{" - )?; - write!( - f, - " let scope = actix_web::Scope::new(\"{}\")", - self.path - )?; - - for handler in self.scope_items.handlers.iter() { - write!(f, ".service({}::{})", module_name, handler)?; - } - - writeln!(f, ";\n")?; - writeln!( - f, - " actix_web::dev::HttpServiceFactory::register(scope, __config)" - )?; - writeln!(f, " }}\n}}") - } -} + TokenStream::from(quote! { #ast }) +} \ No newline at end of file diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index bd864faf0..54a0030ba 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -371,7 +371,7 @@ async fn test_auto_async() { let response = request.send().await.unwrap(); assert!(response.status().is_success()); } - +/* #[actix_web::test] async fn test_wrap() { let srv = actix_test::start(|| App::new().service(get_wrap)); @@ -384,7 +384,20 @@ async fn test_wrap() { let body = String::from_utf8(body.to_vec()).unwrap(); assert!(body.contains("wrong number of parameters")); } +*/ +#[scope("/test")] +mod scope_module { + use actix_web::{HttpResponse, Responder}; + use actix_web_codegen::{ + connect, delete, get, head, options, patch, post, put, route, routes, scope, trace, + }; + #[get("/get_test")] + async fn get_test() -> impl Responder { + HttpResponse::Ok() + } +} +/* #[scope("/test")] const mod_inner: () = { use actix_web::{ @@ -595,3 +608,4 @@ async fn test_scope_v1_v2_async() { let body_str = String::from_utf8(body.to_vec()).unwrap(); assert_eq!(body_str, "version2 works"); } +*/ \ No newline at end of file