Skip to content

Commit 3e93ad3

Browse files
Merge pull request #12 from gpu-cli/fix/issue-10-const-only-property
fix(generator): treat string `const` as single-value enum (closes #10)
2 parents b5d74d0 + 1576c95 commit 3e93ad3

10 files changed

Lines changed: 197 additions & 14 deletions

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "openapi-to-rust"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
edition = "2024"
55
rust-version = "1.85.0"
66
authors = ["James Lal"]

src/openapi.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -344,19 +344,37 @@ impl SchemaDetails {
344344
}
345345

346346
/// Check if this is a string enum
347+
///
348+
/// A standalone string `const` (no `enum` array) is treated as a
349+
/// degenerate single-value enum so the generator emits a tightly-typed
350+
/// single-variant enum instead of a bare `String`. See issue #10.
347351
pub fn is_string_enum(&self) -> bool {
348-
self.enum_values.is_some()
352+
self.enum_values.is_some() || self.const_string_value().is_some()
349353
}
350354

351-
/// Get enum values as strings if this is a string enum
355+
/// Get enum values as strings if this is a string enum.
356+
///
357+
/// Falls back to `[const_value]` when `enum` is absent but `const` is a
358+
/// string, so a property like `{ "type": "string", "const": "X" }`
359+
/// produces a single-variant enum.
352360
pub fn string_enum_values(&self) -> Option<Vec<String>> {
353-
self.enum_values.as_ref().map(|values| {
354-
values
355-
.iter()
356-
.filter_map(|v| v.as_str())
357-
.map(|s| s.to_string())
358-
.collect()
359-
})
361+
if let Some(values) = self.enum_values.as_ref() {
362+
return Some(
363+
values
364+
.iter()
365+
.filter_map(|v| v.as_str())
366+
.map(|s| s.to_string())
367+
.collect(),
368+
);
369+
}
370+
self.const_string_value().map(|s| vec![s])
371+
}
372+
373+
fn const_string_value(&self) -> Option<String> {
374+
self.const_value
375+
.as_ref()
376+
.and_then(|v| v.as_str())
377+
.map(|s| s.to_string())
360378
}
361379

362380
/// Check if a field is required

src/snapshots/openapi_to_rust__test_helpers__array_union_items.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,19 @@ pub enum ToolsRequestToolsItemUnion {
2727
pub struct TextTool {
2828
pub name: String,
2929
}
30+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
31+
pub enum TextToolType {
32+
#[default]
33+
#[serde(rename = "text")]
34+
Text,
35+
}
3036
#[derive(Debug, Clone, Deserialize, Serialize)]
3137
pub struct CodeTool {
3238
pub language: String,
3339
}
40+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
41+
pub enum CodeToolType {
42+
#[default]
43+
#[serde(rename = "code")]
44+
Code,
45+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
source: src/test_helpers.rs
3+
expression: "&generated_code"
4+
---
5+
//! Generated types from OpenAPI specification
6+
//!
7+
//! This file contains all the generated types for the API.
8+
//! Do not edit manually - regenerate using the appropriate script.
9+
#![allow(clippy::large_enum_variant)]
10+
#![allow(clippy::format_in_format_args)]
11+
#![allow(clippy::let_unit_value)]
12+
#![allow(unreachable_patterns)]
13+
use serde::{Deserialize, Serialize};
14+
#[derive(Debug, Clone, Deserialize, Serialize)]
15+
pub struct ConstModifier {
16+
#[serde(rename = "someConstant", skip_serializing_if = "Option::is_none")]
17+
pub some_constant: Option<ConstModifierSomeConstant>,
18+
}
19+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
20+
pub enum ConstModifierSomeConstant {
21+
#[default]
22+
#[serde(rename = "TheOnlyValidValue")]
23+
TheOnlyValidValue,
24+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: src/test_helpers.rs
3+
expression: "&generated_code"
4+
---
5+
//! Generated types from OpenAPI specification
6+
//!
7+
//! This file contains all the generated types for the API.
8+
//! Do not edit manually - regenerate using the appropriate script.
9+
#![allow(clippy::large_enum_variant)]
10+
#![allow(clippy::format_in_format_args)]
11+
#![allow(clippy::let_unit_value)]
12+
#![allow(unreachable_patterns)]
13+
use serde::{Deserialize, Serialize};
14+
#[derive(Debug, Clone, Deserialize, Serialize)]
15+
pub struct RequiredConst {
16+
pub kind: RequiredConstKind,
17+
}
18+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
19+
pub enum RequiredConstKind {
20+
#[default]
21+
#[serde(rename = "fixed")]
22+
Fixed,
23+
}

src/snapshots/openapi_to_rust__test_helpers__debug_test.snap

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,11 @@ expression: "&generated_code"
1313
use serde::{Deserialize, Serialize};
1414
#[derive(Debug, Clone, Deserialize, Serialize)]
1515
pub struct TypedEvent {
16-
pub r#type: String,
16+
pub r#type: TypedEventType,
17+
}
18+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
19+
pub enum TypedEventType {
20+
#[default]
21+
#[serde(rename = "event.created")]
22+
EventCreated,
1723
}

src/snapshots/openapi_to_rust__test_helpers__oneof_in_property.snap

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,24 @@ use serde::{Deserialize, Serialize};
1414
#[derive(Debug, Clone, Deserialize, Serialize)]
1515
pub struct ImageBlock {
1616
pub source: ImageBlockSource,
17-
pub r#type: String,
17+
pub r#type: ImageBlockType,
1818
}
1919
#[derive(Debug, Clone, Deserialize, Serialize)]
2020
pub struct URLImageSource {
2121
pub url: String,
2222
}
23+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
24+
pub enum URLImageSourceType {
25+
#[default]
26+
#[serde(rename = "url")]
27+
Url,
28+
}
29+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
30+
pub enum ImageBlockType {
31+
#[default]
32+
#[serde(rename = "image")]
33+
Image,
34+
}
2335
#[derive(Debug, Clone, Deserialize, Serialize)]
2436
#[serde(tag = "type")]
2537
pub enum ImageBlockSource {
@@ -32,3 +44,9 @@ pub enum ImageBlockSource {
3244
pub struct Base64ImageSource {
3345
pub data: String,
3446
}
47+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
48+
pub enum Base64ImageSourceType {
49+
#[default]
50+
#[serde(rename = "base64")]
51+
Base64,
52+
}

src/snapshots/openapi_to_rust__test_helpers__type_property_only_test.snap

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,11 @@ expression: "&generated_code"
1313
use serde::{Deserialize, Serialize};
1414
#[derive(Debug, Clone, Deserialize, Serialize)]
1515
pub struct TypedEvent {
16-
pub r#type: String,
16+
pub r#type: TypedEventType,
17+
}
18+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
19+
pub enum TypedEventType {
20+
#[default]
21+
#[serde(rename = "event.created")]
22+
EventCreated,
1723
}

tests/const_only_property_test.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Regression test for https://github.com/gpu-cli/openapi-to-rust/issues/10
2+
//!
3+
//! A property with a string `const` and no `enum` array should generate a
4+
//! single-variant enum, not a bare `Option<String>`.
5+
6+
#[cfg(test)]
7+
mod tests {
8+
use openapi_to_rust::test_helpers::*;
9+
use serde_json::json;
10+
11+
#[test]
12+
fn const_only_string_property_generates_single_variant_enum() {
13+
let spec = minimal_spec(json!({
14+
"ConstModifier": {
15+
"type": "object",
16+
"properties": {
17+
"someConstant": {
18+
"type": "string",
19+
"const": "TheOnlyValidValue"
20+
}
21+
}
22+
}
23+
}));
24+
25+
let result = test_generation("const_only_property", spec).expect("Generation failed");
26+
27+
assert!(
28+
result.contains("pub enum ConstModifierSomeConstant"),
29+
"expected ConstModifierSomeConstant enum to be generated; got:\n{result}"
30+
);
31+
assert!(
32+
result.contains("#[serde(rename = \"TheOnlyValidValue\")]"),
33+
"expected serde rename for the const value; got:\n{result}"
34+
);
35+
assert!(
36+
result.contains("TheOnlyValidValue,"),
37+
"expected TheOnlyValidValue variant; got:\n{result}"
38+
);
39+
40+
assert!(
41+
result.contains("pub some_constant: Option<ConstModifierSomeConstant>"),
42+
"field should reference the generated enum, not String; got:\n{result}"
43+
);
44+
assert!(
45+
!result.contains("pub some_constant: Option<String>"),
46+
"field should NOT be Option<String>; got:\n{result}"
47+
);
48+
}
49+
50+
#[test]
51+
fn required_const_only_property_is_not_optional() {
52+
let spec = minimal_spec(json!({
53+
"RequiredConst": {
54+
"type": "object",
55+
"properties": {
56+
"kind": {
57+
"type": "string",
58+
"const": "fixed"
59+
}
60+
},
61+
"required": ["kind"]
62+
}
63+
}));
64+
65+
let result = test_generation("const_only_required", spec).expect("Generation failed");
66+
67+
assert!(
68+
result.contains("pub enum RequiredConstKind"),
69+
"expected RequiredConstKind enum; got:\n{result}"
70+
);
71+
assert!(
72+
result.contains("pub kind: RequiredConstKind"),
73+
"required const field should be non-optional; got:\n{result}"
74+
);
75+
}
76+
}

0 commit comments

Comments
 (0)