mirror of https://github.com/fafhrd91/actix-web
test: add negative tests
This commit is contained in:
parent
a58368f871
commit
eca91697f2
|
@ -83,6 +83,7 @@ use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
mod route;
|
mod route;
|
||||||
|
mod scope;
|
||||||
|
|
||||||
/// Creates resource handler, allowing multiple HTTP method guards.
|
/// Creates resource handler, allowing multiple HTTP method guards.
|
||||||
///
|
///
|
||||||
|
@ -197,6 +198,33 @@ method_macro!(Options, options);
|
||||||
method_macro!(Trace, trace);
|
method_macro!(Trace, trace);
|
||||||
method_macro!(Patch, patch);
|
method_macro!(Patch, patch);
|
||||||
|
|
||||||
|
/// Creates scope.
|
||||||
|
///
|
||||||
|
/// Syntax: `#[scope("/path")]`
|
||||||
|
///
|
||||||
|
/// ## Attributes:
|
||||||
|
///
|
||||||
|
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_web_codegen::scope;
|
||||||
|
/// # use actix_web::{get, HttpResponse, Responder};
|
||||||
|
/// #[scope("/test")]
|
||||||
|
/// mod scope_module {
|
||||||
|
/// #[get("/test")]
|
||||||
|
/// pub async fn test() -> impl Responder {
|
||||||
|
/// // this has path /test/test
|
||||||
|
/// HttpResponse::Ok().finish()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
|
scope::with_scope(args, input)
|
||||||
|
}
|
||||||
|
|
||||||
/// Marks async main function as the Actix Web system entry-point.
|
/// Marks async main function as the Actix Web system entry-point.
|
||||||
///
|
///
|
||||||
/// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is
|
/// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is
|
||||||
|
@ -241,30 +269,14 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates scope
|
/// Converts the error to a token stream and appends it to the original input.
|
||||||
///
|
///
|
||||||
/// Syntax: `#[scope("path")]`
|
/// Returning the original input in addition to the error is good for IDEs which can gracefully
|
||||||
|
/// recover and show more precise errors within the macro body.
|
||||||
///
|
///
|
||||||
/// ## Attributes:
|
/// See <https://github.com/rust-analyzer/rust-analyzer/issues/10468> for more info.
|
||||||
///
|
fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
|
||||||
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
|
let compile_err = TokenStream::from(err.to_compile_error());
|
||||||
///
|
item.extend(compile_err);
|
||||||
/// # Example
|
item
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use actix_web_codegen::{scope};
|
|
||||||
/// #[scope("/test")]
|
|
||||||
/// mod scope_module {
|
|
||||||
/// use actix_web::{get, HttpResponse, Responder};
|
|
||||||
/// #[get("/test")]
|
|
||||||
/// pub async fn test() -> impl Responder {
|
|
||||||
/// // this has path /test/test
|
|
||||||
/// HttpResponse::Ok().finish()
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream {
|
|
||||||
route::with_scope(args, input)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,12 @@ 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};
|
||||||
|
|
||||||
|
use crate::input_and_compile_error;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RouteArgs {
|
pub struct RouteArgs {
|
||||||
path: syn::LitStr,
|
pub(crate) path: syn::LitStr,
|
||||||
options: Punctuated<syn::MetaNameValue, Token![,]>,
|
pub(crate) options: Punctuated<syn::MetaNameValue, Token![,]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl syn::parse::Parse for RouteArgs {
|
impl syn::parse::Parse for RouteArgs {
|
||||||
|
@ -78,7 +80,7 @@ macro_rules! standard_method_type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_path(method: &Path) -> Result<Self, ()> {
|
pub(crate) fn from_path(method: &Path) -> Result<Self, ()> {
|
||||||
match () {
|
match () {
|
||||||
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
|
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
|
@ -542,105 +544,3 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
|
||||||
Err(err) => input_and_compile_error(input, err),
|
Err(err) => input_and_compile_error(input, err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
|
||||||
/// recover and show more precise errors within the macro body.
|
|
||||||
///
|
|
||||||
/// See <https://github.com/rust-analyzer/rust-analyzer/issues/10468> for more info.
|
|
||||||
fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
|
|
||||||
let compile_err = TokenStream::from(err.to_compile_error());
|
|
||||||
item.extend(compile_err);
|
|
||||||
item
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_scope(args: TokenStream, input: TokenStream) -> TokenStream {
|
|
||||||
// Attempt to parse the scope path, returning on error
|
|
||||||
if args.is_empty() {
|
|
||||||
return input_and_compile_error(
|
|
||||||
args.clone(),
|
|
||||||
syn::Error::new(
|
|
||||||
Span::call_site(),
|
|
||||||
"Missing arguments for scope macro, expected: #[scope(\"some path\")]",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let scope_path = syn::parse::<LitStr>(args.clone());
|
|
||||||
if let Err(_err) = scope_path {
|
|
||||||
return input_and_compile_error(
|
|
||||||
args.clone(),
|
|
||||||
syn::Error::new(
|
|
||||||
Span::call_site(),
|
|
||||||
"Missing arguments for scope macro, expected: #[scope(\"some path\")]",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expect macro to be for a module
|
|
||||||
match syn::parse::<syn::ItemMod>(input) {
|
|
||||||
Ok(mut ast) => {
|
|
||||||
// Modify the attributes of functions with method or route(s) by adding scope argument as prefix, if any
|
|
||||||
if let Some((_, ref mut items)) = ast.content {
|
|
||||||
items.iter_mut().for_each(|item| {
|
|
||||||
if let syn::Item::Fn(fun) = item {
|
|
||||||
fun.attrs = fun
|
|
||||||
.attrs
|
|
||||||
.iter()
|
|
||||||
.map(|attr| {
|
|
||||||
modify_attribute_with_scope(
|
|
||||||
attr,
|
|
||||||
&scope_path.clone().unwrap().value(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
TokenStream::from(quote! { #ast })
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
input_and_compile_error(args, syn::Error::new(Span::call_site(), err.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_allowed_methods_in_scope(attr: &syn::Attribute) -> bool {
|
|
||||||
MethodType::from_path(attr.path()).is_ok()
|
|
||||||
|| attr.path().is_ident("route")
|
|
||||||
|| attr.path().is_ident("ROUTE")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the attribute is a method type and has a route path, then modify it
|
|
||||||
fn modify_attribute_with_scope(attr: &syn::Attribute, scope_path: &str) -> syn::Attribute {
|
|
||||||
match (attr.parse_args::<RouteArgs>(), attr.clone().meta) {
|
|
||||||
(Ok(route_args), syn::Meta::List(meta_list)) if has_allowed_methods_in_scope(attr) => {
|
|
||||||
let modified_path = format!("{}{}", scope_path, route_args.path.value());
|
|
||||||
|
|
||||||
let options_tokens: Vec<TokenStream2> = route_args
|
|
||||||
.options
|
|
||||||
.iter()
|
|
||||||
.map(|option| {
|
|
||||||
quote! { ,#option }
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let combined_options_tokens: TokenStream2 =
|
|
||||||
options_tokens
|
|
||||||
.into_iter()
|
|
||||||
.fold(TokenStream2::new(), |mut acc, ts| {
|
|
||||||
acc.extend(std::iter::once(ts));
|
|
||||||
acc
|
|
||||||
});
|
|
||||||
|
|
||||||
syn::Attribute {
|
|
||||||
meta: syn::Meta::List(syn::MetaList {
|
|
||||||
tokens: quote! { #modified_path #combined_options_tokens },
|
|
||||||
..meta_list.clone()
|
|
||||||
}),
|
|
||||||
..attr.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => attr.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
|
use quote::quote;
|
||||||
|
use syn::LitStr;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
input_and_compile_error,
|
||||||
|
route::{MethodType, RouteArgs},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn with_scope(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
|
match with_scope_inner(args, input.clone()) {
|
||||||
|
Ok(stream) => stream,
|
||||||
|
Err(err) => input_and_compile_error(input, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_scope_inner(args: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||||
|
if args.is_empty() {
|
||||||
|
// macro args are missing
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
Span::call_site(),
|
||||||
|
"missing arguments for scope macro, \
|
||||||
|
expected: #[scope(\"/prefix\")]",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope_prefix = syn::parse::<LitStr>(args.clone()).map_err(|err| {
|
||||||
|
// first macro arg is not a string literal
|
||||||
|
syn::Error::new(
|
||||||
|
err.span(),
|
||||||
|
"argument to scope macro is not a string literal, \
|
||||||
|
expected: #[scope(\"/prefix\")]",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut module = syn::parse::<syn::ItemMod>(input).map_err(|err| {
|
||||||
|
syn::Error::new(err.span(), "#[scope] macro must be attached to a module")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// modify any routing macros (method or route[s]) attached to
|
||||||
|
// functions by prefixing them with this scope macro's argument
|
||||||
|
if let Some((_, ref mut items)) = module.content {
|
||||||
|
for item in items {
|
||||||
|
if let syn::Item::Fn(fun) = item {
|
||||||
|
fun.attrs = fun
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.map(|attr| modify_attribute_with_scope(attr, &scope_prefix.value()))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TokenStream::from(quote! { #module }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the attribute is a method type and has a route path, then modify it
|
||||||
|
fn modify_attribute_with_scope(attr: &syn::Attribute, scope_path: &str) -> syn::Attribute {
|
||||||
|
match (attr.parse_args::<RouteArgs>(), attr.clone().meta) {
|
||||||
|
(Ok(route_args), syn::Meta::List(meta_list)) if has_allowed_methods_in_scope(attr) => {
|
||||||
|
let modified_path = format!("{}{}", scope_path, route_args.path.value());
|
||||||
|
|
||||||
|
let options_tokens: Vec<TokenStream2> = route_args
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.map(|option| {
|
||||||
|
quote! { ,#option }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let combined_options_tokens: TokenStream2 =
|
||||||
|
options_tokens
|
||||||
|
.into_iter()
|
||||||
|
.fold(TokenStream2::new(), |mut acc, ts| {
|
||||||
|
acc.extend(std::iter::once(ts));
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
syn::Attribute {
|
||||||
|
meta: syn::Meta::List(syn::MetaList {
|
||||||
|
tokens: quote! { #modified_path #combined_options_tokens },
|
||||||
|
..meta_list.clone()
|
||||||
|
}),
|
||||||
|
..attr.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => attr.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_allowed_methods_in_scope(attr: &syn::Attribute) -> bool {
|
||||||
|
MethodType::from_path(attr.path()).is_ok()
|
||||||
|
|| attr.path().is_ident("route")
|
||||||
|
|| attr.path().is_ident("ROUTE")
|
||||||
|
}
|
|
@ -61,18 +61,18 @@ mod scope_module {
|
||||||
/// Scope doc string to check in cargo expand.
|
/// Scope doc string to check in cargo expand.
|
||||||
#[scope("/v1")]
|
#[scope("/v1")]
|
||||||
mod mod_scope_v1 {
|
mod mod_scope_v1 {
|
||||||
use actix_web::{get, Responder};
|
use super::*;
|
||||||
|
|
||||||
/// Route doc string to check in cargo expand.
|
/// Route doc string to check in cargo expand.
|
||||||
#[get("/test")]
|
#[get("/test")]
|
||||||
pub async fn test() -> impl Responder {
|
pub async fn test() -> impl Responder {
|
||||||
super::scope_module::mod_common("version1 works".to_string())
|
scope_module::mod_common("version1 works".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[scope("/v2")]
|
#[scope("/v2")]
|
||||||
mod mod_scope_v2 {
|
mod mod_scope_v2 {
|
||||||
use actix_web::{get, Responder};
|
use super::*;
|
||||||
|
|
||||||
// check to make sure non-function tokens in the scope block are preserved...
|
// check to make sure non-function tokens in the scope block are preserved...
|
||||||
enum TestEnum {
|
enum TestEnum {
|
||||||
|
@ -85,7 +85,7 @@ mod mod_scope_v2 {
|
||||||
let test_enum = TestEnum::Works;
|
let test_enum = TestEnum::Works;
|
||||||
|
|
||||||
match test_enum {
|
match test_enum {
|
||||||
TestEnum::Works => super::scope_module::mod_common("version2 works".to_string()),
|
TestEnum::Works => scope_module::mod_common("version2 works".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,10 @@ fn compile_macros() {
|
||||||
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
|
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
|
||||||
|
|
||||||
|
t.compile_fail("tests/trybuild/scope-on-handler.rs");
|
||||||
|
t.compile_fail("tests/trybuild/scope-missing-args.rs");
|
||||||
|
t.compile_fail("tests/trybuild/scope-invalid-args.rs");
|
||||||
|
|
||||||
t.pass("tests/trybuild/docstring-ok.rs");
|
t.pass("tests/trybuild/docstring-ok.rs");
|
||||||
|
|
||||||
t.pass("tests/trybuild/test-runtime.rs");
|
t.pass("tests/trybuild/test-runtime.rs");
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
use actix_web_codegen::scope;
|
||||||
|
|
||||||
|
const PATH: &str = "/api";
|
||||||
|
|
||||||
|
#[scope(PATH)]
|
||||||
|
mod api_const {}
|
||||||
|
|
||||||
|
#[scope(true)]
|
||||||
|
mod api_bool {}
|
||||||
|
|
||||||
|
#[scope(123)]
|
||||||
|
mod api_num {}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -0,0 +1,17 @@
|
||||||
|
error: argument to scope macro is not a string literal, expected: #[scope("/prefix")]
|
||||||
|
--> tests/trybuild/scope-invalid-args.rs:5:9
|
||||||
|
|
|
||||||
|
5 | #[scope(PATH)]
|
||||||
|
| ^^^^
|
||||||
|
|
||||||
|
error: argument to scope macro is not a string literal, expected: #[scope("/prefix")]
|
||||||
|
--> tests/trybuild/scope-invalid-args.rs:8:9
|
||||||
|
|
|
||||||
|
8 | #[scope(true)]
|
||||||
|
| ^^^^
|
||||||
|
|
||||||
|
error: argument to scope macro is not a string literal, expected: #[scope("/prefix")]
|
||||||
|
--> tests/trybuild/scope-invalid-args.rs:11:9
|
||||||
|
|
|
||||||
|
11 | #[scope(123)]
|
||||||
|
| ^^^
|
|
@ -0,0 +1,6 @@
|
||||||
|
use actix_web_codegen::scope;
|
||||||
|
|
||||||
|
#[scope]
|
||||||
|
mod api {}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
error: missing arguments for scope macro, expected: #[scope("/prefix")]
|
||||||
|
--> tests/trybuild/scope-missing-args.rs:3:1
|
||||||
|
|
|
||||||
|
3 | #[scope]
|
||||||
|
| ^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in the attribute macro `scope` (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
@ -0,0 +1,8 @@
|
||||||
|
use actix_web_codegen::scope;
|
||||||
|
|
||||||
|
#[scope("/api")]
|
||||||
|
async fn index() -> &'static str {
|
||||||
|
"Hello World!"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -0,0 +1,5 @@
|
||||||
|
error: #[scope] macro must be attached to a module
|
||||||
|
--> tests/trybuild/scope-on-handler.rs:4:1
|
||||||
|
|
|
||||||
|
4 | async fn index() -> &'static str {
|
||||||
|
| ^^^^^
|
Loading…
Reference in New Issue