mirror of
https://github.com/OpenAPITools/openapi-generator.git
synced 2025-10-13 16:03:43 +00:00
This reverts commit ee40887d47f6d7a16318772f49c63b8eb0553488.
This commit is contained in:
parent
44a3be170f
commit
30096d63b7
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 --}}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user