diff --git a/progenitor-impl/src/cli.rs b/progenitor-impl/src/cli.rs index 95dd4fe6..270c1663 100644 --- a/progenitor-impl/src/cli.rs +++ b/progenitor-impl/src/cli.rs @@ -24,7 +24,7 @@ struct CliOperation { impl Generator { /// Generate a `clap`-based CLI. pub fn cli(&mut self, spec: &OpenAPI, crate_name: &str) -> Result { - validate_openapi(spec)?; + validate_openapi(spec, self.settings.operation_id_strategy)?; // Convert our components dictionary to schemars let schemas = spec.components.iter().flat_map(|components| { @@ -53,6 +53,7 @@ impl Generator { let methods = raw_methods .iter() + .filter_map(|method| method.as_ref()) .map(|method| self.cli_method(method)) .collect::>(); @@ -62,15 +63,18 @@ impl Generator { let cli_fns = raw_methods .iter() + .filter_map(|method| method.as_ref()) .map(|method| format_ident!("cli_{}", sanitize(&method.operation_id, Case::Snake))) .collect::>(); let execute_fns = raw_methods .iter() + .filter_map(|method| method.as_ref()) .map(|method| format_ident!("execute_{}", sanitize(&method.operation_id, Case::Snake))) .collect::>(); let cli_variants = raw_methods .iter() + .filter_map(|method| method.as_ref()) .map(|method| format_ident!("{}", sanitize(&method.operation_id, Case::Pascal))) .collect::>(); diff --git a/progenitor-impl/src/httpmock.rs b/progenitor-impl/src/httpmock.rs index 1e7f9745..a615f53a 100644 --- a/progenitor-impl/src/httpmock.rs +++ b/progenitor-impl/src/httpmock.rs @@ -30,7 +30,7 @@ impl Generator { /// the SDK. This can include `::` and instances of `-` in the crate name /// should be converted to `_`. pub fn httpmock(&mut self, spec: &OpenAPI, crate_path: &str) -> Result { - validate_openapi(spec)?; + validate_openapi(spec, self.settings.operation_id_strategy)?; // Convert our components dictionary to schemars let schemas = spec.components.iter().flat_map(|components| { @@ -59,11 +59,13 @@ impl Generator { let methods = raw_methods .iter() + .filter_map(|method| method.as_ref()) .map(|method| self.httpmock_method(method)) .collect::>(); let op = raw_methods .iter() + .filter_map(|method| method.as_ref()) .map(|method| format_ident!("{}", &method.operation_id)) .collect::>(); let when = methods.iter().map(|op| &op.when).collect::>(); diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index c9563871..9512b384 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -67,6 +67,8 @@ pub struct GenerationSettings { post_hook_async: Option, extra_derives: Vec, + operation_id_strategy: OperationIdStrategy, + map_type: Option, unknown_crates: UnknownPolicy, crates: BTreeMap, @@ -112,6 +114,23 @@ impl Default for TagStyle { } } +/// Style for handing operations that do not have an operation ID. +#[derive(Copy, Clone)] +pub enum OperationIdStrategy { + /// The default behaviour. Reject when any operation on the resulting + /// client does not have an operation ID. + RejectMissing, + /// Omit any operation on the resulting client that does not have an + /// operation ID. + OmitMissing, +} + +impl Default for OperationIdStrategy { + fn default() -> Self { + Self::RejectMissing + } +} + impl GenerationSettings { /// Create new generator settings with default values. pub fn new() -> Self { @@ -241,6 +260,16 @@ impl GenerationSettings { self.map_type = Some(map_type.to_string()); self } + + /// Set the strategy to be used when encountering operations that do not + /// have an operation ID. + pub fn with_operation_id_strategy( + &mut self, + operation_id_strategy: OperationIdStrategy, + ) -> &mut Self { + self.operation_id_strategy = operation_id_strategy; + self + } } impl Default for Generator { @@ -306,7 +335,7 @@ impl Generator { /// Emit a [TokenStream] containing the generated client code. pub fn generate_tokens(&mut self, spec: &OpenAPI) -> Result { - validate_openapi(spec)?; + validate_openapi(spec, self.settings.operation_id_strategy)?; // Convert our components dictionary to schemars let schemas = spec.components.iter().flat_map(|components| { @@ -328,8 +357,9 @@ impl Generator { (path.as_str(), method, operation, &item.parameters) }) }) - .map(|(path, method, operation, path_parameters)| { + .filter_map(|(path, method, operation, path_parameters)| { self.process_operation(operation, &spec.components, path, method, path_parameters) + .transpose() }) .collect::>>()?; @@ -674,7 +704,7 @@ fn validate_openapi_spec_version(spec_version: &str) -> Result<()> { } /// Do some very basic checks of the OpenAPI documents. -pub fn validate_openapi(spec: &OpenAPI) -> Result<()> { +pub fn validate_openapi(spec: &OpenAPI, operation_id_strategy: OperationIdStrategy) -> Result<()> { validate_openapi_spec_version(spec.openapi.as_str())?; let mut opids = HashSet::new(); @@ -695,10 +725,15 @@ pub fn validate_openapi(spec: &OpenAPI) -> Result<()> { ))); } } else { - return Err(Error::UnexpectedFormat(format!( - "path {} is missing operation ID", - p.0, - ))); + match operation_id_strategy { + OperationIdStrategy::RejectMissing => { + return Err(Error::UnexpectedFormat(format!( + "path {} is missing operation ID", + p.0, + ))); + } + OperationIdStrategy::OmitMissing => {} + } } Ok(()) }) diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 8bf1d0fe..f006b8f9 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -290,8 +290,10 @@ impl Generator { path: &str, method: &str, path_parameters: &[ReferenceOr], - ) -> Result { - let operation_id = operation.operation_id.as_ref().unwrap(); + ) -> Result> { + let Some(operation_id) = operation.operation_id.as_ref() else { + return Ok(None); + }; let mut combined_path_parameters = parameter_map(path_parameters, components)?; for operation_param in items(&operation.parameters, components) { @@ -534,7 +536,7 @@ impl Generator { ))); } - Ok(OperationMethod { + Ok(Some(OperationMethod { operation_id: sanitize(operation_id, Case::Snake), tags: operation.tags.clone(), method: HttpMethod::from_str(method)?, @@ -545,7 +547,7 @@ impl Generator { responses, dropshot_paginated, dropshot_websocket, - }) + })) } pub(crate) fn positional_method( diff --git a/progenitor-impl/tests/output/src/missing_operation_id_builder.rs b/progenitor-impl/tests/output/src/missing_operation_id_builder.rs new file mode 100644 index 00000000..fdee1931 --- /dev/null +++ b/progenitor-impl/tests/output/src/missing_operation_id_builder.rs @@ -0,0 +1,117 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } +} + +#[derive(Clone, Debug)] +///Client for Parameter name collision test +/// +///Minimal API for testing collision between parameter names and generated code +/// +///Version: v1 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = std::time::Duration::from_secs(15); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "v1" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +impl Client {} +/// Types for composing operation parameters. +#[allow(clippy::all)] +pub mod builder { + use super::types; + #[allow(unused_imports)] + use super::{ + encode_path, ByteStream, ClientHooks, ClientInfo, Error, OperationInfo, RequestBuilderExt, + ResponseValue, + }; +} + +/// Items consumers will typically use such as the Client. +pub mod prelude { + pub use self::super::Client; +} diff --git a/progenitor-impl/tests/output/src/missing_operation_id_builder_tagged.rs b/progenitor-impl/tests/output/src/missing_operation_id_builder_tagged.rs new file mode 100644 index 00000000..384590f1 --- /dev/null +++ b/progenitor-impl/tests/output/src/missing_operation_id_builder_tagged.rs @@ -0,0 +1,118 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } +} + +#[derive(Clone, Debug)] +///Client for Parameter name collision test +/// +///Minimal API for testing collision between parameter names and generated code +/// +///Version: v1 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = std::time::Duration::from_secs(15); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "v1" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +/// Types for composing operation parameters. +#[allow(clippy::all)] +pub mod builder { + use super::types; + #[allow(unused_imports)] + use super::{ + encode_path, ByteStream, ClientHooks, ClientInfo, Error, OperationInfo, RequestBuilderExt, + ResponseValue, + }; +} + +/// Items consumers will typically use such as the Client and +/// extension traits. +pub mod prelude { + #[allow(unused_imports)] + pub use super::Client; +} diff --git a/progenitor-impl/tests/output/src/missing_operation_id_cli.rs b/progenitor-impl/tests/output/src/missing_operation_id_cli.rs new file mode 100644 index 00000000..fe9b0236 --- /dev/null +++ b/progenitor-impl/tests/output/src/missing_operation_id_cli.rs @@ -0,0 +1,53 @@ +use crate::missing_operation_id_builder::*; +pub struct Cli { + client: Client, + config: T, +} + +impl Cli { + pub fn new(client: Client, config: T) -> Self { + Self { client, config } + } + + pub fn get_command(cmd: CliCommand) -> ::clap::Command { + match cmd {} + } + + pub async fn execute( + &self, + cmd: CliCommand, + matches: &::clap::ArgMatches, + ) -> anyhow::Result<()> { + match cmd {} + } +} + +pub trait CliConfig { + fn success_item(&self, value: &ResponseValue) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn success_no_item(&self, value: &ResponseValue<()>); + fn error(&self, value: &Error) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_start(&self) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_item(&self, value: &T) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_end_success(&self) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_end_error(&self, value: &Error) + where + T: schemars::JsonSchema + serde::Serialize + std::fmt::Debug; +} + +#[derive(Copy, Clone, Debug)] +pub enum CliCommand {} +impl CliCommand { + pub fn iter() -> impl Iterator { + vec![].into_iter() + } +} diff --git a/progenitor-impl/tests/output/src/missing_operation_id_httpmock.rs b/progenitor-impl/tests/output/src/missing_operation_id_httpmock.rs new file mode 100644 index 00000000..1ed0eb5f --- /dev/null +++ b/progenitor-impl/tests/output/src/missing_operation_id_httpmock.rs @@ -0,0 +1,13 @@ +pub mod operations { + #![doc = r" [`When`](::httpmock::When) and [`Then`](::httpmock::Then)"] + #![doc = r" wrappers for each operation. Each can be converted to"] + #![doc = r" its inner type with a call to `into_inner()`. This can"] + #![doc = r" be used to explicitly deviate from permitted values."] + use crate::missing_operation_id_builder::*; +} + +#[doc = r" An extension trait for [`MockServer`](::httpmock::MockServer) that"] +#[doc = r" adds a method for each operation. These are the equivalent of"] +#[doc = r" type-checked [`mock()`](::httpmock::MockServer::mock) calls."] +pub trait MockServerExt {} +impl MockServerExt for ::httpmock::MockServer {} diff --git a/progenitor-impl/tests/output/src/missing_operation_id_positional.rs b/progenitor-impl/tests/output/src/missing_operation_id_positional.rs new file mode 100644 index 00000000..c69fe5b1 --- /dev/null +++ b/progenitor-impl/tests/output/src/missing_operation_id_positional.rs @@ -0,0 +1,108 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } +} + +#[derive(Clone, Debug)] +///Client for Parameter name collision test +/// +///Minimal API for testing collision between parameter names and generated code +/// +///Version: v1 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = std::time::Duration::from_secs(15); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "v1" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +#[allow(clippy::all)] +impl Client {} +/// Items consumers will typically use such as the Client. +pub mod prelude { + #[allow(unused_imports)] + pub use super::Client; +} diff --git a/progenitor-impl/tests/test_output.rs b/progenitor-impl/tests/test_output.rs index 14ddc250..e9f04fa0 100644 --- a/progenitor-impl/tests/test_output.rs +++ b/progenitor-impl/tests/test_output.rs @@ -6,7 +6,8 @@ use std::{ }; use progenitor_impl::{ - space_out_items, GenerationSettings, Generator, InterfaceStyle, TagStyle, TypeImpl, TypePatch, + space_out_items, GenerationSettings, Generator, InterfaceStyle, OperationIdStrategy, TagStyle, + TypeImpl, TypePatch, }; use openapiv3::OpenAPI; @@ -41,8 +42,12 @@ fn reformat_code(content: TokenStream) -> String { space_out_items(rustfmt_wrapper::rustfmt_config(rustfmt_config, content).unwrap()).unwrap() } -#[track_caller] fn verify_apis(openapi_file: &str) { + verify_apis_with_settings(openapi_file, &GenerationSettings::default()) +} + +#[track_caller] +fn verify_apis_with_settings(openapi_file: &str, settings: &GenerationSettings) { let mut in_path = PathBuf::from("../sample_openapi"); in_path.push(openapi_file); let openapi_stem = openapi_file.split('.').next().unwrap().replace('-', "_"); @@ -50,76 +55,84 @@ fn verify_apis(openapi_file: &str) { let spec = load_api(in_path); // Positional generation. - let mut generator = Generator::default(); - let output = generate_formatted(&mut generator, &spec); - expectorate::assert_contents( - format!("tests/output/src/{}_positional.rs", openapi_stem), - &output, - ); + { + let mut generator = Generator::new(settings); + let output = generate_formatted(&mut generator, &spec); + expectorate::assert_contents( + format!("tests/output/src/{}_positional.rs", openapi_stem), + &output, + ); + } // Builder generation with derives and patches. - let mut generator = Generator::new( - GenerationSettings::default() - .with_interface(InterfaceStyle::Builder) - .with_tag(TagStyle::Merged) - .with_derive("schemars::JsonSchema") - .with_patch("Name", TypePatch::default().with_derive("Hash")) - .with_conversion( - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::Integer.into()), - format: Some("int32".to_string()), - ..Default::default() - }, - "usize", - [TypeImpl::Display].into_iter(), - ), - ); - let output = generate_formatted(&mut generator, &spec); - expectorate::assert_contents( - format!("tests/output/src/{}_builder.rs", openapi_stem), - &output, - ); + { + let mut settings = settings.clone(); + let mut generator = Generator::new( + settings + .with_interface(InterfaceStyle::Builder) + .with_tag(TagStyle::Merged) + .with_derive("schemars::JsonSchema") + .with_patch("Name", TypePatch::default().with_derive("Hash")) + .with_conversion( + schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::Integer.into()), + format: Some("int32".to_string()), + ..Default::default() + }, + "usize", + [TypeImpl::Display].into_iter(), + ), + ); + let output = generate_formatted(&mut generator, &spec); + expectorate::assert_contents( + format!("tests/output/src/{}_builder.rs", openapi_stem), + &output, + ); + } // Builder generation with tags. - let mut generator = Generator::new( - GenerationSettings::default() - .with_interface(InterfaceStyle::Builder) - .with_tag(TagStyle::Separate), - ); - let output = generate_formatted(&mut generator, &spec); - expectorate::assert_contents( - format!("tests/output/src/{}_builder_tagged.rs", openapi_stem), - &output, - ); - - // CLI generation. - let tokens = generator - .cli(&spec, &format!("crate::{openapi_stem}_builder")) + { + let mut settings = settings.clone(); + let mut generator = Generator::new( + settings + .with_interface(InterfaceStyle::Builder) + .with_tag(TagStyle::Separate), + ); + let output = generate_formatted(&mut generator, &spec); + expectorate::assert_contents( + format!("tests/output/src/{}_builder_tagged.rs", openapi_stem), + &output, + ); + + // CLI generation. + let tokens = generator + .cli(&spec, &format!("crate::{openapi_stem}_builder")) + .unwrap(); + let output = reformat_code(tokens); + + expectorate::assert_contents(format!("tests/output/src/{}_cli.rs", openapi_stem), &output); + + // httpmock generation. + let code = generator + .httpmock(&spec, &format!("crate::{openapi_stem}_builder")) + .unwrap(); + + // TODO pending #368 + let output = rustfmt_wrapper::rustfmt_config( + rustfmt_wrapper::config::Config { + format_strings: Some(true), + ..Default::default() + }, + code, + ) .unwrap(); - let output = reformat_code(tokens); - - expectorate::assert_contents(format!("tests/output/src/{}_cli.rs", openapi_stem), &output); - // httpmock generation. - let code = generator - .httpmock(&spec, &format!("crate::{openapi_stem}_builder")) - .unwrap(); - - // TODO pending #368 - let output = rustfmt_wrapper::rustfmt_config( - rustfmt_wrapper::config::Config { - format_strings: Some(true), - ..Default::default() - }, - code, - ) - .unwrap(); - - let output = progenitor_impl::space_out_items(output).unwrap(); - expectorate::assert_contents( - format!("tests/output/src/{}_httpmock.rs", openapi_stem), - &output, - ); + let output = progenitor_impl::space_out_items(output).unwrap(); + expectorate::assert_contents( + format!("tests/output/src/{}_httpmock.rs", openapi_stem), + &output, + ); + } } #[test] @@ -162,6 +175,14 @@ fn test_cli_gen() { verify_apis("cli-gen.json"); } +#[test] +fn test_missing_operation_id() { + verify_apis_with_settings( + "missing-operation-id.json", + GenerationSettings::new().with_operation_id_strategy(OperationIdStrategy::OmitMissing), + ); +} + // TODO this file is full of inconsistencies and incorrectly specified types. // It's an interesting test to consider whether we try to do our best to // interpret the intent or just fail. diff --git a/progenitor/src/lib.rs b/progenitor/src/lib.rs index 52cd7b3b..b3789680 100644 --- a/progenitor/src/lib.rs +++ b/progenitor/src/lib.rs @@ -19,6 +19,7 @@ pub use progenitor_impl::Error; pub use progenitor_impl::GenerationSettings; pub use progenitor_impl::Generator; pub use progenitor_impl::InterfaceStyle; +pub use progenitor_impl::OperationIdStrategy; pub use progenitor_impl::TagStyle; pub use progenitor_impl::TypeImpl; pub use progenitor_impl::TypePatch; diff --git a/sample_openapi/missing-operation-id.json b/sample_openapi/missing-operation-id.json new file mode 100644 index 00000000..96e9c7cf --- /dev/null +++ b/sample_openapi/missing-operation-id.json @@ -0,0 +1,21 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Minimal API for testing missing operation id strategies", + "title": "Missing operation ids test", + "version": "v1" + }, + "paths": { + "/": { + "get": { + "description": "Ping", + "responses": { + "200": { + "description": "Pong", + "type": "string" + } + } + } + } + } +}