Repository: netwo-io/apistos
Pull request #114 (2024-07-10 09:48:46)

Support actix web either


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 817caff..6c4048f 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.2"
+version = "0.3.3"
 
 [workspace.dependencies]
 actix-service = "2"
diff --git a/apistos-core/Cargo.toml b/apistos-core/Cargo.toml
index 3014e2a..ceb6a5b 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.2", features = ["deserialize"] }
+apistos-models = { path = "../apistos-models", version = "0.3.3", features = ["deserialize"] }
 
 [dev-dependencies]
 assert-json-diff = { workspace = true }
diff --git a/apistos-core/src/api_component.rs b/apistos-core/src/api_component.rs
index 093c4de..ac20057 100644
--- a/apistos-core/src/api_component.rs
+++ b/apistos-core/src/api_component.rs
@@ -1,11 +1,12 @@
 use crate::ApiErrorComponent;
 #[cfg(feature = "actix")]
 use crate::{PathItemDefinition, ResponseWrapper};
+use actix_web::Either;
 use apistos_models::paths::{MediaType, Parameter, RequestBody, Response, Responses};
 use apistos_models::reference_or::ReferenceOr;
 use apistos_models::security::SecurityScheme;
 use apistos_models::Schema;
-use schemars::schema::{ArrayValidation, InstanceType, SchemaObject, SingleOrVec};
+use schemars::schema::{ArrayValidation, InstanceType, SchemaObject, SingleOrVec, SubschemaValidation};
 use std::collections::BTreeMap;
 #[cfg(feature = "actix")]
 use std::future::Future;
@@ -175,6 +176,113 @@ where
   }
 }
 
+impl<T, E> ApiComponent for Either<T, E>
+where
+  T: ApiComponent,
+  E: ApiComponent,
+{
+  fn required() -> bool {
+    T::required() && E::required()
+  }
+
+  fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
+    let mut child_schemas = T::child_schemas();
+    child_schemas.append(&mut E::child_schemas());
+    child_schemas
+  }
+
+  fn raw_schema() -> Option<ReferenceOr<Schema>> {
+    match (T::raw_schema(), E::raw_schema()) {
+      (Some(raw_schema1), Some(raw_schema2)) => {
+        let raw_schema1 = match raw_schema1 {
+          ReferenceOr::Object(schema_obj) => schema_obj,
+          ReferenceOr::Reference { _ref } => Schema::Object(SchemaObject {
+            reference: Some(_ref),
+            ..Default::default()
+          }),
+        };
+        let raw_schema2 = match raw_schema2 {
+          ReferenceOr::Object(schema_obj) => schema_obj,
+          ReferenceOr::Reference { _ref } => Schema::Object(SchemaObject {
+            reference: Some(_ref),
+            ..Default::default()
+          }),
+        };
+        Some(ReferenceOr::Object(Schema::Object(SchemaObject {
+          subschemas: Some(Box::new(SubschemaValidation {
+            one_of: Some(vec![raw_schema1, raw_schema2]),
+            ..Default::default()
+          })),
+          ..Default::default()
+        })))
+      }
+      (Some(raw_schema1), None) => Some(raw_schema1),
+      (None, Some(raw_schema2)) => Some(raw_schema2),
+      (None, None) => None,
+    }
+  }
+
+  fn schema() -> Option<(String, ReferenceOr<Schema>)> {
+    match (T::schema(), E::schema()) {
+      (Some(schema1), Some(schema2)) => {
+        let (schema_name1, schema1) = schema1;
+        let schema1 = match schema1 {
+          ReferenceOr::Object(schema_obj) => schema_obj,
+          ReferenceOr::Reference { _ref } => Schema::Object(SchemaObject {
+            reference: Some(_ref),
+            ..Default::default()
+          }),
+        };
+        let (schema_name2, schema2) = schema2;
+        let schema2 = match schema2 {
+          ReferenceOr::Object(schema_obj) => schema_obj,
+          ReferenceOr::Reference { _ref } => Schema::Object(SchemaObject {
+            reference: Some(_ref),
+            ..Default::default()
+          }),
+        };
+        let schema = ReferenceOr::Object(Schema::Object(SchemaObject {
+          subschemas: Some(Box::new(SubschemaValidation {
+            one_of: Some(vec![schema1, schema2]),
+            ..Default::default()
+          })),
+          ..Default::default()
+        }));
+        let schema_name = format!("Either{}Or{}", schema_name1, schema_name2);
+        Some((schema_name, schema))
+      }
+      (Some(schema1), None) => Some(schema1),
+      (None, Some(schema2)) => Some(schema2),
+      (None, None) => None,
+    }
+  }
+
+  fn error_responses() -> Vec<(String, Response)> {
+    let mut error_responses = T::error_responses();
+    error_responses.append(&mut E::error_responses());
+    error_responses
+  }
+
+  fn error_schemas() -> BTreeMap<String, (String, ReferenceOr<Schema>)> {
+    let mut error_schemas = E::error_schemas();
+    error_schemas.append(&mut T::error_schemas());
+    error_schemas
+  }
+
+  fn responses(content_type: Option<String>) -> Option<Responses> {
+    let responses = T::responses(content_type.clone());
+    match responses {
+      None => E::responses(content_type),
+      Some(mut responses) => {
+        responses
+          .responses
+          .append(&mut E::responses(content_type).map(|r| r.responses).unwrap_or_default());
+        Some(responses)
+      }
+    }
+  }
+}
+
 #[cfg(feature = "actix")]
 impl<T> ApiComponent for actix_web::web::Data<T> {
   fn child_schemas() -> Vec<(String, ReferenceOr<Schema>)> {
diff --git a/apistos-rapidoc/Cargo.toml b/apistos-rapidoc/Cargo.toml
index 7086ce7..b552730 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.2" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.3" }
 
 [lints]
 workspace = true
diff --git a/apistos-redoc/Cargo.toml b/apistos-redoc/Cargo.toml
index 5467678..b5e91a3 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.2" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.3" }
 
 [lints]
 workspace = true
diff --git a/apistos-scalar/Cargo.toml b/apistos-scalar/Cargo.toml
index ee9ea44..4a2bc7f 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.2" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.3" }
 
 [lints]
 workspace = true
diff --git a/apistos-shuttle/Cargo.toml b/apistos-shuttle/Cargo.toml
index a16fb24..5857207 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.2" }
+apistos = { path = "../apistos", version = "0.3.3" }
 
 [lints]
 workspace = true
diff --git a/apistos-swagger-ui/Cargo.toml b/apistos-swagger-ui/Cargo.toml
index 8724119..b3484de 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.2" }
+apistos-plugins = { path = "../apistos-plugins", version = "0.3.3" }
 
 [lints]
 workspace = true
diff --git a/apistos/Cargo.toml b/apistos/Cargo.toml
index 28a676c..a30ba4f 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.2" }
-apistos-gen = { path = "../apistos-gen", version = "0.3.2" }
-apistos-models = { path = "../apistos-models", version = "0.3.2" }
-apistos-plugins = { path = "../apistos-plugins", version = "0.3.2" }
-apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.2", optional = true }
-apistos-redoc = { path = "../apistos-redoc", version = "0.3.2", optional = true }
-apistos-scalar = { path = "../apistos-scalar", version = "0.3.2", optional = true }
-apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.2", optional = 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 }
 
 
 [dev-dependencies]
 actix-web-lab = { workspace = true }
 garde-actix-web = { workspace = true }
 
-apistos-models = { path = "../apistos-models", version = "0.3.2", features = ["deserialize"] }
-apistos-rapidoc = { path = "../apistos-rapidoc", version = "0.3.2" }
-apistos-redoc = { path = "../apistos-redoc", version = "0.3.2" }
-apistos-scalar = { path = "../apistos-scalar", version = "0.3.2" }
-apistos-swagger-ui = { path = "../apistos-swagger-ui", version = "0.3.2" }
+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" }
 
 [lints]
 workspace = true


Test patch:
diff --git a/apistos-gen-test/Cargo.toml b/apistos-gen-test/Cargo.toml
index 9e5713b..6ac73e5 100644
--- a/apistos-gen-test/Cargo.toml
+++ b/apistos-gen-test/Cargo.toml
@@ -16,15 +16,16 @@ license.workspace = true
 [dependencies]
 actix-multipart = { workspace = true }
 actix-web = { workspace = true }
+actix-web-grants = { workspace = true }
 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.2" }
-apistos-gen = { path = "../apistos-gen", version = "0.3.2" }
+apistos-core = { path = "../apistos-core", version = "0.3.3", features = ["actix-web-grants"] }
+apistos-gen = { path = "../apistos-gen", version = "0.3.3" }
 # 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"] }
+serde = { workspace = true, features = ["derive"] }
 serde_json = { workspace = true }
 uuid = { workspace = true, features = ["v4"] }
 
diff --git a/apistos-gen-test/src/tests/api_operation.rs b/apistos-gen-test/src/tests/api_operation.rs
index bae4190..eb35d16 100644
--- a/apistos-gen-test/src/tests/api_operation.rs
+++ b/apistos-gen-test/src/tests/api_operation.rs
@@ -1,9 +1,11 @@
 use actix_multipart::form::MultipartForm;
+use actix_web::dev::ServiceRequest;
 use actix_web::http::header::ContentType;
 use actix_web::web::Json;
-use actix_web::{HttpResponse, Responder};
+use actix_web::{Error, HttpResponse, Responder};
 use assert_json_diff::assert_json_eq;
 use schemars::_serde_json::json;
+use std::collections::HashSet;
 use uuid::Uuid;
 
 use apistos::actix::{AcceptedJson, CreatedJson, NoContent};
@@ -1442,3 +1444,105 @@ fn api_operation_root_vec() {
     })
   );
 }
+
+#[test]
+fn api_operation_actix_web_grant() {
+  #[allow(clippy::unused_async)]
+  async fn extract(_req: &ServiceRequest) -> Result<HashSet<String>, Error> {
+    Ok(Default::default())
+  }
+
+  /// Add a new pet to the store
+  /// Add a new pet to the store
+  /// Plop
+  #[actix_web_grants::protect("ADMIN")]
+  #[api_operation(tag = "pet")]
+  pub(crate) async fn test(
+    _body: Json<test_models::Test>,
+  ) -> Result<Json<Vec<test_models::TestResult>>, test_models::ErrorResponse> {
+    Ok(Json(vec![test_models::TestResult { id: 0 }]))
+  }
+
+  let components = __openapi_test::components();
+  // only one component here because: error does not have schema and Test is used both for query and response
+  assert_eq!(components.len(), 1);
+  let components = serde_json::to_value(components).expect("Unable to serialize as Json");
+
+  let operation = __openapi_test::operation();
+  let operation = serde_json::to_value(operation).expect("Unable to serialize as Json");
+
+  assert_json_eq!(
+    components,
+    json!([
+      {
+        "schemas": {
+          "Test": {
+            "properties": {
+              "test": {
+                "type": "string"
+              }
+            },
+            "required": [
+              "test"
+            ],
+            "title": "Test",
+            "type": "object"
+          },
+          "TestResult": {
+            "properties": {
+              "id": {
+                "format": "uint32",
+                "minimum": 0.0,
+                "type": "integer"
+              }
+            },
+            "required": [
+              "id"
+            ],
+            "title": "TestResult",
+            "type": "object"
+          }
+        }
+      }
+    ])
+  );
+  assert_json_eq!(
+    operation,
+    json!({
+      "deprecated": false,
+      "description": "Add a new pet to the store\\\nPlop",
+      "requestBody": {
+        "content": {
+          "application/json": {
+            "schema": {
+              "$ref": "#/components/schemas/Test"
+            }
+          }
+        },
+        "required": true
+      },
+      "responses": {
+        "200": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "array",
+                "items": {
+                  "$ref": "#/components/schemas/TestResult"
+                }
+              }
+            }
+          },
+          "description": ""
+        },
+        "405": {
+          "description": "Invalid input"
+        }
+      },
+      "summary": "Add a new pet to the store",
+      "tags": [
+        "pet"
+      ]
+    })
+  );
+}