Repository: juhaku/utoipa (Rust)

Issue:
utoipa::path proc_macro with additional actix route proc_macro attibutes causes compilation error
It seems the the utoipa::path does not take into account the fact actix route proc_macros can have additional attributes, I discovered it when attempting to use the wrap attribute, but it seems to cause issue with any attribute or more specifically the `,` used to add additional attributes

Take the route:
```
/// Get Todo by given todo id.
///
/// Return found `Todo` with status 200 or 404 not found if `Todo` is not found from shared in-memory storage.
#[utoipa::path(
    responses(
        (status = 200, description = "Todo found from storage", body = Todo),
        (status = 404, description = "Todo not found by id", body = TodoError)
    ),
    params(
        ("id", description = "Unique storage id of Todo")
    )
)]
#[get("/todo/{id}")]
pub(super) async fn get_todo_by_id(id: Path<i32>, todo_data: Data<TodoData>) -> impl Responder {
    let todos = todo_data.todos.lock().unwrap();
    let id = id.into_inner();

    match todos.get_one(id) {
        Ok(todo) => todo.response(),
        Err(error) => error.response(),
    }
}
```
And lets say we wanted to add a middleware with the wrap attribute:
```
/// Get Todo by given todo id.
///
/// Return found `Todo` with status 200 or 404 not found if `Todo` is not found from shared in-memory storage.
#[utoipa::path(
    responses(
        (status = 200, description = "Todo found from storage", body = Todo),
        (status = 404, description = "Todo not found by id", body = TodoError)
    ),
    params(
        ("id", description = "Unique storage id of Todo")
    )
)]
#[get("/todo/{id}", wrap = "ApiKeyAuthenticator")]
pub(super) async fn get_todo_by_id(id: Path<i32>, todo_data: Data<TodoData>) -> impl Responder {
    let todos = todo_data.todos.lock().unwrap();
    let id = id.into_inner();

    match todos.get_one(id) {
        Ok(todo) => todo.response(),
        Err(error) => error.response(),
    }
}
```
It will cause a compiler error:
```
error: unexpected token
   --> src\controllers\todo.rs:189:19
    |
189 | #[get("/todo/{id}", wrap = "ApiKeyAuthenticator")]
    |                   ^
```

vs without the utoipa proc_macro
```
#[get("/todo/{id}", wrap = "ApiKeyAuthenticator")]
pub(super) async fn get_todo_by_id(id: Path<i32>, todo_data: Data<TodoData>) -> impl Responder {
    let todos = todo_data.todos.lock().unwrap();
    let id = id.into_inner();

    match todos.get_one(id) {
        Ok(todo) => todo.response(),
        Err(error) => error.response(),
    }
}
```
```
Compiling playground-api v0.1.0 (C:\Users\bfall\Development\Rust\playground-api)
Finished dev [unoptimized + debuginfo] target(s) in 3.12s
```


Discussion:
Oh snap. Somehow I have missed that. I tried to look from docs all the possibilities it could take as arguments but didn't find for some reason. To overcome this issue for now you can disable `actix_extras` feature but then you need to define the `path` and `operation` within `#[utoipa::path()]` macro. 
```rust
#[utoipa::path(
    get,
    path = "/todo/{id}",
)]
```
This is definitely bug clear as glass. I will fix it to the next release. Thanks for pointing out. 
While on this topic I wonder if it would be possible to get something like the `utoipa::path(...security(...))` information from the , `wrap = "ApiKeyAuthenticator"`

Though, I'm sure that's easier said then done, if even feasible, or honestly if it would even be a useful abstraction.
That is an interesting topic indeed. It would be quite cool to get that information from that wrap attribute. Though I'm afraid that it is actually next to impossible to do so. As that would require examining ast code of no particular structure since the actual implementation would always be written by the developer of the api. This leads to nightmare of trying to guess what kind of security the user is actually implementing even so because there is more security options to consider as well which are also 
supported.

Although there could be 2 ways to complete this but neither of them would really improve the situation. 
a) If we can enforce the `ApiKeyAuthenticator` to implement some trait (let's say `AuthProvider`) that could be called which then would return the information the `security(...)` section would need that would be doable. However only it would change the `security(...)` section stuff now to separate trait.
b) The `ApiKeyAuthenticator` could have derive macro of some sort wich could be parametrised in similar fashion as now the `#[utoipa::path(...security(...))]` and there would be a way to get access to those macros we could do that. But again that only would change the location of the security declaration to another place. 

Both of these are kind of not worth seeing the trouble because it would make things even more complicated without actual benefit. 

However the `security(...)` can also be defined api wide with the `openapi(...security(...))` as well. 



Patch:
diff --git a/utoipa-gen/src/ext.rs b/utoipa-gen/src/ext.rs
index 8883a2ca..140dd47c 100644
--- a/utoipa-gen/src/ext.rs
+++ b/utoipa-gen/src/ext.rs
@@ -2,6 +2,8 @@
 use proc_macro2::Ident;
 use syn::{punctuated::Punctuated, token::Comma, Attribute, FnArg, ItemFn};
 
+use crate::path::PathOperation;
+
 #[cfg(feature = "actix_extras")]
 pub mod actix;
 
@@ -29,6 +31,11 @@ pub struct ResolvedPath {
     pub args: Vec<String>,
 }
 
+pub struct ResolvedOperation {
+    pub path_operation: PathOperation,
+    pub path: String,
+}
+
 pub trait ArgumentResolver {
     fn resolve_path_arguments<'a>(
         _: &'a Punctuated<FnArg, Comma>,
@@ -45,7 +52,7 @@ pub trait PathResolver {
 }
 
 pub trait PathOperationResolver {
-    fn resolve_operation(_: &ItemFn) -> Option<&Attribute> {
+    fn resolve_operation(_: &ItemFn) -> Option<ResolvedOperation> {
         None
     }
 }
diff --git a/utoipa-gen/src/ext/actix.rs b/utoipa-gen/src/ext/actix.rs
index ed7bd138..ee2300f9 100644
--- a/utoipa-gen/src/ext/actix.rs
+++ b/utoipa-gen/src/ext/actix.rs
@@ -1,17 +1,20 @@
 use std::borrow::Cow;
 
 use lazy_static::lazy_static;
-use proc_macro2::Ident;
-use proc_macro_error::abort_call_site;
+use proc_macro::TokenTree;
+use proc_macro2::{Ident, Literal};
+use proc_macro_error::{abort, abort_call_site};
 use regex::{Captures, Regex};
 use syn::{
-    punctuated::Punctuated, token::Comma, Attribute, FnArg, GenericArgument, ItemFn, LitStr, Pat,
-    PatType, PathArguments, PathSegment, Type, TypePath,
+    parse::Parse, punctuated::Punctuated, token::Comma, Attribute, FnArg, GenericArgument, ItemFn,
+    LitStr, Pat, PatType, PathArguments, PathSegment, Type, TypePath,
 };
 
+use crate::path::PathOperation;
+
 use super::{
     Argument, ArgumentIn, ArgumentResolver, PathOperationResolver, PathOperations, PathResolver,
-    ResolvedPath,
+    ResolvedOperation, ResolvedPath,
 };
 
 impl ArgumentResolver for PathOperations {
@@ -128,10 +131,22 @@ impl PathOperations {
 }
 
 impl PathOperationResolver for PathOperations {
-    fn resolve_operation(item_fn: &ItemFn) -> Option<&Attribute> {
+    fn resolve_operation(item_fn: &ItemFn) -> Option<ResolvedOperation> {
         item_fn.attrs.iter().find_map(|attribute| {
             if is_valid_request_type(attribute.path.get_ident()) {
-                Some(attribute)
+                match attribute.parse_args::<Path>() {
+                    Ok(path) => Some(ResolvedOperation {
+                        path: path.path,
+                        path_operation: PathOperation::from_ident(
+                            attribute.path.get_ident().unwrap(),
+                        ),
+                    }),
+                    Err(error) => abort!(
+                        error.span(),
+                        "parse path of path operation attribute: {}",
+                        error
+                    ),
+                }
             } else {
                 None
             }
@@ -139,6 +154,33 @@ impl PathOperationResolver for PathOperations {
     }
 }
 
+struct Path {
+    path: String,
+}
+
+impl Parse for Path {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let mut path = String::new();
+
+        input.step(|cursor| {
+            let mut rest = *cursor;
+
+            // parse only first literal item and ignore rest of the tokens from actix_web path attribute macro
+            while let Some((tt, next)) = rest.token_tree() {
+                if let proc_macro2::TokenTree::Literal(literal) = tt {
+                    if path.is_empty() {
+                        path.push_str(&literal.to_string().replace('\"', ""));
+                    }
+                }
+                rest = next;
+            }
+            Ok(((), rest))
+        });
+
+        Ok(Self { path })
+    }
+}
+
 impl PathResolver for PathOperations {
     fn resolve_path(path: &Option<String>) -> Option<ResolvedPath> {
         path.as_ref().map(|path| {
diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs
index 0c1c2a93..ea83188c 100644
--- a/utoipa-gen/src/lib.rs
+++ b/utoipa-gen/src/lib.rs
@@ -7,6 +7,8 @@
 #![warn(missing_docs)]
 #![warn(rustdoc::broken_intra_doc_links)]
 
+use std::mem;
+
 use component::Component;
 use doc_comment::CommentAttributes;
 
@@ -22,7 +24,7 @@ use syn::{
     parse::{Parse, ParseBuffer, ParseStream},
     punctuated::Punctuated,
     token::Bracket,
-    DeriveInput, ItemFn, LitStr, Token,
+    DeriveInput, ItemFn, Token,
 };
 
 mod component;
@@ -536,19 +538,20 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
 ///
 /// [^actix_extras]: **actix_extras** feature need to be enabled and **actix-web** framework must be declared in your `Cargo.toml`.
 pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
-    #[cfg(feature = "actix_extras")]
-    let mut path_attribute = syn::parse_macro_input!(attr as PathAttr);
-    #[cfg(not(feature = "actix_extras"))]
     let path_attribute = syn::parse_macro_input!(attr as PathAttr);
 
+    #[cfg(feature = "actix_extras")]
+    let mut path_attribute = path_attribute;
+
     let ast_fn = syn::parse::<ItemFn>(item).unwrap_or_abort();
     let fn_name = &*ast_fn.sig.ident.to_string();
 
-    let operation_attribute = &PathOperations::resolve_operation(&ast_fn);
+    let resolved_operation = &mut PathOperations::resolve_operation(&ast_fn);
     let resolved_path = PathOperations::resolve_path(
-        &operation_attribute
-            .map(|attribute| attribute.parse_args::<LitStr>().unwrap_or_abort().value())
-            .or_else(|| path_attribute.path.as_ref().map(String::to_string)),
+        &resolved_operation
+            .as_mut()
+            .map(|operation| mem::take(&mut operation.path))
+            .or_else(|| path_attribute.path.as_ref().map(String::to_string)), // cannot use mem take because we need this later
     );
 
     #[cfg(feature = "actix_extras")]
@@ -558,10 +561,11 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
     }
 
     let path = Path::new(path_attribute, fn_name)
-        .path_operation(operation_attribute.map(|attribute| {
-            let ident = attribute.path.get_ident().unwrap();
-            PathOperation::from_ident(ident)
-        }))
+        .path_operation(
+            resolved_operation.as_mut().map(|operation| {
+                mem::replace(&mut operation.path_operation, PathOperation::Options)
+            }),
+        )
         .path(|| resolved_path.map(|path| path.path))
         .doc_comments(CommentAttributes::from_attributes(&ast_fn.attrs).0)
         .deprecated(ast_fn.attrs.iter().find_map(|attr| {


Test patch:
diff --git a/tests/path_derive_actix.rs b/tests/path_derive_actix.rs
index eceb312e..7ceffa1d 100644
--- a/tests/path_derive_actix.rs
+++ b/tests/path_derive_actix.rs
@@ -181,6 +181,45 @@ fn derive_path_with_multiple_args() {
     };
 }
 
+#[test]
+fn derive_complex_actix_web_path() {
+    mod mod_derive_complex_actix_path {
+        use actix_web::{get, web, HttpResponse, Responder};
+        use serde_json::json;
+
+        #[utoipa::path(
+            responses(
+                (status = 200, description = "success response")
+            ),
+        )]
+        #[get("/foo/{id}", name = "api_name")]
+        #[allow(unused)]
+        async fn get_foo_by_id(path: web::Path<i64>) -> impl Responder {
+            let id = path.into_inner();
+            HttpResponse::Ok().json(json!({ "id": &format!("{}", id) }))
+        }
+    }
+
+    #[derive(OpenApi, Default)]
+    #[openapi(handlers(mod_derive_complex_actix_path::get_foo_by_id))]
+    struct ApiDoc;
+
+    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
+    dbg!(&doc);
+    let parameters = common::get_json_path(&doc, "paths./foo/{id}.get.parameters");
+
+    common::assert_json_array_len(parameters, 1);
+    assert_value! {parameters=>
+        "[0].in" = r#""path""#, "Parameter in"
+        "[0].name" = r#""id""#, "Parameter name"
+        "[0].description" = r#"null"#, "Parameter description"
+        "[0].required" = r#"true"#, "Parameter required"
+        "[0].deprecated" = r#"false"#, "Parameter deprecated"
+        "[0].schema.type" = r#""integer""#, "Parameter schema type"
+        "[0].schema.format" = r#""int64""#, "Parameter schema format"
+    };
+}
+
 #[test]
 fn derive_path_with_multiple_args_with_descriptions() {
     mod mod_derive_path_multiple_args {