Repository: netwo-io/apistos (rust)

Issue:
Ability to exclude some structs 
Hello,

I am trying to use apistos and actix-identity at the same time. 
When I use

```
#[api_operation(tag = "another", summary = "Operator index endpoint")]
pub async fn test(user: Identity) -> HttpResponse {
```
It gives me error. 
"no function or associated item named `request_body` found for struct `actix_identity::Identity` in the current scope".

For successfully compile the code I have to add skip=true. But now my function is not documented at openapi. Please add ability to ignore variable name or structs. Ignoring Identity struct will solve my problem. 


Pull request description:
Allow skip args in operations
Allow to skip operation parameters in documentation. This prevent errors linked to missing `ApiComponent` implementation.

Fixes #107 

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 0cbc892..6b2fb2c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,7 +22,7 @@ documentation = "https://docs.rs/apistos/"
 license = "MIT OR Apache-2.0"
 rust-version = "1.75"
 publish = true
-version = "0.3.0"
+version = "0.3.1"
 
 [workspace.dependencies]
 actix-service = "2"
diff --git a/apistos-core/Cargo.toml b/apistos-core/Cargo.toml
index f8a4d73..694278a 100644
--- a/apistos-core/Cargo.toml
+++ b/apistos-core/Cargo.toml
@@ -28,7 +28,7 @@ serde_qs = { workspace = true, features = ["actix4"], optional = true }
 uuid = { workspace = true, optional = true }
 url = { workspace = true, optional = true }
 
-apistos-models = { path = "../apistos-models", version = "0.3.0", features = ["deserialize"] }
+apistos-models = { path = "../apistos-models", version = "0.3.1", features = ["deserialize"] }
 
 [dev-dependencies]
 assert-json-diff = { workspace = true }
diff --git a/apistos-gen/src/internal/mod.rs b/apistos-gen/src/internal/mod.rs
index 965680c..295e780 100644
--- a/apistos-gen/src/internal/mod.rs
+++ b/apistos-gen/src/internal/mod.rs
@@ -6,7 +6,7 @@ use proc_macro_error::{abort, emit_error};
 use quote::quote;
 
 use syn::{
-  Expr, FnArg, Ident, ImplGenerics, ItemFn, Lit, Meta, ReturnType, Token, Type, TypeGenerics, TypeTraitObject,
+  Expr, FnArg, Ident, ImplGenerics, ItemFn, Lit, Meta, Pat, ReturnType, Token, Type, TypeGenerics, TypeTraitObject,
   WhereClause,
 };
 
@@ -35,7 +35,7 @@ pub(crate) fn gen_open_api_impl(
       }
     )
   } else {
-    let args = extract_fn_arguments_types(item_ast);
+    let args = extract_fn_arguments_types(item_ast, &operation_attribute.skip_args);
 
     let deprecated = item_ast.attrs.iter().find_map(|attr| {
       if !matches!(attr.path().get_ident(), Some(ident) if &*ident.to_string() == "deprecated") {
@@ -216,14 +216,23 @@ pub(crate) fn gen_item_ast(
   (responder_wrapper, quote!(#item_ast))
 }
 
-fn extract_fn_arguments_types(item_ast: &ItemFn) -> Vec<Type> {
+fn extract_fn_arguments_types(item_ast: &ItemFn, skipped_args: &[Ident]) -> Vec<Type> {
   item_ast
     .sig
     .inputs
     .iter()
     .filter_map(|inp| match inp {
       FnArg::Receiver(_) => None,
-      FnArg::Typed(ref t) => Some(*t.ty.clone()),
+      FnArg::Typed(ref t) => match *t.pat.clone() {
+        Pat::Ident(pi) => {
+          if skipped_args.contains(&pi.ident) {
+            None
+          } else {
+            Some(*t.ty.clone())
+          }
+        }
+        _ => Some(*t.ty.clone()),
+      },
     })
     .collect()
 }
diff --git a/apistos-gen/src/lib.rs b/apistos-gen/src/lib.rs
index e040f5f..be10525 100644
--- a/apistos-gen/src/lib.rs
+++ b/apistos-gen/src/lib.rs
@@ -491,6 +491,8 @@ pub fn derive_api_error(input: TokenStream) -> TokenStream {
 /// # `#[api_operation(...)]` options:
 ///   - `skip` a bool allowing to skip documentation for the decorated handler. No component
 ///  strictly associated to this operation will be document in the resulting openapi definition.
+///   - `skip_args = "..."` an optional list of arguments to skip. `Apistos` will not try to generate the
+///  documentation for those args which prevent errors linked to missing `ApiComponent` implementation.
 ///   - `deprecated` a bool indicating the operation is deprecated. Deprecation can also be declared
 ///  with rust `#[deprecated]` decorator.
 ///   - `operation_id = "..."` an optional operation id for this operation. Default is the handler's fn name.
diff --git a/apistos-gen/src/operation_attr.rs b/apistos-gen/src/operation_attr.rs
index 6b82293..b75e45d 100644
--- a/apistos-gen/src/operation_attr.rs
+++ b/apistos-gen/src/operation_attr.rs
@@ -1,5 +1,6 @@
 use darling::ast::NestedMeta;
 use darling::FromMeta;
+use proc_macro2::Ident;
 use proc_macro_error::abort;
 use std::collections::BTreeMap;
 
@@ -27,6 +28,8 @@ struct OperationAttrInternal {
   error_codes: Vec<u16>,
   consumes: Option<String>,
   produces: Option<String>,
+  #[darling(multiple)]
+  skip_args: Vec<Ident>,
 }
 
 #[derive(FromMeta, Clone)]
@@ -47,6 +50,7 @@ pub(crate) struct OperationAttr {
   pub(crate) error_codes: Vec<u16>,
   pub(crate) consumes: Option<String>,
   pub(crate) produces: Option<String>,
+  pub(crate) skip_args: Vec<Ident>,
 }
 
 impl From<OperationAttrInternal> for OperationAttr {
@@ -66,6 +70,7 @@ impl From<OperationAttrInternal> for OperationAttr {
       error_codes: value.error_codes,
       consumes: value.consumes,
       produces: value.produces,
+      skip_args: value.skip_args,
     }
   }
 }
diff --git a/apistos-rapidoc/Cargo.toml b/apistos-rapidoc/Cargo.toml
index f8b2dd9..45c6728 100644
--- a/apistos-rapidoc/Cargo.toml
+++ b/apistos-rapidoc/Cargo.toml
@@ -14,7 +14,7 @@ rust-version.workspace = true
 license.workspace = true
 
 [dependencies]
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.0" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }
 
 [lints]
 workspace = true
diff --git a/apistos-redoc/Cargo.toml b/apistos-redoc/Cargo.toml
index 917b114..0565300 100644
--- a/apistos-redoc/Cargo.toml
+++ b/apistos-redoc/Cargo.toml
@@ -14,7 +14,7 @@ rust-version.workspace = true
 license.workspace = true
 
 [dependencies]
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.0" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }
 
 [lints]
 workspace = true
diff --git a/apistos-scalar/Cargo.toml b/apistos-scalar/Cargo.toml
index 1941c88..ee11ed4 100644
--- a/apistos-scalar/Cargo.toml
+++ b/apistos-scalar/Cargo.toml
@@ -14,7 +14,7 @@ rust-version.workspace = true
 license.workspace = true
 
 [dependencies]
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.0" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }
 
 [lints]
 workspace = true
diff --git a/apistos-shuttle/Cargo.toml b/apistos-shuttle/Cargo.toml
index 8fe8172..ef3302e 100644
--- a/apistos-shuttle/Cargo.toml
+++ b/apistos-shuttle/Cargo.toml
@@ -18,7 +18,7 @@ actix-web = { workspace = true }
 num_cpus = { workspace = true }
 shuttle-runtime = { workspace = true }
 
-apistos = { path = "../apistos", version = "0.3.0" }
+apistos = { path = "../apistos", version = "0.3.1" }
 
 [lints]
 workspace = true
diff --git a/apistos-swagger-ui/Cargo.toml b/apistos-swagger-ui/Cargo.toml
index b4b07a9..c195211 100644
--- a/apistos-swagger-ui/Cargo.toml
+++ b/apistos-swagger-ui/Cargo.toml
@@ -14,7 +14,7 @@ rust-version.workspace = true
 license.workspace = true
 
 [dependencies]
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.0" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }
 
 [lints]
 workspace = true
diff --git a/apistos/Cargo.toml b/apistos/Cargo.toml
index fcb995e..a71d936 100644
--- a/apistos/Cargo.toml
+++ b/apistos/Cargo.toml
@@ -25,25 +25,25 @@ schemars = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
 
-apistos-core = { path = "../apistos-core", version = "0.3.0" }
-apistos-gen = { path = "../apistos-gen", version = "0.3.0" }
-apistos-models = { path = "../apistos-models", version = "0.3.0" }
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.0" }
-apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.0", optional = true }
-apistos-redoc = { path = "../apistos-redoc", version = "0.3.0", optional = true }
-apistos-scalar = { path = "../apistos-scalar", version = "0.3.0", optional = true }
-apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.0", optional = true }
+apistos-core = { path = "../apistos-core", version = "0.3.1" }
+apistos-gen = { path = "../apistos-gen", version = "0.3.1" }
+apistos-models = { path = "../apistos-models", version = "0.3.1" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }
+apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.1", optional = true }
+apistos-redoc = { path = "../apistos-redoc", version = "0.3.1", optional = true }
+apistos-scalar = { path = "../apistos-scalar", version = "0.3.1", optional = true }
+apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.1", optional = true }
 
 
 [dev-dependencies]
 actix-web-lab = { workspace = true }
 garde-actix-web = { workspace = true }
 
-apistos-models = { path = "../apistos-models", version = "0.3.0", features = ["deserialize"] }
-apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.0" }
-apistos-redoc = { path = "../apistos-redoc", version = "0.3.0" }
-apistos-scalar = { path = "../apistos-scalar", version = "0.3.0" }
-apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.0" }
+apistos-models = { path = "../apistos-models", version = "0.3.1", features = ["deserialize"] }
+apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.1" }
+apistos-redoc = { path = "../apistos-redoc", version = "0.3.1" }
+apistos-scalar = { path = "../apistos-scalar", version = "0.3.1" }
+apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.1" }
 
 [lints]
 workspace = true


Test patch:
diff --git a/apistos-gen-test/Cargo.toml b/apistos-gen-test/Cargo.toml
index 9b23b4b..c5b5456 100644
--- a/apistos-gen-test/Cargo.toml
+++ b/apistos-gen-test/Cargo.toml
@@ -20,8 +20,8 @@ assert-json-diff = { workspace = true }
 chrono = { workspace = true, features = ["serde"] }
 futures-core = { workspace = true }
 apistos = { path = "../apistos", features = ["multipart", "uuid"] }
-apistos-core = { path = "../apistos-core", version = "0.3.0" }
-apistos-gen = { path = "../apistos-gen", version = "0.3.0" }
+apistos-core = { path = "../apistos-core", version = "0.3.1" }
+apistos-gen = { path = "../apistos-gen", version = "0.3.1" }
 # we use the "preserve_order" feature from schemars here following https://github.com/netwo-io/apistos/pull/78
 schemars = { workspace = true, features = ["preserve_order"] }
 serde = { version = "1.0.188", features = ["derive"] }
diff --git a/apistos/tests/operations.rs b/apistos/tests/operations.rs
index 014da33..03158c5 100644
--- a/apistos/tests/operations.rs
+++ b/apistos/tests/operations.rs
@@ -1,14 +1,21 @@
 #![allow(clippy::expect_used)]
 #![allow(clippy::panic)]
 
+use actix_web::http::StatusCode;
 use actix_web::test::{call_service, init_service, try_read_body_json, TestRequest};
-use actix_web::{App, Error};
+use actix_web::web::{Json, Path};
+use actix_web::{App, Error, ResponseError};
 use apistos::app::OpenApiWrapper;
 use apistos::spec::Spec;
-use apistos::web::{put, resource, scope, Scope};
-use apistos_gen::api_operation;
+use apistos::web::{get, put, resource, scope, Scope};
+use apistos_gen::{api_operation, ApiComponent, ApiErrorComponent};
 use apistos_models::info::Info;
+use apistos_models::paths::OperationType;
+use apistos_models::tag::Tag;
 use apistos_models::OpenApi;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::fmt::{Display, Formatter};
 
 #[actix_web::test]
 async fn operations() {
@@ -69,6 +76,119 @@ async fn operations() {
   assert_eq!(path, "/api/rest/v1/finance/account");
 }
 
+#[actix_web::test]
+async fn operation_skip_args() {
+  #[derive(Serialize, Deserialize, Debug, Clone, ApiErrorComponent)]
+  #[openapi_error(status(code = 405, description = "Invalid input"))]
+  pub(crate) enum ErrorResponse {
+    MethodNotAllowed(String),
+  }
+
+  impl Display for ErrorResponse {
+    fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
+      panic!()
+    }
+  }
+
+  impl ResponseError for ErrorResponse {
+    fn status_code(&self) -> StatusCode {
+      panic!()
+    }
+  }
+
+  #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, ApiComponent)]
+  struct Test {
+    id_number: u32,
+    id_string: String,
+  }
+
+  #[api_operation(tag = "pet", skip_args = "_params")]
+  pub(crate) async fn test(_params: Path<(u32, String)>, _test: Json<Test>) -> Result<Json<Test>, ErrorResponse> {
+    panic!()
+  }
+
+  #[api_operation(tag = "pet", skip_args = "_params")]
+  pub(crate) async fn test2(_test: Json<Test>, _params: Path<u32>) -> Result<Json<Test>, ErrorResponse> {
+    panic!()
+  }
+
+  #[api_operation(tag = "pet", skip_args = "_params", skip_args = "_test")]
+  pub(crate) async fn test3(mut _params: Path<u32>, _test: Json<Test>) -> Result<Json<Test>, ErrorResponse> {
+    panic!()
+  }
+
+  let openapi_path = "/test.json";
+  let operation_path = "/test/{plop_id}/{clap_name}";
+  let operation_path2 = "/test/line/{plop_id}";
+  let operation_path3 = "/test/line2/{plop_id}";
+
+  let info = Info {
+    title: "A well documented API".to_string(),
+    description: Some("Really well document I mean it".to_string()),
+    terms_of_service: Some("https://terms.com".to_string()),
+    ..Default::default()
+  };
+  let tags = vec![Tag {
+    name: "A super tag".to_owned(),
+    ..Default::default()
+  }];
+  let spec = Spec {
+    info: info.clone(),
+    tags: tags.clone(),
+    ..Default::default()
+  };
+  let app = App::new()
+    .document(spec)
+    .service(
+      scope("test")
+        .service(resource("/{plop_id}/{clap_name}").route(get().to(test)))
+        .service(resource("/line/{plop_id}").route(get().to(test2)))
+        .service(resource("/line2/{plop_id}").route(get().to(test3))),
+    )
+    .build(openapi_path);
+  let app = init_service(app).await;
+
+  let req = TestRequest::get().uri(openapi_path).to_request();
+  let resp = call_service(&app, req).await;
+  assert!(resp.status().is_success());
+
+  let body: OpenApi = try_read_body_json(resp).await.expect("Unable to read body");
+  let paths = body.paths.paths;
+
+  let operation = paths.get(&operation_path.to_string()).cloned();
+  assert!(operation.is_some());
+  let operation = operation
+    .unwrap_or_default()
+    .operations
+    .get(&OperationType::Get)
+    .cloned()
+    .unwrap_or_default();
+  assert!(operation.request_body.is_some());
+  assert!(operation.parameters.is_empty());
+
+  let operation2 = paths.get(&operation_path2.to_string()).cloned();
+  assert!(operation2.is_some());
+  let operation2 = operation2
+    .unwrap_or_default()
+    .operations
+    .get(&OperationType::Get)
+    .cloned()
+    .unwrap_or_default();
+  assert!(operation2.request_body.is_some());
+  assert!(operation2.parameters.is_empty());
+
+  let operation3 = paths.get(&operation_path3.to_string()).cloned();
+  assert!(operation3.is_some());
+  let operation3 = operation3
+    .unwrap_or_default()
+    .operations
+    .get(&OperationType::Get)
+    .cloned()
+    .unwrap_or_default();
+  assert!(operation3.request_body.is_none());
+  assert!(operation3.parameters.is_empty());
+}
+
 // Imports bellow aim at making clippy happy. Those dependencies are necessary for integration-test.
 use actix_service as _;
 use actix_web_lab as _;