WIP: basic implementation for `routes` macro

This commit is contained in:
Roland Fredenhagen 2022-03-30 17:41:31 +02:00
parent 6a5b370206
commit b962959b82
No known key found for this signature in database
GPG Key ID: 094AF99241035EB6
2 changed files with 160 additions and 60 deletions

View File

@ -104,6 +104,11 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input) route::with_method(None, args, input)
} }
#[proc_macro_attribute]
pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream {
route::with_methods(input)
}
macro_rules! method_macro { macro_rules! method_macro {
($variant:ident, $method:ident) => { ($variant:ident, $method:ident) => {
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]

View File

@ -4,7 +4,7 @@ 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::{format_ident, quote, ToTokens, TokenStreamExt}; use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta}; use syn::{parse_macro_input, Attribute, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
enum ResourceType { enum ResourceType {
Async, Async,
@ -20,7 +20,7 @@ impl ToTokens for ResourceType {
macro_rules! method_type { macro_rules! method_type {
( (
$($variant:ident, $upper:ident,)+ $($variant:ident, $upper:ident, $lower:ident,)+
) => { ) => {
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash)]
pub enum MethodType { pub enum MethodType {
@ -42,20 +42,27 @@ macro_rules! method_type {
_ => Err(format!("Unexpected HTTP method: `{}`", method)), _ => Err(format!("Unexpected HTTP method: `{}`", method)),
} }
} }
fn from_path(method: &Path) -> Result<Self, ()> {
match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()),
}
}
} }
}; };
} }
method_type! { method_type! {
Get, GET, Get, GET, get,
Post, POST, Post, POST, post,
Put, PUT, Put, PUT, put,
Delete, DELETE, Delete, DELETE, delete,
Head, HEAD, Head, HEAD, head,
Connect, CONNECT, Connect, CONNECT, connect,
Options, OPTIONS, Options, OPTIONS, options,
Trace, TRACE, Trace, TRACE, trace,
Patch, PATCH, Patch, PATCH, patch,
} }
impl ToTokens for MethodType { impl ToTokens for MethodType {
@ -90,6 +97,18 @@ impl Args {
let mut wrappers = Vec::new(); let mut wrappers = Vec::new();
let mut methods = HashSet::new(); let mut methods = HashSet::new();
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<some path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let is_route_macro = method.is_none(); let is_route_macro = method.is_none();
if let Some(method) = method { if let Some(method) = method {
methods.insert(method); methods.insert(method);
@ -184,7 +203,7 @@ impl Args {
pub struct Route { pub struct Route {
name: syn::Ident, name: syn::Ident,
args: Args, args: Vec<Args>,
ast: syn::ItemFn, ast: syn::ItemFn,
resource_type: ResourceType, resource_type: ResourceType,
@ -220,18 +239,6 @@ impl Route {
ast: syn::ItemFn, ast: syn::ItemFn,
method: Option<MethodType>, method: Option<MethodType>,
) -> syn::Result<Self> { ) -> syn::Result<Self> {
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<some path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
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.
@ -265,6 +272,41 @@ impl Route {
} }
}; };
Ok(Self {
name,
args: vec![args],
ast,
resource_type,
doc_attributes,
})
}
fn multiple(args: Vec<Args>, ast: syn::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.
// Note that multi line doc comments are converted to multiple doc attributes.
let doc_attributes = ast
.attrs
.iter()
.filter(|attr| attr.path.is_ident("doc"))
.cloned()
.collect();
let resource_type = if ast.sig.asyncness.is_some() {
ResourceType::Async
} else {
match ast.sig.output {
syn::ReturnType::Default => {
return Err(syn::Error::new_spanned(
ast,
"Function has no return type. Cannot be used as handler",
));
}
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()),
}
};
Ok(Self { Ok(Self {
name, name,
args, args,
@ -280,17 +322,21 @@ impl ToTokens for Route {
let Self { let Self {
name, name,
ast, ast,
args: args,
Args { resource_type,
doc_attributes,
} = self;
let registrations: TokenStream2 = args
.iter()
.map(
|Args {
path, path,
resource_name, resource_name,
guards, guards,
wrappers, wrappers,
methods, methods,
}, }| {
resource_type,
doc_attributes,
} = self;
let resource_name = resource_name let resource_name = resource_name
.as_ref() .as_ref()
.map_or_else(|| name.to_string(), LitStr::value); .map_or_else(|| name.to_string(), LitStr::value);
@ -312,6 +358,19 @@ impl ToTokens for Route {
} }
} }
}; };
quote! {
let __resource = ::actix_web::Resource::new(#path)
.name(#resource_name)
#method_guards
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
}
},
)
.collect();
let stream = quote! { let stream = quote! {
#(#doc_attributes)* #(#doc_attributes)*
@ -321,14 +380,7 @@ impl ToTokens for Route {
impl ::actix_web::dev::HttpServiceFactory for #name { impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) { fn register(self, __config: &mut actix_web::dev::AppService) {
#ast #ast
let __resource = ::actix_web::Resource::new(#path) #registrations
.name(#resource_name)
#method_guards
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config)
} }
} }
}; };
@ -357,6 +409,49 @@ pub(crate) fn with_method(
} }
} }
pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
let mut ast = match syn::parse::<syn::ItemFn>(input.clone()) {
Ok(ast) => ast,
// on parse error, make IDEs happy; see fn docs
Err(err) => return input_and_compile_error(input, err),
};
let (methods, others): (Vec<Result<(MethodType, Attribute), Attribute>>, _) = ast
.attrs
.into_iter()
.map(|attr| match MethodType::from_path(&attr.path) {
Ok(method) => Ok((method, attr)),
Err(_) => Err(attr),
})
.partition(Result::is_ok);
ast.attrs = others.into_iter().map(Result::unwrap_err).collect();
let methods = match methods
.into_iter()
.map(Result::unwrap)
.map(|(method, attr)| {
attr.parse_meta().and_then(|args| {
if let Meta::List(args) = args {
Args::new(args.nested.into_iter().collect(), Some(method))
} else {
Err(syn::Error::new_spanned(attr, "Invalid input for macro"))
}
})
})
.collect()
{
Ok(methods) => methods,
Err(err) => return input_and_compile_error(input, err),
};
match Route::multiple(methods, ast) {
Ok(route) => route.into_token_stream().into(),
// on macro related error, make IDEs happy; see fn docs
Err(err) => input_and_compile_error(input, err),
}
}
/// Converts the error to a token stream and appends it to the original input. /// Converts the error to a token stream and appends it to the original input.
/// ///
/// Returning the original input in addition to the error is good for IDEs which can gracefully /// Returning the original input in addition to the error is good for IDEs which can gracefully