mirror of
https://github.com/actix/actix-web.git
synced 2024-05-19 16:58:14 +00:00
Compare commits
42 commits
5b73589342
...
e6611ffa69
Author | SHA1 | Date | |
---|---|---|---|
e6611ffa69 | |||
d35801cf12 | |||
89f78190bd | |||
6d3fff281b | |||
6eabb23bac | |||
2f4b859dab | |||
bcc0bed4f5 | |||
4fb51ad70a | |||
828be28199 | |||
ab73c72596 | |||
455c064728 | |||
c4520d909a | |||
f8f93d4dab | |||
16c84c2805 | |||
4a8b05c837 | |||
687667f1f7 | |||
f01b4cd68d | |||
88883b781d | |||
f82e740776 | |||
d6ee2779cd | |||
3ee0dc9329 | |||
f9bd3472a2 | |||
567deaa3ec | |||
5d291e1cc1 | |||
b05197accf | |||
22e51a4287 | |||
e3432c88ce | |||
4f1135804a | |||
bbf5cf3cd0 | |||
03501cbf98 | |||
40b8ec7b97 | |||
e931a58092 | |||
3c3b5d0cdf | |||
4ae7a00b3e | |||
3e4c6438f3 | |||
3a5714836c | |||
ec4633a911 | |||
efe990dca5 | |||
6bedb958e4 | |||
db69279557 | |||
ac82b56ad7 | |||
ad90bc926d |
|
@ -3,6 +3,7 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.72.
|
- Minimum supported Rust version (MSRV) is now 1.72.
|
||||||
|
- Add a scope macro that takes a path
|
||||||
|
|
||||||
## 4.2.2
|
## 4.2.2
|
||||||
|
|
||||||
|
|
|
@ -240,3 +240,31 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
output.extend(item);
|
output.extend(item);
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates scope
|
||||||
|
///
|
||||||
|
/// Syntax: `#[scope("path")]`
|
||||||
|
///
|
||||||
|
/// ## Attributes:
|
||||||
|
///
|
||||||
|
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```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)
|
||||||
|
}
|
||||||
|
|
|
@ -554,3 +554,93 @@ fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStrea
|
||||||
item.extend(compile_err);
|
item.extend(compile_err);
|
||||||
item
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use actix_web::{
|
||||||
web, App, Error, HttpRequest, HttpResponse, Responder,
|
web, App, Error, HttpRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use actix_web_codegen::{
|
use actix_web_codegen::{
|
||||||
connect, delete, get, head, options, patch, post, put, route, routes, trace,
|
connect, delete, get, head, options, patch, post, put, route, routes, scope, trace,
|
||||||
};
|
};
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
|
@ -384,3 +384,195 @@ 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::{delete, get, post, route, routes, web, HttpResponse, Responder};
|
||||||
|
|
||||||
|
use crate::guard_module;
|
||||||
|
|
||||||
|
#[get("/test/guard", guard = "guard_module::guard")]
|
||||||
|
pub async fn test_guard() -> impl Responder {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/test")]
|
||||||
|
pub async fn test() -> impl Responder {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/twicetest/{value}")]
|
||||||
|
pub async fn test_twice(value: web::Path<String>) -> impl actix_web::Responder {
|
||||||
|
let int_value: i32 = value.parse().unwrap_or(0);
|
||||||
|
let doubled = int_value * 2;
|
||||||
|
HttpResponse::Ok().body(format!("Twice value: {}", doubled))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/test")]
|
||||||
|
pub async fn test_post() -> impl Responder {
|
||||||
|
HttpResponse::Ok().body(format!("post works"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/test")]
|
||||||
|
pub async fn test_delete() -> impl Responder {
|
||||||
|
HttpResponse::Ok().body("delete works")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[route("/test", method = "PUT", method = "PATCH", method = "CUSTOM")]
|
||||||
|
pub async fn test_multiple_shared_path() -> impl Responder {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
#[head("/test")]
|
||||||
|
#[connect("/test")]
|
||||||
|
#[options("/test")]
|
||||||
|
#[trace("/test")]
|
||||||
|
async fn test_multiple_separate_paths() -> impl Responder {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// test calling this from other mod scope with scope attribute...
|
||||||
|
pub fn mod_common(message: String) -> impl actix_web::Responder {
|
||||||
|
HttpResponse::Ok().body(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[scope("/v1")]
|
||||||
|
mod mod_scope_v1 {
|
||||||
|
use actix_web::{get, Responder};
|
||||||
|
|
||||||
|
#[get("/test")]
|
||||||
|
#[doc = "doc string to check in cargo expand"]
|
||||||
|
pub async fn test() -> impl Responder {
|
||||||
|
super::scope_module::mod_common("version1 works".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[scope("/v2")]
|
||||||
|
mod mod_scope_v2 {
|
||||||
|
use actix_web::{get, Responder};
|
||||||
|
|
||||||
|
// check to make sure non-function tokens in the scope block are preserved...
|
||||||
|
enum TestEnum {
|
||||||
|
Works,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/test")]
|
||||||
|
pub async fn test() -> impl Responder {
|
||||||
|
// make sure this type still exists...
|
||||||
|
let test_enum = TestEnum::Works;
|
||||||
|
|
||||||
|
match test_enum {
|
||||||
|
TestEnum::Works => super::scope_module::mod_common("version2 works".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_scope_get_async() {
|
||||||
|
let srv = actix_test::start(|| App::new().service(scope_module::test));
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/test/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_scope_get_param_async() {
|
||||||
|
let srv = actix_test::start(|| App::new().service(scope_module::test_twice));
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/test/twicetest/4"));
|
||||||
|
let mut response = request.send().await.unwrap();
|
||||||
|
let body = response.body().await.unwrap();
|
||||||
|
let body_str = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
assert_eq!(body_str, "Twice value: 8");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_scope_post_async() {
|
||||||
|
let srv = actix_test::start(|| App::new().service(scope_module::test_post));
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::POST, srv.url("/test/test"));
|
||||||
|
let mut response = request.send().await.unwrap();
|
||||||
|
let body = response.body().await.unwrap();
|
||||||
|
let body_str = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
assert_eq!(body_str, "post works");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_multiple_shared_path_async() {
|
||||||
|
let srv = actix_test::start(|| App::new().service(scope_module::test_multiple_shared_path));
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::PUT, srv.url("/test/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::PATCH, srv.url("/test/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_multiple_multipaths_async() {
|
||||||
|
let srv = actix_test::start(|| App::new().service(scope_module::test_multiple_separate_paths));
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::CONNECT, srv.url("/test/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::OPTIONS, srv.url("/test/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::TRACE, srv.url("/test/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::HEAD, srv.url("/test/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_scope_delete_async() {
|
||||||
|
let srv = actix_test::start(|| App::new().service(scope_module::test_delete));
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::DELETE, srv.url("/test/test"));
|
||||||
|
let mut response = request.send().await.unwrap();
|
||||||
|
let body = response.body().await.unwrap();
|
||||||
|
let body_str = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
assert_eq!(body_str, "delete works");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_scope_get_with_guard_async() {
|
||||||
|
let srv = actix_test::start(|| App::new().service(scope_module::test_guard));
|
||||||
|
|
||||||
|
let request = srv
|
||||||
|
.request(http::Method::GET, srv.url("/test/test/guard"))
|
||||||
|
.insert_header(("Accept", "image/*"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_scope_v1_v2_async() {
|
||||||
|
let srv = actix_test::start(|| {
|
||||||
|
App::new()
|
||||||
|
.service(mod_scope_v1::test)
|
||||||
|
.service(mod_scope_v2::test)
|
||||||
|
});
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/v1/test"));
|
||||||
|
let mut response = request.send().await.unwrap();
|
||||||
|
let body = response.body().await.unwrap();
|
||||||
|
let body_str = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
assert_eq!(body_str, "version1 works");
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/v2/test"));
|
||||||
|
let mut response = request.send().await.unwrap();
|
||||||
|
let body = response.body().await.unwrap();
|
||||||
|
let body_str = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
assert_eq!(body_str, "version2 works");
|
||||||
|
}
|
||||||
|
|
|
@ -142,5 +142,6 @@ codegen_reexport!(delete);
|
||||||
codegen_reexport!(trace);
|
codegen_reexport!(trace);
|
||||||
codegen_reexport!(connect);
|
codegen_reexport!(connect);
|
||||||
codegen_reexport!(options);
|
codegen_reexport!(options);
|
||||||
|
codegen_reexport!(scope);
|
||||||
|
|
||||||
pub(crate) type BoxError = Box<dyn std::error::Error>;
|
pub(crate) type BoxError = Box<dyn std::error::Error>;
|
||||||
|
|
Loading…
Reference in a new issue