diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 37f0e898f..4efede614 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -17,6 +17,7 @@ https://github.com/oxidecomputer/dropshot/compare/v0.8.0\...HEAD[Full list of co * https://github.com/oxidecomputer/dropshot/pull/452[#452] Dropshot no longer enables the `slog` cargo features `max_level_trace` and `release_max_level_debug`. Previously, clients were unable to set a release log level of `trace`; now they can. However, clients that did not select their own max log levels will see behavior change from the levels Dropshot was choosing to the default levels of `slog` itself (`debug` for debug builds and `info` for release builds). * https://github.com/oxidecomputer/dropshot/pull/451[#451] There are now response types to support 302 ("Found"), 303 ("See Other"), and 307 ("Temporary Redirect") HTTP response codes. See `HttpResponseFound`, `HttpResponseSeeOther`, and `HttpResponseTemporaryRedirect`. +* https://github.com/oxidecomputer/dropshot/pull/503[#503] Add an optional `deprecated` field to the `#[endpoint]` macro. == 0.8.0 (released 2022-09-09) diff --git a/dropshot/src/api_description.rs b/dropshot/src/api_description.rs index e472c1879..e4771bc32 100644 --- a/dropshot/src/api_description.rs +++ b/dropshot/src/api_description.rs @@ -47,6 +47,7 @@ pub struct ApiEndpoint { pub tags: Vec, pub extension_mode: ExtensionMode, pub visible: bool, + pub deprecated: bool, } impl<'a, Context: ServerContext> ApiEndpoint { @@ -80,6 +81,7 @@ impl<'a, Context: ServerContext> ApiEndpoint { tags: vec![], extension_mode: func_parameters.extension_mode, visible: true, + deprecated: false, } } @@ -102,6 +104,11 @@ impl<'a, Context: ServerContext> ApiEndpoint { self.visible = visible; self } + + pub fn deprecated(mut self, deprecated: bool) -> Self { + self.deprecated = deprecated; + self + } } /** @@ -588,6 +595,7 @@ impl ApiDescription { operation.summary = endpoint.summary.clone(); operation.description = endpoint.description.clone(); operation.tags = endpoint.tags.clone(); + operation.deprecated = endpoint.deprecated; operation.parameters = endpoint .parameters diff --git a/dropshot/src/router.rs b/dropshot/src/router.rs index 06e90200c..936d8d6ad 100644 --- a/dropshot/src/router.rs +++ b/dropshot/src/router.rs @@ -824,6 +824,7 @@ mod test { tags: vec![], extension_mode: Default::default(), visible: true, + deprecated: false, } } diff --git a/dropshot/tests/test_openapi.json b/dropshot/tests/test_openapi.json index 98e209d01..7bcc240ab 100644 --- a/dropshot/tests/test_openapi.json +++ b/dropshot/tests/test_openapi.json @@ -408,6 +408,36 @@ } } }, + "/test/deprecated": { + "get": { + "tags": [ + "it" + ], + "operationId": "handler24", + "responses": { + "307": { + "description": "redirect (temporary redirect)", + "headers": { + "location": { + "description": "HTTP \"Location\" header", + "style": "simple", + "required": true, + "schema": { + "type": "string" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "deprecated": true + } + }, "/test/man/{x}": { "delete": { "tags": [ diff --git a/dropshot/tests/test_openapi.rs b/dropshot/tests/test_openapi.rs index 3f7ce94e9..6e581f257 100644 --- a/dropshot/tests/test_openapi.rs +++ b/dropshot/tests/test_openapi.rs @@ -455,6 +455,18 @@ async fn handler23( Ok(http_response_temporary_redirect(String::from("/path3")).unwrap()) } +#[endpoint { + method = GET, + path = "/test/deprecated", + tags = [ "it"], + deprecated = true, +}] +async fn handler24( + _rqctx: Arc>, +) -> Result { + unimplemented!() +} + fn make_api( maybe_tag_config: Option, ) -> Result, String> { @@ -487,6 +499,7 @@ fn make_api( api.register(handler21)?; api.register(handler22)?; api.register(handler23)?; + api.register(handler24)?; Ok(api) } diff --git a/dropshot/tests/test_openapi_fuller.json b/dropshot/tests/test_openapi_fuller.json index b29b35fe9..1db2b1073 100644 --- a/dropshot/tests/test_openapi_fuller.json +++ b/dropshot/tests/test_openapi_fuller.json @@ -416,6 +416,36 @@ } } }, + "/test/deprecated": { + "get": { + "tags": [ + "it" + ], + "operationId": "handler24", + "responses": { + "307": { + "description": "redirect (temporary redirect)", + "headers": { + "location": { + "description": "HTTP \"Location\" header", + "style": "simple", + "required": true, + "schema": { + "type": "string" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "deprecated": true + } + }, "/test/man/{x}": { "delete": { "tags": [ diff --git a/dropshot_endpoint/src/lib.rs b/dropshot_endpoint/src/lib.rs index ea49a0cd0..5ee6709fa 100644 --- a/dropshot_endpoint/src/lib.rs +++ b/dropshot_endpoint/src/lib.rs @@ -51,8 +51,12 @@ impl MethodType { struct EndpointMetadata { method: MethodType, path: String, - tags: Option>, - unpublished: Option, + #[serde(default)] + tags: Vec, + #[serde(default)] + unpublished: bool, + #[serde(default)] + deprecated: bool, content_type: Option, _dropshot_crate: Option, } @@ -67,8 +71,12 @@ enum ChannelProtocol { struct ChannelMetadata { protocol: ChannelProtocol, path: String, - tags: Option>, - unpublished: Option, + #[serde(default)] + tags: Vec, + #[serde(default)] + unpublished: bool, + #[serde(default)] + deprecated: bool, _dropshot_crate: Option, } @@ -94,12 +102,14 @@ const USAGE: &str = "Endpoint handlers must have the following signature: /// method = { DELETE | GET | OPTIONS | PATCH | POST | PUT }, /// path = "/path/name/with/{named}/{variables}", /// -/// // Optional tags for the API description +/// // Optional tags for the operation's description /// tags = [ "all", "your", "OpenAPI", "tags" ], -/// // A value of `true` causes the API to be omitted from the API description -/// unpublished = { true | false }, /// // Specifies the media type used to encode the request body /// content_type = { "application/json" | "application/x-www-form-urlencoded" } +/// // A value of `true` marks the operation as deprecated +/// deprecated = { true | false }, +/// // A value of `true` causes the operation to be omitted from the API description +/// unpublished = { true | false }, /// }] /// ``` /// @@ -153,8 +163,14 @@ fn do_channel( attr: proc_macro2::TokenStream, item: proc_macro2::TokenStream, ) -> Result<(proc_macro2::TokenStream, Vec), Error> { - let ChannelMetadata { protocol, path, tags, unpublished, _dropshot_crate } = - from_tokenstream(&attr)?; + let ChannelMetadata { + protocol, + path, + tags, + unpublished, + deprecated, + _dropshot_crate, + } = from_tokenstream(&attr)?; match protocol { ChannelProtocol::WEBSOCKETS => { // here we construct a wrapper function and mutate the arguments a bit @@ -225,6 +241,7 @@ fn do_channel( path, tags, unpublished, + deprecated, content_type: Some("application/json".to_string()), _dropshot_crate, }; @@ -347,18 +364,15 @@ fn do_endpoint_inner( let tags = metadata .tags - .map(|v| { - v.iter() - .map(|tag| { - quote! { - .tag(#tag) - } - }) - .collect::>() + .iter() + .map(|tag| { + quote! { + .tag(#tag) + } }) - .unwrap_or_default(); + .collect::>(); - let visible = if let Some(true) = metadata.unpublished { + let visible = if metadata.unpublished { quote! { .visible(false) } @@ -366,6 +380,14 @@ fn do_endpoint_inner( quote! {} }; + let deprecated = if metadata.deprecated { + quote! { + .deprecated(true) + } + } else { + quote! {} + }; + let dropshot = get_crate(metadata._dropshot_crate); let first_arg = match ast.sig.inputs.first() { @@ -553,6 +575,7 @@ fn do_endpoint_inner( #description #(#tags)* #visible + #deprecated } } else { quote! { @@ -599,7 +622,7 @@ fn do_endpoint_inner( errors.insert(0, Error::new_spanned(&ast.sig, USAGE)); } - if path.contains(":.*}") && metadata.unpublished != Some(true) { + if path.contains(":.*}") && !metadata.unpublished { errors.push(Error::new_spanned( &attr, "paths that contain a wildcard match must include 'unpublished = \