Revert "fix(rust): Add anyOf support to Rust client generator (#21896)" (#22038)

This reverts commit ee40887d47f6d7a16318772f49c63b8eb0553488.
This commit is contained in:
William Cheng 2025-09-28 12:23:50 +08:00 committed by GitHub
parent 44a3be170f
commit 30096d63b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 264 deletions

View File

@ -307,69 +307,6 @@ public class RustClientCodegen extends AbstractRustCodegen implements CodegenCon
mdl.getComposedSchemas().setOneOf(newOneOfs);
}
// Handle anyOf schemas similarly to oneOf
// This is pragmatic since Rust's untagged enum will deserialize to the first matching variant
if (mdl.getComposedSchemas() != null && mdl.getComposedSchemas().getAnyOf() != null
&& !mdl.getComposedSchemas().getAnyOf().isEmpty()) {
List<CodegenProperty> newAnyOfs = mdl.getComposedSchemas().getAnyOf().stream()
.map(CodegenProperty::clone)
.collect(Collectors.toList());
List<Schema> schemas = ModelUtils.getInterfaces(model);
if (newAnyOfs.size() != schemas.size()) {
// For safety reasons, this should never happen unless there is an error in the code
throw new RuntimeException("anyOf size does not match the model");
}
Map<String, String> refsMapping = Optional.ofNullable(model.getDiscriminator())
.map(Discriminator::getMapping).orElse(Collections.emptyMap());
// Reverse mapped references to use as baseName for anyOf, but different keys may point to the same $ref.
// Thus, we group them by the value
Map<String, List<String>> mappedNamesByRef = refsMapping.entrySet().stream()
.collect(Collectors.groupingBy(Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
));
for (int i = 0; i < newAnyOfs.size(); i++) {
CodegenProperty anyOf = newAnyOfs.get(i);
Schema schema = schemas.get(i);
if (mappedNamesByRef.containsKey(schema.get$ref())) {
// prefer mapped names if present
// remove mapping not in order not to reuse for the next occurrence of the ref
List<String> names = mappedNamesByRef.get(schema.get$ref());
String mappedName = names.remove(0);
anyOf.setBaseName(mappedName);
anyOf.setName(toModelName(mappedName));
} else if (!org.apache.commons.lang3.StringUtils.isEmpty(schema.get$ref())) {
// use $ref if it's reference
String refName = ModelUtils.getSimpleRef(schema.get$ref());
if (refName != null) {
String modelName = toModelName(refName);
anyOf.setName(modelName);
anyOf.setBaseName(refName);
}
} else if (anyOf.isArray) {
// If the type is an array, extend the name with the inner type to prevent name collisions
// in case multiple arrays with different types are defined. If the user has manually specified
// a name, use that name instead.
String collectionWithTypeName = toModelName(schema.getType()) + anyOf.containerTypeMapped + anyOf.items.dataType;
String anyOfName = Optional.ofNullable(schema.getTitle()).orElse(collectionWithTypeName);
anyOf.setName(anyOfName);
}
else {
// In-placed type (primitive), because there is no mapping or ref for it.
// use camelized `title` if present, otherwise use `type`
String anyOfName = Optional.ofNullable(schema.getTitle()).orElseGet(schema::getType);
anyOf.setName(toModelName(anyOfName));
}
}
// Set anyOf as oneOf for template processing since we want the same output
mdl.getComposedSchemas().setOneOf(newAnyOfs);
}
return mdl;
}

View File

@ -121,145 +121,80 @@ impl Default for {{classname}} {
{{!-- for non-enum schemas --}}
{{^isEnum}}
{{^discriminator}}
{{#composedSchemas}}
{{#oneOf}}
{{#-first}}
{{! Model with composedSchemas.oneOf - generate enum}}
{{#vendorExtensions.x-rust-has-byte-array}}#[serde_as]
{{/vendorExtensions.x-rust-has-byte-array}}{{#oneOf.isEmpty}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct {{{classname}}} {
{{#vars}}
{{#description}}
/// {{{.}}}
{{/description}}
{{#isByteArray}}
{{#vendorExtensions.isMandatory}}#[serde_as(as = "serde_with::base64::Base64")]{{/vendorExtensions.isMandatory}}{{^vendorExtensions.isMandatory}}#[serde_as(as = "{{^serdeAsDoubleOption}}Option{{/serdeAsDoubleOption}}{{#serdeAsDoubleOption}}super::DoubleOption{{/serdeAsDoubleOption}}<serde_with::base64::Base64>")]{{/vendorExtensions.isMandatory}}
{{/isByteArray}}
#[serde(rename = "{{{baseName}}}"{{^required}}{{#isNullable}}, default{{^isByteArray}}, with = "::serde_with::rust::double_option"{{/isByteArray}}{{/isNullable}}{{/required}}{{^required}}, skip_serializing_if = "Option::is_none"{{/required}}{{#required}}{{#isNullable}}, deserialize_with = "Option::deserialize"{{/isNullable}}{{/required}})]
pub {{{name}}}: {{!
### Option Start
}}{{#isNullable}}Option<{{/isNullable}}{{^required}}Option<{{/required}}{{!
### Enums
}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{!
### Non-Enums Start
}}{{^isEnum}}{{!
### Models
}}{{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{{dataType}}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}{{!
### Primative datatypes
}}{{^isModel}}{{#isByteArray}}Vec<u8>{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isModel}}{{!
### Non-Enums End
}}{{/isEnum}}{{!
### Option End (and trailing comma)
}}{{#isNullable}}>{{/isNullable}}{{^required}}>{{/required}},
{{/vars}}
}
impl {{{classname}}} {
{{#description}}
/// {{{.}}}
{{/description}}
pub fn new({{#requiredVars}}{{{name}}}: {{!
### Option Start
}}{{#isNullable}}Option<{{/isNullable}}{{!
### Enums
}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{!
### Non-Enums
}}{{^isEnum}}{{#isByteArray}}Vec<u8>{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isEnum}}{{!
### Option End
}}{{#isNullable}}>{{/isNullable}}{{!
### Comma for next arguement
}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} {
{{{classname}}} {
{{#vars}}
{{{name}}}{{^required}}: None{{/required}}{{#required}}{{#isModel}}{{^avoidBoxedModels}}: {{^isNullable}}Box::new({{{name}}}){{/isNullable}}{{#isNullable}}if let Some(x) = {{{name}}} {Some(Box::new(x))} else {None}{{/isNullable}}{{/avoidBoxedModels}}{{/isModel}}{{/required}},
{{/vars}}
}
}
}
{{/oneOf.isEmpty}}
{{^oneOf.isEmpty}}
{{! TODO: add other vars that are not part of the oneOf}}
{{#description}}
/// {{{.}}}
{{/description}}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum {{classname}} {
{{/-first}}
{{/oneOf}}
{{/composedSchemas}}
{{#composedSchemas}}
{{#oneOf}}
{{#composedSchemas.oneOf}}
{{#description}}
/// {{{.}}}
{{/description}}
{{{name}}}({{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{/isModel}}{{{dataType}}}{{#isModel}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}),
{{/oneOf}}
{{/composedSchemas}}
{{#composedSchemas}}
{{#oneOf}}
{{#-last}}
{{/composedSchemas.oneOf}}
}
impl Default for {{classname}} {
fn default() -> Self {
{{#oneOf}}{{#-first}}Self::{{{name}}}(Default::default()){{/-first}}{{/oneOf}}
{{#composedSchemas.oneOf}}{{#-first}}Self::{{{name}}}(Default::default()){{/-first}}{{/composedSchemas.oneOf}}
}
}
{{/-last}}
{{/oneOf}}
{{^oneOf}}
{{! composedSchemas exists but no oneOf - generate normal struct}}
{{#vendorExtensions.x-rust-has-byte-array}}#[serde_as]
{{/vendorExtensions.x-rust-has-byte-array}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct {{{classname}}} {
{{#vars}}
{{#description}}
/// {{{.}}}
{{/description}}
{{#isByteArray}}
{{#vendorExtensions.isMandatory}}#[serde_as(as = "serde_with::base64::Base64")]{{/vendorExtensions.isMandatory}}{{^vendorExtensions.isMandatory}}#[serde_as(as = "{{^serdeAsDoubleOption}}Option{{/serdeAsDoubleOption}}{{#serdeAsDoubleOption}}super::DoubleOption{{/serdeAsDoubleOption}}<serde_with::base64::Base64>")]{{/vendorExtensions.isMandatory}}
{{/isByteArray}}
#[serde(rename = "{{{baseName}}}"{{^required}}{{#isNullable}}, default{{^isByteArray}}, with = "::serde_with::rust::double_option"{{/isByteArray}}{{/isNullable}}{{/required}}{{^required}}, skip_serializing_if = "Option::is_none"{{/required}}{{#required}}{{#isNullable}}, deserialize_with = "Option::deserialize"{{/isNullable}}{{/required}})]
pub {{{name}}}: {{!
### Option Start
}}{{#isNullable}}Option<{{/isNullable}}{{^required}}Option<{{/required}}{{!
### Enums
}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{!
### Non-Enums Start
}}{{^isEnum}}{{!
### Models
}}{{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{{dataType}}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}{{!
### Primative datatypes
}}{{^isModel}}{{#isByteArray}}Vec<u8>{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isModel}}{{!
### Non-Enums End
}}{{/isEnum}}{{!
### Option End (and trailing comma)
}}{{#isNullable}}>{{/isNullable}}{{^required}}>{{/required}},
{{/vars}}
}
impl {{{classname}}} {
{{#description}}
/// {{{.}}}
{{/description}}
pub fn new({{#requiredVars}}{{{name}}}: {{!
### Option Start
}}{{#isNullable}}Option<{{/isNullable}}{{!
### Enums
}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{!
### Non-Enums
}}{{^isEnum}}{{#isByteArray}}Vec<u8>{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isEnum}}{{!
### Option End
}}{{#isNullable}}>{{/isNullable}}{{!
### Comma for next arguement
}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} {
{{{classname}}} {
{{#vars}}
{{{name}}}{{^required}}: None{{/required}}{{#required}}{{#isModel}}{{^avoidBoxedModels}}: {{^isNullable}}Box::new({{{name}}}){{/isNullable}}{{#isNullable}}if let Some(x) = {{{name}}} {Some(Box::new(x))} else {None}{{/isNullable}}{{/avoidBoxedModels}}{{/isModel}}{{/required}},
{{/vars}}
}
}
}
{{/oneOf}}
{{/composedSchemas}}
{{^composedSchemas}}
{{! Normal struct without composedSchemas}}
{{#vendorExtensions.x-rust-has-byte-array}}#[serde_as]
{{/vendorExtensions.x-rust-has-byte-array}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct {{{classname}}} {
{{#vars}}
{{#description}}
/// {{{.}}}
{{/description}}
{{#isByteArray}}
{{#vendorExtensions.isMandatory}}#[serde_as(as = "serde_with::base64::Base64")]{{/vendorExtensions.isMandatory}}{{^vendorExtensions.isMandatory}}#[serde_as(as = "{{^serdeAsDoubleOption}}Option{{/serdeAsDoubleOption}}{{#serdeAsDoubleOption}}super::DoubleOption{{/serdeAsDoubleOption}}<serde_with::base64::Base64>")]{{/vendorExtensions.isMandatory}}
{{/isByteArray}}
#[serde(rename = "{{{baseName}}}"{{^required}}{{#isNullable}}, default{{^isByteArray}}, with = "::serde_with::rust::double_option"{{/isByteArray}}{{/isNullable}}{{/required}}{{^required}}, skip_serializing_if = "Option::is_none"{{/required}}{{#required}}{{#isNullable}}, deserialize_with = "Option::deserialize"{{/isNullable}}{{/required}})]
pub {{{name}}}: {{!
### Option Start
}}{{#isNullable}}Option<{{/isNullable}}{{^required}}Option<{{/required}}{{!
### Enums
}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{!
### Non-Enums Start
}}{{^isEnum}}{{!
### Models
}}{{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{{dataType}}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}{{!
### Primative datatypes
}}{{^isModel}}{{#isByteArray}}Vec<u8>{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isModel}}{{!
### Non-Enums End
}}{{/isEnum}}{{!
### Option End (and trailing comma)
}}{{#isNullable}}>{{/isNullable}}{{^required}}>{{/required}},
{{/vars}}
}
impl {{{classname}}} {
{{#description}}
/// {{{.}}}
{{/description}}
pub fn new({{#requiredVars}}{{{name}}}: {{!
### Option Start
}}{{#isNullable}}Option<{{/isNullable}}{{!
### Enums
}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{!
### Non-Enums
}}{{^isEnum}}{{#isByteArray}}Vec<u8>{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isEnum}}{{!
### Option End
}}{{#isNullable}}>{{/isNullable}}{{!
### Comma for next arguement
}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} {
{{{classname}}} {
{{#vars}}
{{{name}}}{{^required}}: None{{/required}}{{#required}}{{#isModel}}{{^avoidBoxedModels}}: {{^isNullable}}Box::new({{{name}}}){{/isNullable}}{{#isNullable}}if let Some(x) = {{{name}}} {Some(Box::new(x))} else {None}{{/isNullable}}{{/avoidBoxedModels}}{{/isModel}}{{/required}},
{{/vars}}
}
}
}
{{/composedSchemas}}
{{/oneOf.isEmpty}}
{{/discriminator}}
{{/isEnum}}
{{!-- for properties that are of enum type --}}

View File

@ -271,37 +271,4 @@ public class RustClientCodegenTest {
TestUtils.assertFileExists(outputPath);
TestUtils.assertFileContains(outputPath, enumSpec);
}
@Test
public void testAnyOfSupport() throws IOException {
Path target = Files.createTempDirectory("test-anyof");
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("rust")
.setInputSpec("src/test/resources/3_0/rust/rust-anyof-test.yaml")
.setSkipOverwrite(false)
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
files.forEach(File::deleteOnExit);
// Test that ModelIdentifier generates an untagged enum, not an empty struct
Path modelIdentifierPath = Path.of(target.toString(), "/src/models/model_identifier.rs");
TestUtils.assertFileExists(modelIdentifierPath);
// Should generate an untagged enum
TestUtils.assertFileContains(modelIdentifierPath, "#[serde(untagged)]");
TestUtils.assertFileContains(modelIdentifierPath, "pub enum ModelIdentifier");
// Should have String variant (for anyOf with string types)
TestUtils.assertFileContains(modelIdentifierPath, "String(String)");
// Should NOT generate an empty struct
TestUtils.assertFileNotContains(modelIdentifierPath, "pub struct ModelIdentifier {");
TestUtils.assertFileNotContains(modelIdentifierPath, "pub fn new()");
// Test AnotherAnyOfTest with mixed types
Path anotherTestPath = Path.of(target.toString(), "/src/models/another_any_of_test.rs");
TestUtils.assertFileExists(anotherTestPath);
TestUtils.assertFileContains(anotherTestPath, "#[serde(untagged)]");
TestUtils.assertFileContains(anotherTestPath, "pub enum AnotherAnyOfTest");
}
}

View File

@ -1,42 +0,0 @@
openapi: 3.0.0
info:
title: Rust anyOf Test
version: 1.0.0
paths:
/model:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TestResponse'
components:
schemas:
TestResponse:
type: object
properties:
model:
$ref: '#/components/schemas/ModelIdentifier'
status:
type: string
ModelIdentifier:
description: Model identifier that can be a string or specific enum value
anyOf:
- type: string
description: Any model name as string
- type: string
enum:
- gpt-4
- gpt-3.5-turbo
- dall-e-3
description: Known model enum values
AnotherAnyOfTest:
description: Another test case with different types
anyOf:
- type: string
- type: integer
- type: array
items:
type: string