From 3d65786117ec3237ddbf44dde37354fa6be6dfb0 Mon Sep 17 00:00:00 2001 From: Victoria Casasampere Fernandez Date: Wed, 8 Jan 2025 10:38:23 +0100 Subject: [PATCH] [Rust-Axum][Breaking Change] Improve the `oneOf` model generator (#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 --- CI/circle_parallel.sh | 10 + bin/configs/manual/rust-axum-oneof-v3.yaml | 11 + bin/utils/test_file_list.yaml | 5 + .../openapitools/codegen/CodegenModel.java | 4 +- .../languages/RustAxumServerCodegen.java | 102 +- .../src/main/resources/rust-axum/lib.mustache | 3 + .../main/resources/rust-axum/models.mustache | 95 +- .../main/resources/rust-axum/tests.mustache | 9 + .../3_0/rust-axum/rust-axum-oneof.yaml | 86 ++ .../apikey-auths/.openapi-generator/FILES | 1 + .../rust-axum/output/apikey-auths/src/lib.rs | 3 + .../output/apikey-auths/src/tests.rs | 9 + .../multipart-v3/.openapi-generator/FILES | 1 + .../rust-axum/output/multipart-v3/src/lib.rs | 3 + .../output/multipart-v3/src/tests.rs | 9 + .../openapi-v3/.openapi-generator/FILES | 1 + .../rust-axum/output/openapi-v3/src/lib.rs | 3 + .../rust-axum/output/openapi-v3/src/models.rs | 34 +- .../rust-axum/output/openapi-v3/src/tests.rs | 41 + .../output/ops-v3/.openapi-generator/FILES | 1 + .../rust-axum/output/ops-v3/src/lib.rs | 3 + .../rust-axum/output/ops-v3/src/tests.rs | 9 + .../.openapi-generator/FILES | 1 + .../src/lib.rs | 3 + .../src/tests.rs | 9 + .../output/petstore/.openapi-generator/FILES | 1 + .../rust-axum/output/petstore/src/lib.rs | 3 + .../rust-axum/output/petstore/src/tests.rs | 9 + .../ping-bearer-auth/.openapi-generator/FILES | 1 + .../output/ping-bearer-auth/src/lib.rs | 3 + .../output/ping-bearer-auth/src/tests.rs | 9 + .../.openapi-generator/FILES | 1 + .../output/rust-axum-header-uuid/src/lib.rs | 3 + .../output/rust-axum-header-uuid/src/tests.rs | 9 + .../output/rust-axum-oneof/.gitignore | 2 + .../rust-axum-oneof/.openapi-generator-ignore | 23 + .../rust-axum-oneof/.openapi-generator/FILES | 11 + .../.openapi-generator/VERSION | 1 + .../output/rust-axum-oneof/Cargo.toml | 46 + .../output/rust-axum-oneof/README.md | 91 ++ .../rust-axum-oneof/src/apis/default.rs | 30 + .../output/rust-axum-oneof/src/apis/mod.rs | 1 + .../output/rust-axum-oneof/src/header.rs | 197 ++++ .../output/rust-axum-oneof/src/lib.rs | 31 + .../output/rust-axum-oneof/src/models.rs | 985 ++++++++++++++++++ .../output/rust-axum-oneof/src/server/mod.rs | 109 ++ .../output/rust-axum-oneof/src/tests.rs | 101 ++ .../output/rust-axum-oneof/src/types.rs | 790 ++++++++++++++ .../rust-axum-test/.openapi-generator/FILES | 1 + .../output/rust-axum-test/src/lib.rs | 3 + .../output/rust-axum-test/src/tests.rs | 9 + .../.openapi-generator/FILES | 1 + .../rust-axum-validation-test/src/lib.rs | 3 + .../rust-axum-validation-test/src/tests.rs | 9 + 54 files changed, 2909 insertions(+), 30 deletions(-) create mode 100644 bin/configs/manual/rust-axum-oneof-v3.yaml create mode 100644 modules/openapi-generator/src/main/resources/rust-axum/tests.mustache create mode 100644 modules/openapi-generator/src/test/resources/3_0/rust-axum/rust-axum-oneof.yaml create mode 100644 samples/server/petstore/rust-axum/output/apikey-auths/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/multipart-v3/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/openapi-v3/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/ops-v3/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/petstore/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/ping-bearer-auth/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-header-uuid/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/.gitignore create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator-ignore create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator/FILES create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator/VERSION create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/Cargo.toml create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/README.md create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/src/apis/default.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/src/apis/mod.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/src/header.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/src/lib.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/src/models.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/src/server/mod.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-oneof/src/types.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-test/src/tests.rs create mode 100644 samples/server/petstore/rust-axum/output/rust-axum-validation-test/src/tests.rs diff --git a/CI/circle_parallel.sh b/CI/circle_parallel.sh index cc7e015f4c34..0d02dce87a7c 100755 --- a/CI/circle_parallel.sh +++ b/CI/circle_parallel.sh @@ -15,12 +15,22 @@ if [ "$NODE_INDEX" = "1" ]; then sudo apt-get -y install cpanminus + # install rust + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" + + echo "Testing perl" (cd samples/client/petstore/perl && /bin/bash ./test.bash) + + echo "Testing ruby" (cd samples/client/petstore/ruby && mvn integration-test) (cd samples/client/petstore/ruby-faraday && mvn integration-test) (cd samples/client/petstore/ruby-httpx && mvn integration-test) (cd samples/client/petstore/ruby-autoload && mvn integration-test) + echo "Testing rust" + (cd samples/server/petstore/rust-axum && mvn integration-test) + elif [ "$NODE_INDEX" = "2" ]; then echo "Running node $NODE_INDEX to test Go" # install haskell diff --git a/bin/configs/manual/rust-axum-oneof-v3.yaml b/bin/configs/manual/rust-axum-oneof-v3.yaml new file mode 100644 index 000000000000..4d1bb07c1d71 --- /dev/null +++ b/bin/configs/manual/rust-axum-oneof-v3.yaml @@ -0,0 +1,11 @@ +generatorName: rust-axum +outputDir: samples/server/petstore/rust-axum/output/rust-axum-oneof +inputSpec: modules/openapi-generator/src/test/resources/3_0/rust-axum/rust-axum-oneof.yaml +templateDir: modules/openapi-generator/src/main/resources/rust-axum +generateAliasAsModel: true +additionalProperties: + hideGenerationTimestamp: "true" + packageName: rust-axum-oneof +globalProperties: + skipFormModel: "false" +enablePostProcessFile: true diff --git a/bin/utils/test_file_list.yaml b/bin/utils/test_file_list.yaml index 5c63f356e280..3e224f97d191 100644 --- a/bin/utils/test_file_list.yaml +++ b/bin/utils/test_file_list.yaml @@ -49,3 +49,8 @@ sha256: 67a9e63e13ebddac21cb236aa015edce30f5d3bd8d6adcf50044cad00d48c45e - filename: "samples/openapi3/client/petstore/java/jersey2-java8/src/test/java/org/openapitools/client/model/ZebraTest.java" sha256: 15eeb6d8a9a79d0f1930b861540d9c5780d6c49ea4fdb68269ac3e7ec481e142 +# rust axum test files +- filename: "samples/server/petstore/rust-axum/output/rust-axum-oneof/src/tests.rs" + sha256: 3d4198174018cc7fd9d4bcffd950609a5bd306cf03b2fa780516f4e22a566e8c +- filename: "samples/server/petstore/rust-axum/output/openapi-v3/src/tests.rs" + sha256: 356ac684b1fce91b153c63caefc1fe7472ea600ac436a19631e16bc00e986c50 diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java index 41a3a176ab2a..5303598ed7a6 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java @@ -1083,8 +1083,8 @@ public String toString() { sb.append(", items='").append(items).append('\''); sb.append(", additionalProperties='").append(additionalProperties).append('\''); sb.append(", isModel='").append(isModel).append('\''); - sb.append(", isNull='").append(isNull); - sb.append(", hasValidation='").append(hasValidation); + sb.append(", isNull='").append(isNull).append('\''); + sb.append(", hasValidation='").append(hasValidation).append('\''); sb.append(", getAdditionalPropertiesIsAnyType=").append(getAdditionalPropertiesIsAnyType()); sb.append(", getHasDiscriminatorWithNonEmptyMapping=").append(hasDiscriminatorWithNonEmptyMapping); sb.append(", getIsAnyType=").append(getIsAnyType()); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java index 059d6ac6fb54..79ea15e5b882 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java @@ -236,8 +236,9 @@ public RustAxumServerCodegen() { supportingFiles.add(new SupportingFile("header.mustache", "src", "header.rs")); supportingFiles.add(new SupportingFile("server-mod.mustache", "src/server", "mod.rs")); supportingFiles.add(new SupportingFile("apis-mod.mustache", apiPackage().replace('.', File.separatorChar), "mod.rs")); - supportingFiles.add(new SupportingFile("README.mustache", "", "README.md") - .doNotOverwrite()); + // The file gets overwritten regardless + supportingFiles.add(new SupportingFile("tests.mustache", "src", "tests.rs").doNotOverwrite()); + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md").doNotOverwrite()); } @Override @@ -594,8 +595,105 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation return op; } + private void postProcessOneOfModels(List allModels) { + final HashMap> oneOfMapDiscriminator = new HashMap<>(); + + for (ModelMap mo : allModels) { + final CodegenModel cm = mo.getModel(); + + final CodegenComposedSchemas cs = cm.getComposedSchemas(); + if (cs != null) { + final List csOneOf = cs.getOneOf(); + + if (csOneOf != null) { + for (CodegenProperty model : csOneOf) { + // Generate a valid name for the enum variant. + // Mainly needed for primitive types. + String[] modelParts = model.dataType.replace("<", "Of").replace(">", "").split("::"); + model.datatypeWithEnum = camelize(modelParts[modelParts.length - 1]); + + // Primitive type is not properly set, this overrides it to guarantee adequate model generation. + if (!model.getDataType().matches(String.format(Locale.ROOT, ".*::%s", model.getDatatypeWithEnum()))) { + model.isPrimitiveType = true; + } + } + + cs.setOneOf(csOneOf); + cm.setComposedSchemas(cs); + } + } + + if (cm.discriminator != null) { + for (String model : cm.oneOf) { + List discriminators = oneOfMapDiscriminator.getOrDefault(model, new ArrayList<>()); + discriminators.add(cm.discriminator.getPropertyName()); + oneOfMapDiscriminator.put(model, discriminators); + } + } + } + + for (ModelMap mo : allModels) { + final CodegenModel cm = mo.getModel(); + + for (CodegenProperty var : cm.vars) { + var.isDiscriminator = false; + } + + final List discriminatorsForModel = oneOfMapDiscriminator.get(cm.getSchemaName()); + + if (discriminatorsForModel != null) { + for (String discriminator : discriminatorsForModel) { + boolean hasDiscriminatorDefined = false; + + for (CodegenProperty var : cm.vars) { + if (var.baseName.equals(discriminator)) { + var.isDiscriminator = true; + hasDiscriminatorDefined = true; + break; + } + } + + // If the discriminator field is not a defined attribute in the variant structure, create it. + if (!hasDiscriminatorDefined) { + CodegenProperty property = new CodegenProperty(); + + // Static attributes + // Only strings are supported by serde for tag field types, so it's the only one we'll deal with + property.openApiType = "string"; + property.complexType = "string"; + property.dataType = "String"; + property.datatypeWithEnum = "String"; + property.baseType = "string"; + property.required = true; + property.isPrimitiveType = true; + property.isString = true; + property.isDiscriminator = true; + + // Attributes based on the discriminator value + property.baseName = discriminator; + property.name = discriminator; + property.nameInCamelCase = camelize(discriminator); + property.nameInPascalCase = property.nameInCamelCase.substring(0, 1).toUpperCase(Locale.ROOT) + property.nameInCamelCase.substring(1); + property.nameInSnakeCase = underscore(discriminator).toUpperCase(Locale.ROOT); + property.getter = String.format(Locale.ROOT, "get%s", property.nameInPascalCase); + property.setter = String.format(Locale.ROOT, "set%s", property.nameInPascalCase); + property.defaultValueWithParam = String.format(Locale.ROOT, " = data.%s;", property.name); + + // Attributes based on the model name + property.defaultValue = String.format(Locale.ROOT, "r#\"%s\"#.to_string()", cm.getSchemaName()); + property.jsonSchema = String.format(Locale.ROOT, "{ \"default\":\"%s\"; \"type\":\"string\" }", cm.getSchemaName()); + + cm.vars.add(property); + } + } + } + } + } + @Override public OperationsMap postProcessOperationsWithModels(final OperationsMap operationsMap, List allModels) { + postProcessOneOfModels(allModels); + final OperationMap operations = operationsMap.getOperations(); operations.put("classnamePascalCase", camelize(operations.getClassname())); diff --git a/modules/openapi-generator/src/main/resources/rust-axum/lib.mustache b/modules/openapi-generator/src/main/resources/rust-axum/lib.mustache index 7f197ef5f0ff..98b6bc99145a 100644 --- a/modules/openapi-generator/src/main/resources/rust-axum/lib.mustache +++ b/modules/openapi-generator/src/main/resources/rust-axum/lib.mustache @@ -28,3 +28,6 @@ pub mod apis; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/modules/openapi-generator/src/main/resources/rust-axum/models.mustache b/modules/openapi-generator/src/main/resources/rust-axum/models.mustache index 6c158a02b0b3..f64ef16400d1 100644 --- a/modules/openapi-generator/src/main/resources/rust-axum/models.mustache +++ b/modules/openapi-generator/src/main/resources/rust-axum/models.mustache @@ -573,21 +573,70 @@ impl PartialEq for {{{classname}}} { self.0.get() == other.0.get() } } + {{/anyOf.size}} {{#oneOf.size}} -/// One of: -{{#oneOf}} -/// - {{{.}}} -{{/oneOf}} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct {{{classname}}}(Box); +{{#discriminator}} +#[derive(Debug, Clone, PartialEq, serde::Deserialize)] +#[serde(tag = "{{{propertyBaseName}}}")] +{{/discriminator}} +{{^discriminator}} +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +{{/discriminator}} +#[allow(non_camel_case_types)] +pub enum {{{classname}}} { + {{#composedSchemas}} + {{#oneOf}} + {{{datatypeWithEnum}}}(Box<{{{dataType}}}>), + {{/oneOf}} + {{/composedSchemas}} +} impl validator::Validate for {{{classname}}} { fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> { - std::result::Result::Ok(()) + match self { + {{#composedSchemas}} + {{#oneOf}} + {{#isPrimitiveType}} + Self::{{{datatypeWithEnum}}}(_) => std::result::Result::Ok(()), + {{/isPrimitiveType}} + {{^isPrimitiveType}} + Self::{{{datatypeWithEnum}}}(x) => x.validate(), + {{/isPrimitiveType}} + {{/oneOf}} + {{/composedSchemas}} + } + } +} + +{{#discriminator}} +impl serde::Serialize for {{{classname}}} { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + match self { + {{#composedSchemas}} + {{#oneOf}} + Self::{{{datatypeWithEnum}}}(x) => x.serialize(serializer), + {{/oneOf}} + {{/composedSchemas}} + } } } +{{/discriminator}} + + + +{{#composedSchemas}} +{{#oneOf}} +impl From<{{{dataType}}}> for {{{classname}}} { + fn from(value: {{{dataType}}}) -> Self { + Self::{{{datatypeWithEnum}}}(Box::new(value)) + } +} +{{/oneOf}} +{{/composedSchemas}} /// Converts Query Parameters representation (style=form, explode=false) to a {{{classname}}} value /// as specified in https://swagger.io/docs/specification/serialization/ @@ -600,11 +649,6 @@ impl std::str::FromStr for {{{classname}}} { } } -impl PartialEq for {{{classname}}} { - fn eq(&self, other: &Self) -> bool { - self.0.get() == other.0.get() - } -} {{/oneOf.size}} {{^anyOf.size}} {{^oneOf.size}} @@ -613,11 +657,15 @@ impl PartialEq for {{{classname}}} { pub struct {{{classname}}} { {{#vars}} {{#description}} -/// {{{.}}} + /// {{{.}}} {{/description}} {{#isEnum}} -/// Note: inline enums are not fully supported by openapi-generator + /// Note: inline enums are not fully supported by openapi-generator {{/isEnum}} +{{#isDiscriminator}} + #[serde(default = "{{{classname}}}::_name_for_{{{name}}}")] + #[serde(serialize_with = "{{{classname}}}::_serialize_{{{name}}}")] +{{/isDiscriminator}} #[serde(rename = "{{{baseName}}}")] {{#hasValidation}} #[validate( @@ -685,6 +733,25 @@ pub struct {{{classname}}} { {{/vars}} } + +{{#vars}} +{{#isDiscriminator}} +impl {{{classname}}} { + fn _name_for_{{{name}}}() -> String { + String::from("{{{classname}}}") + } + + fn _serialize_{{{name}}}(_: &String, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_str(&Self::_name_for_{{{name}}}()) + } +} +{{/isDiscriminator}} +{{/vars}} + + {{#vars}} {{#hasValidation}} {{#pattern}} diff --git a/modules/openapi-generator/src/main/resources/rust-axum/tests.mustache b/modules/openapi-generator/src/main/resources/rust-axum/tests.mustache new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-axum/tests.mustache @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/modules/openapi-generator/src/test/resources/3_0/rust-axum/rust-axum-oneof.yaml b/modules/openapi-generator/src/test/resources/3_0/rust-axum/rust-axum-oneof.yaml new file mode 100644 index 000000000000..f66fd7ef6d95 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/rust-axum/rust-axum-oneof.yaml @@ -0,0 +1,86 @@ +openapi: "3.0.4" +info: + title: "test" + version: "0.0.1" +paths: + "/": + post: + operationId: foo + requestBody: + required: true + content: + "application/json": + schema: + "$ref": "#/components/schemas/Message" + responses: + "200": + description: Re-serialize and echo the request data + content: + "application/json": + schema: + "$ref": "#/components/schemas/Message" +components: + schemas: + Message: + type: object + additionalProperties: false + discriminator: + propertyName: op + oneOf: + - "$ref": "#/components/schemas/Hello" + - "$ref": "#/components/schemas/Greeting" + - "$ref": "#/components/schemas/Goodbye" + title: Message + Hello: + type: object + additionalProperties: false + title: Hello + properties: + op: + type: string + enum: + - Hello + default: Hello + d: + type: object + properties: + welcome_message: + type: string + required: + - welcome_message + required: + - op + - d + Greeting: + type: object + additionalProperties: false + title: Greeting + properties: + d: + type: object + properties: + greet_message: + type: string + required: + - greet_message + required: + - d + Goodbye: + type: object + additionalProperties: false + title: Goodbye + properties: + op: + type: string + enum: + - Goodbye + d: + type: object + properties: + goodbye_message: + type: string + required: + - goodbye_message + required: + - op + - d \ No newline at end of file diff --git a/samples/server/petstore/rust-axum/output/apikey-auths/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/apikey-auths/.openapi-generator/FILES index 282f205d1167..d511a90a1573 100644 --- a/samples/server/petstore/rust-axum/output/apikey-auths/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/apikey-auths/.openapi-generator/FILES @@ -7,4 +7,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/apikey-auths/src/lib.rs b/samples/server/petstore/rust-axum/output/apikey-auths/src/lib.rs index d461afebe02e..9c4ee6594d97 100644 --- a/samples/server/petstore/rust-axum/output/apikey-auths/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/apikey-auths/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/apikey-auths/src/tests.rs b/samples/server/petstore/rust-axum/output/apikey-auths/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/apikey-auths/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/multipart-v3/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/multipart-v3/.openapi-generator/FILES index 683914c4c643..47de4e6caefd 100644 --- a/samples/server/petstore/rust-axum/output/multipart-v3/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/multipart-v3/.openapi-generator/FILES @@ -7,4 +7,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/multipart-v3/src/lib.rs b/samples/server/petstore/rust-axum/output/multipart-v3/src/lib.rs index cc053adc1ca5..ebfcd1448c3f 100644 --- a/samples/server/petstore/rust-axum/output/multipart-v3/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/multipart-v3/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/multipart-v3/src/tests.rs b/samples/server/petstore/rust-axum/output/multipart-v3/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/multipart-v3/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/openapi-v3/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/openapi-v3/.openapi-generator/FILES index 2c6783a8355b..9b02e1fdc236 100644 --- a/samples/server/petstore/rust-axum/output/openapi-v3/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/openapi-v3/.openapi-generator/FILES @@ -9,4 +9,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/openapi-v3/src/lib.rs b/samples/server/petstore/rust-axum/output/openapi-v3/src/lib.rs index cc053adc1ca5..ebfcd1448c3f 100644 --- a/samples/server/petstore/rust-axum/output/openapi-v3/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/openapi-v3/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/openapi-v3/src/models.rs b/samples/server/petstore/rust-axum/output/openapi-v3/src/models.rs index b484a7a0ce3b..9ca228cebfd5 100644 --- a/samples/server/petstore/rust-axum/output/openapi-v3/src/models.rs +++ b/samples/server/petstore/rust-axum/output/openapi-v3/src/models.rs @@ -2764,15 +2764,31 @@ impl std::ops::DerefMut for Ok { } } -/// One of: -/// - Vec -/// - i32 -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct OneOfGet200Response(Box); +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +#[allow(non_camel_case_types)] +pub enum OneOfGet200Response { + I32(Box), + VecOfString(Box>), +} impl validator::Validate for OneOfGet200Response { fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> { - std::result::Result::Ok(()) + match self { + Self::I32(_) => std::result::Result::Ok(()), + Self::VecOfString(_) => std::result::Result::Ok(()), + } + } +} + +impl From for OneOfGet200Response { + fn from(value: i32) -> Self { + Self::I32(Box::new(value)) + } +} +impl From> for OneOfGet200Response { + fn from(value: Vec) -> Self { + Self::VecOfString(Box::new(value)) } } @@ -2787,12 +2803,6 @@ impl std::str::FromStr for OneOfGet200Response { } } -impl PartialEq for OneOfGet200Response { - fn eq(&self, other: &Self) -> bool { - self.0.get() == other.0.get() - } -} - #[derive(Debug, Clone, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] pub struct OptionalObjectHeader(i32); diff --git a/samples/server/petstore/rust-axum/output/openapi-v3/src/tests.rs b/samples/server/petstore/rust-axum/output/openapi-v3/src/tests.rs new file mode 100644 index 000000000000..f0c221ea431e --- /dev/null +++ b/samples/server/petstore/rust-axum/output/openapi-v3/src/tests.rs @@ -0,0 +1,41 @@ +#[test] +fn test_oneof_schema_untagged() { + use crate::models::OneOfGet200Response; + + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] + struct Test { + value: OneOfGet200Response, + } + + let test0 = r#"{"value": "ignored"}"#; + let test1 = r#"{"value": 123}"#; + let test2 = r#"{"value": ["foo", "bar"]}"#; + + let test3 = Test { + value: OneOfGet200Response::I32(123.into()), + }; + let test4 = Test { + value: OneOfGet200Response::VecOfString(vec!["foo".to_string(), "bar".to_string()].into()), + }; + + let test5 = r#"{"value":123}"#; + let test6 = r#"{"value":["foo","bar"]}"#; + + assert!(serde_json::from_str::(test0).is_err()); + assert!(serde_json::from_str::(test1).is_ok()); + assert!(serde_json::from_str::(test2).is_ok()); + + assert_eq!( + serde_json::to_string(&test3).expect("Serialization error"), + test5 + ); + assert_eq!( + serde_json::to_string(&test4).expect("Serialization error"), + test6 + ); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/ops-v3/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/ops-v3/.openapi-generator/FILES index 683914c4c643..47de4e6caefd 100644 --- a/samples/server/petstore/rust-axum/output/ops-v3/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/ops-v3/.openapi-generator/FILES @@ -7,4 +7,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/ops-v3/src/lib.rs b/samples/server/petstore/rust-axum/output/ops-v3/src/lib.rs index bfdd7c899236..788f8dbd060b 100644 --- a/samples/server/petstore/rust-axum/output/ops-v3/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/ops-v3/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/ops-v3/src/tests.rs b/samples/server/petstore/rust-axum/output/ops-v3/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/ops-v3/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES index c4774ff1a7cc..95528256a329 100644 --- a/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES @@ -12,4 +12,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs b/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs index 91d8f87a36a8..ffffb88b1927 100644 --- a/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/src/tests.rs b/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/petstore-with-fake-endpoints-models-for-testing/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/petstore/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/petstore/.openapi-generator/FILES index 2520d7516ecb..fdeb33c57d16 100644 --- a/samples/server/petstore/rust-axum/output/petstore/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/petstore/.openapi-generator/FILES @@ -9,4 +9,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/petstore/src/lib.rs b/samples/server/petstore/rust-axum/output/petstore/src/lib.rs index 91d8f87a36a8..ffffb88b1927 100644 --- a/samples/server/petstore/rust-axum/output/petstore/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/petstore/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/petstore/src/tests.rs b/samples/server/petstore/rust-axum/output/petstore/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/petstore/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/ping-bearer-auth/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/ping-bearer-auth/.openapi-generator/FILES index 683914c4c643..47de4e6caefd 100644 --- a/samples/server/petstore/rust-axum/output/ping-bearer-auth/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/ping-bearer-auth/.openapi-generator/FILES @@ -7,4 +7,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/ping-bearer-auth/src/lib.rs b/samples/server/petstore/rust-axum/output/ping-bearer-auth/src/lib.rs index 127b67da835e..75b135c4513e 100644 --- a/samples/server/petstore/rust-axum/output/ping-bearer-auth/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/ping-bearer-auth/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/ping-bearer-auth/src/tests.rs b/samples/server/petstore/rust-axum/output/ping-bearer-auth/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/ping-bearer-auth/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/.openapi-generator/FILES index 683914c4c643..47de4e6caefd 100644 --- a/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/.openapi-generator/FILES @@ -7,4 +7,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/src/lib.rs b/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/src/lib.rs index 7f6af70e59f7..05c534e28424 100644 --- a/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/src/tests.rs b/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-header-uuid/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/.gitignore b/samples/server/petstore/rust-axum/output/rust-axum-oneof/.gitignore new file mode 100644 index 000000000000..a9d37c560c6a --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator-ignore b/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator/FILES new file mode 100644 index 000000000000..47de4e6caefd --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator/FILES @@ -0,0 +1,11 @@ +.gitignore +Cargo.toml +README.md +src/apis/default.rs +src/apis/mod.rs +src/header.rs +src/lib.rs +src/models.rs +src/server/mod.rs +src/tests.rs +src/types.rs diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator/VERSION b/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator/VERSION new file mode 100644 index 000000000000..884119126398 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.11.0-SNAPSHOT diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/Cargo.toml b/samples/server/petstore/rust-axum/output/rust-axum-oneof/Cargo.toml new file mode 100644 index 000000000000..849859898918 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "rust-axum-oneof" +version = "0.0.1" +authors = ["OpenAPI Generator team and contributors"] +description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)" +edition = "2021" + +[features] +default = ["server"] +server = [] +conversion = [ + "frunk", + "frunk_derives", + "frunk_core", + "frunk-enum-core", + "frunk-enum-derive", +] + +[dependencies] +async-trait = "0.1" +axum = "0.7" +axum-extra = { version = "0.9", features = ["cookie", "multipart"] } +base64 = "0.22" +bytes = "1" +chrono = { version = "0.4", features = ["serde"] } +frunk = { version = "0.4", optional = true } +frunk-enum-core = { version = "0.3", optional = true } +frunk-enum-derive = { version = "0.3", optional = true } +frunk_core = { version = "0.4", optional = true } +frunk_derives = { version = "0.4", optional = true } +http = "1" +lazy_static = "1" +regex = "1" +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["raw_value"] } +serde_urlencoded = "0.7" +tokio = { version = "1", default-features = false, features = [ + "signal", + "rt-multi-thread", +] } +tracing = { version = "0.1", features = ["attributes"] } +uuid = { version = "1", features = ["serde"] } +validator = { version = "0.19", features = ["derive"] } + +[dev-dependencies] +tracing-subscriber = "0.3" diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/README.md b/samples/server/petstore/rust-axum/output/rust-axum-oneof/README.md new file mode 100644 index 000000000000..6c4665e80ba0 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/README.md @@ -0,0 +1,91 @@ +# Rust API for rust-axum-oneof + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +## Overview + +This server was generated by the [openapi-generator] +(https://openapi-generator.tech) project. By using the +[OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote +server, you can easily generate a server stub. + +To see how to make this your own, look here: [README]((https://openapi-generator.tech)) + +- API version: 0.0.1 +- Generator version: 7.11.0-SNAPSHOT + + + +This autogenerated project defines an API crate `rust-axum-oneof` which contains: +* An `Api` trait defining the API in Rust. +* Data types representing the underlying data model. +* Axum router which accepts HTTP requests and invokes the appropriate `Api` method for each operation. + * Request validations (path, query, body params) are included. + +## Using the generated library + +The generated library has a few optional features that can be activated through Cargo. + +* `server` + * This defaults to enabled and creates the basic skeleton of a server implementation based on Axum. + * To create the server stack you'll need to provide an implementation of the API trait to provide the server function. +* `conversions` + * This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types. + +See https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section for how to use features in your `Cargo.toml`. + +### Example + +```rust +struct ServerImpl { + // database: sea_orm::DbConn, +} + +#[allow(unused_variables)] +#[async_trait] +impl rust-axum-oneof::Api for ServerImpl { + // API implementation goes here +} + +pub async fn start_server(addr: &str) { + // initialize tracing + tracing_subscriber::fmt::init(); + + // Init Axum router + let app = rust-axum-oneof::server::new(Arc::new(ServerImpl)); + + // Add layers to the router + let app = app.layer(...); + + // Run the server with graceful shutdown + let listener = TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} +``` diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/apis/default.rs b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/apis/default.rs new file mode 100644 index 000000000000..1873aebd9db8 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/apis/default.rs @@ -0,0 +1,30 @@ +use async_trait::async_trait; +use axum::extract::*; +use axum_extra::extract::{CookieJar, Multipart}; +use bytes::Bytes; +use http::Method; +use serde::{Deserialize, Serialize}; + +use crate::{models, types::*}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum FooResponse { + /// Re-serialize and echo the request data + Status200_Re(models::Message), +} + +/// Default +#[async_trait] +#[allow(clippy::ptr_arg)] +pub trait Default { + /// Foo - POST / + async fn foo( + &self, + method: Method, + host: Host, + cookies: CookieJar, + body: models::Message, + ) -> Result; +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/apis/mod.rs b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/apis/mod.rs new file mode 100644 index 000000000000..1be8d340b8e0 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/apis/mod.rs @@ -0,0 +1 @@ +pub mod default; diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/header.rs b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/header.rs new file mode 100644 index 000000000000..7c530892fbf2 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/header.rs @@ -0,0 +1,197 @@ +use std::{convert::TryFrom, fmt, ops::Deref}; + +use chrono::{DateTime, Utc}; +use http::HeaderValue; + +/// A struct to allow homogeneous conversion into a HeaderValue. We can't +/// implement the From/Into trait on HeaderValue because we don't own +/// either of the types. +#[derive(Debug, Clone)] +pub(crate) struct IntoHeaderValue(pub T); + +// Generic implementations + +impl Deref for IntoHeaderValue { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +// Derive for each TryFrom in http::HeaderValue + +macro_rules! ihv_generate { + ($t:ident) => { + impl TryFrom for IntoHeaderValue<$t> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match hdr_value.parse::<$t>() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)), + Err(e) => Err(format!( + "Unable to parse {} as a string: {}", + stringify!($t), + e + )), + }, + Err(e) => Err(format!( + "Unable to parse header {:?} as a string - {}", + hdr_value, e + )), + } + } + } + + impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue<$t>) -> Result { + Ok(hdr_value.0.into()) + } + } + }; +} + +ihv_generate!(u64); +ihv_generate!(i64); +ihv_generate!(i16); +ihv_generate!(u16); +ihv_generate!(u32); +ihv_generate!(usize); +ihv_generate!(isize); +ihv_generate!(i32); + +// Custom derivations + +// Vec + +impl TryFrom for IntoHeaderValue> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => Ok(IntoHeaderValue( + hdr_value + .split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y.to_string()), + }) + .collect(), + )), + Err(e) => Err(format!( + "Unable to parse header: {:?} as a string - {}", + hdr_value, e + )), + } + } +} + +impl TryFrom>> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue>) -> Result { + match HeaderValue::from_str(&hdr_value.0.join(", ")) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + "Unable to convert {:?} into a header - {}", + hdr_value, e + )), + } + } +} + +// String + +impl TryFrom for IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value.to_string())), + Err(e) => Err(format!("Unable to convert header {:?} to {}", hdr_value, e)), + } + } +} + +impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue) -> Result { + match HeaderValue::from_str(&hdr_value.0) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + "Unable to convert {:?} from a header {}", + hdr_value, e + )), + } + } +} + +// Bool + +impl TryFrom for IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match hdr_value.parse() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)), + Err(e) => Err(format!("Unable to parse bool from {} - {}", hdr_value, e)), + }, + Err(e) => Err(format!( + "Unable to convert {:?} from a header {}", + hdr_value, e + )), + } + } +} + +impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue) -> Result { + match HeaderValue::from_str(&hdr_value.0.to_string()) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + "Unable to convert: {:?} into a header: {}", + hdr_value, e + )), + } + } +} + +// DateTime + +impl TryFrom for IntoHeaderValue> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match DateTime::parse_from_rfc3339(hdr_value) { + Ok(date) => Ok(IntoHeaderValue(date.with_timezone(&Utc))), + Err(e) => Err(format!("Unable to parse: {} as date - {}", hdr_value, e)), + }, + Err(e) => Err(format!( + "Unable to convert header {:?} to string {}", + hdr_value, e + )), + } + } +} + +impl TryFrom>> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue>) -> Result { + match HeaderValue::from_str(hdr_value.0.to_rfc3339().as_str()) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!( + "Unable to convert {:?} to a header: {}", + hdr_value, e + )), + } + } +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/lib.rs b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/lib.rs new file mode 100644 index 000000000000..788f8dbd060b --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/lib.rs @@ -0,0 +1,31 @@ +#![allow( + missing_docs, + trivial_casts, + unused_variables, + unused_mut, + unused_extern_crates, + non_camel_case_types, + unused_imports, + unused_attributes +)] +#![allow( + clippy::derive_partial_eq_without_eq, + clippy::disallowed_names, + clippy::too_many_arguments +)] + +pub const BASE_PATH: &str = ""; +pub const API_VERSION: &str = "0.0.1"; + +#[cfg(feature = "server")] +pub mod server; + +pub mod apis; +pub mod models; +pub mod types; + +#[cfg(feature = "server")] +pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/models.rs b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/models.rs new file mode 100644 index 000000000000..832b86893511 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/models.rs @@ -0,0 +1,985 @@ +#![allow(unused_qualifications)] + +use http::HeaderValue; +use validator::Validate; + +#[cfg(feature = "server")] +use crate::header; +use crate::{models, types::*}; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Goodbye { + /// Note: inline enums are not fully supported by openapi-generator + #[serde(default = "Goodbye::_name_for_op")] + #[serde(serialize_with = "Goodbye::_serialize_op")] + #[serde(rename = "op")] + pub op: String, + + #[serde(rename = "d")] + pub d: models::GoodbyeD, +} + +impl Goodbye { + fn _name_for_op() -> String { + String::from("Goodbye") + } + + fn _serialize_op(_: &String, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_str(&Self::_name_for_op()) + } +} + +impl Goodbye { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(op: String, d: models::GoodbyeD) -> Goodbye { + Goodbye { op, d } + } +} + +/// Converts the Goodbye value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Goodbye { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + Some("op".to_string()), + Some(self.op.to_string()), + // Skipping d in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Goodbye value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Goodbye { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub op: Vec, + pub d: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Goodbye".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "op" => intermediate_rep.op.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "d" => intermediate_rep.d.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Goodbye".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Goodbye { + op: intermediate_rep + .op + .into_iter() + .next() + .ok_or_else(|| "op missing in Goodbye".to_string())?, + d: intermediate_rep + .d + .into_iter() + .next() + .ok_or_else(|| "d missing in Goodbye".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Invalid header value for Goodbye - value: {} is invalid {}", + hdr_value, e + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + "Unable to convert header value '{}' into Goodbye - {}", + value, err + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Unable to convert header: {:?} to string: {}", + hdr_value, e + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct GoodbyeD { + #[serde(rename = "goodbye_message")] + pub goodbye_message: String, +} + +impl GoodbyeD { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(goodbye_message: String) -> GoodbyeD { + GoodbyeD { goodbye_message } + } +} + +/// Converts the GoodbyeD value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for GoodbyeD { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + Some("goodbye_message".to_string()), + Some(self.goodbye_message.to_string()), + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a GoodbyeD value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for GoodbyeD { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub goodbye_message: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing GoodbyeD".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "goodbye_message" => intermediate_rep.goodbye_message.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing GoodbyeD".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(GoodbyeD { + goodbye_message: intermediate_rep + .goodbye_message + .into_iter() + .next() + .ok_or_else(|| "goodbye_message missing in GoodbyeD".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Invalid header value for GoodbyeD - value: {} is invalid {}", + hdr_value, e + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + "Unable to convert header value '{}' into GoodbyeD - {}", + value, err + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Unable to convert header: {:?} to string: {}", + hdr_value, e + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Greeting { + #[serde(rename = "d")] + pub d: models::GreetingD, + + #[serde(default = "Greeting::_name_for_op")] + #[serde(serialize_with = "Greeting::_serialize_op")] + #[serde(rename = "op")] + pub op: String, +} + +impl Greeting { + fn _name_for_op() -> String { + String::from("Greeting") + } + + fn _serialize_op(_: &String, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_str(&Self::_name_for_op()) + } +} + +impl Greeting { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(d: models::GreetingD) -> Greeting { + Greeting { + d, + op: r#"Greeting"#.to_string(), + } + } +} + +/// Converts the Greeting value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Greeting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + // Skipping d in query parameter serialization + Some("op".to_string()), + Some(self.op.to_string()), + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Greeting value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Greeting { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub d: Vec, + pub op: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Greeting".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "d" => intermediate_rep.d.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "op" => intermediate_rep.op.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Greeting".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Greeting { + d: intermediate_rep + .d + .into_iter() + .next() + .ok_or_else(|| "d missing in Greeting".to_string())?, + op: intermediate_rep + .op + .into_iter() + .next() + .ok_or_else(|| "op missing in Greeting".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Invalid header value for Greeting - value: {} is invalid {}", + hdr_value, e + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + "Unable to convert header value '{}' into Greeting - {}", + value, err + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Unable to convert header: {:?} to string: {}", + hdr_value, e + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct GreetingD { + #[serde(rename = "greet_message")] + pub greet_message: String, +} + +impl GreetingD { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(greet_message: String) -> GreetingD { + GreetingD { greet_message } + } +} + +/// Converts the GreetingD value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for GreetingD { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + Some("greet_message".to_string()), + Some(self.greet_message.to_string()), + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a GreetingD value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for GreetingD { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub greet_message: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing GreetingD".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "greet_message" => intermediate_rep.greet_message.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing GreetingD".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(GreetingD { + greet_message: intermediate_rep + .greet_message + .into_iter() + .next() + .ok_or_else(|| "greet_message missing in GreetingD".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Invalid header value for GreetingD - value: {} is invalid {}", + hdr_value, e + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + "Unable to convert header value '{}' into GreetingD - {}", + value, err + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Unable to convert header: {:?} to string: {}", + hdr_value, e + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Hello { + /// Note: inline enums are not fully supported by openapi-generator + #[serde(default = "Hello::_name_for_op")] + #[serde(serialize_with = "Hello::_serialize_op")] + #[serde(rename = "op")] + pub op: String, + + #[serde(rename = "d")] + pub d: models::HelloD, +} + +impl Hello { + fn _name_for_op() -> String { + String::from("Hello") + } + + fn _serialize_op(_: &String, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_str(&Self::_name_for_op()) + } +} + +impl Hello { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(d: models::HelloD) -> Hello { + Hello { + op: r#"Hello"#.to_string(), + d, + } + } +} + +/// Converts the Hello value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Hello { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + Some("op".to_string()), + Some(self.op.to_string()), + // Skipping d in query parameter serialization + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Hello value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Hello { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub op: Vec, + pub d: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing Hello".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "op" => intermediate_rep.op.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + #[allow(clippy::redundant_clone)] + "d" => intermediate_rep.d.push( + ::from_str(val) + .map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing Hello".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Hello { + op: intermediate_rep + .op + .into_iter() + .next() + .ok_or_else(|| "op missing in Hello".to_string())?, + d: intermediate_rep + .d + .into_iter() + .next() + .ok_or_else(|| "d missing in Hello".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Invalid header value for Hello - value: {} is invalid {}", + hdr_value, e + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + "Unable to convert header value '{}' into Hello - {}", + value, err + )), + }, + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Unable to convert header: {:?} to string: {}", + hdr_value, e + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct HelloD { + #[serde(rename = "welcome_message")] + pub welcome_message: String, +} + +impl HelloD { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(welcome_message: String) -> HelloD { + HelloD { welcome_message } + } +} + +/// Converts the HelloD value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for HelloD { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + Some("welcome_message".to_string()), + Some(self.welcome_message.to_string()), + ]; + + write!( + f, + "{}", + params.into_iter().flatten().collect::>().join(",") + ) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a HelloD value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for HelloD { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub welcome_message: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => { + return std::result::Result::Err( + "Missing value while parsing HelloD".to_string(), + ) + } + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "welcome_message" => intermediate_rep.welcome_message.push( + ::from_str(val).map_err(|x| x.to_string())?, + ), + _ => { + return std::result::Result::Err( + "Unexpected key while parsing HelloD".to_string(), + ) + } + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(HelloD { + welcome_message: intermediate_rep + .welcome_message + .into_iter() + .next() + .ok_or_else(|| "welcome_message missing in HelloD".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from( + hdr_value: header::IntoHeaderValue, + ) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Invalid header value for HelloD - value: {} is invalid {}", + hdr_value, e + )), + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => { + std::result::Result::Ok(header::IntoHeaderValue(value)) + } + std::result::Result::Err(err) => std::result::Result::Err(format!( + "Unable to convert header value '{}' into HelloD - {}", + value, err + )), + } + } + std::result::Result::Err(e) => std::result::Result::Err(format!( + "Unable to convert header: {:?} to string: {}", + hdr_value, e + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Deserialize)] +#[serde(tag = "op")] +#[allow(non_camel_case_types)] +pub enum Message { + Hello(Box), + Greeting(Box), + Goodbye(Box), +} + +impl validator::Validate for Message { + fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> { + match self { + Self::Hello(x) => x.validate(), + Self::Greeting(x) => x.validate(), + Self::Goodbye(x) => x.validate(), + } + } +} + +impl serde::Serialize for Message { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Hello(x) => x.serialize(serializer), + Self::Greeting(x) => x.serialize(serializer), + Self::Goodbye(x) => x.serialize(serializer), + } + } +} + +impl From for Message { + fn from(value: models::Hello) -> Self { + Self::Hello(Box::new(value)) + } +} +impl From for Message { + fn from(value: models::Greeting) -> Self { + Self::Greeting(Box::new(value)) + } +} +impl From for Message { + fn from(value: models::Goodbye) -> Self { + Self::Goodbye(Box::new(value)) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Message value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Message { + type Err = serde_json::Error; + + fn from_str(s: &str) -> std::result::Result { + serde_json::from_str(s) + } +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/server/mod.rs b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/server/mod.rs new file mode 100644 index 000000000000..8db98b873f15 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/server/mod.rs @@ -0,0 +1,109 @@ +use std::collections::HashMap; + +use axum::{body::Body, extract::*, response::Response, routing::*}; +use axum_extra::extract::{CookieJar, Multipart}; +use bytes::Bytes; +use http::{header::CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, Method, StatusCode}; +use tracing::error; +use validator::{Validate, ValidationErrors}; + +use crate::{header, types::*}; + +#[allow(unused_imports)] +use crate::{apis, models}; + +/// Setup API Server. +pub fn new(api_impl: I) -> Router +where + I: AsRef + Clone + Send + Sync + 'static, + A: apis::default::Default + 'static, +{ + // build our application with a route + Router::new() + .route("/", post(foo::)) + .with_state(api_impl) +} + +#[derive(validator::Validate)] +#[allow(dead_code)] +struct FooBodyValidator<'a> { + #[validate(nested)] + body: &'a models::Message, +} + +#[tracing::instrument(skip_all)] +fn foo_validation( + body: models::Message, +) -> std::result::Result<(models::Message,), ValidationErrors> { + let b = FooBodyValidator { body: &body }; + b.validate()?; + + Ok((body,)) +} +/// Foo - POST / +#[tracing::instrument(skip_all)] +async fn foo( + method: Method, + host: Host, + cookies: CookieJar, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::default::Default, +{ + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || foo_validation(body)) + .await + .unwrap(); + + let Ok((body,)) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl.as_ref().foo(method, host, cookies, body).await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::default::FooResponse::Status200_Re(body) => { + let mut response = response.status(200); + { + let mut response_headers = response.headers_mut().unwrap(); + response_headers.insert( + CONTENT_TYPE, + HeaderValue::from_str("application/json").map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + })?, + ); + } + + let body_content = tokio::task::spawn_blocking(move || { + serde_json::to_vec(&body).map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) + }) + .await + .unwrap()?; + response.body(Body::from(body_content)) + } + }, + Err(_) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + response.status(500).body(Body::empty()) + } + }; + + resp.map_err(|e| { + error!(error = ?e); + StatusCode::INTERNAL_SERVER_ERROR + }) +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/tests.rs b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/tests.rs new file mode 100644 index 000000000000..aa3cfff54899 --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/tests.rs @@ -0,0 +1,101 @@ +#[test] +fn test_oneof_schema_with_discriminator() { + use crate::models::*; + + let test0 = r#"{"op": "ignored", "d": {"welcome_message": "test0"}}"#; + + let test1 = r#"{"op": "Hello", "d": {"welcome_message": "test1"}}"#; + let test2 = r#"{"op": "Greeting", "d": {"greet_message": "test2"}}"#; + let test3 = r#"{"op": "Goodbye", "d": {"goodbye_message": "test3"}}"#; + + let test4 = Hello { + op: "ignored".to_string(), + d: HelloD { + welcome_message: "test4".to_string(), + }, + }; + + let test5 = Greeting { + op: "ignored".to_string(), + d: GreetingD { + greet_message: "test5".to_string(), + }, + }; + + let test6 = Goodbye { + op: "ignored".to_string(), + d: GoodbyeD { + goodbye_message: "test6".to_string(), + }, + }; + + let test7 = Message::Hello(test4.clone().into()); + let test8 = Message::Greeting(test5.clone().into()); + let test9 = Message::Goodbye(test6.clone().into()); + + let test10: Message = test4.clone().into(); + let test11: Message = test5.clone().into(); + let test12: Message = test6.clone().into(); + + let test13 = r#"{"op":"Hello","d":{"welcome_message":"test4"}}"#; + let test14 = r#"{"d":{"greet_message":"test5"},"op":"Greeting"}"#; + let test15 = r#"{"op":"Goodbye","d":{"goodbye_message":"test6"}}"#; + + assert!(serde_json::from_str::(test0).is_err()); + + assert!(serde_json::from_str::(test0).is_ok()); + assert!(serde_json::from_str::(test0).is_err()); + assert!(serde_json::from_str::(test0).is_err()); + + assert!(serde_json::from_str::(test1).is_ok()); + assert!(serde_json::from_str::(test2).is_ok()); + assert!(serde_json::from_str::(test3).is_ok()); + + assert!(serde_json::from_str::(test1).is_ok()); + assert!(serde_json::from_str::(test2).is_ok()); + assert!(serde_json::from_str::(test3).is_ok()); + + assert_eq!( + serde_json::to_string(&test4).expect("Serialization error"), + test13 + ); + assert_eq!( + serde_json::to_string(&test5).expect("Serialization error"), + test14 + ); + assert_eq!( + serde_json::to_string(&test6).expect("Serialization error"), + test15 + ); + + assert_eq!( + serde_json::to_string(&test7).expect("Serialization error"), + test13 + ); + assert_eq!( + serde_json::to_string(&test8).expect("Serialization error"), + test14 + ); + assert_eq!( + serde_json::to_string(&test9).expect("Serialization error"), + test15 + ); + + assert_eq!( + serde_json::to_string(&test10).expect("Serialization error"), + test13 + ); + assert_eq!( + serde_json::to_string(&test11).expect("Serialization error"), + test14 + ); + assert_eq!( + serde_json::to_string(&test12).expect("Serialization error"), + test15 + ); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/types.rs b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/types.rs new file mode 100644 index 000000000000..5c5197e19c1e --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-oneof/src/types.rs @@ -0,0 +1,790 @@ +use std::{mem, str::FromStr}; + +use base64::{engine::general_purpose, Engine}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[allow(dead_code)] +pub struct Object(serde_json::Value); + +impl validator::Validate for Object { + fn validate(&self) -> Result<(), validator::ValidationErrors> { + Ok(()) + } +} + +impl FromStr for Object { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(Self(serde_json::Value::String(s.to_owned()))) + } +} + +/// Serde helper function to create a default `Option>` while +/// deserializing +pub fn default_optional_nullable() -> Option> { + None +} + +/// Serde helper function to deserialize into an `Option>` +pub fn deserialize_optional_nullable<'de, D, T>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + Option::::deserialize(deserializer).map(|val| match val { + Some(inner) => Some(Nullable::Present(inner)), + None => Some(Nullable::Null), + }) +} + +/// The Nullable type. Represents a value which may be specified as null on an API. +/// Note that this is distinct from a value that is optional and not present! +/// +/// Nullable implements many of the same methods as the Option type (map, unwrap, etc). +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum Nullable { + /// Null value + Null, + /// Value is present + Present(T), +} + +impl Nullable { + ///////////////////////////////////////////////////////////////////////// + // Querying the contained values + ///////////////////////////////////////////////////////////////////////// + + /// Returns `true` if the Nullable is a `Present` value. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x: Nullable = Nullable::Present(2); + /// assert_eq!(x.is_present(), true); + /// + /// let x: Nullable = Nullable::Null; + /// assert_eq!(x.is_present(), false); + /// ``` + #[inline] + pub fn is_present(&self) -> bool { + match *self { + Nullable::Present(_) => true, + Nullable::Null => false, + } + } + + /// Returns `true` if the Nullable is a `Null` value. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x: Nullable = Nullable::Present(2); + /// assert_eq!(x.is_null(), false); + /// + /// let x: Nullable = Nullable::Null; + /// assert_eq!(x.is_null(), true); + /// ``` + #[inline] + pub fn is_null(&self) -> bool { + !self.is_present() + } + + ///////////////////////////////////////////////////////////////////////// + // Adapter for working with references + ///////////////////////////////////////////////////////////////////////// + + /// Converts from `Nullable` to `Nullable<&T>`. + /// + /// # Examples + /// + /// Convert an `Nullable<`[`String`]`>` into a `Nullable<`[`usize`]`>`, preserving the original. + /// The [`map`] method takes the `self` argument by value, consuming the original, + /// so this technique uses `as_ref` to first take a `Nullable` to a reference + /// to the value inside the original. + /// + /// [`map`]: enum.Nullable.html#method.map + /// [`String`]: ../../std/string/struct.String.html + /// [`usize`]: ../../std/primitive.usize.html + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let num_as_str: Nullable = Nullable::Present("10".to_string()); + /// // First, cast `Nullable` to `Nullable<&String>` with `as_ref`, + /// // then consume *that* with `map`, leaving `num_as_str` on the stack. + /// let num_as_int: Nullable = num_as_str.as_ref().map(|n| n.len()); + /// println!("still can print num_as_str: {:?}", num_as_str); + /// ``` + #[inline] + pub fn as_ref(&self) -> Nullable<&T> { + match *self { + Nullable::Present(ref x) => Nullable::Present(x), + Nullable::Null => Nullable::Null, + } + } + + /// Converts from `Nullable` to `Nullable<&mut T>`. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let mut x = Nullable::Present(2); + /// match x.as_mut() { + /// Nullable::Present(v) => *v = 42, + /// Nullable::Null => {}, + /// } + /// assert_eq!(x, Nullable::Present(42)); + /// ``` + #[inline] + pub fn as_mut(&mut self) -> Nullable<&mut T> { + match *self { + Nullable::Present(ref mut x) => Nullable::Present(x), + Nullable::Null => Nullable::Null, + } + } + + ///////////////////////////////////////////////////////////////////////// + // Getting to contained values + ///////////////////////////////////////////////////////////////////////// + + /// Unwraps a Nullable, yielding the content of a `Nullable::Present`. + /// + /// # Panics + /// + /// Panics if the value is a [`Nullable::Null`] with a custom panic message provided by + /// `msg`. + /// + /// [`Nullable::Null`]: #variant.Null + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = Nullable::Present("value"); + /// assert_eq!(x.expect("the world is ending"), "value"); + /// ``` + /// + /// ```should_panic + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x: Nullable<&str> = Nullable::Null; + /// x.expect("the world is ending"); // panics with `the world is ending` + /// ``` + #[inline] + pub fn expect(self, msg: &str) -> T { + match self { + Nullable::Present(val) => val, + Nullable::Null => expect_failed(msg), + } + } + + /// Moves the value `v` out of the `Nullable` if it is `Nullable::Present(v)`. + /// + /// In general, because this function may panic, its use is discouraged. + /// Instead, prefer to use pattern matching and handle the `Nullable::Null` + /// case explicitly. + /// + /// # Panics + /// + /// Panics if the self value equals [`Nullable::Null`]. + /// + /// [`Nullable::Null`]: #variant.Null + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = Nullable::Present("air"); + /// assert_eq!(x.unwrap(), "air"); + /// ``` + /// + /// ```should_panic + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.unwrap(), "air"); // fails + /// ``` + #[inline] + pub fn unwrap(self) -> T { + match self { + Nullable::Present(val) => val, + Nullable::Null => panic!("called `Nullable::unwrap()` on a `Nullable::Null` value"), + } + } + + /// Returns the contained value or a default. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// assert_eq!(Nullable::Present("car").unwrap_or("bike"), "car"); + /// assert_eq!(Nullable::Null.unwrap_or("bike"), "bike"); + /// ``` + #[inline] + pub fn unwrap_or(self, def: T) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => def, + } + } + + /// Returns the contained value or computes it from a closure. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let k = 10; + /// assert_eq!(Nullable::Present(4).unwrap_or_else(|| 2 * k), 4); + /// assert_eq!(Nullable::Null.unwrap_or_else(|| 2 * k), 20); + /// ``` + #[inline] + pub fn unwrap_or_else T>(self, f: F) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => f(), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Transforming contained values + ///////////////////////////////////////////////////////////////////////// + + /// Maps a `Nullable` to `Nullable` by applying a function to a contained value. + /// + /// # Examples + /// + /// Convert a `Nullable<`[`String`]`>` into a `Nullable<`[`usize`]`>`, consuming the original: + /// + /// [`String`]: ../../std/string/struct.String.html + /// [`usize`]: ../../std/primitive.usize.html + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let maybe_some_string = Nullable::Present(String::from("Hello, World!")); + /// // `Nullable::map` takes self *by value*, consuming `maybe_some_string` + /// let maybe_some_len = maybe_some_string.map(|s| s.len()); + /// + /// assert_eq!(maybe_some_len, Nullable::Present(13)); + /// ``` + #[inline] + pub fn map U>(self, f: F) -> Nullable { + match self { + Nullable::Present(x) => Nullable::Present(f(x)), + Nullable::Null => Nullable::Null, + } + } + + /// Applies a function to the contained value (if any), + /// or returns a `default` (if not). + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.map_or(42, |v| v.len()), 3); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.map_or(42, |v| v.len()), 42); + /// ``` + #[inline] + pub fn map_or U>(self, default: U, f: F) -> U { + match self { + Nullable::Present(t) => f(t), + Nullable::Null => default, + } + } + + /// Applies a function to the contained value (if any), + /// or computes a `default` (if not). + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let k = 21; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 3); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 42); + /// ``` + #[inline] + pub fn map_or_else U, F: FnOnce(T) -> U>(self, default: D, f: F) -> U { + match self { + Nullable::Present(t) => f(t), + Nullable::Null => default(), + } + } + + /// Transforms the `Nullable` into a [`Result`], mapping `Nullable::Present(v)` to + /// [`Ok(v)`] and `Nullable::Null` to [`Err(err)`][Err]. + /// + /// [`Result`]: ../../std/result/enum.Result.html + /// [`Ok(v)`]: ../../std/result/enum.Result.html#variant.Ok + /// [Err]: ../../std/result/enum.Result.html#variant.Err + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.ok_or(0), Ok("foo")); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.ok_or(0), Err(0)); + /// ``` + #[inline] + pub fn ok_or(self, err: E) -> Result { + match self { + Nullable::Present(v) => Ok(v), + Nullable::Null => Err(err), + } + } + + /// Transforms the `Nullable` into a [`Result`], mapping `Nullable::Present(v)` to + /// [`Ok(v)`] and `Nullable::Null` to [`Err(err())`][Err]. + /// + /// [`Result`]: ../../std/result/enum.Result.html + /// [`Ok(v)`]: ../../std/result/enum.Result.html#variant.Ok + /// [Err]: ../../std/result/enum.Result.html#variant.Err + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.ok_or_else(|| 0), Ok("foo")); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.ok_or_else(|| 0), Err(0)); + /// ``` + #[inline] + pub fn ok_or_else E>(self, err: F) -> Result { + match self { + Nullable::Present(v) => Ok(v), + Nullable::Null => Err(err()), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Boolean operations on the values, eager and lazy + ///////////////////////////////////////////////////////////////////////// + + /// Returns `Nullable::Null` if the Nullable is `Nullable::Null`, otherwise returns `optb`. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = Nullable::Present(2); + /// let y: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.and(y), Nullable::Null); + /// + /// let x: Nullable = Nullable::Null; + /// let y = Nullable::Present("foo"); + /// assert_eq!(x.and(y), Nullable::Null); + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Present("foo"); + /// assert_eq!(x.and(y), Nullable::Present("foo")); + /// + /// let x: Nullable = Nullable::Null; + /// let y: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.and(y), Nullable::Null); + /// ``` + #[inline] + pub fn and(self, optb: Nullable) -> Nullable { + match self { + Nullable::Present(_) => optb, + Nullable::Null => Nullable::Null, + } + } + + /// Returns `Nullable::Null` if the Nullable is `Nullable::Null`, otherwise calls `f` with the + /// wrapped value and returns the result. + /// + /// Some languages call this operation flatmap. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// fn sq(x: u32) -> Nullable { Nullable::Present(x * x) } + /// fn nope(_: u32) -> Nullable { Nullable::Null } + /// + /// assert_eq!(Nullable::Present(2).and_then(sq).and_then(sq), Nullable::Present(16)); + /// assert_eq!(Nullable::Present(2).and_then(sq).and_then(nope), Nullable::Null); + /// assert_eq!(Nullable::Present(2).and_then(nope).and_then(sq), Nullable::Null); + /// assert_eq!(Nullable::Null.and_then(sq).and_then(sq), Nullable::Null); + /// ``` + #[inline] + pub fn and_then Nullable>(self, f: F) -> Nullable { + match self { + Nullable::Present(x) => f(x), + Nullable::Null => Nullable::Null, + } + } + + /// Returns the Nullable if it contains a value, otherwise returns `optb`. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Null; + /// assert_eq!(x.or(y), Nullable::Present(2)); + /// + /// let x = Nullable::Null; + /// let y = Nullable::Present(100); + /// assert_eq!(x.or(y), Nullable::Present(100)); + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Present(100); + /// assert_eq!(x.or(y), Nullable::Present(2)); + /// + /// let x: Nullable = Nullable::Null; + /// let y = Nullable::Null; + /// assert_eq!(x.or(y), Nullable::Null); + /// ``` + #[inline] + pub fn or(self, optb: Nullable) -> Nullable { + match self { + Nullable::Present(_) => self, + Nullable::Null => optb, + } + } + + /// Returns the Nullable if it contains a value, otherwise calls `f` and + /// returns the result. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// fn nobody() -> Nullable<&'static str> { Nullable::Null } + /// fn vikings() -> Nullable<&'static str> { Nullable::Present("vikings") } + /// + /// assert_eq!(Nullable::Present("barbarians").or_else(vikings), + /// Nullable::Present("barbarians")); + /// assert_eq!(Nullable::Null.or_else(vikings), Nullable::Present("vikings")); + /// assert_eq!(Nullable::Null.or_else(nobody), Nullable::Null); + /// ``` + #[inline] + pub fn or_else Nullable>(self, f: F) -> Nullable { + match self { + Nullable::Present(_) => self, + Nullable::Null => f(), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Misc + ///////////////////////////////////////////////////////////////////////// + + /// Takes the value out of the Nullable, leaving a `Nullable::Null` in its place. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let mut x = Nullable::Present(2); + /// x.take(); + /// assert_eq!(x, Nullable::Null); + /// + /// let mut x: Nullable = Nullable::Null; + /// x.take(); + /// assert_eq!(x, Nullable::Null); + /// ``` + #[inline] + pub fn take(&mut self) -> Nullable { + mem::replace(self, Nullable::Null) + } +} + +impl Nullable<&T> { + /// Maps an `Nullable<&T>` to an `Nullable` by cloning the contents of the + /// Nullable. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = 12; + /// let opt_x = Nullable::Present(&x); + /// assert_eq!(opt_x, Nullable::Present(&12)); + /// let cloned = opt_x.cloned(); + /// assert_eq!(cloned, Nullable::Present(12)); + /// ``` + pub fn cloned(self) -> Nullable { + self.map(Clone::clone) + } +} + +impl Nullable { + /// Returns the contained value or a default + /// + /// Consumes the `self` argument then, if `Nullable::Present`, returns the contained + /// value, otherwise if `Nullable::Null`, returns the default value for that + /// type. + /// + /// # Examples + /// + /// ``` + /// # use rust_axum_oneof::types::Nullable; + /// + /// let x = Nullable::Present(42); + /// assert_eq!(42, x.unwrap_or_default()); + /// + /// let y: Nullable = Nullable::Null; + /// assert_eq!(0, y.unwrap_or_default()); + /// ``` + #[inline] + pub fn unwrap_or_default(self) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => Default::default(), + } + } +} + +impl Default for Nullable { + /// Returns None. + #[inline] + fn default() -> Nullable { + Nullable::Null + } +} + +impl From for Nullable { + fn from(val: T) -> Nullable { + Nullable::Present(val) + } +} + +impl Serialize for Nullable +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Nullable::Present(ref inner) => serializer.serialize_some(&inner), + Nullable::Null => serializer.serialize_none(), + } + } +} + +impl<'de, T> Deserialize<'de> for Nullable +where + T: serde::de::DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + // In order to deserialize a required, but nullable, value, we first have to check whether + // the value is present at all. To do this, we deserialize to a serde_json::Value, which + // fails if the value is missing, or gives serde_json::Value::Null if the value is present. + // If that succeeds as null, we can easily return a Null. + // If that succeeds as some value, we deserialize that value and return a Present. + // If that errors, we return the error. + let presence: Result<::serde_json::Value, _> = + serde::Deserialize::deserialize(deserializer); + match presence { + Ok(serde_json::Value::Null) => Ok(Nullable::Null), + Ok(some_value) => serde_json::from_value(some_value) + .map(Nullable::Present) + .map_err(serde::de::Error::custom), + Err(x) => Err(x), + } + } +} + +impl validator::Validate for Nullable +where + T: validator::Validate, +{ + fn validate(&self) -> Result<(), validator::ValidationErrors> { + match self { + Self::Present(x) => x.validate(), + Self::Null => Ok(()), + } + } +} + +impl<'a, T> validator::ValidateArgs<'a> for Nullable +where + T: validator::ValidateArgs<'a>, +{ + type Args = T::Args; + fn validate_with_args(&self, args: Self::Args) -> Result<(), validator::ValidationErrors> { + match self { + Self::Present(x) => x.validate_with_args(args), + Self::Null => Ok(()), + } + } +} + +impl validator::ValidateEmail for Nullable +where + T: validator::ValidateEmail, +{ + fn as_email_string(&self) -> Option> { + match self { + Self::Present(x) => x.as_email_string(), + Self::Null => None, + } + } +} + +impl validator::ValidateUrl for Nullable +where + T: validator::ValidateUrl, +{ + fn as_url_string(&self) -> Option> { + match self { + Self::Present(x) => x.as_url_string(), + Self::Null => None, + } + } +} + +impl validator::ValidateContains for Nullable +where + T: validator::ValidateContains, +{ + fn validate_contains(&self, needle: &str) -> bool { + match self { + Self::Present(x) => x.validate_contains(needle), + Self::Null => true, + } + } +} + +impl validator::ValidateRequired for Nullable +where + T: validator::ValidateRequired, +{ + fn is_some(&self) -> bool { + self.is_present() + } +} + +impl validator::ValidateRegex for Nullable +where + T: validator::ValidateRegex, +{ + fn validate_regex(&self, regex: impl validator::AsRegex) -> bool { + match self { + Self::Present(x) => x.validate_regex(regex), + Self::Null => true, + } + } +} + +impl validator::ValidateRange for Nullable +where + T: validator::ValidateRange, +{ + fn greater_than(&self, max: I) -> Option { + use validator::ValidateRange; + match self { + Self::Present(x) => x.greater_than(max), + Self::Null => None, + } + } + fn less_than(&self, min: I) -> Option { + use validator::ValidateRange; + match self { + Self::Present(x) => x.less_than(min), + Self::Null => None, + } + } +} + +impl validator::ValidateLength for Nullable +where + T: validator::ValidateLength, + I: PartialEq + PartialOrd, +{ + fn length(&self) -> Option { + use validator::ValidateLength; + match self { + Self::Present(x) => x.length(), + Self::Null => None, + } + } +} + +impl From> for Option { + fn from(value: Nullable) -> Option { + match value { + Nullable::Present(x) => Some(x), + Nullable::Null => None, + } + } +} + +#[inline(never)] +#[cold] +fn expect_failed(msg: &str) -> ! { + panic!("{}", msg) +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +/// Base64-encoded byte array +pub struct ByteArray(pub Vec); + +impl Serialize for ByteArray { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&general_purpose::STANDARD.encode(&self.0)) + } +} + +impl<'de> Deserialize<'de> for ByteArray { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match general_purpose::STANDARD.decode(s) { + Ok(bin) => Ok(ByteArray(bin)), + _ => Err(serde::de::Error::custom("invalid base64")), + } + } +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-test/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/rust-axum-test/.openapi-generator/FILES index 683914c4c643..47de4e6caefd 100644 --- a/samples/server/petstore/rust-axum/output/rust-axum-test/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/rust-axum-test/.openapi-generator/FILES @@ -7,4 +7,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/rust-axum-test/src/lib.rs b/samples/server/petstore/rust-axum/output/rust-axum-test/src/lib.rs index d9085fbe8b9b..17d18b59d83a 100644 --- a/samples/server/petstore/rust-axum/output/rust-axum-test/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/rust-axum-test/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/rust-axum-test/src/tests.rs b/samples/server/petstore/rust-axum/output/rust-axum-test/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-test/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +} diff --git a/samples/server/petstore/rust-axum/output/rust-axum-validation-test/.openapi-generator/FILES b/samples/server/petstore/rust-axum/output/rust-axum-validation-test/.openapi-generator/FILES index 683914c4c643..47de4e6caefd 100644 --- a/samples/server/petstore/rust-axum/output/rust-axum-validation-test/.openapi-generator/FILES +++ b/samples/server/petstore/rust-axum/output/rust-axum-validation-test/.openapi-generator/FILES @@ -7,4 +7,5 @@ src/header.rs src/lib.rs src/models.rs src/server/mod.rs +src/tests.rs src/types.rs diff --git a/samples/server/petstore/rust-axum/output/rust-axum-validation-test/src/lib.rs b/samples/server/petstore/rust-axum/output/rust-axum-validation-test/src/lib.rs index bfdd7c899236..788f8dbd060b 100644 --- a/samples/server/petstore/rust-axum/output/rust-axum-validation-test/src/lib.rs +++ b/samples/server/petstore/rust-axum/output/rust-axum-validation-test/src/lib.rs @@ -26,3 +26,6 @@ pub mod types; #[cfg(feature = "server")] pub(crate) mod header; + +#[cfg(test)] +mod tests; diff --git a/samples/server/petstore/rust-axum/output/rust-axum-validation-test/src/tests.rs b/samples/server/petstore/rust-axum/output/rust-axum-validation-test/src/tests.rs new file mode 100644 index 000000000000..3a811047352f --- /dev/null +++ b/samples/server/petstore/rust-axum/output/rust-axum-validation-test/src/tests.rs @@ -0,0 +1,9 @@ +#[test] +fn std_test() { + assert!(true); +} + +#[tokio::test] +async fn tokio_test() { + assert!(true); +}