mirror of
https://github.com/actix/actix-web.git
synced 2024-11-22 17:41:11 +00:00
routes
macro allowing multiple paths per handler (#2718)
* WIP: basic implementation for `routes` macro * chore: changelog, docs, tests * error on missing methods * Apply suggestions from code review Co-authored-by: Igor Aleksanov <popzxc@yandex.ru> * update test stderr expectation * add additional tests * fix stderr output * remove useless ResourceType this is dead code from back when .to and .to_async were different ways to add a service Co-authored-by: Igor Aleksanov <popzxc@yandex.ru> Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
parent
c0d5d7bdb5
commit
8759d79b03
13 changed files with 384 additions and 112 deletions
|
@ -1,8 +1,11 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
[#2718]: https://github.com/actix/actix-web/pull/2718
|
||||||
|
|
||||||
|
|
||||||
## 4.0.1 - 2022-06-11
|
## 4.0.1 - 2022-06-11
|
||||||
- Fix support for guard paths in route handler macros. [#2771]
|
- Fix support for guard paths in route handler macros. [#2771]
|
||||||
|
|
|
@ -18,7 +18,7 @@ proc-macro = true
|
||||||
actix-router = "0.5.0"
|
actix-router = "0.5.0"
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
syn = { version = "1", features = ["full", "parsing"] }
|
syn = { version = "1", features = ["full", "extra-traits"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-macros = "0.2.3"
|
actix-macros = "0.2.3"
|
||||||
|
|
|
@ -46,9 +46,20 @@
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Multiple Path Handlers
|
//! # Multiple Path Handlers
|
||||||
//! There are no macros to generate multi-path handlers. Let us know in [this issue].
|
//! Acts as a wrapper for multiple single method handler macros. It takes no arguments and
|
||||||
|
//! delegates those to the macros for the individual methods. See [macro@routes] macro docs.
|
||||||
//!
|
//!
|
||||||
//! [this issue]: https://github.com/actix/actix-web/issues/1709
|
//! ```
|
||||||
|
//! # use actix_web::HttpResponse;
|
||||||
|
//! # use actix_web_codegen::routes;
|
||||||
|
//! #[routes]
|
||||||
|
//! #[get("/test")]
|
||||||
|
//! #[get("/test2")]
|
||||||
|
//! #[delete("/test")]
|
||||||
|
//! async fn example() -> HttpResponse {
|
||||||
|
//! HttpResponse::Ok().finish()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
|
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
|
||||||
//! [GET]: macro@get
|
//! [GET]: macro@get
|
||||||
|
@ -104,6 +115,39 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
route::with_method(None, args, input)
|
route::with_method(None, args, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates resource handler, allowing multiple HTTP methods and paths.
|
||||||
|
///
|
||||||
|
/// # Syntax
|
||||||
|
/// ```plain
|
||||||
|
/// #[routes]
|
||||||
|
/// #[<method>("path", ...)]
|
||||||
|
/// #[<method>("path", ...)]
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Attributes
|
||||||
|
/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for
|
||||||
|
/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post).
|
||||||
|
///
|
||||||
|
/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_web::HttpResponse;
|
||||||
|
/// # use actix_web_codegen::routes;
|
||||||
|
/// #[routes]
|
||||||
|
/// #[get("/test")]
|
||||||
|
/// #[get("/test2")]
|
||||||
|
/// #[delete("/test")]
|
||||||
|
/// async fn example() -> HttpResponse {
|
||||||
|
/// HttpResponse::Ok().finish()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[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), "`.")]
|
||||||
|
|
|
@ -3,24 +3,12 @@ use std::{collections::HashSet, convert::TryFrom};
|
||||||
use actix_router::ResourceDef;
|
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::{quote, ToTokens, TokenStreamExt};
|
||||||
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta, Path};
|
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
|
||||||
|
|
||||||
enum ResourceType {
|
|
||||||
Async,
|
|
||||||
Sync,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToTokens for ResourceType {
|
|
||||||
fn to_tokens(&self, stream: &mut TokenStream2) {
|
|
||||||
let ident = format_ident!("to");
|
|
||||||
stream.append(ident);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 +30,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 +85,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 #[{}("<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);
|
||||||
|
@ -183,55 +190,27 @@ impl Args {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Route {
|
pub struct Route {
|
||||||
|
/// Name of the handler function being annotated.
|
||||||
name: syn::Ident,
|
name: syn::Ident,
|
||||||
args: Args,
|
|
||||||
|
/// Args passed to routing macro.
|
||||||
|
///
|
||||||
|
/// When using `#[routes]`, this will contain args for each specific routing macro.
|
||||||
|
args: Vec<Args>,
|
||||||
|
|
||||||
|
/// AST of the handler function being annotated.
|
||||||
ast: syn::ItemFn,
|
ast: syn::ItemFn,
|
||||||
resource_type: ResourceType,
|
|
||||||
|
|
||||||
/// The doc comment attributes to copy to generated struct, if any.
|
/// The doc comment attributes to copy to generated struct, if any.
|
||||||
doc_attributes: Vec<syn::Attribute>,
|
doc_attributes: Vec<syn::Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
|
|
||||||
let mut guess = ResourceType::Sync;
|
|
||||||
|
|
||||||
if let syn::Type::ImplTrait(typ) = typ {
|
|
||||||
for bound in typ.bounds.iter() {
|
|
||||||
if let syn::TypeParamBound::Trait(bound) = bound {
|
|
||||||
for bound in bound.path.segments.iter() {
|
|
||||||
if bound.ident == "Future" {
|
|
||||||
guess = ResourceType::Async;
|
|
||||||
break;
|
|
||||||
} else if bound.ident == "Responder" {
|
|
||||||
guess = ResourceType::Sync;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guess
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Route {
|
impl Route {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
args: AttributeArgs,
|
args: AttributeArgs,
|
||||||
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.
|
||||||
|
@ -244,6 +223,7 @@ impl Route {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let args = Args::new(args, method)?;
|
let args = Args::new(args, method)?;
|
||||||
|
|
||||||
if args.methods.is_empty() {
|
if args.methods.is_empty() {
|
||||||
return Err(syn::Error::new(
|
return Err(syn::Error::new(
|
||||||
Span::call_site(),
|
Span::call_site(),
|
||||||
|
@ -251,25 +231,44 @@ impl Route {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let resource_type = if ast.sig.asyncness.is_some() {
|
if matches!(ast.sig.output, syn::ReturnType::Default) {
|
||||||
ResourceType::Async
|
|
||||||
} else {
|
|
||||||
match ast.sig.output {
|
|
||||||
syn::ReturnType::Default => {
|
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
ast,
|
ast,
|
||||||
"Function has no return type. Cannot be used as handler",
|
"Function has no return type. Cannot be used as handler",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()),
|
|
||||||
|
Ok(Self {
|
||||||
|
name,
|
||||||
|
args: vec![args],
|
||||||
|
ast,
|
||||||
|
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();
|
||||||
|
|
||||||
|
if matches!(ast.sig.output, syn::ReturnType::Default) {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
ast,
|
||||||
|
"Function has no return type. Cannot be used as handler",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
name,
|
name,
|
||||||
args,
|
args,
|
||||||
ast,
|
ast,
|
||||||
resource_type,
|
|
||||||
doc_attributes,
|
doc_attributes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -280,22 +279,28 @@ impl ToTokens for Route {
|
||||||
let Self {
|
let Self {
|
||||||
name,
|
name,
|
||||||
ast,
|
ast,
|
||||||
args:
|
args,
|
||||||
Args {
|
doc_attributes,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let registrations: TokenStream2 = args
|
||||||
|
.iter()
|
||||||
|
.map(|args| {
|
||||||
|
let Args {
|
||||||
path,
|
path,
|
||||||
resource_name,
|
resource_name,
|
||||||
guards,
|
guards,
|
||||||
wrappers,
|
wrappers,
|
||||||
methods,
|
methods,
|
||||||
},
|
} = args;
|
||||||
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);
|
||||||
|
|
||||||
let method_guards = {
|
let method_guards = {
|
||||||
let mut others = methods.iter();
|
let mut others = methods.iter();
|
||||||
|
|
||||||
// unwrapping since length is checked to be at least one
|
// unwrapping since length is checked to be at least one
|
||||||
let first = others.next().unwrap();
|
let first = others.next().unwrap();
|
||||||
|
|
||||||
|
@ -313,6 +318,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))*
|
||||||
|
.to(#name);
|
||||||
|
|
||||||
|
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let stream = quote! {
|
let stream = quote! {
|
||||||
#(#doc_attributes)*
|
#(#doc_attributes)*
|
||||||
#[allow(non_camel_case_types, missing_docs)]
|
#[allow(non_camel_case_types, missing_docs)]
|
||||||
|
@ -321,14 +339,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 +368,57 @@ 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) = ast
|
||||||
|
.attrs
|
||||||
|
.into_iter()
|
||||||
|
.map(|attr| match MethodType::from_path(&attr.path) {
|
||||||
|
Ok(method) => Ok((method, attr)),
|
||||||
|
Err(_) => Err(attr),
|
||||||
|
})
|
||||||
|
.partition::<Vec<_>, _>(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::<Result<Vec<_>, _>>()
|
||||||
|
{
|
||||||
|
Ok(methods) if methods.is_empty() => return input_and_compile_error(
|
||||||
|
input,
|
||||||
|
syn::Error::new(
|
||||||
|
Span::call_site(),
|
||||||
|
"The #[routes] macro requires at least one `#[<method>(..)]` attribute.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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
|
||||||
|
|
|
@ -8,9 +8,11 @@ use actix_web::{
|
||||||
header::{HeaderName, HeaderValue},
|
header::{HeaderName, HeaderValue},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
},
|
},
|
||||||
web, App, Error, HttpResponse, Responder,
|
web, App, Error, HttpRequest, HttpResponse, Responder,
|
||||||
|
};
|
||||||
|
use actix_web_codegen::{
|
||||||
|
connect, delete, get, head, options, patch, post, put, route, routes, trace,
|
||||||
};
|
};
|
||||||
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace};
|
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
// Make sure that we can name function as 'config'
|
// Make sure that we can name function as 'config'
|
||||||
|
@ -89,8 +91,41 @@ async fn route_test() -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
#[get("/routes/test")]
|
||||||
|
#[get("/routes/test2")]
|
||||||
|
#[post("/routes/test")]
|
||||||
|
async fn routes_test() -> impl Responder {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
// routes overlap with the more specific route first, therefore accessible
|
||||||
|
#[routes]
|
||||||
|
#[get("/routes/overlap/test")]
|
||||||
|
#[get("/routes/overlap/{foo}")]
|
||||||
|
async fn routes_overlapping_test(req: HttpRequest) -> impl Responder {
|
||||||
|
// foo is only populated when route is not /routes/overlap/test
|
||||||
|
match req.match_info().get("foo") {
|
||||||
|
None => assert!(req.uri() == "/routes/overlap/test"),
|
||||||
|
Some(_) => assert!(req.uri() != "/routes/overlap/test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
// routes overlap with the more specific route last, therefore inaccessible
|
||||||
|
#[routes]
|
||||||
|
#[get("/routes/overlap2/{foo}")]
|
||||||
|
#[get("/routes/overlap2/test")]
|
||||||
|
async fn routes_overlapping_inaccessible_test(req: HttpRequest) -> impl Responder {
|
||||||
|
// foo is always populated even when path is /routes/overlap2/test
|
||||||
|
assert!(req.match_info().get("foo").is_some());
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/custom_resource_name", name = "custom")]
|
#[get("/custom_resource_name", name = "custom")]
|
||||||
async fn custom_resource_name_test<'a>(req: actix_web::HttpRequest) -> impl Responder {
|
async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder {
|
||||||
assert!(req.url_for_static("custom").is_ok());
|
assert!(req.url_for_static("custom").is_ok());
|
||||||
assert!(req.url_for_static("custom_resource_name_test").is_err());
|
assert!(req.url_for_static("custom_resource_name_test").is_err());
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
|
@ -201,6 +236,9 @@ async fn test_body() {
|
||||||
.service(patch_test)
|
.service(patch_test)
|
||||||
.service(test_handler)
|
.service(test_handler)
|
||||||
.service(route_test)
|
.service(route_test)
|
||||||
|
.service(routes_overlapping_test)
|
||||||
|
.service(routes_overlapping_inaccessible_test)
|
||||||
|
.service(routes_test)
|
||||||
.service(custom_resource_name_test)
|
.service(custom_resource_name_test)
|
||||||
.service(guard_test)
|
.service(guard_test)
|
||||||
});
|
});
|
||||||
|
@ -258,6 +296,38 @@ async fn test_body() {
|
||||||
let response = request.send().await.unwrap();
|
let response = request.send().await.unwrap();
|
||||||
assert!(!response.status().is_success());
|
assert!(!response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/test2"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::POST, srv.url("/routes/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/not-set"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_client_error());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/overlap/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/overlap/bar"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/bar"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
let request = srv.request(http::Method::GET, srv.url("/custom_resource_name"));
|
let request = srv.request(http::Method::GET, srv.url("/custom_resource_name"));
|
||||||
let response = request.send().await.unwrap();
|
let response = request.send().await.unwrap();
|
||||||
assert!(response.status().is_success());
|
assert!(response.status().is_success());
|
||||||
|
|
|
@ -12,6 +12,10 @@ fn compile_macros() {
|
||||||
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
|
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
|
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
|
||||||
|
|
||||||
|
t.pass("tests/trybuild/routes-ok.rs");
|
||||||
|
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
||||||
|
t.compile_fail("tests/trybuild/routes-missing-args-fail.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");
|
||||||
|
|
14
actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs
Normal file
14
actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use actix_web_codegen::*;
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
#[get]
|
||||||
|
async fn index() -> String {
|
||||||
|
"Hello World!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
use actix_web::App;
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
error: invalid service definition, expected #[get("<path>")]
|
||||||
|
--> tests/trybuild/routes-missing-args-fail.rs:4:1
|
||||||
|
|
|
||||||
|
4 | #[get]
|
||||||
|
| ^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
|
|
||||||
|
error: Invalid input for macro
|
||||||
|
--> tests/trybuild/routes-missing-args-fail.rs:4:1
|
||||||
|
|
|
||||||
|
4 | #[get]
|
||||||
|
| ^^^^^^
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
||||||
|
--> tests/trybuild/routes-missing-args-fail.rs:13:55
|
||||||
|
|
|
||||||
|
13 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
|
@ -0,0 +1,13 @@
|
||||||
|
use actix_web_codegen::*;
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
async fn index() -> String {
|
||||||
|
"Hello World!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
use actix_web::App;
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
error: The #[routes] macro requires at least one `#[<method>(..)]` attribute.
|
||||||
|
--> tests/trybuild/routes-missing-method-fail.rs:3:1
|
||||||
|
|
|
||||||
|
3 | #[routes]
|
||||||
|
| ^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in the attribute macro `routes` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
||||||
|
--> tests/trybuild/routes-missing-method-fail.rs:12:55
|
||||||
|
|
|
||||||
|
12 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
23
actix-web-codegen/tests/trybuild/routes-ok.rs
Normal file
23
actix-web-codegen/tests/trybuild/routes-ok.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use actix_web_codegen::*;
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
#[get("/")]
|
||||||
|
#[post("/")]
|
||||||
|
async fn index() -> String {
|
||||||
|
"Hello World!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
use actix_web::App;
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
|
||||||
|
let request = srv.get("/");
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.post("/");
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
}
|
|
@ -2,12 +2,14 @@
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
### Added
|
### Added
|
||||||
|
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
|
||||||
- Add `ServiceRequest::{parts, request}()` getter methods. [#2786]
|
- Add `ServiceRequest::{parts, request}()` getter methods. [#2786]
|
||||||
- Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752]
|
- Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
[#2718]: https://github.com/actix/actix-web/pull/2718
|
||||||
[#2752]: https://github.com/actix/actix-web/pull/2752
|
[#2752]: https://github.com/actix/actix-web/pull/2752
|
||||||
[#2786]: https://github.com/actix/actix-web/pull/2786
|
[#2786]: https://github.com/actix/actix-web/pull/2786
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,7 @@ macro_rules! codegen_reexport {
|
||||||
codegen_reexport!(main);
|
codegen_reexport!(main);
|
||||||
codegen_reexport!(test);
|
codegen_reexport!(test);
|
||||||
codegen_reexport!(route);
|
codegen_reexport!(route);
|
||||||
|
codegen_reexport!(routes);
|
||||||
codegen_reexport!(head);
|
codegen_reexport!(head);
|
||||||
codegen_reexport!(get);
|
codegen_reexport!(get);
|
||||||
codegen_reexport!(post);
|
codegen_reexport!(post);
|
||||||
|
|
Loading…
Reference in a new issue