Skip to content

Commit 07944e8

Browse files
committed
refactor(rust): implement true anyOf support with OR semantics
- anyOf now generates struct with optional fields instead of enum - Each anyOf option becomes an optional field prefixed with 'as_' - Added validate_any_of() method to ensure at least one field is set - Maintains semantic difference between anyOf (OR) and oneOf (XOR) - oneOf continues to use untagged enums (exactly one match) - anyOf uses struct with optional fields (one or more matches possible) BREAKING CHANGE: anyOf schemas now generate different code structure. Previously generated enums, now generates structs with optional fields to properly support OpenAPI anyOf semantics where multiple schemas can validate simultaneously.
1 parent ee40887 commit 07944e8

File tree

3 files changed

+56
-15
lines changed

3 files changed

+56
-15
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,8 @@ public CodegenModel fromModel(String name, Schema model) {
307307
mdl.getComposedSchemas().setOneOf(newOneOfs);
308308
}
309309

310-
// Handle anyOf schemas similarly to oneOf
311-
// This is pragmatic since Rust's untagged enum will deserialize to the first matching variant
310+
// Handle anyOf schemas with true OR semantics (one or more schemas must match)
311+
// Unlike oneOf (XOR - exactly one), anyOf allows multiple schemas to validate
312312
if (mdl.getComposedSchemas() != null && mdl.getComposedSchemas().getAnyOf() != null
313313
&& !mdl.getComposedSchemas().getAnyOf().isEmpty()) {
314314

@@ -366,8 +366,9 @@ public CodegenModel fromModel(String name, Schema model) {
366366
}
367367
}
368368

369-
// Set anyOf as oneOf for template processing since we want the same output
370-
mdl.getComposedSchemas().setOneOf(newAnyOfs);
369+
// Keep anyOf separate from oneOf - they have different semantics
370+
// anyOf will be processed with a different template structure
371+
// that allows multiple schemas to match (OR logic)
371372
}
372373

373374
return mdl;

modules/openapi-generator/src/main/resources/rust/model.mustache

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,46 @@ impl Default for {{classname}} {
152152
{{/-last}}
153153
{{/oneOf}}
154154
{{^oneOf}}
155-
{{! composedSchemas exists but no oneOf - generate normal struct}}
155+
{{#composedSchemas.anyOf}}
156+
{{#-first}}
157+
{{! Model with composedSchemas.anyOf - generate struct with optional fields for true OR semantics}}
158+
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
159+
pub struct {{classname}} {
160+
{{/-first}}
161+
{{/composedSchemas.anyOf}}
162+
{{#composedSchemas.anyOf}}
163+
{{#description}}
164+
/// {{{.}}} (anyOf option)
165+
{{/description}}
166+
#[serde(skip_serializing_if = "Option::is_none", rename = "as_{{{baseName}}}")]
167+
pub as_{{{name}}}: Option<{{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{/isModel}}{{{dataType}}}{{#isModel}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}>,
168+
{{/composedSchemas.anyOf}}
169+
{{#composedSchemas.anyOf}}
170+
{{#-last}}
171+
}
172+
173+
impl {{classname}} {
174+
/// Creates a new {{classname}} with all fields set to None
175+
pub fn new() -> Self {
176+
Default::default()
177+
}
178+
179+
/// Validates that at least one anyOf field is set (OR semantics)
180+
pub fn validate_any_of(&self) -> Result<(), String> {
181+
{{#composedSchemas.anyOf}}
182+
{{#-first}}
183+
if {{/-first}}self.as_{{{name}}}.is_none(){{^-last}}
184+
&& {{/-last}}{{#-last}} {
185+
return Err("At least one anyOf field must be set".to_string());
186+
}{{/-last}}
187+
{{/composedSchemas.anyOf}}
188+
Ok(())
189+
}
190+
}
191+
{{/-last}}
192+
{{/composedSchemas.anyOf}}
193+
{{^composedSchemas.anyOf}}
194+
{{! composedSchemas exists but no oneOf or anyOf - generate normal struct}}
156195
{{#vendorExtensions.x-rust-has-byte-array}}#[serde_as]
157196
{{/vendorExtensions.x-rust-has-byte-array}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
158197
pub struct {{{classname}}} {
@@ -204,6 +243,7 @@ impl {{{classname}}} {
204243
}
205244
}
206245
}
246+
{{/composedSchemas.anyOf}}
207247
{{/oneOf}}
208248
{{/composedSchemas}}
209249
{{^composedSchemas}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,21 +287,21 @@ public void testAnyOfSupport() throws IOException {
287287
Path modelIdentifierPath = Path.of(target.toString(), "/src/models/model_identifier.rs");
288288
TestUtils.assertFileExists(modelIdentifierPath);
289289

290-
// Should generate an untagged enum
291-
TestUtils.assertFileContains(modelIdentifierPath, "#[serde(untagged)]");
292-
TestUtils.assertFileContains(modelIdentifierPath, "pub enum ModelIdentifier");
290+
// Should generate a struct with optional fields for anyOf (true OR semantics)
291+
TestUtils.assertFileContains(modelIdentifierPath, "pub struct ModelIdentifier");
292+
TestUtils.assertFileContains(modelIdentifierPath, "Option<String>");
293293

294-
// Should have String variant (for anyOf with string types)
295-
TestUtils.assertFileContains(modelIdentifierPath, "String(String)");
294+
// Should have validation method for anyOf
295+
TestUtils.assertFileContains(modelIdentifierPath, "pub fn validate_any_of(&self)");
296296

297-
// Should NOT generate an empty struct
298-
TestUtils.assertFileNotContains(modelIdentifierPath, "pub struct ModelIdentifier {");
299-
TestUtils.assertFileNotContains(modelIdentifierPath, "pub fn new()");
297+
// Should NOT generate an enum (that would be oneOf behavior)
298+
TestUtils.assertFileNotContains(modelIdentifierPath, "pub enum ModelIdentifier");
299+
TestUtils.assertFileNotContains(modelIdentifierPath, "#[serde(untagged)]");
300300

301301
// Test AnotherAnyOfTest with mixed types
302302
Path anotherTestPath = Path.of(target.toString(), "/src/models/another_any_of_test.rs");
303303
TestUtils.assertFileExists(anotherTestPath);
304-
TestUtils.assertFileContains(anotherTestPath, "#[serde(untagged)]");
305-
TestUtils.assertFileContains(anotherTestPath, "pub enum AnotherAnyOfTest");
304+
TestUtils.assertFileContains(anotherTestPath, "pub struct AnotherAnyOfTest");
305+
TestUtils.assertFileContains(anotherTestPath, "pub fn validate_any_of(&self)");
306306
}
307307
}

0 commit comments

Comments
 (0)