From f3e7f3aaf758e0be7f3d29b43d475cf2288b5192 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Sun, 28 Sep 2025 12:08:22 +0800 Subject: [PATCH] Revert "fix(rust): Add anyOf support to Rust client generator (#21896)" This reverts commit ee40887d47f6d7a16318772f49c63b8eb0553488. --- .../codegen/languages/RustClientCodegen.java | 63 ------------ .../src/main/resources/rust/model.mustache | 99 ++++--------------- .../codegen/rust/RustClientCodegenTest.java | 33 ------- .../resources/3_0/rust/rust-anyof-test.yaml | 42 -------- 4 files changed, 17 insertions(+), 220 deletions(-) delete mode 100644 modules/openapi-generator/src/test/resources/3_0/rust/rust-anyof-test.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java index fc2aa74e9216..b1727a6f3d65 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java @@ -307,69 +307,6 @@ public CodegenModel fromModel(String name, Schema model) { 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 newAnyOfs = mdl.getComposedSchemas().getAnyOf().stream() - .map(CodegenProperty::clone) - .collect(Collectors.toList()); - List 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 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> 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 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; } diff --git a/modules/openapi-generator/src/main/resources/rust/model.mustache b/modules/openapi-generator/src/main/resources/rust/model.mustache index dac0172ea93e..a4970abf4299 100644 --- a/modules/openapi-generator/src/main/resources/rust/model.mustache +++ b/modules/openapi-generator/src/main/resources/rust/model.mustache @@ -121,40 +121,8 @@ impl Default for {{classname}} { {{!-- for non-enum schemas --}} {{^isEnum}} {{^discriminator}} -{{#composedSchemas}} -{{#oneOf}} -{{#-first}} -{{! Model with composedSchemas.oneOf - generate enum}} -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum {{classname}} { -{{/-first}} -{{/oneOf}} -{{/composedSchemas}} -{{#composedSchemas}} -{{#oneOf}} - {{#description}} - /// {{{.}}} - {{/description}} - {{{name}}}({{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{/isModel}}{{{dataType}}}{{#isModel}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}), -{{/oneOf}} -{{/composedSchemas}} -{{#composedSchemas}} -{{#oneOf}} -{{#-last}} -} - -impl Default for {{classname}} { - fn default() -> Self { - {{#oneOf}}{{#-first}}Self::{{{name}}}(Default::default()){{/-first}}{{/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)] +{{/vendorExtensions.x-rust-has-byte-array}}{{#oneOf.isEmpty}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct {{{classname}}} { {{#vars}} {{#description}} @@ -204,62 +172,29 @@ impl {{{classname}}} { } } } -{{/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}} +{{/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}} { +{{#composedSchemas.oneOf}} {{#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}}")]{{/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{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isModel}}{{! - ### Non-Enums End - }}{{/isEnum}}{{! - ### Option End (and trailing comma) - }}{{#isNullable}}>{{/isNullable}}{{^required}}>{{/required}}, -{{/vars}} + {{{name}}}({{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{/isModel}}{{{dataType}}}{{#isModel}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}), +{{/composedSchemas.oneOf}} } -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{{/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}} - } +impl Default for {{classname}} { + fn default() -> Self { + {{#composedSchemas.oneOf}}{{#-first}}Self::{{{name}}}(Default::default()){{/-first}}{{/composedSchemas.oneOf}} } } -{{/composedSchemas}} +{{/oneOf.isEmpty}} {{/discriminator}} {{/isEnum}} {{!-- for properties that are of enum type --}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java index 05527e99247e..c092ad3d7aeb 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java @@ -271,37 +271,4 @@ public void testMultipleArrayTypesEnum() throws IOException { 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 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"); - } } diff --git a/modules/openapi-generator/src/test/resources/3_0/rust/rust-anyof-test.yaml b/modules/openapi-generator/src/test/resources/3_0/rust/rust-anyof-test.yaml deleted file mode 100644 index 2be206184426..000000000000 --- a/modules/openapi-generator/src/test/resources/3_0/rust/rust-anyof-test.yaml +++ /dev/null @@ -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 \ No newline at end of file