Skip to content

Commit 3d65786

Browse files
[Rust-Axum][Breaking Change] Improve the oneOf model generator (OpenAPITools#20336)
* Improve the implementation of oneOf * Fixed 2.0 schemas; possible freeze present * Fix generate-samples.sh freezing * Fix validation of primitive types * Move oneOf handling to its own method * Fix formatting and add comments * Remove allOf based discriminator handling * Implement a test for v3 oneOf * Implement oneOf tests for rust axum * Fix circle CI * Fix pom path, ensure cargo is present * Implement untagged test * Add final and fix double underscore typo
1 parent eb96380 commit 3d65786

File tree

54 files changed

+2909
-30
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2909
-30
lines changed

CI/circle_parallel.sh

+10
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,22 @@ if [ "$NODE_INDEX" = "1" ]; then
1515

1616
sudo apt-get -y install cpanminus
1717

18+
# install rust
19+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
20+
source "$HOME/.cargo/env"
21+
22+
echo "Testing perl"
1823
(cd samples/client/petstore/perl && /bin/bash ./test.bash)
24+
25+
echo "Testing ruby"
1926
(cd samples/client/petstore/ruby && mvn integration-test)
2027
(cd samples/client/petstore/ruby-faraday && mvn integration-test)
2128
(cd samples/client/petstore/ruby-httpx && mvn integration-test)
2229
(cd samples/client/petstore/ruby-autoload && mvn integration-test)
2330

31+
echo "Testing rust"
32+
(cd samples/server/petstore/rust-axum && mvn integration-test)
33+
2434
elif [ "$NODE_INDEX" = "2" ]; then
2535
echo "Running node $NODE_INDEX to test Go"
2636
# install haskell
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
generatorName: rust-axum
2+
outputDir: samples/server/petstore/rust-axum/output/rust-axum-oneof
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust-axum/rust-axum-oneof.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/rust-axum
5+
generateAliasAsModel: true
6+
additionalProperties:
7+
hideGenerationTimestamp: "true"
8+
packageName: rust-axum-oneof
9+
globalProperties:
10+
skipFormModel: "false"
11+
enablePostProcessFile: true

bin/utils/test_file_list.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,8 @@
4949
sha256: 67a9e63e13ebddac21cb236aa015edce30f5d3bd8d6adcf50044cad00d48c45e
5050
- filename: "samples/openapi3/client/petstore/java/jersey2-java8/src/test/java/org/openapitools/client/model/ZebraTest.java"
5151
sha256: 15eeb6d8a9a79d0f1930b861540d9c5780d6c49ea4fdb68269ac3e7ec481e142
52+
# rust axum test files
53+
- filename: "samples/server/petstore/rust-axum/output/rust-axum-oneof/src/tests.rs"
54+
sha256: 3d4198174018cc7fd9d4bcffd950609a5bd306cf03b2fa780516f4e22a566e8c
55+
- filename: "samples/server/petstore/rust-axum/output/openapi-v3/src/tests.rs"
56+
sha256: 356ac684b1fce91b153c63caefc1fe7472ea600ac436a19631e16bc00e986c50

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1083,8 +1083,8 @@ public String toString() {
10831083
sb.append(", items='").append(items).append('\'');
10841084
sb.append(", additionalProperties='").append(additionalProperties).append('\'');
10851085
sb.append(", isModel='").append(isModel).append('\'');
1086-
sb.append(", isNull='").append(isNull);
1087-
sb.append(", hasValidation='").append(hasValidation);
1086+
sb.append(", isNull='").append(isNull).append('\'');
1087+
sb.append(", hasValidation='").append(hasValidation).append('\'');
10881088
sb.append(", getAdditionalPropertiesIsAnyType=").append(getAdditionalPropertiesIsAnyType());
10891089
sb.append(", getHasDiscriminatorWithNonEmptyMapping=").append(hasDiscriminatorWithNonEmptyMapping);
10901090
sb.append(", getIsAnyType=").append(getIsAnyType());

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

+100-2
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,9 @@ public RustAxumServerCodegen() {
236236
supportingFiles.add(new SupportingFile("header.mustache", "src", "header.rs"));
237237
supportingFiles.add(new SupportingFile("server-mod.mustache", "src/server", "mod.rs"));
238238
supportingFiles.add(new SupportingFile("apis-mod.mustache", apiPackage().replace('.', File.separatorChar), "mod.rs"));
239-
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")
240-
.doNotOverwrite());
239+
// The file gets overwritten regardless
240+
supportingFiles.add(new SupportingFile("tests.mustache", "src", "tests.rs").doNotOverwrite());
241+
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md").doNotOverwrite());
241242
}
242243

243244
@Override
@@ -594,8 +595,105 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
594595
return op;
595596
}
596597

598+
private void postProcessOneOfModels(List<ModelMap> allModels) {
599+
final HashMap<String, List<String>> oneOfMapDiscriminator = new HashMap<>();
600+
601+
for (ModelMap mo : allModels) {
602+
final CodegenModel cm = mo.getModel();
603+
604+
final CodegenComposedSchemas cs = cm.getComposedSchemas();
605+
if (cs != null) {
606+
final List<CodegenProperty> csOneOf = cs.getOneOf();
607+
608+
if (csOneOf != null) {
609+
for (CodegenProperty model : csOneOf) {
610+
// Generate a valid name for the enum variant.
611+
// Mainly needed for primitive types.
612+
String[] modelParts = model.dataType.replace("<", "Of").replace(">", "").split("::");
613+
model.datatypeWithEnum = camelize(modelParts[modelParts.length - 1]);
614+
615+
// Primitive type is not properly set, this overrides it to guarantee adequate model generation.
616+
if (!model.getDataType().matches(String.format(Locale.ROOT, ".*::%s", model.getDatatypeWithEnum()))) {
617+
model.isPrimitiveType = true;
618+
}
619+
}
620+
621+
cs.setOneOf(csOneOf);
622+
cm.setComposedSchemas(cs);
623+
}
624+
}
625+
626+
if (cm.discriminator != null) {
627+
for (String model : cm.oneOf) {
628+
List<String> discriminators = oneOfMapDiscriminator.getOrDefault(model, new ArrayList<>());
629+
discriminators.add(cm.discriminator.getPropertyName());
630+
oneOfMapDiscriminator.put(model, discriminators);
631+
}
632+
}
633+
}
634+
635+
for (ModelMap mo : allModels) {
636+
final CodegenModel cm = mo.getModel();
637+
638+
for (CodegenProperty var : cm.vars) {
639+
var.isDiscriminator = false;
640+
}
641+
642+
final List<String> discriminatorsForModel = oneOfMapDiscriminator.get(cm.getSchemaName());
643+
644+
if (discriminatorsForModel != null) {
645+
for (String discriminator : discriminatorsForModel) {
646+
boolean hasDiscriminatorDefined = false;
647+
648+
for (CodegenProperty var : cm.vars) {
649+
if (var.baseName.equals(discriminator)) {
650+
var.isDiscriminator = true;
651+
hasDiscriminatorDefined = true;
652+
break;
653+
}
654+
}
655+
656+
// If the discriminator field is not a defined attribute in the variant structure, create it.
657+
if (!hasDiscriminatorDefined) {
658+
CodegenProperty property = new CodegenProperty();
659+
660+
// Static attributes
661+
// Only strings are supported by serde for tag field types, so it's the only one we'll deal with
662+
property.openApiType = "string";
663+
property.complexType = "string";
664+
property.dataType = "String";
665+
property.datatypeWithEnum = "String";
666+
property.baseType = "string";
667+
property.required = true;
668+
property.isPrimitiveType = true;
669+
property.isString = true;
670+
property.isDiscriminator = true;
671+
672+
// Attributes based on the discriminator value
673+
property.baseName = discriminator;
674+
property.name = discriminator;
675+
property.nameInCamelCase = camelize(discriminator);
676+
property.nameInPascalCase = property.nameInCamelCase.substring(0, 1).toUpperCase(Locale.ROOT) + property.nameInCamelCase.substring(1);
677+
property.nameInSnakeCase = underscore(discriminator).toUpperCase(Locale.ROOT);
678+
property.getter = String.format(Locale.ROOT, "get%s", property.nameInPascalCase);
679+
property.setter = String.format(Locale.ROOT, "set%s", property.nameInPascalCase);
680+
property.defaultValueWithParam = String.format(Locale.ROOT, " = data.%s;", property.name);
681+
682+
// Attributes based on the model name
683+
property.defaultValue = String.format(Locale.ROOT, "r#\"%s\"#.to_string()", cm.getSchemaName());
684+
property.jsonSchema = String.format(Locale.ROOT, "{ \"default\":\"%s\"; \"type\":\"string\" }", cm.getSchemaName());
685+
686+
cm.vars.add(property);
687+
}
688+
}
689+
}
690+
}
691+
}
692+
597693
@Override
598694
public OperationsMap postProcessOperationsWithModels(final OperationsMap operationsMap, List<ModelMap> allModels) {
695+
postProcessOneOfModels(allModels);
696+
599697
final OperationMap operations = operationsMap.getOperations();
600698
operations.put("classnamePascalCase", camelize(operations.getClassname()));
601699

modules/openapi-generator/src/main/resources/rust-axum/lib.mustache

+3
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ pub mod apis;
2828

2929
#[cfg(feature = "server")]
3030
pub(crate) mod header;
31+
32+
#[cfg(test)]
33+
mod tests;

modules/openapi-generator/src/main/resources/rust-axum/models.mustache

+81-14
Original file line numberDiff line numberDiff line change
@@ -573,21 +573,70 @@ impl PartialEq for {{{classname}}} {
573573
self.0.get() == other.0.get()
574574
}
575575
}
576+
576577
{{/anyOf.size}}
577578
{{#oneOf.size}}
578-
/// One of:
579-
{{#oneOf}}
580-
/// - {{{.}}}
581-
{{/oneOf}}
582-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
583-
pub struct {{{classname}}}(Box<serde_json::value::RawValue>);
579+
{{#discriminator}}
580+
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
581+
#[serde(tag = "{{{propertyBaseName}}}")]
582+
{{/discriminator}}
583+
{{^discriminator}}
584+
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
585+
#[serde(untagged)]
586+
{{/discriminator}}
587+
#[allow(non_camel_case_types)]
588+
pub enum {{{classname}}} {
589+
{{#composedSchemas}}
590+
{{#oneOf}}
591+
{{{datatypeWithEnum}}}(Box<{{{dataType}}}>),
592+
{{/oneOf}}
593+
{{/composedSchemas}}
594+
}
584595
585596
impl validator::Validate for {{{classname}}}
586597
{
587598
fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> {
588-
std::result::Result::Ok(())
599+
match self {
600+
{{#composedSchemas}}
601+
{{#oneOf}}
602+
{{#isPrimitiveType}}
603+
Self::{{{datatypeWithEnum}}}(_) => std::result::Result::Ok(()),
604+
{{/isPrimitiveType}}
605+
{{^isPrimitiveType}}
606+
Self::{{{datatypeWithEnum}}}(x) => x.validate(),
607+
{{/isPrimitiveType}}
608+
{{/oneOf}}
609+
{{/composedSchemas}}
610+
}
611+
}
612+
}
613+
614+
{{#discriminator}}
615+
impl serde::Serialize for {{{classname}}} {
616+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
617+
where S: serde::Serializer {
618+
match self {
619+
{{#composedSchemas}}
620+
{{#oneOf}}
621+
Self::{{{datatypeWithEnum}}}(x) => x.serialize(serializer),
622+
{{/oneOf}}
623+
{{/composedSchemas}}
624+
}
589625
}
590626
}
627+
{{/discriminator}}
628+
629+
630+
631+
{{#composedSchemas}}
632+
{{#oneOf}}
633+
impl From<{{{dataType}}}> for {{{classname}}} {
634+
fn from(value: {{{dataType}}}) -> Self {
635+
Self::{{{datatypeWithEnum}}}(Box::new(value))
636+
}
637+
}
638+
{{/oneOf}}
639+
{{/composedSchemas}}
591640
592641
/// Converts Query Parameters representation (style=form, explode=false) to a {{{classname}}} value
593642
/// as specified in https://swagger.io/docs/specification/serialization/
@@ -600,11 +649,6 @@ impl std::str::FromStr for {{{classname}}} {
600649
}
601650
}
602651
603-
impl PartialEq for {{{classname}}} {
604-
fn eq(&self, other: &Self) -> bool {
605-
self.0.get() == other.0.get()
606-
}
607-
}
608652
{{/oneOf.size}}
609653
{{^anyOf.size}}
610654
{{^oneOf.size}}
@@ -613,11 +657,15 @@ impl PartialEq for {{{classname}}} {
613657
pub struct {{{classname}}} {
614658
{{#vars}}
615659
{{#description}}
616-
/// {{{.}}}
660+
/// {{{.}}}
617661
{{/description}}
618662
{{#isEnum}}
619-
/// Note: inline enums are not fully supported by openapi-generator
663+
/// Note: inline enums are not fully supported by openapi-generator
620664
{{/isEnum}}
665+
{{#isDiscriminator}}
666+
#[serde(default = "{{{classname}}}::_name_for_{{{name}}}")]
667+
#[serde(serialize_with = "{{{classname}}}::_serialize_{{{name}}}")]
668+
{{/isDiscriminator}}
621669
#[serde(rename = "{{{baseName}}}")]
622670
{{#hasValidation}}
623671
#[validate(
@@ -685,6 +733,25 @@ pub struct {{{classname}}} {
685733
{{/vars}}
686734
}
687735
736+
737+
{{#vars}}
738+
{{#isDiscriminator}}
739+
impl {{{classname}}} {
740+
fn _name_for_{{{name}}}() -> String {
741+
String::from("{{{classname}}}")
742+
}
743+
744+
fn _serialize_{{{name}}}<S>(_: &String, s: S) -> Result<S::Ok, S::Error>
745+
where
746+
S: serde::Serializer,
747+
{
748+
s.serialize_str(&Self::_name_for_{{{name}}}())
749+
}
750+
}
751+
{{/isDiscriminator}}
752+
{{/vars}}
753+
754+
688755
{{#vars}}
689756
{{#hasValidation}}
690757
{{#pattern}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#[test]
2+
fn std_test() {
3+
assert!(true);
4+
}
5+
6+
#[tokio::test]
7+
async fn tokio_test() {
8+
assert!(true);
9+
}

0 commit comments

Comments
 (0)