Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<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;
}

Expand Down
99 changes: 17 additions & 82 deletions modules/openapi-generator/src/main/resources/rust/model.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down Expand Up @@ -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}}<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}}
{{{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<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}}
}
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 --}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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");
}
}

This file was deleted.

Loading