From 1f0fbedb3fced4802eb51cc78b26ae14ec350669 Mon Sep 17 00:00:00 2001 From: Romain Lebran Date: Fri, 12 Jul 2024 14:00:40 +0200 Subject: [PATCH 1/4] Handle multiple generic parameter in operations --- apistos-gen/src/lib.rs | 25 +++++++++++-- apistos/tests/operations.rs | 74 +++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/apistos-gen/src/lib.rs b/apistos-gen/src/lib.rs index be10525b..e6fd0700 100644 --- a/apistos-gen/src/lib.rs +++ b/apistos-gen/src/lib.rs @@ -15,8 +15,8 @@ use darling::ast::NestedMeta; use darling::Error; use proc_macro::TokenStream; use proc_macro_error::{abort, proc_macro_error, OptionExt}; -use quote::quote; -use syn::{DeriveInput, Ident, ItemFn}; +use quote::{format_ident, quote}; +use syn::{DeriveInput, GenericParam, Ident, ItemFn}; mod internal; mod openapi_cookie_attr; @@ -632,9 +632,25 @@ pub fn api_operation(attr: TokenStream, item: TokenStream) -> TokenStream { let mut generics_call = quote!(); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let openapi_struct_def = if !generics.params.is_empty() { + let mut generic_types_idents = vec![]; + for param in &generics.params { + match param { + GenericParam::Lifetime(_) => {} + GenericParam::Const(_) => {} + GenericParam::Type(_type) => generic_types_idents.push(_type.ident.clone()), + } + } let turbofish = ty_generics.as_turbofish(); - generics_call = quote!(#turbofish { p: std::marker::PhantomData }); - quote!(struct #openapi_struct #impl_generics #where_clause { p: std::marker::PhantomData #ty_generics } ) + let mut phantom_params = quote!(); + let mut phantom_params_names = quote!(); + for generic_types_ident in generic_types_idents { + let param_name = Ident::new(&format_ident!("p_{}", generic_types_ident).to_string().to_lowercase(), Span::call_site()); + phantom_params_names.extend(quote!(#param_name: std::marker::PhantomData,)); + phantom_params.extend(quote!(#param_name: std::marker::PhantomData < #generic_types_ident >,)) + } + generics_call = quote!(#turbofish { #phantom_params_names }); + + quote!(struct #openapi_struct #impl_generics #where_clause { #phantom_params }) } else { quote!(struct #openapi_struct;) }; @@ -669,6 +685,7 @@ pub fn api_operation(attr: TokenStream, item: TokenStream) -> TokenStream { use apistos as _; #[cfg(test)] use garde as _; +use proc_macro2::Span; #[cfg(test)] use schemars as _; #[cfg(test)] diff --git a/apistos/tests/operations.rs b/apistos/tests/operations.rs index 03158c5a..be7b7247 100644 --- a/apistos/tests/operations.rs +++ b/apistos/tests/operations.rs @@ -189,6 +189,80 @@ async fn operation_skip_args() { assert!(operation3.parameters.is_empty()); } +#[actix_web::test] +async fn operation_generics() { + #[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) -> Result, ErrorResponse> { + panic!() + } + + let openapi_path = "/test.json"; + let operation_path = "/test/{plop_id}/{clap_name}"; + + 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::)))) + .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()); +} + // Imports bellow aim at making clippy happy. Those dependencies are necessary for integration-test. use actix_service as _; use actix_web_lab as _; From 27d5515f818e1a51c627d2b79f1e99046b20f94c Mon Sep 17 00:00:00 2001 From: Romain Lebran Date: Fri, 12 Jul 2024 14:02:07 +0200 Subject: [PATCH 2/4] Fix import --- apistos-gen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apistos-gen/src/lib.rs b/apistos-gen/src/lib.rs index e6fd0700..487defb5 100644 --- a/apistos-gen/src/lib.rs +++ b/apistos-gen/src/lib.rs @@ -15,6 +15,7 @@ use darling::ast::NestedMeta; use darling::Error; use proc_macro::TokenStream; use proc_macro_error::{abort, proc_macro_error, OptionExt}; +use proc_macro2::Span; use quote::{format_ident, quote}; use syn::{DeriveInput, GenericParam, Ident, ItemFn}; @@ -685,7 +686,6 @@ pub fn api_operation(attr: TokenStream, item: TokenStream) -> TokenStream { use apistos as _; #[cfg(test)] use garde as _; -use proc_macro2::Span; #[cfg(test)] use schemars as _; #[cfg(test)] From cb4366e0e6787ca4c0459a609991053c930b0db1 Mon Sep 17 00:00:00 2001 From: Romain Lebran Date: Fri, 12 Jul 2024 14:03:20 +0200 Subject: [PATCH 3/4] Fix fmt --- apistos-gen/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apistos-gen/src/lib.rs b/apistos-gen/src/lib.rs index 487defb5..6b0da233 100644 --- a/apistos-gen/src/lib.rs +++ b/apistos-gen/src/lib.rs @@ -14,8 +14,8 @@ use convert_case::{Case, Casing}; use darling::ast::NestedMeta; use darling::Error; use proc_macro::TokenStream; -use proc_macro_error::{abort, proc_macro_error, OptionExt}; use proc_macro2::Span; +use proc_macro_error::{abort, proc_macro_error, OptionExt}; use quote::{format_ident, quote}; use syn::{DeriveInput, GenericParam, Ident, ItemFn}; @@ -645,7 +645,10 @@ pub fn api_operation(attr: TokenStream, item: TokenStream) -> TokenStream { let mut phantom_params = quote!(); let mut phantom_params_names = quote!(); for generic_types_ident in generic_types_idents { - let param_name = Ident::new(&format_ident!("p_{}", generic_types_ident).to_string().to_lowercase(), Span::call_site()); + let param_name = Ident::new( + &format_ident!("p_{}", generic_types_ident).to_string().to_lowercase(), + Span::call_site(), + ); phantom_params_names.extend(quote!(#param_name: std::marker::PhantomData,)); phantom_params.extend(quote!(#param_name: std::marker::PhantomData < #generic_types_ident >,)) } From f039cab7036da0cf20bfd51cf3fa12c4a39b5e14 Mon Sep 17 00:00:00 2001 From: Romain Lebran Date: Fri, 12 Jul 2024 15:42:19 +0200 Subject: [PATCH 4/4] Prepare 0.3.4 --- Cargo.toml | 2 +- apistos-core/Cargo.toml | 2 +- apistos-gen-test/Cargo.toml | 4 ++-- apistos-rapidoc/Cargo.toml | 2 +- apistos-redoc/Cargo.toml | 2 +- apistos-scalar/Cargo.toml | 2 +- apistos-shuttle/Cargo.toml | 2 +- apistos-swagger-ui/Cargo.toml | 2 +- apistos/Cargo.toml | 26 +++++++++++++------------- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6c4048fc..92cc4d59 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.3" +version = "0.3.4" [workspace.dependencies] actix-service = "2" diff --git a/apistos-core/Cargo.toml b/apistos-core/Cargo.toml index ceb6a5b3..56e511aa 100644 --- a/apistos-core/Cargo.toml +++ b/apistos-core/Cargo.toml @@ -29,7 +29,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.3", features = ["deserialize"] } +apistos-models = { path = "../apistos-models", version = "0.3.4", features = ["deserialize"] } [dev-dependencies] assert-json-diff = { workspace = true } diff --git a/apistos-gen-test/Cargo.toml b/apistos-gen-test/Cargo.toml index 6ac73e5c..265fab34 100644 --- a/apistos-gen-test/Cargo.toml +++ b/apistos-gen-test/Cargo.toml @@ -21,8 +21,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.3", features = ["actix-web-grants"] } -apistos-gen = { path = "../apistos-gen", version = "0.3.3" } +apistos-core = { path = "../apistos-core", version = "0.3.4", features = ["actix-web-grants"] } +apistos-gen = { path = "../apistos-gen", version = "0.3.4" } # 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 = { workspace = true, features = ["derive"] } diff --git a/apistos-rapidoc/Cargo.toml b/apistos-rapidoc/Cargo.toml index b552730c..56909daa 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.3" } +apistos-plugins = { path = "../apistos-plugins", version = "0.3.4" } [lints] workspace = true diff --git a/apistos-redoc/Cargo.toml b/apistos-redoc/Cargo.toml index b5e91a34..5ccd124b 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.3" } +apistos-plugins = { path = "../apistos-plugins", version = "0.3.4" } [lints] workspace = true diff --git a/apistos-scalar/Cargo.toml b/apistos-scalar/Cargo.toml index 4a2bc7f3..03cfab05 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.3" } +apistos-plugins = { path = "../apistos-plugins", version = "0.3.4" } [lints] workspace = true diff --git a/apistos-shuttle/Cargo.toml b/apistos-shuttle/Cargo.toml index 5857207e..9c7d1e77 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.3" } +apistos = { path = "../apistos", version = "0.3.4" } [lints] workspace = true diff --git a/apistos-swagger-ui/Cargo.toml b/apistos-swagger-ui/Cargo.toml index b3484de5..6cc841c1 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.3" } +apistos-plugins = { path = "../apistos-plugins", version = "0.3.4" } [lints] workspace = true diff --git a/apistos/Cargo.toml b/apistos/Cargo.toml index a30ba4f1..2940949d 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.3" } -apistos-gen = { path = "../apistos-gen", version = "0.3.3" } -apistos-models = { path = "../apistos-models", version = "0.3.3" } -apistos-plugins = { path = "../apistos-plugins", version = "0.3.3" } -apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.3", optional = true } -apistos-redoc = { path = "../apistos-redoc", version = "0.3.3", optional = true } -apistos-scalar = { path = "../apistos-scalar", version = "0.3.3", optional = true } -apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.3", optional = true } +apistos-core = { path = "../apistos-core", version = "0.3.4" } +apistos-gen = { path = "../apistos-gen", version = "0.3.4" } +apistos-models = { path = "../apistos-models", version = "0.3.4" } +apistos-plugins = { path = "../apistos-plugins", version = "0.3.4" } +apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.4", optional = true } +apistos-redoc = { path = "../apistos-redoc", version = "0.3.4", optional = true } +apistos-scalar = { path = "../apistos-scalar", version = "0.3.4", optional = true } +apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.4", optional = true } [dev-dependencies] actix-web-lab = { workspace = true } garde-actix-web = { workspace = true } -apistos-models = { path = "../apistos-models", version = "0.3.3", features = ["deserialize"] } -apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.3" } -apistos-redoc = { path = "../apistos-redoc", version = "0.3.3" } -apistos-scalar = { path = "../apistos-scalar", version = "0.3.3" } -apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.3" } +apistos-models = { path = "../apistos-models", version = "0.3.4", features = ["deserialize"] } +apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.4" } +apistos-redoc = { path = "../apistos-redoc", version = "0.3.4" } +apistos-scalar = { path = "../apistos-scalar", version = "0.3.4" } +apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.4" } [lints] workspace = true