Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scope macro #3136

Open
wants to merge 46 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ad90bc9
add scope proc macro
pmd3d Sep 3, 2023
ac82b56
Update scope macro code to work with current HttpServiceFactory
pmd3d Sep 10, 2023
db69279
started some test code
pmd3d Sep 10, 2023
6bedb95
add some unit tests
pmd3d Sep 14, 2023
efe990d
Merge branch 'master' into scope_work
pmd3d Sep 14, 2023
ec4633a
code formatting cleanup
pmd3d Sep 14, 2023
3a57148
Merge branch 'master' into scope_work
pmd3d Sep 16, 2023
3e4c643
add another test for combining and calling 2 scopes
pmd3d Sep 16, 2023
4ae7a00
Merge branch 'master' into scope_work
pmd3d Sep 18, 2023
3c3b5d0
Merge branch 'master' into scope_work
pmd3d Sep 21, 2023
e931a58
format code with formatter
pmd3d Sep 21, 2023
40b8ec7
Merge branch 'master' into scope_work
pmd3d Oct 9, 2023
03501cb
Merge branch 'master' into scope_work
pmd3d Oct 12, 2023
bbf5cf3
Merge branch 'master' into scope_work
pmd3d Oct 15, 2023
4f11358
Merge branch 'master' into scope_work
pmd3d Oct 24, 2023
e3432c8
Merge branch 'master' into scope_work
pmd3d Nov 3, 2023
22e51a4
Merge branch 'master' into scope_work
pmd3d Nov 20, 2023
b05197a
Merge branch 'master' into scope_work
pmd3d Nov 20, 2023
5d291e1
Merge branch 'master' into scope_work
pmd3d Nov 20, 2023
567deaa
Merge branch 'master' into scope_work
pmd3d Nov 28, 2023
f9bd347
Merge branch 'master' into scope_work
pmd3d Dec 7, 2023
3ee0dc9
Update actix-web-codegen/src/lib.rs with comment documentation fix
pmd3d Dec 10, 2023
d6ee277
Merge branch 'master' into scope_work
pmd3d Dec 16, 2023
f82e740
Merge branch 'master' into scope_work
pmd3d Dec 22, 2023
88883b7
Merge branch 'master' into scope_work
pmd3d Dec 29, 2023
f01b4cd
Merge branch 'master' into scope_work
pmd3d Jan 6, 2024
687667f
Merge branch 'master' into scope_work
pmd3d Jan 9, 2024
4a8b05c
Merge branch 'master' into scope_work
pmd3d Jan 11, 2024
16c84c2
Merge branch 'master' into scope_work
pmd3d Jan 25, 2024
f8f93d4
Merge branch 'master' into scope_work
pmd3d Feb 18, 2024
c4520d9
work in progress. revised procedural macro to change othe macro call
pmd3d Apr 1, 2024
455c064
Merge branch 'master' into scope_work
pmd3d Apr 1, 2024
ab73c72
add tests again. refactor nested code.
pmd3d Apr 1, 2024
828be28
clean up code. fix bugs with route and method attributes with parameters
pmd3d Apr 3, 2024
4fb51ad
clean up for rust fmt
pmd3d Apr 3, 2024
bcc0bed
clean up for rust fmt
pmd3d Apr 3, 2024
2f4b859
fix out of date comment for scope macro
pmd3d Apr 3, 2024
6eabb23
Merge branch 'master' into scope_work
pmd3d Apr 9, 2024
6d3fff2
Merge branch 'master' into scope_work
pmd3d Apr 17, 2024
89f7819
sync to master branch by adding test_wrap
pmd3d Apr 17, 2024
d35801c
needed to format code
pmd3d Apr 17, 2024
3137987
Merge branch 'master' into scope_work
pmd3d May 5, 2024
4606644
Merge branch 'master' into scope_work
pmd3d May 10, 2024
7b93780
Merge branch 'master' into scope_work
robjtede May 14, 2024
a3d6a49
Merge branch 'master' into scope_work
robjtede May 14, 2024
c3af757
Merge branch 'master' into scope_work
pmd3d May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions actix-web-codegen/CHANGES.md
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Minimum supported Rust version (MSRV) is now 1.72.
- Add a scope macro that takes a path

## 4.2.2

Expand Down
28 changes: 28 additions & 0 deletions actix-web-codegen/src/lib.rs
Expand Up @@ -240,3 +240,31 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
output.extend(item);
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)
}
90 changes: 90 additions & 0 deletions actix-web-codegen/src/route.rs
Expand Up @@ -554,3 +554,93 @@ fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStrea
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(),
}
}
194 changes: 193 additions & 1 deletion actix-web-codegen/tests/test_macro.rs
Expand Up @@ -11,7 +11,7 @@ use actix_web::{
web, App, Error, HttpRequest, HttpResponse, Responder,
};
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;

Expand Down Expand Up @@ -384,3 +384,195 @@ 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::{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");
}
1 change: 1 addition & 0 deletions actix-web/src/lib.rs
Expand Up @@ -142,5 +142,6 @@ codegen_reexport!(delete);
codegen_reexport!(trace);
codegen_reexport!(connect);
codegen_reexport!(options);
codegen_reexport!(scope);

pub(crate) type BoxError = Box<dyn std::error::Error>;