1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-05-19 16:58:14 +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
///
/// 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 `<name>_scope`.
///
#[proc_macro_attribute]
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 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<Args>,
/// 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<syn::Attribute>,
}
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();
// 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();
// 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::<syn::ItemFn>(input.clone()) {
let ast = match syn::parse::<ItemFn>(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::<syn::ItemFn>(input.clone()) {
let mut ast = match syn::parse::<ItemFn>(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::<syn::LitStr>(args.clone()).map_err(|_| syn::Error::new(
proc_macro2::Span::call_site(),
"Scope macro needs a path to be specified.",
));
struct ScopeItems {
handlers: Vec<String>,
}
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::<RouteArgs>() {
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::<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}}")
}
}
TokenStream::from(quote! { #ast })
}

View file

@ -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");
}
*/