Skip to content

Commit

Permalink
Handle multiple generic parameter in operations (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
rlebran committed Jul 12, 2024
2 parents e01a62d + f039cab commit ef48a7d
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion apistos-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
4 changes: 2 additions & 2 deletions apistos-gen-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
28 changes: 24 additions & 4 deletions apistos-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ use convert_case::{Case, Casing};
use darling::ast::NestedMeta;
use darling::Error;
use proc_macro::TokenStream;
use proc_macro2::Span;
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;
Expand Down Expand Up @@ -632,9 +633,28 @@ 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;)
};
Expand Down
2 changes: 1 addition & 1 deletion apistos-rapidoc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion apistos-redoc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion apistos-scalar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion apistos-shuttle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion apistos-swagger-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 13 additions & 13 deletions apistos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions apistos/tests/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, U>(_params: Path<(u32, String)>, _test: Json<Test>) -> Result<Json<Test>, 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::<String, String>))))
.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 _;
Expand Down

0 comments on commit ef48a7d

Please sign in to comment.