Skip to content

Commit bde88ae

Browse files
authored
allow endpoints and channels to specify the operation_id (#1127)
1 parent 1c0a353 commit bde88ae

11 files changed

+542
-5
lines changed

dropshot/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@
304304
//! path = "/path/name/with/{named}/{variables}",
305305
//!
306306
//! // Optional fields
307+
//! operation_id = "my_operation" // (default: name of the function)
307308
//! tags = [ "all", "your", "OpenAPI", "tags" ],
308309
//! }]
309310
//! ```

dropshot/tests/test_openapi.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,32 @@
200200
}
201201
}
202202
},
203+
"/first_thing": {
204+
"get": {
205+
"tags": [
206+
"it"
207+
],
208+
"operationId": "vzeroupper",
209+
"responses": {
210+
"201": {
211+
"description": "successful creation",
212+
"content": {
213+
"application/json": {
214+
"schema": {
215+
"$ref": "#/components/schemas/Response"
216+
}
217+
}
218+
}
219+
},
220+
"4XX": {
221+
"$ref": "#/components/responses/Error"
222+
},
223+
"5XX": {
224+
"$ref": "#/components/responses/Error"
225+
}
226+
}
227+
}
228+
},
203229
"/impairment": {
204230
"get": {
205231
"tags": [
@@ -269,6 +295,25 @@
269295
}
270296
}
271297
},
298+
"/other_thing": {
299+
"get": {
300+
"tags": [
301+
"it"
302+
],
303+
"operationId": "vzerolower",
304+
"responses": {
305+
"default": {
306+
"description": "",
307+
"content": {
308+
"*/*": {
309+
"schema": {}
310+
}
311+
}
312+
}
313+
},
314+
"x-dropshot-websocket": {}
315+
}
316+
},
272317
"/playing/a/bit/nicer": {
273318
"get": {
274319
"tags": [

dropshot/tests/test_openapi.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// Copyright 2023 Oxide Computer Company
22

3-
use dropshot::Body;
43
use dropshot::{
5-
endpoint, http_response_found, http_response_see_other,
4+
channel, endpoint, http_response_found, http_response_see_other,
65
http_response_temporary_redirect, ApiDescription,
76
ApiDescriptionRegisterError, FreeformBody, HttpError, HttpResponseAccepted,
87
HttpResponseCreated, HttpResponseDeleted, HttpResponseFound,
@@ -11,6 +10,7 @@ use dropshot::{
1110
PaginationParams, Path, Query, RequestContext, ResultsPage, TagConfig,
1211
TagDetails, TypedBody, UntypedBody,
1312
};
13+
use dropshot::{Body, WebsocketConnection};
1414
use schemars::JsonSchema;
1515
use serde::{Deserialize, Serialize};
1616
use std::{collections::HashMap, io::Cursor, str::from_utf8};
@@ -473,6 +473,33 @@ async fn handler25(
473473
Ok(HttpResponseCreated(Response {}))
474474
}
475475

476+
// test: Overridden operation id
477+
#[endpoint {
478+
operation_id = "vzeroupper",
479+
method = GET,
480+
path = "/first_thing",
481+
tags = ["it"]
482+
}]
483+
async fn handler26(
484+
_rqctx: RequestContext<()>,
485+
) -> Result<HttpResponseCreated<Response>, HttpError> {
486+
Ok(HttpResponseCreated(Response {}))
487+
}
488+
489+
// test: websocket using overriden operation id
490+
#[channel {
491+
protocol = WEBSOCKETS,
492+
operation_id = "vzerolower",
493+
path = "/other_thing",
494+
tags = ["it"]
495+
}]
496+
async fn handler27(
497+
_rqctx: RequestContext<()>,
498+
_: WebsocketConnection,
499+
) -> dropshot::WebsocketChannelResult {
500+
Ok(())
501+
}
502+
476503
fn make_api(
477504
maybe_tag_config: Option<TagConfig>,
478505
) -> Result<ApiDescription<()>, ApiDescriptionRegisterError> {
@@ -507,6 +534,8 @@ fn make_api(
507534
api.register(handler23)?;
508535
api.register(handler24)?;
509536
api.register(handler25)?;
537+
api.register(handler26)?;
538+
api.register(handler27)?;
510539
Ok(api)
511540
}
512541

dropshot/tests/test_openapi_fuller.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,32 @@
208208
}
209209
}
210210
},
211+
"/first_thing": {
212+
"get": {
213+
"tags": [
214+
"it"
215+
],
216+
"operationId": "vzeroupper",
217+
"responses": {
218+
"201": {
219+
"description": "successful creation",
220+
"content": {
221+
"application/json": {
222+
"schema": {
223+
"$ref": "#/components/schemas/Response"
224+
}
225+
}
226+
}
227+
},
228+
"4XX": {
229+
"$ref": "#/components/responses/Error"
230+
},
231+
"5XX": {
232+
"$ref": "#/components/responses/Error"
233+
}
234+
}
235+
}
236+
},
211237
"/impairment": {
212238
"get": {
213239
"tags": [
@@ -277,6 +303,25 @@
277303
}
278304
}
279305
},
306+
"/other_thing": {
307+
"get": {
308+
"tags": [
309+
"it"
310+
],
311+
"operationId": "vzerolower",
312+
"responses": {
313+
"default": {
314+
"description": "",
315+
"content": {
316+
"*/*": {
317+
"schema": {}
318+
}
319+
}
320+
}
321+
},
322+
"x-dropshot-websocket": {}
323+
}
324+
},
280325
"/playing/a/bit/nicer": {
281326
"get": {
282327
"tags": [

dropshot_endpoint/src/api_trait.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,4 +1759,41 @@ mod tests {
17591759
&prettyplease::unparse(&parse_quote! { #item }),
17601760
);
17611761
}
1762+
1763+
#[test]
1764+
fn test_api_trait_operation_id() {
1765+
let (item, errors) = do_trait(
1766+
quote! {},
1767+
quote! {
1768+
trait MyTrait {
1769+
type Context;
1770+
1771+
#[endpoint {
1772+
operation_id = "vzerolower",
1773+
method = GET,
1774+
path = "/xyz"
1775+
}]
1776+
async fn handler_xyz(
1777+
rqctx: RequestContext<Self::Context>,
1778+
) -> Result<HttpResponseOk<()>, HttpError>;
1779+
1780+
#[channel {
1781+
protocol = WEBSOCKETS,
1782+
path = "/ws",
1783+
operation_id = "vzeroupper",
1784+
}]
1785+
async fn handler_ws(
1786+
rqctx: RequestContext<Self::Context>,
1787+
upgraded: WebsocketConnection,
1788+
) -> WebsocketChannelResult;
1789+
}
1790+
},
1791+
);
1792+
1793+
assert!(errors.is_empty());
1794+
assert_contents(
1795+
"tests/output/api_trait_operation_id.rs",
1796+
&prettyplease::unparse(&parse_quote! { #item }),
1797+
);
1798+
}
17621799
}

dropshot_endpoint/src/channel.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,4 +613,29 @@ mod tests {
613613
&prettyplease::unparse(&parse_quote! { #item }),
614614
);
615615
}
616+
617+
#[test]
618+
fn test_channel_operation_id() {
619+
let (item, errors) = do_channel(
620+
quote! {
621+
protocol = WEBSOCKETS,
622+
path = "/my/ws/channel",
623+
operation_id = "vzeroupper"
624+
},
625+
quote! {
626+
async fn handler_xyz(
627+
_rqctx: RequestContext<()>,
628+
_ws: WebsocketConnection,
629+
) -> Result<HttpResponseOk<()>, HttpError> {
630+
Ok(())
631+
}
632+
},
633+
);
634+
635+
assert!(errors.is_empty());
636+
assert_contents(
637+
"tests/output/channel_operation_id.rs",
638+
&prettyplease::unparse(&parse_quote! { #item }),
639+
);
640+
}
616641
}

dropshot_endpoint/src/endpoint.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,4 +920,28 @@ mod tests {
920920
Some("endpoint `handler_xyz` must have at least one RequestContext argument".to_string())
921921
);
922922
}
923+
924+
#[test]
925+
fn test_operation_id() {
926+
let (item, errors) = do_endpoint(
927+
quote! {
928+
method = GET,
929+
path = "/a/b/c",
930+
operation_id = "vzeroupper"
931+
},
932+
quote! {
933+
pub async fn handler_xyz(
934+
_rqctx: RequestContext<()>,
935+
) -> Result<HttpResponseOk<()>, HttpError> {
936+
Ok(())
937+
}
938+
},
939+
);
940+
941+
assert!(errors.is_empty());
942+
assert_contents(
943+
"tests/output/endpoint_operation_id.rs",
944+
&prettyplease::unparse(&parse_quote! { #item }),
945+
);
946+
}
923947
}

dropshot_endpoint/src/metadata.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ impl MethodType {
4141

4242
#[derive(Deserialize, Debug)]
4343
pub(crate) struct EndpointMetadata {
44+
#[serde(default)]
45+
pub(crate) operation_id: Option<String>,
4446
pub(crate) method: MethodType,
4547
pub(crate) path: String,
4648
#[serde(default)]
@@ -59,7 +61,7 @@ impl EndpointMetadata {
5961
get_crate(self._dropshot_crate.as_deref())
6062
}
6163

62-
/// Validates metadata, returning an `EndpointMetadata` if valid.
64+
/// Validates metadata, returning a `ValidatedEndpointMetadata` if valid.
6365
///
6466
/// Note: the only reason we pass in attr here is to provide a span for
6567
/// error reporting. As of Rust 1.76, just passing in `attr.span()` produces
@@ -74,6 +76,7 @@ impl EndpointMetadata {
7476
let errors = errors.new();
7577

7678
let EndpointMetadata {
79+
operation_id,
7780
method,
7881
path,
7982
tags,
@@ -130,6 +133,7 @@ impl EndpointMetadata {
130133
None
131134
} else if let Some(content_type) = content_type {
132135
Some(ValidatedEndpointMetadata {
136+
operation_id,
133137
method,
134138
path,
135139
tags,
@@ -145,6 +149,7 @@ impl EndpointMetadata {
145149

146150
/// A validated form of endpoint metadata.
147151
pub(crate) struct ValidatedEndpointMetadata {
152+
operation_id: Option<String>,
148153
method: MethodType,
149154
path: String,
150155
tags: Vec<String>,
@@ -163,6 +168,8 @@ impl ValidatedEndpointMetadata {
163168
) -> TokenStream {
164169
let path = &self.path;
165170
let content_type = self.content_type;
171+
let operation_id =
172+
self.operation_id.as_deref().unwrap_or(endpoint_name);
166173
let method_ident = format_ident!("{}", self.method.as_str());
167174

168175
let summary = doc.summary.as_ref().map(|summary| {
@@ -192,7 +199,7 @@ impl ValidatedEndpointMetadata {
192199
ApiEndpointKind::Regular(endpoint_fn) => {
193200
quote_spanned! {endpoint_fn.span()=>
194201
#dropshot::ApiEndpoint::new(
195-
#endpoint_name.to_string(),
202+
#operation_id.to_string(),
196203
#endpoint_fn,
197204
#dropshot::Method::#method_ident,
198205
#content_type,
@@ -212,7 +219,7 @@ impl ValidatedEndpointMetadata {
212219
// Seems pretty unobjectionable.
213220
quote_spanned! {attr.pound_token.span()=>
214221
#dropshot::ApiEndpoint::new_for_types::<(#(#extractor_types,)*), #ret_ty>(
215-
#endpoint_name.to_string(),
222+
#operation_id.to_string(),
216223
#dropshot::Method::#method_ident,
217224
#content_type,
218225
#path,
@@ -241,6 +248,8 @@ pub(crate) enum ChannelProtocol {
241248
#[derive(Deserialize, Debug)]
242249
pub(crate) struct ChannelMetadata {
243250
pub(crate) protocol: ChannelProtocol,
251+
#[serde(default)]
252+
pub(crate) operation_id: Option<String>,
244253
pub(crate) path: String,
245254
#[serde(default)]
246255
pub(crate) tags: Vec<String>,
@@ -273,6 +282,7 @@ impl ChannelMetadata {
273282

274283
let ChannelMetadata {
275284
protocol: ChannelProtocol::WEBSOCKETS,
285+
operation_id,
276286
path,
277287
tags,
278288
unpublished,
@@ -307,6 +317,7 @@ impl ChannelMetadata {
307317
// Validating channel metadata also validates the corresponding
308318
// endpoint metadata.
309319
let inner = ValidatedEndpointMetadata {
320+
operation_id,
310321
method: MethodType::GET,
311322
path,
312323
tags,

0 commit comments

Comments
 (0)