diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index 995c05ae1a1..14f5faba494 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableMap; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache.Lambda; import com.samskivert.mustache.Template; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import lombok.Getter; @@ -33,6 +34,7 @@ import org.openapitools.codegen.model.ModelMap; import org.openapitools.codegen.model.ModelsMap; import org.openapitools.codegen.model.OperationMap; import org.openapitools.codegen.model.OperationsMap; +import org.openapitools.codegen.utils.ModelUtils; import org.openapitools.codegen.utils.URLPathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -768,6 +770,11 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen public void preprocessOpenAPI(OpenAPI openAPI) { super.preprocessOpenAPI(openAPI); + if (SPRING_BOOT.equals(library) && ModelUtils.containsEnums(this.openAPI)) { + supportingFiles.add(new SupportingFile("converter.mustache", + (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.kt")); + } + if (!additionalProperties.containsKey(TITLE)) { // The purpose of the title is for: // - README documentation diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 621c91bfa84..76be2f1f379 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -18,7 +18,6 @@ package org.openapitools.codegen.languages; import com.samskivert.mustache.Mustache; -import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -40,6 +39,7 @@ import org.openapitools.codegen.model.OperationsMap; import org.openapitools.codegen.templating.mustache.SplitStringLambda; import org.openapitools.codegen.templating.mustache.SpringHttpStatusLambda; import org.openapitools.codegen.templating.mustache.TrimWhitespaceLambda; +import org.openapitools.codegen.utils.ModelUtils; import org.openapitools.codegen.utils.ProcessUtils; import org.openapitools.codegen.utils.URLPathUtils; import org.slf4j.Logger; @@ -648,20 +648,6 @@ public class SpringCodegen extends AbstractJavaCodegen supportsAdditionalPropertiesWithComposedSchema = true; } - private boolean containsEnums() { - if (openAPI == null) { - return false; - } - - Components components = this.openAPI.getComponents(); - if (components == null || components.getSchemas() == null) { - return false; - } - - return components.getSchemas().values().stream() - .anyMatch(it -> it.getEnum() != null && !it.getEnum().isEmpty()); - } - private boolean supportLibraryUseTags() { return SPRING_BOOT.equals(library) || SPRING_CLOUD_LIBRARY.equals(library); } @@ -696,7 +682,7 @@ public class SpringCodegen extends AbstractJavaCodegen public void preprocessOpenAPI(OpenAPI openAPI) { super.preprocessOpenAPI(openAPI); - if (SPRING_BOOT.equals(library) && containsEnums()) { + if (SPRING_BOOT.equals(library) && ModelUtils.containsEnums(this.openAPI)) { supportingFiles.add(new SupportingFile("converter.mustache", (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.java")); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 405934531f0..2c79c8a6ca8 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -2435,6 +2435,19 @@ public class ModelUtils { schema.getContentSchema() != null); } + /** + * Returns true if the OpenAPI specification contains any schemas which are enums. + * @param openAPI OpenAPI specification + * @return true if the OpenAPI specification contains any schemas which are enums. + */ + public static boolean containsEnums(OpenAPI openAPI) { + Map schemaMap = getSchemas(openAPI); + if (schemaMap.isEmpty()) { + return false; + } + + return schemaMap.values().stream().anyMatch(ModelUtils::isEnumSchema); + } @FunctionalInterface private interface OpenAPISchemaVisitor { diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/converter.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/converter.mustache new file mode 100644 index 00000000000..acdd13073fc --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/converter.mustache @@ -0,0 +1,38 @@ +package {{configPackage}} + +{{#models}} + {{#model}} + {{#isEnum}} +import {{modelPackage}}.{{classname}} + {{/isEnum}} + {{/model}} +{{/models}} + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter + +/** + * This class provides Spring Converter beans for the enum models in the OpenAPI specification. + * + * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent + * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than + * `original` or the specification has an integer enum. + */ +@Configuration(value = "{{configPackage}}.enumConverterConfiguration") +class EnumConverterConfiguration { + +{{#models}} +{{#model}} +{{#isEnum}} + @Bean(name = ["{{configPackage}}.EnumConverterConfiguration.{{classVarName}}Converter"]) + fun {{classVarName}}Converter(): Converter<{{{dataType}}}, {{classname}}> { + return object: Converter<{{{dataType}}}, {{classname}}> { + override fun convert(source: {{{dataType}}}): {{classname}} = {{classname}}.forValue(source) + } + } +{{/isEnum}} +{{/model}} +{{/models}} + +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index ba7b73e78ee..5a0d24cdcf1 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -5,6 +5,8 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.parser.core.models.ParseOptions; +import java.util.HashMap; +import java.util.function.Consumer; import org.apache.commons.io.FileUtils; import org.assertj.core.api.Assertions; import org.jetbrains.annotations.NotNull; @@ -12,6 +14,8 @@ import org.openapitools.codegen.ClientOptInput; import org.openapitools.codegen.CodegenConstants; import org.openapitools.codegen.DefaultGenerator; import org.openapitools.codegen.TestUtils; +import org.openapitools.codegen.config.CodegenConfigurator; +import org.openapitools.codegen.java.assertions.JavaFileAssert; import org.openapitools.codegen.kotlin.KotlinTestUtils; import org.openapitools.codegen.languages.KotlinSpringServerCodegen; import org.openapitools.codegen.languages.features.CXFServerFeatures; @@ -31,8 +35,10 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; import static org.openapitools.codegen.TestUtils.assertFileContains; import static org.openapitools.codegen.TestUtils.assertFileNotContains; +import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.ANNOTATION_LIBRARY; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DOCUMENTATION_PROVIDER; @@ -748,6 +754,40 @@ public class KotlinSpringServerCodegenTest { ); } + @Test + public void contractWithoutEnumDoesNotContainEnumConverter() throws IOException { + Map output = generateFromContract("src/test/resources/3_0/generic.yaml"); + + assertThat(output).doesNotContainKey("EnumConverterConfiguration.kt"); + } + + @Test + public void contractWithEnumContainsEnumConverter() throws IOException { + Map output = generateFromContract("src/test/resources/3_0/enum.yaml"); + + File enumConverterFile = output.get("EnumConverterConfiguration.kt"); + assertThat(enumConverterFile).isNotNull(); + assertFileContains(enumConverterFile.toPath(), "fun typeConverter(): Converter {"); + assertFileContains(enumConverterFile.toPath(), "return object: Converter {"); + assertFileContains(enumConverterFile.toPath(), "override fun convert(source: kotlin.String): Type = Type.forValue(source)"); + } + + @Test + public void contractWithResolvedInnerEnumContainsEnumConverter() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/inner_enum.yaml", + new HashMap<>(), + new HashMap<>(), + configurator -> configurator.addInlineSchemaOption("RESOLVE_INLINE_ENUMS", "true") + ); + + File enumConverterFile = files.get("EnumConverterConfiguration.kt"); + assertThat(enumConverterFile).isNotNull(); + assertFileContains(enumConverterFile.toPath(), "fun ponyTypeConverter(): Converter {"); + assertFileContains(enumConverterFile.toPath(), "return object: Converter {"); + assertFileContains(enumConverterFile.toPath(), "override fun convert(source: kotlin.String): PonyType = PonyType.forValue(source)"); + } + @Test public void givenMultipartFormArray_whenGenerateDelegateAndService_thenParameterIsCreatedAsListOfMultipartFile() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); @@ -1192,4 +1232,54 @@ public class KotlinSpringServerCodegenTest { "@NotNull", "@Valid", "@Pattern(regexp=\"^[a-zA-Z0-9]+[a-zA-Z0-9\\\\.\\\\-_]*[a-zA-Z0-9]+$\")"); } + private Map generateFromContract(String url) throws IOException { + return generateFromContract(url, new HashMap<>(), new HashMap<>()); + } + + private Map generateFromContract(String url, Map additionalProperties) throws IOException { + return generateFromContract(url, additionalProperties, new HashMap<>()); + } + + private Map generateFromContract( + String url, + Map additionalProperties, + Map generatorPropertyDefaults + ) throws IOException { + return generateFromContract(url, additionalProperties, generatorPropertyDefaults, codegen -> { + }); + } + + /** + * Generate the contract with additional configuration. + *

+ * use CodegenConfigurator instead of CodegenConfig for easier configuration like in JavaClientCodeGenTest + */ + private Map generateFromContract( + String url, + Map additionalProperties, + Map generatorPropertyDefaults, + Consumer consumer + ) throws IOException { + + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin-spring") + .setAdditionalProperties(additionalProperties) + .setValidateSpec(false) + .setInputSpec(url) + .setLibrary(SPRING_BOOT) + .setOutputDir(output.getAbsolutePath()); + + consumer.accept(configurator); + + ClientOptInput input = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generatorPropertyDefaults.forEach(generator::setGeneratorPropertyDefault); + + return generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + } } diff --git a/samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES b/samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES index 97df5ba6fbb..d35b88d1309 100644 --- a/samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES +++ b/samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES @@ -9,5 +9,6 @@ settings.gradle src/main/kotlin/org/openapitools/api/ApiUtil.kt src/main/kotlin/org/openapitools/api/DefaultApi.kt src/main/kotlin/org/openapitools/api/Exceptions.kt +src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt src/main/kotlin/org/openapitools/model/ApiError.kt src/main/kotlin/org/openapitools/model/ReasonCode.kt diff --git a/samples/server/petstore/kotlin-springboot-integer-enum/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt b/samples/server/petstore/kotlin-springboot-integer-enum/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt new file mode 100644 index 00000000000..8d2f1d58548 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-integer-enum/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt @@ -0,0 +1,26 @@ +package org.openapitools.configuration + +import org.openapitools.model.ReasonCode + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter + +/** + * This class provides Spring Converter beans for the enum models in the OpenAPI specification. + * + * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent + * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than + * `original` or the specification has an integer enum. + */ +@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration") +class EnumConverterConfiguration { + + @Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.reasonCodeConverter"]) + fun reasonCodeConverter(): Converter { + return object: Converter { + override fun convert(source: kotlin.Int): ReasonCode = ReasonCode.forValue(source) + } + } + +} diff --git a/samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES b/samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES index 24c5ef9ccc8..2ea9e1431b3 100644 --- a/samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES +++ b/samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES @@ -12,6 +12,7 @@ src/main/kotlin/org/openapitools/SpringDocConfiguration.kt src/main/kotlin/org/openapitools/api/ApiUtil.kt src/main/kotlin/org/openapitools/api/Exceptions.kt src/main/kotlin/org/openapitools/api/MultipartMixedApiController.kt +src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt src/main/kotlin/org/openapitools/model/MultipartMixedRequestMarker.kt src/main/kotlin/org/openapitools/model/MultipartMixedStatus.kt src/main/resources/application.yaml diff --git a/samples/server/petstore/kotlin-springboot-multipart-request-model/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt b/samples/server/petstore/kotlin-springboot-multipart-request-model/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt new file mode 100644 index 00000000000..5aff967ce62 --- /dev/null +++ b/samples/server/petstore/kotlin-springboot-multipart-request-model/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt @@ -0,0 +1,26 @@ +package org.openapitools.configuration + +import org.openapitools.model.MultipartMixedStatus + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter + +/** + * This class provides Spring Converter beans for the enum models in the OpenAPI specification. + * + * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent + * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than + * `original` or the specification has an integer enum. + */ +@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration") +class EnumConverterConfiguration { + + @Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.multipartMixedStatusConverter"]) + fun multipartMixedStatusConverter(): Converter { + return object: Converter { + override fun convert(source: kotlin.String): MultipartMixedStatus = MultipartMixedStatus.forValue(source) + } + } + +}