Skip to content

Commit

Permalink
Allow skip args in operations (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
rlebran authored Jul 9, 2024
2 parents 2ec1fc9 + 1dba4d4 commit 5a90250
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 29 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.0"
version = "0.3.1"

[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 @@ -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 }
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 @@ -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"] }
Expand Down
17 changes: 13 additions & 4 deletions apistos-gen/src/internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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()
}
2 changes: 2 additions & 0 deletions apistos-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions apistos-gen/src/operation_attr.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use darling::ast::NestedMeta;
use darling::FromMeta;
use proc_macro2::Ident;
use proc_macro_error::abort;
use std::collections::BTreeMap;

Expand Down Expand Up @@ -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)]
Expand All @@ -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 {
Expand All @@ -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,
}
}
}
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.0" }
apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }

[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.0" }
apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }

[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.0" }
apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }

[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.0" }
apistos = { path = "../apistos", version = "0.3.1" }

[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.0" }
apistos-plugins = { path = "../apistos-plugins", version = "0.3.1" }

[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.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
Expand Down
126 changes: 123 additions & 3 deletions apistos/tests/operations.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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 _;
Expand Down

0 comments on commit 5a90250

Please sign in to comment.