Repository: netwo-io/apistos
Pull request #42 (2024-01-29 17:31:22)

Fix api_operation for generic handlers
Fix api operation when generic handler where used without the where clause for type bounds.


Patch:
diff --git a/apistos-gen/src/internal/mod.rs b/apistos-gen/src/internal/mod.rs
index d4a5fa0..8ba896a 100644
--- a/apistos-gen/src/internal/mod.rs
+++ b/apistos-gen/src/internal/mod.rs
@@ -4,11 +4,10 @@ use crate::operation_attr::OperationAttr;
 use proc_macro2::{Span, TokenStream as TokenStream2};
 use proc_macro_error::{abort, emit_error};
 use quote::quote;
-use syn::punctuated::Punctuated;
-use syn::token::Comma;
+
 use syn::{
-  Expr, FnArg, GenericParam, Ident, ImplGenerics, ItemFn, Lit, Meta, ReturnType, Token, Type, TypeGenerics,
-  TypeTraitObject, WhereClause,
+  Expr, FnArg, Ident, ImplGenerics, ItemFn, Lit, Meta, ReturnType, Token, Type, TypeGenerics, TypeTraitObject,
+  WhereClause,
 };
 
 mod components;
@@ -217,10 +216,6 @@ pub(crate) fn gen_item_ast(
   (responder_wrapper, quote!(#item_ast))
 }
 
-pub(crate) fn extract_generics_params(item_ast: &ItemFn) -> Punctuated<GenericParam, Comma> {
-  item_ast.sig.generics.params.clone()
-}
-
 fn extract_fn_arguments_types(item_ast: &ItemFn) -> Vec<Type> {
   item_ast
     .sig
diff --git a/apistos-gen/src/lib.rs b/apistos-gen/src/lib.rs
index 60d31a2..05de7fb 100644
--- a/apistos-gen/src/lib.rs
+++ b/apistos-gen/src/lib.rs
@@ -4,7 +4,7 @@
 
 use crate::internal::schemas::Schemas;
 use crate::internal::utils::extract_deprecated_from_attr;
-use crate::internal::{extract_generics_params, gen_item_ast, gen_open_api_impl};
+use crate::internal::{gen_item_ast, gen_open_api_impl};
 use crate::openapi_cookie_attr::parse_openapi_cookie_attrs;
 use crate::openapi_error_attr::parse_openapi_error_attrs;
 use crate::openapi_header_attr::parse_openapi_header_attrs;
@@ -632,8 +632,7 @@ pub fn api_operation(attr: TokenStream, item: TokenStream) -> TokenStream {
   let openapi_struct_def = if !generics.params.is_empty() {
     let turbofish = ty_generics.as_turbofish();
     generics_call = quote!(#turbofish { p: std::marker::PhantomData });
-    let generics_params = extract_generics_params(&item_ast);
-    quote!(struct #openapi_struct #ty_generics { p: std::marker::PhantomData<(#generics_params)> } )
+    quote!(struct #openapi_struct #impl_generics #where_clause { p: std::marker::PhantomData #ty_generics } )
   } else {
     quote!(struct #openapi_struct;)
   };


Test patch:
diff --git a/apistos/tests/parametric_handlers.rs b/apistos/tests/parametric_handlers.rs
new file mode 100644
index 0000000..7a0794c
--- /dev/null
+++ b/apistos/tests/parametric_handlers.rs
@@ -0,0 +1,189 @@
+#![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::web::{Json, Path};
+use actix_web::{App, ResponseError};
+use apistos::app::OpenApiWrapper;
+use apistos::spec::Spec;
+use apistos::web::{get, resource, scope};
+use apistos::ApiComponent;
+use apistos_gen::{api_operation, ApiErrorComponent};
+use apistos_models::info::Info;
+use apistos_models::paths::{OperationType, Parameter, ParameterDefinition};
+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 path_parameter_replacement() {
+  #[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")]
+  pub(crate) async fn test<T>(_params: Path<(u32, T)>) -> Result<Json<Test>, ErrorResponse>
+  where
+    T: ToString + Serialize + ApiComponent + JsonSchema,
+  {
+    panic!()
+  }
+
+  #[api_operation(tag = "pet")]
+  pub(crate) async fn test2<T: Serialize + ApiComponent + JsonSchema>(
+    _params: Path<T>,
+  ) -> 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 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::<String>)))
+        .service(resource("/line/{plop_id}").route(get().to(test2::<u32>))),
+    )
+    .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 parameters: Vec<Parameter> = body
+    .paths
+    .paths
+    .get(&operation_path.to_string())
+    .cloned()
+    .unwrap_or_default()
+    .operations
+    .get(&OperationType::Get)
+    .cloned()
+    .unwrap_or_default()
+    .parameters
+    .iter()
+    .filter_map(|p| match p {
+      ReferenceOr::Reference { .. } => None,
+      ReferenceOr::Object(obj) => Some(obj.clone()),
+    })
+    .collect();
+
+  assert_eq!(parameters.len(), 2);
+
+  let first_parameter = parameters.first().cloned().unwrap_or_default();
+  assert_eq!(first_parameter.name, "plop_id");
+  let first_parameter_schema = first_parameter
+    .definition
+    .and_then(|p| match p {
+      ParameterDefinition::Schema(ReferenceOr::Object(sch)) => Some(sch.into_object().clone()),
+      _ => None,
+    })
+    .unwrap_or_default();
+  assert_eq!(
+    first_parameter_schema.instance_type,
+    Some(SingleOrVec::Single(Box::new(InstanceType::Integer)))
+  );
+
+  let last_parameter = parameters.last().cloned().unwrap_or_default();
+  assert_eq!(last_parameter.name, "clap_name");
+  let last_parameter_schema = last_parameter
+    .definition
+    .and_then(|p| match p {
+      ParameterDefinition::Schema(ReferenceOr::Object(sch)) => Some(sch.into_object().clone()),
+      _ => None,
+    })
+    .unwrap_or_default();
+  assert_eq!(
+    last_parameter_schema.instance_type,
+    Some(SingleOrVec::Single(Box::new(InstanceType::String)))
+  );
+
+  let parameters: Vec<Parameter> = body
+    .paths
+    .paths
+    .get(&operation_path2.to_string())
+    .cloned()
+    .unwrap_or_default()
+    .operations
+    .get(&OperationType::Get)
+    .cloned()
+    .unwrap_or_default()
+    .parameters
+    .iter()
+    .filter_map(|p| match p {
+      ReferenceOr::Reference { .. } => None,
+      ReferenceOr::Object(obj) => Some(obj.clone()),
+    })
+    .collect();
+
+  assert_eq!(parameters.len(), 1);
+
+  let parameter = parameters.first().cloned().unwrap_or_default();
+  assert_eq!(first_parameter.name, "plop_id");
+  let parameter_schema = parameter
+    .definition
+    .and_then(|p| match p {
+      ParameterDefinition::Schema(ReferenceOr::Object(sch)) => Some(sch.into_object().clone()),
+      _ => None,
+    })
+    .unwrap_or_default();
+  assert_eq!(
+    parameter_schema.instance_type,
+    Some(SingleOrVec::Single(Box::new(InstanceType::Integer)))
+  );
+}
+
+// Imports bellow aim at making clippy happy. Those dependencies are necessary for integration-test.
+use actix_service as _;
+use apistos_core as _;
+use apistos_models::reference_or::ReferenceOr;
+use indexmap as _;
+use log as _;
+use md5 as _;
+use once_cell as _;
+use regex as _;
+use schemars::schema::{InstanceType, SingleOrVec};
+use serde_json as _;