1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-06-11 01:39:33 +00:00

work in progress. revised procedural macro to change othe macro call

This commit is contained in:
Jon Lim 2024-03-31 20:44:08 -07:00
parent f8f93d4dab
commit c4520d909a
3 changed files with 86 additions and 146 deletions

View file

@ -254,10 +254,10 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
/// ///
/// # Example /// # Example
/// ///
/// scope("/test")
/// ```rust /// ```rust
/// use actix_web_codegen::{scope}; /// use actix_web_codegen::{scope};
/// ///
/// #[scope("/test")]
/// const mod_inner: () = { /// const mod_inner: () = {
/// use actix_web::{HttpResponse, HttpRequest, Responder, put, head, connect, options, trace, patch, delete, web }; /// use actix_web::{HttpResponse, HttpRequest, Responder, put, head, connect, options, trace, patch, delete, web };
/// use actix_web::web::Json; /// 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 `<name>_scope`.
///
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream { pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream {
route::ScopeArgs::new(args, input).generate() route::with_scope(args, input)
} }

View file

@ -1,10 +1,11 @@
use std::{collections::HashSet, fmt}; use std::{collections::HashSet};
use actix_router::ResourceDef; use actix_router::ResourceDef;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt}; 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)] #[derive(Debug)]
pub struct RouteArgs { pub struct RouteArgs {
@ -331,14 +332,14 @@ pub struct Route {
args: Vec<Args>, args: Vec<Args>,
/// AST of the handler function being annotated. /// AST of the handler function being annotated.
ast: syn::ItemFn, ast: ItemFn,
/// The doc comment attributes to copy to generated struct, if any. /// The doc comment attributes to copy to generated struct, if any.
doc_attributes: Vec<syn::Attribute>, doc_attributes: Vec<syn::Attribute>,
} }
impl Route { impl Route {
pub fn new(args: RouteArgs, ast: syn::ItemFn, method: Option<MethodType>) -> syn::Result<Self> { pub fn new(args: RouteArgs, ast: ItemFn, method: Option<MethodType>) -> syn::Result<Self> {
let name = ast.sig.ident.clone(); let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct. // 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<Args>, ast: syn::ItemFn) -> syn::Result<Self> { fn multiple(args: Vec<Args>, ast: ItemFn) -> syn::Result<Self> {
let name = ast.sig.ident.clone(); let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct. // 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), Err(err) => return input_and_compile_error(input, err),
}; };
let ast = match syn::parse::<syn::ItemFn>(input.clone()) { let ast = match syn::parse::<ItemFn>(input.clone()) {
Ok(ast) => ast, Ok(ast) => ast,
// on parse error, make IDEs happy; see fn docs // on parse error, make IDEs happy; see fn docs
Err(err) => return input_and_compile_error(input, err), 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 { pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
let mut ast = match syn::parse::<syn::ItemFn>(input.clone()) { let mut ast = match syn::parse::<ItemFn>(input.clone()) {
Ok(ast) => ast, Ok(ast) => ast,
// on parse error, make IDEs happy; see fn docs // on parse error, make IDEs happy; see fn docs
Err(err) => return input_and_compile_error(input, err), 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 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::<syn::LitStr>(args.clone()).map_err(|_| syn::Error::new(
proc_macro2::Span::call_site(),
"Scope macro needs a path to be specified.",
));
struct ScopeItems { // modify the attribute of functions with method attributes in the module
handlers: Vec<String>, 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
impl ScopeItems { if let Some((_, ref mut all_items)) = ast.content {
pub fn from_items(items: &[syn::Item]) -> Self { for item in all_items.iter_mut() {
let mut handlers = Vec::new(); if let Fn(fun) = item {
let mut new_attrs = Vec::new();
for item in items { for attr in fun.attrs.iter() {
match item { if let Ok(_method) = MethodType::from_path(attr.path()) {
syn::Item::Fn(ref fun) => { if let Ok(route_args) = attr.parse_args::<RouteArgs>() {
for attr in fun.attrs.iter() { if let syn::Meta::List(meta_list) = attr.clone().meta {
for bound in attr.path().segments.iter() { let modified_path = format!("{}{}", scope_path.value(), route_args.path.value());
if bound.ident == "get" let modified_tokens = quote! {
|| bound.ident == "post" #modified_path
|| bound.ident == "put" };
|| bound.ident == "head" let new_attr = syn::Attribute {
|| bound.ident == "connect" pound_token: attr.pound_token,
|| bound.ident == "options" style: attr.style,
|| bound.ident == "trace" bracket_token: attr.bracket_token,
|| bound.ident == "patch" meta: syn::Meta::List(syn::MetaList {
|| bound.ident == "delete" path: meta_list.path,
{ delimiter: meta_list.delimiter,
handlers.push(format!("{}", fun.sig.ident)); tokens: modified_tokens,
break; })
};
new_attrs.push(new_attr);
}
else {
new_attrs.push(attr.clone());
}
}
else {
new_attrs.push(attr.clone());
}
}
else {
new_attrs.push(attr.clone());
} }
} }
} fun.attrs = new_attrs;
}
_ => 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"), },
Err(err) => {
return input_and_compile_error(args, err);
} }
};
let scope_items = ScopeItems::from_items(&items); TokenStream::from(quote! { #ast })
}
let mut path = None;
if let Ok(parsed) = syn::parse::<syn::LitStr>(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}}")
}
}

View file

@ -371,7 +371,7 @@ async fn test_auto_async() {
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
} }
/*
#[actix_web::test] #[actix_web::test]
async fn test_wrap() { async fn test_wrap() {
let srv = actix_test::start(|| App::new().service(get_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(); let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("wrong number of parameters")); 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")] #[scope("/test")]
const mod_inner: () = { const mod_inner: () = {
use actix_web::{ use actix_web::{
@ -595,3 +608,4 @@ async fn test_scope_v1_v2_async() {
let body_str = String::from_utf8(body.to_vec()).unwrap(); let body_str = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(body_str, "version2 works"); assert_eq!(body_str, "version2 works");
} }
*/