diff --git a/Cargo.lock b/Cargo.lock index 13660af..30d417f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,12 +84,12 @@ dependencies = [ [[package]] name = "actix-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -245,12 +245,12 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] name = "actix-web-codegen-const-routes" -version = "4.0.0" +version = "0.1.0" dependencies = [ "actix-macros", "actix-router", @@ -262,7 +262,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.38", "trybuild", ] @@ -474,7 +474,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.99", ] [[package]] @@ -847,18 +847,18 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -969,7 +969,7 @@ checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -1051,6 +1051,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index 8da2c2b..6301515 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ name = "actix-web-codegen-const-routes" version = "0.1.0" description = "Routing and runtime macros for Actix Web with support for const routes" -homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" authors = [ "Nikolay Kim ", @@ -16,18 +15,18 @@ edition = "2021" proc-macro = true [dependencies] -actix-router = "0.5.0" +actix-router = "0.5" proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full", "parsing"] } +syn = { version = "2", features = ["full", "extra-traits"] } [dev-dependencies] -actix-macros = "0.2.3" +actix-macros = "0.2.4" actix-rt = "2.2" -actix-test = "0.1.0-beta.13" -actix-utils = "3.0.0" -actix-web = "4.0.0" +actix-test = "0.1" +actix-utils = "3" +actix-web = "4" -futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } +futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } trybuild = "1" rustversion = "1" diff --git a/src/lib.rs b/src/lib.rs index fb34905..a3c2c1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,9 +46,20 @@ //! ``` //! //! # 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_const_routes::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 //! [GET]: macro@get @@ -64,6 +75,7 @@ #![recursion_limit = "512"] #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] use proc_macro::TokenStream; use quote::quote; @@ -79,6 +91,7 @@ mod route; /// /// # Attributes /// - `"path"`: Raw literal string with path for which to register handler. +/// - `path="variable_name"` - Variable name that contains path for which to register handler /// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function /// name of handler is used. /// - `method = "HTTP_METHOD"`: Registers HTTP method to provide guard for. Upper-case string, @@ -94,7 +107,7 @@ mod route; /// ``` /// # use actix_web::HttpResponse; /// # use actix_web_codegen_const_routes::route; -/// #[route("/test", method = "GET", method = "HEAD")] +/// #[route("/test", method = "GET", method = "HEAD", method = "CUSTOM")] /// async fn example() -> HttpResponse { /// HttpResponse::Ok().finish() /// } @@ -104,6 +117,39 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream { route::with_method(None, args, input) } +/// Creates resource handler, allowing multiple HTTP methods and paths. +/// +/// # Syntax +/// ```plain +/// #[routes] +/// #[("path", ...)] +/// #[("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_const_routes::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 { ($variant:ident, $method:ident) => { #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] @@ -115,8 +161,8 @@ macro_rules! method_macro { /// /// # Attributes /// - `"path"`: Raw literal string with path for which to register handler. - /// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function - /// name of handler is used. + /// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the + /// function name of handler is used. /// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`. /// - `wrap = "Middleware"`: Registers a resource middleware. /// @@ -174,7 +220,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { output } -/// Marks async test functions to use the actix system entry-point. +/// Marks async test functions to use the Actix Web system entry-point. /// /// # Examples /// ``` diff --git a/src/route.rs b/src/route.rs index 3af845f..7b31d3a 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,28 +1,56 @@ -use std::{collections::HashSet, convert::TryFrom}; +use std::collections::HashSet; use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote, ToTokens, TokenStreamExt}; -use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token}; -enum ResourceType { - Async, - Sync, +#[derive(Debug)] +pub struct RouteArgs { + //path: syn::LitStr, + path: Option>, + options: Punctuated, } -impl ToTokens for ResourceType { - fn to_tokens(&self, stream: &mut TokenStream2) { - let ident = format_ident!("to"); - stream.append(ident); +impl syn::parse::Parse for RouteArgs { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + // path to match: "/foo" + + let path = if let Ok(path) = input.parse::() { + // verify that path pattern is valid + let _ = ResourceDef::new(path.value()); + let path: Box = Box::new(path); + Some(path) + } else { + None + }; + + if path.is_some() { + // if there's no comma, assume that no options are provided + if !input.peek(Token![,]) { + return Ok(Self { + path, + options: Punctuated::new(), + }); + } else { + // advance past comma separator + input.parse::()?; + } + } + + // zero or more options: name = "foo" + let options = input.parse_terminated(syn::MetaNameValue::parse, Token![,])?; + + Ok(Self { path, options }) } } -macro_rules! method_type { +macro_rules! standard_method_type { ( - $($variant:ident, $upper:ident,)+ + $($variant:ident, $upper:ident, $lower:ident,)+ ) => { - #[derive(Debug, PartialEq, Eq, Hash)] + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum MethodType { $( $variant, @@ -39,30 +67,30 @@ macro_rules! method_type { fn parse(method: &str) -> Result { match method { $(stringify!($upper) => Ok(Self::$variant),)+ - _ => Err(format!("Unexpected HTTP method: `{}`", method)), + _ => Err(format!("HTTP method must be uppercase: `{}`", method)), + } + } + + fn from_path(method: &Path) -> Result { + match () { + $(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+ + _ => Err(()), } } } }; } -method_type! { - Get, GET, - Post, POST, - Put, PUT, - Delete, DELETE, - Head, HEAD, - Connect, CONNECT, - Options, OPTIONS, - Trace, TRACE, - Patch, PATCH, -} - -impl ToTokens for MethodType { - fn to_tokens(&self, stream: &mut TokenStream2) { - let ident = Ident::new(self.as_str(), Span::call_site()); - stream.append(ident); - } +standard_method_type! { + Get, GET, get, + Post, POST, post, + Put, PUT, put, + Delete, DELETE, delete, + Head, HEAD, head, + Connect, CONNECT, connect, + Options, OPTIONS, options, + Trace, TRACE, trace, + Patch, PATCH, patch, } impl TryFrom<&syn::LitStr> for MethodType { @@ -74,22 +102,133 @@ impl TryFrom<&syn::LitStr> for MethodType { } } +impl ToTokens for MethodType { + fn to_tokens(&self, stream: &mut TokenStream2) { + let ident = Ident::new(self.as_str(), Span::call_site()); + stream.append(ident); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum MethodTypeExt { + Standard(MethodType), + Custom(LitStr), +} + +impl MethodTypeExt { + /// Returns a single method guard token stream. + fn to_tokens_single_guard(&self) -> TokenStream2 { + match self { + MethodTypeExt::Standard(method) => { + quote! { + .guard(::actix_web::guard::#method()) + } + } + MethodTypeExt::Custom(lit) => { + quote! { + .guard(::actix_web::guard::Method( + ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap() + )) + } + } + } + } + + /// Returns a multi-method guard chain token stream. + fn to_tokens_multi_guard(&self, or_chain: Vec) -> TokenStream2 { + debug_assert!( + !or_chain.is_empty(), + "empty or_chain passed to multi-guard constructor" + ); + + match self { + MethodTypeExt::Standard(method) => { + quote! { + .guard( + ::actix_web::guard::Any(::actix_web::guard::#method()) + #(#or_chain)* + ) + } + } + MethodTypeExt::Custom(lit) => { + quote! { + .guard( + ::actix_web::guard::Any( + ::actix_web::guard::Method( + ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap() + ) + ) + #(#or_chain)* + ) + } + } + } + } + + /// Returns a token stream containing the `.or` chain to be passed in to + /// [`MethodTypeExt::to_tokens_multi_guard()`]. + fn to_tokens_multi_guard_or_chain(&self) -> TokenStream2 { + match self { + MethodTypeExt::Standard(method_type) => { + quote! { + .or(::actix_web::guard::#method_type()) + } + } + MethodTypeExt::Custom(lit) => { + quote! { + .or( + ::actix_web::guard::Method( + ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap() + ) + ) + } + } + } + } +} + +impl ToTokens for MethodTypeExt { + fn to_tokens(&self, stream: &mut TokenStream2) { + match self { + MethodTypeExt::Custom(lit_str) => { + let ident = Ident::new(lit_str.value().as_str(), Span::call_site()); + stream.append(ident); + } + MethodTypeExt::Standard(method) => method.to_tokens(stream), + } + } +} + +impl TryFrom<&syn::LitStr> for MethodTypeExt { + type Error = syn::Error; + + fn try_from(value: &syn::LitStr) -> Result { + match MethodType::try_from(value) { + Ok(method) => Ok(MethodTypeExt::Standard(method)), + Err(_) if value.value().chars().all(|c| c.is_ascii_uppercase()) => { + Ok(MethodTypeExt::Custom(value.clone())) + } + Err(err) => Err(err), + } + } +} + struct Args { path: Box, resource_name: Option, - guards: Vec, - wrappers: Vec, - methods: HashSet, + guards: Vec, + wrappers: Vec, + methods: HashSet, } -trait PathMarker: quote::ToTokens {} +trait PathMarker: quote::ToTokens + std::fmt::Debug {} impl PathMarker for syn::Ident {} impl PathMarker for syn::LitStr {} impl PathMarker for syn::Expr {} impl Args { - fn new(args: AttributeArgs, method: Option) -> syn::Result { + fn new(args: RouteArgs, method: Option) -> syn::Result { let mut path: Option> = None; let mut resource_name = None; let mut guards = Vec::new(); @@ -98,109 +237,119 @@ impl Args { let is_route_macro = method.is_none(); if let Some(method) = method { - methods.insert(method); + methods.insert(MethodTypeExt::Standard(method)); } - for arg in args { - match arg { - NestedMeta::Lit(syn::Lit::Str(lit)) => match path { - None => { - let _ = ResourceDef::new(lit.value()); - path = Some(Box::new(lit)); - } - - _ => { - return Err(syn::Error::new_spanned( - lit, - "Multiple paths specified! Should be only one!", - )); - } - }, - NestedMeta::Meta(syn::Meta::NameValue(nv)) => { - if nv.path.is_ident("name") { - if let syn::Lit::Str(lit) = nv.lit { - resource_name = Some(lit); - } else { - return Err(syn::Error::new_spanned( - nv.lit, - "Attribute name expects literal string!", - )); - } - } else if nv.path.is_ident("path") { - if let syn::Lit::Str(lit) = nv.lit { - match path { - None => { - path = Some(Box::new(lit.parse::()?)); - } - _ => { - return Err(syn::Error::new_spanned( - lit, - "Multiple paths specified! Should be only one!", - )); - } - } - } else { - return Err(syn::Error::new_spanned( - nv.lit, - "Attribute path expects literal string!", - )); - } - } else if nv.path.is_ident("guard") { - if let syn::Lit::Str(lit) = nv.lit { - guards.push(Ident::new(&lit.value(), Span::call_site())); - } else { - return Err(syn::Error::new_spanned( - nv.lit, - "Attribute guard expects literal string!", - )); - } - } else if nv.path.is_ident("wrap") { - if let syn::Lit::Str(lit) = nv.lit { - wrappers.push(lit.parse()?); - } else { - return Err(syn::Error::new_spanned( - nv.lit, - "Attribute wrap expects type", - )); - } - } else if nv.path.is_ident("method") { - if !is_route_macro { - return Err(syn::Error::new_spanned( - &nv, - "HTTP method forbidden here. To handle multiple methods, use `route` instead", - )); - } else if let syn::Lit::Str(ref lit) = nv.lit { - let method = MethodType::try_from(lit)?; - if !methods.insert(method) { + for nv in args.options { + if nv.path.is_ident("name") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value + { + resource_name = Some(lit); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute name expects literal string", + )); + } + } else if nv.path.is_ident("path") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value + { + // if let syn::Lit::Str(lit) = nv.lit { + match path { + None => { + if args.path.is_some() { return Err(syn::Error::new_spanned( - &nv.lit, - &format!( - "HTTP method defined more than once: `{}`", - lit.value() - ), + lit, + "Multiple paths specified! Should be only one!", )); } - } else { + + path = Some(Box::new(lit.parse::()?)); + } + _ => { return Err(syn::Error::new_spanned( - nv.lit, - "Attribute method expects literal string!", + lit, + "Multiple paths specified! Should be only one!", )); } - } else { + } + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute path expects literal string!", + )); + } + } else if nv.path.is_ident("guard") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value + { + guards.push(lit.parse::()?); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute guard expects literal string", + )); + } + } else if nv.path.is_ident("wrap") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value + { + wrappers.push(lit.parse()?); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute wrap expects type", + )); + } + } else if nv.path.is_ident("method") { + if !is_route_macro { + return Err(syn::Error::new_spanned( + &nv, + "HTTP method forbidden here; to handle multiple methods, use `route` instead", + )); + } else if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value.clone() + { + if !methods.insert(MethodTypeExt::try_from(&lit)?) { return Err(syn::Error::new_spanned( - nv.path, - "Unknown attribute key is specified. Allowed: guard, method and wrap", + nv.value, + format!("HTTP method defined more than once: `{}`", lit.value()), )); } + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute method expects literal string", + )); } - arg => { - return Err(syn::Error::new_spanned(arg, "Unknown attribute.")); - } + } else { + return Err(syn::Error::new_spanned( + nv.path, + "Unknown attribute key is specified; allowed: guard, method and wrap", + )); } } + let path = if let Some(path) = path { + path + } else { + args.path.unwrap() + }; + Ok(Args { - path: path.unwrap(), + path, resource_name, guards, wrappers, @@ -210,55 +359,23 @@ impl Args { } pub struct Route { + /// Name of the handler function being annotated. name: syn::Ident, - args: Args, + + /// Args passed to routing macro. + /// + /// When using `#[routes]`, this will contain args for each specific routing macro. + args: Vec, + + /// AST of the handler function being annotated. ast: syn::ItemFn, - resource_type: ResourceType, /// The doc comment attributes to copy to generated struct, if any. doc_attributes: Vec, } -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 { - pub fn new( - args: AttributeArgs, - ast: syn::ItemFn, - method: Option, - ) -> syn::Result { - if args.is_empty() { - return Err(syn::Error::new( - Span::call_site(), - format!( - r#"invalid service definition, expected #[{}("")]"#, - method - .map_or("route", |it| it.as_str()) - .to_ascii_lowercase() - ), - )); - } - + pub fn new(args: RouteArgs, ast: syn::ItemFn, method: Option) -> syn::Result { let name = ast.sig.ident.clone(); // Try and pull out the doc comments so that we can reapply them to the generated struct. @@ -266,11 +383,12 @@ impl Route { let doc_attributes = ast .attrs .iter() - .filter(|attr| attr.path.is_ident("doc")) + .filter(|attr| attr.path().is_ident("doc")) .cloned() .collect(); let args = Args::new(args, method)?; + if args.methods.is_empty() { return Err(syn::Error::new( Span::call_site(), @@ -278,25 +396,44 @@ impl Route { )); } - 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()), - } - }; + 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 { + name, + args: vec![args], + ast, + doc_attributes, + }) + } + + fn multiple(args: Vec, ast: syn::ItemFn) -> syn::Result { + 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 { name, args, ast, - resource_type, doc_attributes, }) } @@ -307,40 +444,55 @@ impl ToTokens for Route { let Self { name, ast, - args: - Args { + args, + doc_attributes, + } = self; + + let registrations: TokenStream2 = args + .iter() + .map(|args| { + let Args { path, resource_name, guards, wrappers, methods, - }, - resource_type, - doc_attributes, - } = self; - let resource_name = resource_name - .as_ref() - .map_or_else(|| name.to_string(), LitStr::value); - let method_guards = { - let mut others = methods.iter(); - // unwrapping since length is checked to be at least one - let first = others.next().unwrap(); + } = args; - if methods.len() > 1 { - quote! { - .guard( - ::actix_web::guard::Any(::actix_web::guard::#first()) - #(.or(::actix_web::guard::#others()))* - ) - } - } else { - quote! { - .guard(::actix_web::guard::#first()) - } - } - }; + let resource_name = resource_name + .as_ref() + .map_or_else(|| name.to_string(), LitStr::value); - let path = path.as_ref(); + let method_guards = { + debug_assert!(!methods.is_empty(), "Args::methods should not be empty"); + + let mut others = methods.iter(); + let first = others.next().unwrap(); + + if methods.len() > 1 { + let other_method_guards = others + .map(|method_ext| method_ext.to_tokens_multi_guard_or_chain()) + .collect(); + + first.to_tokens_multi_guard(other_method_guards) + } else { + first.to_tokens_single_guard() + } + }; + + let path = path.as_ref(); + + 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! { #(#doc_attributes)* @@ -350,17 +502,11 @@ impl ToTokens for Route { impl ::actix_web::dev::HttpServiceFactory for #name { fn register(self, __config: &mut actix_web::dev::AppService) { #ast - 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) + #registrations } } }; + output.extend(stream); } } @@ -370,7 +516,11 @@ pub(crate) fn with_method( args: TokenStream, input: TokenStream, ) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); + let args = match syn::parse(args) { + Ok(args) => args, + // on parse error, make IDEs happy; see fn docs + Err(err) => return input_and_compile_error(input, err), + }; let ast = match syn::parse::(input.clone()) { Ok(ast) => ast, @@ -385,6 +535,53 @@ pub(crate) fn with_method( } } +pub(crate) fn with_methods(input: TokenStream) -> TokenStream { + let mut ast = match syn::parse::(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::, _>(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_args() + .and_then(|args| Args::new(args, Some(method))) + }) + .collect::, _>>() + { + 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 `#[(..)]` 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. /// /// Returning the original input in addition to the error is good for IDEs which can gracefully diff --git a/tests/test_macro.rs b/tests/test_macro.rs index bf81c9e..e66d708 100644 --- a/tests/test_macro.rs +++ b/tests/test_macro.rs @@ -8,10 +8,10 @@ use actix_web::{ header::{HeaderName, HeaderValue}, StatusCode, }, - web, App, Error, HttpResponse, Responder, + web, App, Error, HttpRequest, HttpResponse, Responder, }; use actix_web_codegen_const_routes::{ - connect, delete, get, head, options, patch, post, put, route, trace, + connect, delete, get, head, options, patch, post, put, route, routes, trace, }; use futures_core::future::LocalBoxFuture; @@ -100,18 +100,77 @@ async fn get_param_test(_: web::Path) -> impl Responder { HttpResponse::Ok() } -#[route("/multi", method = "GET", method = "POST", method = "HEAD")] +#[route("/hello", method = "HELLO")] +async fn custom_route_test() -> impl Responder { + HttpResponse::Ok() +} + +#[route( + "/multi", + method = "GET", + method = "POST", + method = "HEAD", + method = "HELLO" +)] async fn route_test() -> impl Responder { 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")] -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_resource_name_test").is_err()); HttpResponse::Ok() } +mod guard_module { + use actix_web::{guard::GuardContext, http::header}; + + pub fn guard(ctx: &GuardContext) -> bool { + ctx.header::() + .map(|h| h.preference() == "image/*") + .unwrap_or(false) + } +} + +#[get("/test/guard", guard = "guard_module::guard")] +async fn guard_test() -> impl Responder { + HttpResponse::Ok() +} + pub struct ChangeStatusCode; impl Transform for ChangeStatusCode @@ -167,6 +226,19 @@ async fn get_wrap(_: web::Path) -> impl Responder { HttpResponse::Ok() } +/// Using expression, not just path to type, in wrap attribute. +/// +/// Regression from . +#[route( + "/catalog", + method = "GET", + method = "HEAD", + wrap = "actix_web::middleware::Compress::default()" +)] +async fn get_catalog() -> impl Responder { + HttpResponse::Ok().body("123123123") +} + #[actix_rt::test] async fn test_params() { let srv = actix_test::start(|| { @@ -174,16 +246,7 @@ async fn test_params() { .service(get_param_test) .service(put_param_test) .service(delete_param_test) - .service(path) - .service(field_path) }); - let request = srv.request(http::Method::GET, srv.url(FIELD_PATH.0)); - let response = request.send().await.unwrap(); - assert_eq!(response.status(), http::StatusCode::OK); - - let request = srv.request(http::Method::GET, srv.url(PATH)); - let response = request.send().await.unwrap(); - assert_eq!(response.status(), http::StatusCode::OK); let request = srv.request(http::Method::GET, srv.url("/test/it")); let response = request.send().await.unwrap(); @@ -211,7 +274,13 @@ async fn test_body() { .service(patch_test) .service(test_handler) .service(route_test) + .service(routes_overlapping_test) + .service(routes_overlapping_inaccessible_test) + .service(routes_test) .service(custom_resource_name_test) + .service(guard_test) + .service(path) + .service(field_path) }); let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); @@ -267,9 +336,55 @@ async fn test_body() { let response = request.send().await.unwrap(); 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 response = request.send().await.unwrap(); assert!(response.status().is_success()); + + let request = srv + .request(http::Method::GET, srv.url("/test/guard")) + .insert_header(("Accept", "image/*")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url(FIELD_PATH.0)); + let response = request.send().await.unwrap(); + assert_eq!(response.status(), http::StatusCode::OK); + + let request = srv.request(http::Method::GET, srv.url(PATH)); + let response = request.send().await.unwrap(); + assert_eq!(response.status(), http::StatusCode::OK); } #[actix_rt::test] diff --git a/tests/trybuild.rs b/tests/trybuild.rs index b2d9ce1..8e1f58a 100644 --- a/tests/trybuild.rs +++ b/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.54)] // MSRV +#[rustversion::stable(1.68)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); @@ -9,9 +9,15 @@ fn compile_macros() { t.pass("tests/trybuild/route-ok.rs"); t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); - t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); + t.pass("tests/trybuild/route-custom-method.rs"); + t.compile_fail("tests/trybuild/route-custom-lowercase.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/test-runtime.rs"); diff --git a/tests/trybuild/route-custom-lowercase.rs b/tests/trybuild/route-custom-lowercase.rs new file mode 100644 index 0000000..27b47e1 --- /dev/null +++ b/tests/trybuild/route-custom-lowercase.rs @@ -0,0 +1,19 @@ +use actix_web_codegen_const_routes::*; +use actix_web::http::Method; +use std::str::FromStr; + +#[route("/", method = "hello")] +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.request(Method::from_str("hello").unwrap(), srv.url("/")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/tests/trybuild/route-custom-lowercase.stderr b/tests/trybuild/route-custom-lowercase.stderr new file mode 100644 index 0000000..88198a5 --- /dev/null +++ b/tests/trybuild/route-custom-lowercase.stderr @@ -0,0 +1,29 @@ +error: HTTP method must be uppercase: `hello` + --> tests/trybuild/route-custom-lowercase.rs:5:23 + | +5 | #[route("/", method = "hello")] + | ^^^^^^^ + +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> tests/trybuild/route-custom-lowercase.rs:14:55 + | +14 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for fn item `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call + | + = help: the following other types implement trait `HttpServiceFactory`: + (A, B) + (A, B, C) + (A, B, C, D) + (A, B, C, D, E) + (A, B, C, D, E, F) + (A, B, C, D, E, F, G) + (A, B, C, D, E, F, G, H) + (A, B, C, D, E, F, G, H, I) + and $N others +note: required by a bound in `App::::service` + --> $WORKSPACE/actix-web/src/app.rs + | + | F: HttpServiceFactory + 'static, + | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/tests/trybuild/route-custom-method.rs b/tests/trybuild/route-custom-method.rs new file mode 100644 index 0000000..38f2ec1 --- /dev/null +++ b/tests/trybuild/route-custom-method.rs @@ -0,0 +1,37 @@ +use std::str::FromStr; + +use actix_web::http::Method; +use actix_web_codegen_const_routes::route; + +#[route("/single", method = "CUSTOM")] +async fn index() -> String { + "Hello Single!".to_owned() +} + +#[route("/multi", method = "GET", method = "CUSTOM")] +async fn custom() -> String { + "Hello Multi!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::App; + + let srv = actix_test::start(|| App::new().service(index).service(custom)); + + let request = srv.request(Method::GET, srv.url("/")); + let response = request.send().await.unwrap(); + assert!(response.status().is_client_error()); + + let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/single")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(Method::GET, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/tests/trybuild/route-duplicate-method-fail.stderr b/tests/trybuild/route-duplicate-method-fail.stderr index 90cff1b..bda7363 100644 --- a/tests/trybuild/route-duplicate-method-fail.stderr +++ b/tests/trybuild/route-duplicate-method-fail.stderr @@ -1,11 +1,29 @@ error: HTTP method defined more than once: `GET` - --> $DIR/route-duplicate-method-fail.rs:3:35 + --> tests/trybuild/route-duplicate-method-fail.rs:3:35 | 3 | #[route("/", method="GET", method="GET")] | ^^^^^ -error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> $DIR/route-duplicate-method-fail.rs:12:55 +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> tests/trybuild/route-duplicate-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}` + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for fn item `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call + | + = help: the following other types implement trait `HttpServiceFactory`: + (A, B) + (A, B, C) + (A, B, C, D) + (A, B, C, D, E) + (A, B, C, D, E, F) + (A, B, C, D, E, F, G) + (A, B, C, D, E, F, G, H) + (A, B, C, D, E, F, G, H, I) + and $N others +note: required by a bound in `App::::service` + --> $WORKSPACE/actix-web/src/app.rs + | + | F: HttpServiceFactory + 'static, + | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/tests/trybuild/route-missing-method-fail.stderr b/tests/trybuild/route-missing-method-fail.stderr index b1cefaf..9f2f788 100644 --- a/tests/trybuild/route-missing-method-fail.stderr +++ b/tests/trybuild/route-missing-method-fail.stderr @@ -6,8 +6,26 @@ error: The #[route(..)] macro requires at least one `method` attribute | = note: this error originates in the attribute macro `route` (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 +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied --> tests/trybuild/route-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}` + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for fn item `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call + | + = help: the following other types implement trait `HttpServiceFactory`: + (A, B) + (A, B, C) + (A, B, C, D) + (A, B, C, D, E) + (A, B, C, D, E, F) + (A, B, C, D, E, F, G) + (A, B, C, D, E, F, G, H) + (A, B, C, D, E, F, G, H, I) + and $N others +note: required by a bound in `App::::service` + --> $WORKSPACE/actix-web/src/app.rs + | + | F: HttpServiceFactory + 'static, + | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/tests/trybuild/route-unexpected-method-fail.stderr b/tests/trybuild/route-unexpected-method-fail.stderr deleted file mode 100644 index dda3660..0000000 --- a/tests/trybuild/route-unexpected-method-fail.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: Unexpected HTTP method: `UNEXPECTED` - --> $DIR/route-unexpected-method-fail.rs:3:21 - | -3 | #[route("/", method="UNEXPECTED")] - | ^^^^^^^^^^^^ - -error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> $DIR/route-unexpected-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}` diff --git a/tests/trybuild/routes-missing-args-fail.rs b/tests/trybuild/routes-missing-args-fail.rs new file mode 100644 index 0000000..3c06f58 --- /dev/null +++ b/tests/trybuild/routes-missing-args-fail.rs @@ -0,0 +1,14 @@ +use actix_web_codegen_const_routes::*; + +#[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)); +} diff --git a/tests/trybuild/routes-missing-args-fail.stderr b/tests/trybuild/routes-missing-args-fail.stderr new file mode 100644 index 0000000..2e84c29 --- /dev/null +++ b/tests/trybuild/routes-missing-args-fail.stderr @@ -0,0 +1,45 @@ +error: unexpected end of input, expected string literal + --> 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 service definition, expected #[("")] + --> 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: expected attribute arguments in parentheses: #[get(...)] + --> tests/trybuild/routes-missing-args-fail.rs:4:3 + | +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 item `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call + | + = help: the following other types implement trait `HttpServiceFactory`: + (A, B) + (A, B, C) + (A, B, C, D) + (A, B, C, D, E) + (A, B, C, D, E, F) + (A, B, C, D, E, F, G) + (A, B, C, D, E, F, G, H) + (A, B, C, D, E, F, G, H, I) + and $N others +note: required by a bound in `App::::service` + --> $WORKSPACE/actix-web/src/app.rs + | + | F: HttpServiceFactory + 'static, + | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/tests/trybuild/routes-missing-method-fail.rs b/tests/trybuild/routes-missing-method-fail.rs new file mode 100644 index 0000000..74d8fce --- /dev/null +++ b/tests/trybuild/routes-missing-method-fail.rs @@ -0,0 +1,13 @@ +use actix_web_codegen_const_routes::*; + +#[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)); +} diff --git a/tests/trybuild/routes-missing-method-fail.stderr b/tests/trybuild/routes-missing-method-fail.stderr new file mode 100644 index 0000000..228dced --- /dev/null +++ b/tests/trybuild/routes-missing-method-fail.stderr @@ -0,0 +1,31 @@ +error: The #[routes] macro requires at least one `#[(..)]` 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 item `fn() -> impl std::future::Future {index}` + | | + | required by a bound introduced by this call + | + = help: the following other types implement trait `HttpServiceFactory`: + (A, B) + (A, B, C) + (A, B, C, D) + (A, B, C, D, E) + (A, B, C, D, E, F) + (A, B, C, D, E, F, G) + (A, B, C, D, E, F, G, H) + (A, B, C, D, E, F, G, H, I) + and $N others +note: required by a bound in `App::::service` + --> $WORKSPACE/actix-web/src/app.rs + | + | F: HttpServiceFactory + 'static, + | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/tests/trybuild/route-unexpected-method-fail.rs b/tests/trybuild/routes-ok.rs similarity index 68% rename from tests/trybuild/route-unexpected-method-fail.rs rename to tests/trybuild/routes-ok.rs index 99e6b59..d2fdcdc 100644 --- a/tests/trybuild/route-unexpected-method-fail.rs +++ b/tests/trybuild/routes-ok.rs @@ -1,6 +1,8 @@ use actix_web_codegen_const_routes::*; -#[route("/", method="UNEXPECTED")] +#[routes] +#[get("/")] +#[post("/")] async fn index() -> String { "Hello World!".to_owned() } @@ -14,4 +16,8 @@ async fn main() { 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()); } diff --git a/tests/trybuild/simple-fail.stderr b/tests/trybuild/simple-fail.stderr index a091528..6a1bb28 100644 --- a/tests/trybuild/simple-fail.stderr +++ b/tests/trybuild/simple-fail.stderr @@ -1,28 +1,44 @@ -error: Unknown attribute. - --> $DIR/simple-fail.rs:3:15 +error: expected `=` + --> $DIR/simple-fail.rs:3:1 | 3 | #[get("/one", other)] - | ^^^^^ + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected identifier or literal +error: expected string literal --> $DIR/simple-fail.rs:8:8 | 8 | #[post(/two)] | ^ -error: Unknown attribute. +error: invalid service definition, expected #[("")] + --> $DIR/simple-fail.rs:8:8 + | +8 | #[post(/two)] + | ^ + +error: expected string literal --> $DIR/simple-fail.rs:15:9 | 15 | #[patch(PATCH_PATH)] | ^^^^^^^^^^ -error: Multiple paths specified! Should be only one! - --> $DIR/simple-fail.rs:20:19 +error: invalid service definition, expected #[("")] + --> $DIR/simple-fail.rs:15:9 + | +15 | #[patch(PATCH_PATH)] + | ^^^^^^^^^^ + +error: Multiple paths specified! There should be only one. + --> $DIR/simple-fail.rs:20:1 | 20 | #[delete("/four", "/five")] - | ^^^^^^^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `delete` (in Nightly builds, run with -Z macro-backtrace for more info) -error: HTTP method forbidden here. To handle multiple methods, use `route` instead +error: HTTP method forbidden here; to handle multiple methods, use `route` instead --> $DIR/simple-fail.rs:25:19 | 25 | #[delete("/five", method="GET")]