mirror of
https://github.com/actix/actix-web.git
synced 2024-11-10 19:01:05 +00:00
work in progress. revised procedural macro to change othe macro call
This commit is contained in:
parent
f8f93d4dab
commit
c4520d909a
3 changed files with 86 additions and 146 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
*/
|
Loading…
Reference in a new issue