From ad90bc926d5869aca0b411b3b595c3e1c0808d16 Mon Sep 17 00:00:00 2001 From: Jon Lim Date: Sun, 3 Sep 2023 14:05:21 -0700 Subject: [PATCH] add scope proc macro --- actix-web-codegen/src/lib.rs | 44 ++++++++++++ actix-web-codegen/src/route.rs | 120 +++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 6d6c9ab5c..129919e65 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -240,3 +240,47 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { output.extend(item); output } + +/// Generates scope +/// +/// Syntax: `#[scope("path")]` +/// +/// Due to current limitation it cannot be applied to modules themself. +/// Instead one should create const variable that contains module code. +/// +/// ## Attributes: +/// +/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. +/// +/// # Example +/// +/// ```rust +/// use actix_web_cute_codegen::{scope}; +/// +/// #[scope("/scope")] +/// const mod_inner: () = { +/// use actix_web_cute_codegen::{get, hook}; +/// use actix_web::{HttpResponse, Responder}; +/// use futures::{Future, future}; +/// +/// #[get("/test")] +/// pub fn test() -> impl Responder { +/// HttpResponse::Ok() +/// } +/// +/// #[get("/test_async")] +/// pub fn auto_sync() -> impl Future { +/// future::ok(HttpResponse::Ok().finish()) +/// } +/// }; +/// ``` +/// +/// # Note +/// +/// Internally the macro generate struct with name of scope (e.g. `mod_inner`) +/// And create public module as `_scope` +/// +#[proc_macro_attribute] +pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream { + route::ScopeArgs::new(args, input).generate() +} \ No newline at end of file diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 7a2dfc051..d8fb2a5da 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -4,6 +4,7 @@ use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; +use std::fmt; use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token}; #[derive(Debug)] @@ -554,3 +555,122 @@ fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStrea item.extend(compile_err); item } + +/// Implements scope proc macro +/// + +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; + } + } + } + }, + _ => 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, + } + }, + _ => panic!("Scope should containt only code block"), + } + + 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::ServiceConfig

) {{")?; + 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}}") + } +} \ No newline at end of file