Skip to content

Commit edecb25

Browse files
committed
add an operationId strategy to reject or ignore missing operations
1 parent 772bd3d commit edecb25

File tree

12 files changed

+574
-79
lines changed

12 files changed

+574
-79
lines changed

progenitor-impl/src/cli.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct CliOperation {
2424
impl Generator {
2525
/// Generate a `clap`-based CLI.
2626
pub fn cli(&mut self, spec: &OpenAPI, crate_name: &str) -> Result<TokenStream> {
27-
validate_openapi(spec)?;
27+
validate_openapi(spec, self.settings.operation_id_strategy)?;
2828

2929
// Convert our components dictionary to schemars
3030
let schemas = spec.components.iter().flat_map(|components| {
@@ -53,6 +53,7 @@ impl Generator {
5353

5454
let methods = raw_methods
5555
.iter()
56+
.filter_map(|method| method.as_ref())
5657
.map(|method| self.cli_method(method))
5758
.collect::<Vec<_>>();
5859

@@ -62,15 +63,18 @@ impl Generator {
6263

6364
let cli_fns = raw_methods
6465
.iter()
66+
.filter_map(|method| method.as_ref())
6567
.map(|method| format_ident!("cli_{}", sanitize(&method.operation_id, Case::Snake)))
6668
.collect::<Vec<_>>();
6769
let execute_fns = raw_methods
6870
.iter()
71+
.filter_map(|method| method.as_ref())
6972
.map(|method| format_ident!("execute_{}", sanitize(&method.operation_id, Case::Snake)))
7073
.collect::<Vec<_>>();
7174

7275
let cli_variants = raw_methods
7376
.iter()
77+
.filter_map(|method| method.as_ref())
7478
.map(|method| format_ident!("{}", sanitize(&method.operation_id, Case::Pascal)))
7579
.collect::<Vec<_>>();
7680

progenitor-impl/src/httpmock.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ impl Generator {
3030
/// the SDK. This can include `::` and instances of `-` in the crate name
3131
/// should be converted to `_`.
3232
pub fn httpmock(&mut self, spec: &OpenAPI, crate_path: &str) -> Result<TokenStream> {
33-
validate_openapi(spec)?;
33+
validate_openapi(spec, self.settings.operation_id_strategy)?;
3434

3535
// Convert our components dictionary to schemars
3636
let schemas = spec.components.iter().flat_map(|components| {
@@ -59,11 +59,13 @@ impl Generator {
5959

6060
let methods = raw_methods
6161
.iter()
62+
.filter_map(|method| method.as_ref())
6263
.map(|method| self.httpmock_method(method))
6364
.collect::<Vec<_>>();
6465

6566
let op = raw_methods
6667
.iter()
68+
.filter_map(|method| method.as_ref())
6769
.map(|method| format_ident!("{}", &method.operation_id))
6870
.collect::<Vec<_>>();
6971
let when = methods.iter().map(|op| &op.when).collect::<Vec<_>>();

progenitor-impl/src/lib.rs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ pub struct GenerationSettings {
6767
post_hook_async: Option<TokenStream>,
6868
extra_derives: Vec<String>,
6969

70+
operation_id_strategy: OperationIdStrategy,
71+
7072
map_type: Option<String>,
7173
unknown_crates: UnknownPolicy,
7274
crates: BTreeMap<String, CrateSpec>,
@@ -112,6 +114,23 @@ impl Default for TagStyle {
112114
}
113115
}
114116

117+
/// Style for handing operations that do not have an operation ID.
118+
#[derive(Copy, Clone)]
119+
pub enum OperationIdStrategy {
120+
/// The default behaviour. Reject when any operation on the resulting
121+
/// client does not have an operation ID.
122+
RejectMissing,
123+
/// Omit any operation on the resulting client that does not have an
124+
/// operation ID.
125+
OmitMissing,
126+
}
127+
128+
impl Default for OperationIdStrategy {
129+
fn default() -> Self {
130+
Self::RejectMissing
131+
}
132+
}
133+
115134
impl GenerationSettings {
116135
/// Create new generator settings with default values.
117136
pub fn new() -> Self {
@@ -241,6 +260,16 @@ impl GenerationSettings {
241260
self.map_type = Some(map_type.to_string());
242261
self
243262
}
263+
264+
/// Set the strategy to be used when encountering operations that do not
265+
/// have an operation ID.
266+
pub fn with_operation_id_strategy(
267+
&mut self,
268+
operation_id_strategy: OperationIdStrategy,
269+
) -> &mut Self {
270+
self.operation_id_strategy = operation_id_strategy;
271+
self
272+
}
244273
}
245274

246275
impl Default for Generator {
@@ -306,7 +335,7 @@ impl Generator {
306335

307336
/// Emit a [TokenStream] containing the generated client code.
308337
pub fn generate_tokens(&mut self, spec: &OpenAPI) -> Result<TokenStream> {
309-
validate_openapi(spec)?;
338+
validate_openapi(spec, self.settings.operation_id_strategy)?;
310339

311340
// Convert our components dictionary to schemars
312341
let schemas = spec.components.iter().flat_map(|components| {
@@ -328,8 +357,9 @@ impl Generator {
328357
(path.as_str(), method, operation, &item.parameters)
329358
})
330359
})
331-
.map(|(path, method, operation, path_parameters)| {
360+
.filter_map(|(path, method, operation, path_parameters)| {
332361
self.process_operation(operation, &spec.components, path, method, path_parameters)
362+
.transpose()
333363
})
334364
.collect::<Result<Vec<_>>>()?;
335365

@@ -674,7 +704,7 @@ fn validate_openapi_spec_version(spec_version: &str) -> Result<()> {
674704
}
675705

676706
/// Do some very basic checks of the OpenAPI documents.
677-
pub fn validate_openapi(spec: &OpenAPI) -> Result<()> {
707+
pub fn validate_openapi(spec: &OpenAPI, operation_id_strategy: OperationIdStrategy) -> Result<()> {
678708
validate_openapi_spec_version(spec.openapi.as_str())?;
679709

680710
let mut opids = HashSet::new();
@@ -695,10 +725,15 @@ pub fn validate_openapi(spec: &OpenAPI) -> Result<()> {
695725
)));
696726
}
697727
} else {
698-
return Err(Error::UnexpectedFormat(format!(
699-
"path {} is missing operation ID",
700-
p.0,
701-
)));
728+
match operation_id_strategy {
729+
OperationIdStrategy::RejectMissing => {
730+
return Err(Error::UnexpectedFormat(format!(
731+
"path {} is missing operation ID",
732+
p.0,
733+
)));
734+
}
735+
OperationIdStrategy::OmitMissing => {}
736+
}
702737
}
703738
Ok(())
704739
})

progenitor-impl/src/method.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,10 @@ impl Generator {
290290
path: &str,
291291
method: &str,
292292
path_parameters: &[ReferenceOr<Parameter>],
293-
) -> Result<OperationMethod> {
294-
let operation_id = operation.operation_id.as_ref().unwrap();
293+
) -> Result<Option<OperationMethod>> {
294+
let Some(operation_id) = operation.operation_id.as_ref() else {
295+
return Ok(None);
296+
};
295297

296298
let mut combined_path_parameters = parameter_map(path_parameters, components)?;
297299
for operation_param in items(&operation.parameters, components) {
@@ -534,7 +536,7 @@ impl Generator {
534536
)));
535537
}
536538

537-
Ok(OperationMethod {
539+
Ok(Some(OperationMethod {
538540
operation_id: sanitize(operation_id, Case::Snake),
539541
tags: operation.tags.clone(),
540542
method: HttpMethod::from_str(method)?,
@@ -545,7 +547,7 @@ impl Generator {
545547
responses,
546548
dropshot_paginated,
547549
dropshot_websocket,
548-
})
550+
}))
549551
}
550552

551553
pub(crate) fn positional_method(
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#[allow(unused_imports)]
2+
use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt};
3+
#[allow(unused_imports)]
4+
pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue};
5+
/// Types used as operation parameters and responses.
6+
#[allow(clippy::all)]
7+
pub mod types {
8+
/// Error types.
9+
pub mod error {
10+
/// Error from a `TryFrom` or `FromStr` implementation.
11+
pub struct ConversionError(::std::borrow::Cow<'static, str>);
12+
impl ::std::error::Error for ConversionError {}
13+
impl ::std::fmt::Display for ConversionError {
14+
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
15+
::std::fmt::Display::fmt(&self.0, f)
16+
}
17+
}
18+
19+
impl ::std::fmt::Debug for ConversionError {
20+
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
21+
::std::fmt::Debug::fmt(&self.0, f)
22+
}
23+
}
24+
25+
impl From<&'static str> for ConversionError {
26+
fn from(value: &'static str) -> Self {
27+
Self(value.into())
28+
}
29+
}
30+
31+
impl From<String> for ConversionError {
32+
fn from(value: String) -> Self {
33+
Self(value.into())
34+
}
35+
}
36+
}
37+
}
38+
39+
#[derive(Clone, Debug)]
40+
///Client for Parameter name collision test
41+
///
42+
///Minimal API for testing collision between parameter names and generated code
43+
///
44+
///Version: v1
45+
pub struct Client {
46+
pub(crate) baseurl: String,
47+
pub(crate) client: reqwest::Client,
48+
}
49+
50+
impl Client {
51+
/// Create a new client.
52+
///
53+
/// `baseurl` is the base URL provided to the internal
54+
/// `reqwest::Client`, and should include a scheme and hostname,
55+
/// as well as port and a path stem if applicable.
56+
pub fn new(baseurl: &str) -> Self {
57+
#[cfg(not(target_arch = "wasm32"))]
58+
let client = {
59+
let dur = std::time::Duration::from_secs(15);
60+
reqwest::ClientBuilder::new()
61+
.connect_timeout(dur)
62+
.timeout(dur)
63+
};
64+
#[cfg(target_arch = "wasm32")]
65+
let client = reqwest::ClientBuilder::new();
66+
Self::new_with_client(baseurl, client.build().unwrap())
67+
}
68+
69+
/// Construct a new client with an existing `reqwest::Client`,
70+
/// allowing more control over its configuration.
71+
///
72+
/// `baseurl` is the base URL provided to the internal
73+
/// `reqwest::Client`, and should include a scheme and hostname,
74+
/// as well as port and a path stem if applicable.
75+
pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self {
76+
Self {
77+
baseurl: baseurl.to_string(),
78+
client,
79+
}
80+
}
81+
}
82+
83+
impl ClientInfo<()> for Client {
84+
fn api_version() -> &'static str {
85+
"v1"
86+
}
87+
88+
fn baseurl(&self) -> &str {
89+
self.baseurl.as_str()
90+
}
91+
92+
fn client(&self) -> &reqwest::Client {
93+
&self.client
94+
}
95+
96+
fn inner(&self) -> &() {
97+
&()
98+
}
99+
}
100+
101+
impl ClientHooks<()> for &Client {}
102+
impl Client {}
103+
/// Types for composing operation parameters.
104+
#[allow(clippy::all)]
105+
pub mod builder {
106+
use super::types;
107+
#[allow(unused_imports)]
108+
use super::{
109+
encode_path, ByteStream, ClientHooks, ClientInfo, Error, OperationInfo, RequestBuilderExt,
110+
ResponseValue,
111+
};
112+
}
113+
114+
/// Items consumers will typically use such as the Client.
115+
pub mod prelude {
116+
pub use self::super::Client;
117+
}

0 commit comments

Comments
 (0)