diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index a158b0019..0e9c47b03 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -27,6 +27,7 @@ https://github.com/oxidecomputer/dropshot/compare/v0.5.1\...HEAD[Full list of co * https://github.com/oxidecomputer/dropshot/pull/100[#100] The type used for the "limit" argument for paginated resources has changed. This limit refers to the number of items that an HTTP client can ask for in a single request to a paginated endpoint. The limit is now 4294967295, where it may have previously been larger. This is not expected to affect consumers because this limit is far larger than practical. For details, see #100. * https://github.com/oxidecomputer/dropshot/pull/116[#116] Unused, non-`pub` endpoints from the `#[endpoint { ... }]` macro now produce a lint warning. This is *technically* a breaking change for those who may have had unused endpoints and compiled with `#[deny(warning)]` or `#[deny(dead_code)]` thus implicitly relying on the *absence* of a warning about the endpoint being unused. * https://github.com/oxidecomputer/dropshot/pull/118[#118] Path handling has changed. Escape sequences are decoded so that path parameters will no longer include those escape sequences. In addition, paths for endpoints added via `ApiDescription::register()` may not contain consecutive "/" characters. +* https://github.com/oxidecomputer/dropshot/pull/161[#161] The `ApiDescription::print_openapi()` interface (previously deprecated) has been removed. Now use `ApiDescription::openapi()` followed by a call to `OpenApiDefinition::write()` for equivalent functionality. == 0.5.1 (released 2021-03-18) diff --git a/Cargo.lock b/Cargo.lock index a11ef9b73..7d7119397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,12 +218,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dtoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" - [[package]] name = "dtrace-parser" version = "0.1.11" @@ -534,12 +528,6 @@ version = "0.2.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" -[[package]] -name = "linked-hash-map" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" - [[package]] name = "lock_api" version = "0.4.5" @@ -678,14 +666,13 @@ checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] name = "openapiv3" -version = "0.5.0" +version = "1.0.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90228d34f20d9fff3174d19b0d41e67f52711045270b540a2a2c2dc41ccb4085" +checksum = "f360315c88221c3142def78cd6fff55426b3119ae362bd52d1fe22928284e825" dependencies = [ "indexmap", "serde", "serde_json", - "serde_yaml", ] [[package]] @@ -938,18 +925,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af" -dependencies = [ - "dtoa", - "indexmap", - "serde", - "yaml-rust", -] - [[package]] name = "sha-1" version = "0.8.2" @@ -1408,15 +1383,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "zerocopy" version = "0.3.0" diff --git a/dropshot/Cargo.toml b/dropshot/Cargo.toml index ff7d999ea..1518f8ea5 100644 --- a/dropshot/Cargo.toml +++ b/dropshot/Cargo.toml @@ -18,7 +18,7 @@ futures = "0.3.1" hostname = "0.3.0" http = "0.2.5" indexmap = "1.7.0" -openapiv3 = "0.5.0" +openapiv3 = "1.0.0-beta.3" paste = "1.0.6" percent-encoding = "2.1.0" proc-macro2 = "1.0.32" diff --git a/dropshot/src/api_description.rs b/dropshot/src/api_description.rs index e143418fd..ea2b4f2d2 100644 --- a/dropshot/src/api_description.rs +++ b/dropshot/src/api_description.rs @@ -428,52 +428,6 @@ impl ApiDescription { OpenApiDefinition::new(self, title.as_ref(), version.as_ref()) } - /** - * Emit the OpenAPI Spec document describing this API in its JSON form. - * - * This routine is deprecated in favour of the new openapi() builder - * routine. - */ - #[allow(clippy::too_many_arguments)] - #[deprecated(note = "switch to openapi()")] - pub fn print_openapi( - &self, - out: &mut dyn std::io::Write, - title: &dyn ToString, - description: Option<&dyn ToString>, - terms_of_service: Option<&dyn ToString>, - contact_name: Option<&dyn ToString>, - contact_url: Option<&dyn ToString>, - contact_email: Option<&dyn ToString>, - license_name: Option<&dyn ToString>, - license_url: Option<&dyn ToString>, - version: &dyn ToString, - ) -> serde_json::Result<()> { - let mut oapi = self.openapi(title.to_string(), version.to_string()); - if let Some(s) = description { - oapi.description(s.to_string()); - } - if let Some(s) = terms_of_service { - oapi.terms_of_service(s.to_string()); - } - if let Some(s) = contact_name { - oapi.contact_name(s.to_string()); - } - if let Some(s) = contact_url { - oapi.contact_url(s.to_string()); - } - if let Some(s) = contact_email { - oapi.contact_email(s.to_string()); - } - if let (Some(name), Some(url)) = (license_name, license_url) { - oapi.license(name.to_string(), url.to_string()); - } else if let Some(name) = license_name { - oapi.license_name(name.to_string()); - } - - oapi.write(out) - } - /** * Internal routine for constructing the OpenAPI definition describing this * API in its JSON form. @@ -493,7 +447,7 @@ impl ApiDescription { if !endpoint.visible { continue; } - let path = openapi.paths.entry(path).or_insert( + let path = openapi.paths.paths.entry(path).or_insert( openapiv3::ReferenceOr::Item(openapiv3::PathItem::default()), ); @@ -612,9 +566,7 @@ impl ApiDescription { mime_type.to_string(), openapiv3::MediaType { schema: Some(schema), - example: None, - examples: indexmap::IndexMap::new(), - encoding: indexmap::IndexMap::new(), + ..Default::default() }, ); @@ -653,9 +605,7 @@ impl ApiDescription { CONTENT_TYPE_JSON.to_string(), openapiv3::MediaType { schema: Some(j2oas_schema(name.as_ref(), &js)), - example: None, - examples: indexmap::IndexMap::new(), - encoding: indexmap::IndexMap::new(), + ..Default::default() }, ); } @@ -788,7 +738,7 @@ fn j2oas_schema_object( let ty = match &obj.instance_type { Some(schemars::schema::SingleOrVec::Single(ty)) => Some(ty.as_ref()), Some(schemars::schema::SingleOrVec::Vec(_)) => { - unimplemented!("unsupported by openapiv3") + panic!("unsupported by openapiv3") } None => None, }; @@ -870,8 +820,7 @@ fn j2oas_subschemas( .map(|schema| j2oas_schema(None, schema)) .collect::>(), }, - (None, None, None) => todo!("missed a valid case"), - _ => panic!("invalid"), + _ => panic!("invalid subschema {:#?}", subschemas), } } @@ -1045,7 +994,13 @@ fn j2oas_string( let enumeration = enum_values .iter() - .flat_map(|v| v.iter().map(|vv| vv.as_str().unwrap().to_string())) + .flat_map(|v| { + v.iter().map(|vv| match vv { + serde_json::Value::Null => None, + serde_json::Value::String(s) => Some(s.clone()), + _ => panic!("unexpected enumeration value {:?}", vv), + }) + }) .collect::>(); openapiv3::SchemaKind::Type(openapiv3::Type::String( @@ -1067,9 +1022,12 @@ fn j2oas_array( openapiv3::SchemaKind::Type(openapiv3::Type::Array(openapiv3::ArrayType { items: match &arr.items { Some(schemars::schema::SingleOrVec::Single(schema)) => { - box_reference_or(j2oas_schema(None, &schema)) + Some(box_reference_or(j2oas_schema(None, &schema))) + } + Some(schemars::schema::SingleOrVec::Vec(_)) => { + panic!("OpenAPI v3.0.x cannot support tuple-like arrays") } - _ => unimplemented!("don't think this is valid"), + None => None, }, min_items: arr.min_items.map(|n| n as usize), max_items: arr.max_items.map(|n| n as usize), diff --git a/dropshot/tests/test_openapi.rs b/dropshot/tests/test_openapi.rs index 0a1a98b49..bd995af67 100644 --- a/dropshot/tests/test_openapi.rs +++ b/dropshot/tests/test_openapi.rs @@ -285,37 +285,13 @@ fn make_api() -> Result, String> { Ok(api) } -#[test] -fn test_openapi_old() -> Result<(), String> { - let api = make_api()?; - let mut output = Cursor::new(Vec::new()); - - #[allow(deprecated)] - let _ = api.print_openapi( - &mut output, - &"test", - None, - None, - None, - None, - None, - None, - None, - &"threeve", - ); - let actual = from_utf8(&output.get_ref()).unwrap(); - - expectorate::assert_contents("tests/test_openapi_old.json", actual); - Ok(()) -} - #[test] fn test_openapi() -> Result<(), String> { let api = make_api()?; let mut output = Cursor::new(Vec::new()); let _ = api.openapi("test", "threeve").write(&mut output); - let actual = from_utf8(&output.get_ref()).unwrap(); + let actual = from_utf8(output.get_ref()).unwrap(); expectorate::assert_contents("tests/test_openapi.json", actual); Ok(()) @@ -333,7 +309,7 @@ fn test_openapi_fuller() -> Result<(), String> { .license_name("CDDL") .terms_of_service("no hat, no cane? no service!") .write(&mut output); - let actual = from_utf8(&output.get_ref()).unwrap(); + let actual = from_utf8(output.get_ref()).unwrap(); expectorate::assert_contents("tests/test_openapi_fuller.json", actual); Ok(()) diff --git a/dropshot/tests/test_openapi_old.json b/dropshot/tests/test_openapi_old.json deleted file mode 100644 index ac87cc9ff..000000000 --- a/dropshot/tests/test_openapi_old.json +++ /dev/null @@ -1,457 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "test", - "version": "threeve" - }, - "paths": { - "/datagoeshere": { - "put": { - "operationId": "handler7", - "requestBody": { - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation" - } - } - } - }, - "/dup1": { - "get": { - "operationId": "handler8", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NeverDuplicatedResponseTopLevel" - } - } - } - } - } - } - }, - "/dup2": { - "get": { - "operationId": "handler9", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NeverDuplicatedResponseTopLevel" - } - } - } - } - } - } - }, - "/dup5": { - "put": { - "operationId": "handler10", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NeverDuplicatedBodyTopLevel" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation" - } - } - } - }, - "/dup6": { - "put": { - "operationId": "handler11", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NeverDuplicatedBodyTopLevel" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation" - } - } - } - }, - "/dup7": { - "put": { - "operationId": "handler12", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NeverDuplicatedTop" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation" - } - } - } - }, - "/dup8": { - "get": { - "operationId": "handler13", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NeverDuplicatedTop" - } - } - } - } - } - } - }, - "/impairment": { - "get": { - "operationId": "handler6", - "parameters": [ - { - "in": "query", - "name": "a_number", - "schema": { - "type": "integer", - "format": "uint16", - "minimum": 0 - }, - "style": "form" - }, - { - "in": "query", - "name": "limit", - "schema": { - "nullable": true, - "description": "Maximum number of items returned by a single call", - "type": "integer", - "format": "uint32", - "minimum": 1 - }, - "style": "form" - }, - { - "in": "query", - "name": "page_token", - "schema": { - "nullable": true, - "description": "Token returned by previous call to retreive the subsequent page", - "type": "string" - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseItemResultsPage" - } - } - } - } - }, - "x-dropshot-pagination": true - } - }, - "/test/camera": { - "post": { - "operationId": "handler4", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BodyParam" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "successful creation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Response" - } - } - } - } - } - } - }, - "/test/man/{x}": { - "delete": { - "operationId": "handler3", - "parameters": [ - { - "in": "path", - "name": "x", - "required": true, - "schema": { - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "204": { - "description": "successful deletion" - } - } - } - }, - "/test/person": { - "get": { - "description": "This is a multi-line comment. It uses Rust-style.", - "operationId": "handler1", - "responses": { - "200": { - "description": "successful operation" - } - } - } - }, - "/test/tv/{x}": { - "post": { - "tags": [ - "person", - "woman", - "man", - "camera", - "tv" - ], - "operationId": "handler5", - "parameters": [ - { - "in": "path", - "name": "x", - "required": true, - "schema": { - "type": "string" - }, - "style": "simple" - }, - { - "in": "query", - "name": "tomax", - "required": true, - "schema": { - "type": "string" - }, - "style": "form" - }, - { - "in": "query", - "name": "xamot", - "schema": { - "nullable": true, - "type": "string" - }, - "style": "form" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BodyParam" - } - } - }, - "required": true - }, - "responses": { - "202": { - "description": "successfully enqueued operation" - } - } - } - }, - "/test/woman": { - "put": { - "description": "This is a multi-line comment. It uses C-style.", - "operationId": "handler2", - "parameters": [ - { - "in": "query", - "name": "tomax", - "required": true, - "schema": { - "type": "string" - }, - "style": "form" - }, - { - "in": "query", - "name": "xamot", - "schema": { - "nullable": true, - "type": "string" - }, - "style": "form" - } - ], - "responses": { - "204": { - "description": "resource updated" - } - } - } - } - }, - "components": { - "schemas": { - "BodyParam": { - "type": "object", - "properties": { - "any": {}, - "x": { - "type": "string" - } - }, - "required": [ - "any", - "x" - ] - }, - "NeverDuplicatedBodyNextLevel": { - "type": "object", - "properties": { - "v": { - "type": "boolean" - } - }, - "required": [ - "v" - ] - }, - "NeverDuplicatedBodyTopLevel": { - "type": "object", - "properties": { - "_b": { - "$ref": "#/components/schemas/NeverDuplicatedBodyNextLevel" - } - }, - "required": [ - "_b" - ] - }, - "NeverDuplicatedNext": { - "type": "object", - "properties": { - "v": { - "type": "boolean" - } - }, - "required": [ - "v" - ] - }, - "NeverDuplicatedResponseNextLevel": { - "type": "object", - "properties": { - "v": { - "type": "boolean" - } - }, - "required": [ - "v" - ] - }, - "NeverDuplicatedResponseTopLevel": { - "type": "object", - "properties": { - "b": { - "$ref": "#/components/schemas/NeverDuplicatedResponseNextLevel" - } - }, - "required": [ - "b" - ] - }, - "NeverDuplicatedTop": { - "type": "object", - "properties": { - "b": { - "$ref": "#/components/schemas/NeverDuplicatedNext" - } - }, - "required": [ - "b" - ] - }, - "Response": { - "type": "object" - }, - "ResponseItem": { - "type": "object", - "properties": { - "word": { - "type": "string" - } - }, - "required": [ - "word" - ] - }, - "ResponseItemResultsPage": { - "description": "A single page of results", - "type": "object", - "properties": { - "items": { - "description": "list of items on this page of results", - "type": "array", - "items": { - "$ref": "#/components/schemas/ResponseItem" - } - }, - "next_page": { - "nullable": true, - "description": "token used to fetch the next page of results (if any)", - "type": "string" - } - }, - "required": [ - "items" - ] - } - } - } -} \ No newline at end of file