diff --git a/Cargo.lock b/Cargo.lock index 53e10dd..19e9c18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -936,7 +936,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openapi-to-rust" -version = "0.2.1" +version = "0.3.0" dependencies = [ "clap", "heck", diff --git a/Cargo.toml b/Cargo.toml index 2ef3501..1340740 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openapi-to-rust" -version = "0.2.1" +version = "0.3.0" edition = "2024" rust-version = "1.85.0" authors = ["James Lal"] diff --git a/src/openapi.rs b/src/openapi.rs index 5db8e7e..22b8813 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -344,19 +344,37 @@ impl SchemaDetails { } /// Check if this is a string enum + /// + /// A standalone string `const` (no `enum` array) is treated as a + /// degenerate single-value enum so the generator emits a tightly-typed + /// single-variant enum instead of a bare `String`. See issue #10. pub fn is_string_enum(&self) -> bool { - self.enum_values.is_some() + self.enum_values.is_some() || self.const_string_value().is_some() } - /// Get enum values as strings if this is a string enum + /// Get enum values as strings if this is a string enum. + /// + /// Falls back to `[const_value]` when `enum` is absent but `const` is a + /// string, so a property like `{ "type": "string", "const": "X" }` + /// produces a single-variant enum. pub fn string_enum_values(&self) -> Option> { - self.enum_values.as_ref().map(|values| { - values - .iter() - .filter_map(|v| v.as_str()) - .map(|s| s.to_string()) - .collect() - }) + if let Some(values) = self.enum_values.as_ref() { + return Some( + values + .iter() + .filter_map(|v| v.as_str()) + .map(|s| s.to_string()) + .collect(), + ); + } + self.const_string_value().map(|s| vec![s]) + } + + fn const_string_value(&self) -> Option { + self.const_value + .as_ref() + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) } /// Check if a field is required diff --git a/src/snapshots/openapi_to_rust__test_helpers__array_union_items.snap b/src/snapshots/openapi_to_rust__test_helpers__array_union_items.snap index a0fa051..0e9a79d 100644 --- a/src/snapshots/openapi_to_rust__test_helpers__array_union_items.snap +++ b/src/snapshots/openapi_to_rust__test_helpers__array_union_items.snap @@ -27,7 +27,19 @@ pub enum ToolsRequestToolsItemUnion { pub struct TextTool { pub name: String, } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum TextToolType { + #[default] + #[serde(rename = "text")] + Text, +} #[derive(Debug, Clone, Deserialize, Serialize)] pub struct CodeTool { pub language: String, } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum CodeToolType { + #[default] + #[serde(rename = "code")] + Code, +} diff --git a/src/snapshots/openapi_to_rust__test_helpers__const_only_property.snap b/src/snapshots/openapi_to_rust__test_helpers__const_only_property.snap new file mode 100644 index 0000000..c2e159e --- /dev/null +++ b/src/snapshots/openapi_to_rust__test_helpers__const_only_property.snap @@ -0,0 +1,24 @@ +--- +source: src/test_helpers.rs +expression: "&generated_code" +--- +//! Generated types from OpenAPI specification +//! +//! This file contains all the generated types for the API. +//! Do not edit manually - regenerate using the appropriate script. +#![allow(clippy::large_enum_variant)] +#![allow(clippy::format_in_format_args)] +#![allow(clippy::let_unit_value)] +#![allow(unreachable_patterns)] +use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ConstModifier { + #[serde(rename = "someConstant", skip_serializing_if = "Option::is_none")] + pub some_constant: Option, +} +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum ConstModifierSomeConstant { + #[default] + #[serde(rename = "TheOnlyValidValue")] + TheOnlyValidValue, +} diff --git a/src/snapshots/openapi_to_rust__test_helpers__const_only_required.snap b/src/snapshots/openapi_to_rust__test_helpers__const_only_required.snap new file mode 100644 index 0000000..510bea1 --- /dev/null +++ b/src/snapshots/openapi_to_rust__test_helpers__const_only_required.snap @@ -0,0 +1,23 @@ +--- +source: src/test_helpers.rs +expression: "&generated_code" +--- +//! Generated types from OpenAPI specification +//! +//! This file contains all the generated types for the API. +//! Do not edit manually - regenerate using the appropriate script. +#![allow(clippy::large_enum_variant)] +#![allow(clippy::format_in_format_args)] +#![allow(clippy::let_unit_value)] +#![allow(unreachable_patterns)] +use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RequiredConst { + pub kind: RequiredConstKind, +} +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum RequiredConstKind { + #[default] + #[serde(rename = "fixed")] + Fixed, +} diff --git a/src/snapshots/openapi_to_rust__test_helpers__debug_test.snap b/src/snapshots/openapi_to_rust__test_helpers__debug_test.snap index f071326..c478327 100644 --- a/src/snapshots/openapi_to_rust__test_helpers__debug_test.snap +++ b/src/snapshots/openapi_to_rust__test_helpers__debug_test.snap @@ -13,5 +13,11 @@ expression: "&generated_code" use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TypedEvent { - pub r#type: String, + pub r#type: TypedEventType, +} +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum TypedEventType { + #[default] + #[serde(rename = "event.created")] + EventCreated, } diff --git a/src/snapshots/openapi_to_rust__test_helpers__oneof_in_property.snap b/src/snapshots/openapi_to_rust__test_helpers__oneof_in_property.snap index f579682..030a656 100644 --- a/src/snapshots/openapi_to_rust__test_helpers__oneof_in_property.snap +++ b/src/snapshots/openapi_to_rust__test_helpers__oneof_in_property.snap @@ -14,12 +14,24 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ImageBlock { pub source: ImageBlockSource, - pub r#type: String, + pub r#type: ImageBlockType, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct URLImageSource { pub url: String, } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum URLImageSourceType { + #[default] + #[serde(rename = "url")] + Url, +} +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum ImageBlockType { + #[default] + #[serde(rename = "image")] + Image, +} #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type")] pub enum ImageBlockSource { @@ -32,3 +44,9 @@ pub enum ImageBlockSource { pub struct Base64ImageSource { pub data: String, } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum Base64ImageSourceType { + #[default] + #[serde(rename = "base64")] + Base64, +} diff --git a/src/snapshots/openapi_to_rust__test_helpers__type_property_only_test.snap b/src/snapshots/openapi_to_rust__test_helpers__type_property_only_test.snap index f071326..c478327 100644 --- a/src/snapshots/openapi_to_rust__test_helpers__type_property_only_test.snap +++ b/src/snapshots/openapi_to_rust__test_helpers__type_property_only_test.snap @@ -13,5 +13,11 @@ expression: "&generated_code" use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TypedEvent { - pub r#type: String, + pub r#type: TypedEventType, +} +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum TypedEventType { + #[default] + #[serde(rename = "event.created")] + EventCreated, } diff --git a/tests/const_only_property_test.rs b/tests/const_only_property_test.rs new file mode 100644 index 0000000..9ef285e --- /dev/null +++ b/tests/const_only_property_test.rs @@ -0,0 +1,76 @@ +//! Regression test for https://github.com/gpu-cli/openapi-to-rust/issues/10 +//! +//! A property with a string `const` and no `enum` array should generate a +//! single-variant enum, not a bare `Option`. + +#[cfg(test)] +mod tests { + use openapi_to_rust::test_helpers::*; + use serde_json::json; + + #[test] + fn const_only_string_property_generates_single_variant_enum() { + let spec = minimal_spec(json!({ + "ConstModifier": { + "type": "object", + "properties": { + "someConstant": { + "type": "string", + "const": "TheOnlyValidValue" + } + } + } + })); + + let result = test_generation("const_only_property", spec).expect("Generation failed"); + + assert!( + result.contains("pub enum ConstModifierSomeConstant"), + "expected ConstModifierSomeConstant enum to be generated; got:\n{result}" + ); + assert!( + result.contains("#[serde(rename = \"TheOnlyValidValue\")]"), + "expected serde rename for the const value; got:\n{result}" + ); + assert!( + result.contains("TheOnlyValidValue,"), + "expected TheOnlyValidValue variant; got:\n{result}" + ); + + assert!( + result.contains("pub some_constant: Option"), + "field should reference the generated enum, not String; got:\n{result}" + ); + assert!( + !result.contains("pub some_constant: Option"), + "field should NOT be Option; got:\n{result}" + ); + } + + #[test] + fn required_const_only_property_is_not_optional() { + let spec = minimal_spec(json!({ + "RequiredConst": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "fixed" + } + }, + "required": ["kind"] + } + })); + + let result = test_generation("const_only_required", spec).expect("Generation failed"); + + assert!( + result.contains("pub enum RequiredConstKind"), + "expected RequiredConstKind enum; got:\n{result}" + ); + assert!( + result.contains("pub kind: RequiredConstKind"), + "required const field should be non-optional; got:\n{result}" + ); + } +}