diff --git a/Cargo.toml b/Cargo.toml index 56fc0a40..ecf5e424 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ members = [ "examples/secure_request_guard", "examples/streams", "examples/dyn_templates", + "examples/test-flatten-extension-field", ] diff --git a/examples/test-flatten-extension-field/Cargo.toml b/examples/test-flatten-extension-field/Cargo.toml new file mode 100644 index 00000000..e42cd79d --- /dev/null +++ b/examples/test-flatten-extension-field/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "test-flatten-extension-field" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rocket = { version = "=0.5.0-rc.2", default-features = false, features = ["json"] } +rocket_okapi = {path = "../../rocket-okapi", features = ["swagger"]} + +[dev-dependencies] +pretty_assertions = "1.2.1" \ No newline at end of file diff --git a/examples/test-flatten-extension-field/src/main.rs b/examples/test-flatten-extension-field/src/main.rs new file mode 100644 index 00000000..2174b6b8 --- /dev/null +++ b/examples/test-flatten-extension-field/src/main.rs @@ -0,0 +1,71 @@ +#[macro_use] extern crate rocket; + +use rocket::{Build, Config, Rocket}; +use rocket::config::LogLevel; +use rocket::http::Status; +use rocket_okapi::{openapi_get_routes, openapi}; +use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig}; + + + +fn get_docs() -> SwaggerUIConfig { + SwaggerUIConfig { + url: "/openapi.json".to_string(), + ..Default::default() + } +} + + +#[openapi] +#[get("/")] +pub(crate) async fn test_index() -> Result { + Ok("I am here as a dummy route for the OpenAPI spec".to_string()) +} + + +#[launch] +fn rocket() -> Rocket { + let config = Config { + log_level : LogLevel::Normal, + ..Config::debug_default() + }; + + rocket::custom(&config) + .mount("/", openapi_get_routes![test_index]) + .mount("/swagger", make_swagger_ui(&get_docs())) +} + +#[cfg(test)] +mod expected_unit_test { + use rocket::serde::json::serde_json; + use rocket_okapi::okapi::openapi3::OpenApi; + use rocket_okapi::openapi_get_spec; + use pretty_assertions::{assert_eq}; + use crate::*; + + #[test] + fn openapi_cmp() { + let ref_data = std::fs::read_to_string("./tests/expected/openapi.json").unwrap(); + let expected_spec : OpenApi = serde_json::from_str(&ref_data).unwrap(); + let current_spec : OpenApi = openapi_get_spec![test_index]; + assert_eq!(expected_spec, current_spec); + } + + #[test] + fn string_cmp() { + // Expected to ALWAYS pass. Included to ensure the expected-file is correct. + + let ref_data = std::fs::read_to_string("./tests/expected/openapi.json").unwrap(); + let current_spec: OpenApi = openapi_get_spec![test_index]; + /* Note: + Depending on whether or not the reference file is pretty printed we have to use to_string_pretty or to_string. + + The "better" way would be to deserialize ref_data to type OpenApi, however currently the deserialization is not working as expected + due to an error with flattening some 'extensions' fields in structs of okapi::openapi3. + */ + let current_str = serde_json::to_string_pretty(¤t_spec).unwrap(); + // below: not-pretty-printed version + // let current_str = serde_json::to_string(¤t_spec).unwrap(); + assert_eq!(ref_data, current_str); + } +} \ No newline at end of file diff --git a/examples/test-flatten-extension-field/tests/expected/openapi.json b/examples/test-flatten-extension-field/tests/expected/openapi.json new file mode 100644 index 00000000..b9360393 --- /dev/null +++ b/examples/test-flatten-extension-field/tests/expected/openapi.json @@ -0,0 +1,30 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "test-flatten-extension-field", + "version": "0.1.0" + }, + "paths": { + "/": { + "get": { + "operationId": "test_index", + "responses": { + "200": { + "description": "", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "default": { + "description": "" + } + } + } + } + }, + "components": {} +} \ No newline at end of file diff --git a/okapi/Cargo.toml b/okapi/Cargo.toml index d923eff5..5924272d 100644 --- a/okapi/Cargo.toml +++ b/okapi/Cargo.toml @@ -11,7 +11,8 @@ keywords = ["rust", "openapi", "swagger"] categories = ["web-programming"] [dependencies] -schemars = { version = "0.8", features = ["uuid1"] } +schemars = { git = "https://github.com/snpschaaf/schemars.git", branch = "feature-aware-serde-for-map", features = ["uuid1"] } +indexmap = { version = "1.2", features = ["serde-1"]} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" log = "0.4" diff --git a/okapi/src/openapi3.rs b/okapi/src/openapi3.rs index a82c1817..63b3e16d 100644 --- a/okapi/src/openapi3.rs +++ b/okapi/src/openapi3.rs @@ -59,7 +59,7 @@ pub struct OpenApi { pub tags: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub external_docs: Option, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -78,7 +78,7 @@ pub struct Info { #[serde(default, skip_serializing_if = "Option::is_none")] pub license: Option, pub version: String, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -92,7 +92,7 @@ pub struct Contact { pub url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub email: Option, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -103,7 +103,7 @@ pub struct License { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub url: Option, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -116,7 +116,7 @@ pub struct Server { pub description: Option, #[serde(default, skip_serializing_if = "Map::is_empty")] pub variables: Map, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -129,7 +129,7 @@ pub struct ServerVariable { pub default: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -163,7 +163,7 @@ pub struct PathItem { pub servers: Option>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub parameters: Vec>, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -194,7 +194,7 @@ pub struct Operation { pub security: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub servers: Option>, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -206,7 +206,7 @@ pub struct Responses { pub default: Option>, #[serde(flatten)] pub responses: Map>, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -232,7 +232,7 @@ pub struct Components { pub links: Map>, #[serde(default, skip_serializing_if = "Map::is_empty")] pub callbacks: Map>, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -247,7 +247,7 @@ pub struct Response { pub content: Map, #[serde(default, skip_serializing_if = "Map::is_empty")] pub links: Map>, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -269,7 +269,7 @@ pub struct Parameter { pub allow_empty_value: bool, #[serde(flatten)] pub value: ParameterValue, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -320,7 +320,7 @@ pub struct Example { pub description: Option, #[serde(flatten)] pub value: ExampleValue, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -341,7 +341,7 @@ pub struct RequestBody { pub content: Map, #[serde(default, skip_serializing_if = "is_false")] pub required: bool, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -359,7 +359,7 @@ pub struct Header { pub allow_empty_value: bool, #[serde(flatten)] pub value: ParameterValue, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -372,7 +372,7 @@ pub struct SecurityScheme { // This also sets `type` #[serde(flatten)] pub data: SecuritySchemeData, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -409,7 +409,7 @@ pub enum OAuthFlows { #[serde(default, skip_serializing_if = "Option::is_none")] refresh_url: Option, scopes: Map, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] extensions: Object, }, #[serde(rename_all = "camelCase")] @@ -418,7 +418,7 @@ pub enum OAuthFlows { #[serde(default, skip_serializing_if = "Option::is_none")] refresh_url: Option, scopes: Map, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] extensions: Object, }, #[serde(rename_all = "camelCase")] @@ -427,7 +427,7 @@ pub enum OAuthFlows { #[serde(default, skip_serializing_if = "Option::is_none")] refresh_url: Option, scopes: Map, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] extensions: Object, }, #[serde(rename_all = "camelCase")] @@ -437,7 +437,7 @@ pub enum OAuthFlows { #[serde(default, skip_serializing_if = "Option::is_none")] refresh_url: Option, scopes: Map, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] extensions: Object, }, } @@ -459,7 +459,7 @@ pub struct Link { pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub server: Option, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -469,7 +469,7 @@ pub struct Link { pub struct Callback { #[serde(flatten)] pub callbacks: Map, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -485,7 +485,7 @@ pub struct MediaType { pub examples: Option>, #[serde(skip_serializing_if = "Map::is_empty")] pub encoding: Map, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -498,7 +498,7 @@ pub struct Tag { pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub external_docs: Option, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -509,7 +509,7 @@ pub struct ExternalDocs { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub url: String, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } @@ -527,7 +527,7 @@ pub struct Encoding { pub explode: Option, #[serde(skip_serializing_if = "is_false")] pub allow_reserved: bool, - #[serde(flatten)] + #[serde(default, with = "schemars::map_serde")] pub extensions: Object, } diff --git a/rocket-okapi/Cargo.toml b/rocket-okapi/Cargo.toml index ef819fad..8c79aaa1 100644 --- a/rocket-okapi/Cargo.toml +++ b/rocket-okapi/Cargo.toml @@ -12,7 +12,7 @@ categories = ["web-programming"] [dependencies] rocket = { version = "=0.5.0-rc.2", default-features = false, features = ["json"] } -schemars = { version = "0.8.10" } +schemars = { git = "https://github.com/snpschaaf/schemars.git", branch = "feature-aware-serde-for-map" } okapi = { version = "0.7.0-rc.1", path = "../okapi" } rocket_okapi_codegen = { version = "=0.8.0-rc.2", path = "../rocket-okapi-codegen" } serde = "1.0"