Skip to content

Commit

Permalink
Support actix web either (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
rlebran authored Jul 11, 2024
2 parents 1868179 + ff7bee6 commit e01a62d
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 25 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.2"
version = "0.3.3"

[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.2", features = ["deserialize"] }
apistos-models = { path = "../apistos-models", version = "0.3.3", features = ["deserialize"] }

[dev-dependencies]
assert-json-diff = { workspace = true }
Expand Down
110 changes: 109 additions & 1 deletion apistos-core/src/api_component.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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>)> {
Expand Down
7 changes: 4 additions & 3 deletions apistos-gen-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
106 changes: 105 additions & 1 deletion apistos-gen-test/src/tests/api_operation.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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"
]
})
);
}
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.2" }
apistos-plugins = { path = "../apistos-plugins", version = "0.3.3" }

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

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

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

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

[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.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
Expand Down

0 comments on commit e01a62d

Please sign in to comment.