Skip to content

Commit bfb6938

Browse files
[Bug][kotlin-spring] add a Spring type converter for enum values #21564 (#21579)
* [kotlin-spring] add a Spring type converter for enum values #21564 * [kotlin-spring] simplify unit test for inner enum converter * update samples * code review feedback; move containsEnums to ModelUtils * code review feedback; provide comment for generated EnumConverterConfiguration.kt * update samples --------- Co-authored-by: Chris Gual <[email protected]>
1 parent 31089c0 commit bfb6938

File tree

9 files changed

+204
-16
lines changed

9 files changed

+204
-16
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.samskivert.mustache.Mustache;
2121
import com.samskivert.mustache.Mustache.Lambda;
2222
import com.samskivert.mustache.Template;
23+
import io.swagger.v3.oas.models.Components;
2324
import io.swagger.v3.oas.models.OpenAPI;
2425
import io.swagger.v3.oas.models.Operation;
2526
import lombok.Getter;
@@ -33,6 +34,7 @@
3334
import org.openapitools.codegen.model.ModelsMap;
3435
import org.openapitools.codegen.model.OperationMap;
3536
import org.openapitools.codegen.model.OperationsMap;
37+
import org.openapitools.codegen.utils.ModelUtils;
3638
import org.openapitools.codegen.utils.URLPathUtils;
3739
import org.slf4j.Logger;
3840
import org.slf4j.LoggerFactory;
@@ -768,6 +770,11 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera
768770
public void preprocessOpenAPI(OpenAPI openAPI) {
769771
super.preprocessOpenAPI(openAPI);
770772

773+
if (SPRING_BOOT.equals(library) && ModelUtils.containsEnums(this.openAPI)) {
774+
supportingFiles.add(new SupportingFile("converter.mustache",
775+
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.kt"));
776+
}
777+
771778
if (!additionalProperties.containsKey(TITLE)) {
772779
// The purpose of the title is for:
773780
// - README documentation

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

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
package org.openapitools.codegen.languages;
1919

2020
import com.samskivert.mustache.Mustache;
21-
import io.swagger.v3.oas.models.Components;
2221
import io.swagger.v3.oas.models.OpenAPI;
2322
import io.swagger.v3.oas.models.Operation;
2423
import io.swagger.v3.oas.models.PathItem;
@@ -40,6 +39,7 @@
4039
import org.openapitools.codegen.templating.mustache.SplitStringLambda;
4140
import org.openapitools.codegen.templating.mustache.SpringHttpStatusLambda;
4241
import org.openapitools.codegen.templating.mustache.TrimWhitespaceLambda;
42+
import org.openapitools.codegen.utils.ModelUtils;
4343
import org.openapitools.codegen.utils.ProcessUtils;
4444
import org.openapitools.codegen.utils.URLPathUtils;
4545
import org.slf4j.Logger;
@@ -648,20 +648,6 @@ public void processOpts() {
648648
supportsAdditionalPropertiesWithComposedSchema = true;
649649
}
650650

651-
private boolean containsEnums() {
652-
if (openAPI == null) {
653-
return false;
654-
}
655-
656-
Components components = this.openAPI.getComponents();
657-
if (components == null || components.getSchemas() == null) {
658-
return false;
659-
}
660-
661-
return components.getSchemas().values().stream()
662-
.anyMatch(it -> it.getEnum() != null && !it.getEnum().isEmpty());
663-
}
664-
665651
private boolean supportLibraryUseTags() {
666652
return SPRING_BOOT.equals(library) || SPRING_CLOUD_LIBRARY.equals(library);
667653
}
@@ -696,7 +682,7 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera
696682
public void preprocessOpenAPI(OpenAPI openAPI) {
697683
super.preprocessOpenAPI(openAPI);
698684

699-
if (SPRING_BOOT.equals(library) && containsEnums()) {
685+
if (SPRING_BOOT.equals(library) && ModelUtils.containsEnums(this.openAPI)) {
700686
supportingFiles.add(new SupportingFile("converter.mustache",
701687
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.java"));
702688
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2435,6 +2435,19 @@ public static boolean isMetadataOnlySchema(Schema schema) {
24352435
schema.getContentSchema() != null);
24362436
}
24372437

2438+
/**
2439+
* Returns true if the OpenAPI specification contains any schemas which are enums.
2440+
* @param openAPI OpenAPI specification
2441+
* @return true if the OpenAPI specification contains any schemas which are enums.
2442+
*/
2443+
public static boolean containsEnums(OpenAPI openAPI) {
2444+
Map<String, Schema> schemaMap = getSchemas(openAPI);
2445+
if (schemaMap.isEmpty()) {
2446+
return false;
2447+
}
2448+
2449+
return schemaMap.values().stream().anyMatch(ModelUtils::isEnumSchema);
2450+
}
24382451

24392452
@FunctionalInterface
24402453
private interface OpenAPISchemaVisitor {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package {{configPackage}}
2+
3+
{{#models}}
4+
{{#model}}
5+
{{#isEnum}}
6+
import {{modelPackage}}.{{classname}}
7+
{{/isEnum}}
8+
{{/model}}
9+
{{/models}}
10+
11+
import org.springframework.context.annotation.Bean
12+
import org.springframework.context.annotation.Configuration
13+
import org.springframework.core.convert.converter.Converter
14+
15+
/**
16+
* This class provides Spring Converter beans for the enum models in the OpenAPI specification.
17+
*
18+
* By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
19+
* correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
20+
* `original` or the specification has an integer enum.
21+
*/
22+
@Configuration(value = "{{configPackage}}.enumConverterConfiguration")
23+
class EnumConverterConfiguration {
24+
25+
{{#models}}
26+
{{#model}}
27+
{{#isEnum}}
28+
@Bean(name = ["{{configPackage}}.EnumConverterConfiguration.{{classVarName}}Converter"])
29+
fun {{classVarName}}Converter(): Converter<{{{dataType}}}, {{classname}}> {
30+
return object: Converter<{{{dataType}}}, {{classname}}> {
31+
override fun convert(source: {{{dataType}}}): {{classname}} = {{classname}}.forValue(source)
32+
}
33+
}
34+
{{/isEnum}}
35+
{{/model}}
36+
{{/models}}
37+
38+
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
import io.swagger.v3.oas.models.info.Info;
66
import io.swagger.v3.oas.models.servers.Server;
77
import io.swagger.v3.parser.core.models.ParseOptions;
8+
import java.util.HashMap;
9+
import java.util.function.Consumer;
810
import org.apache.commons.io.FileUtils;
911
import org.assertj.core.api.Assertions;
1012
import org.jetbrains.annotations.NotNull;
1113
import org.openapitools.codegen.ClientOptInput;
1214
import org.openapitools.codegen.CodegenConstants;
1315
import org.openapitools.codegen.DefaultGenerator;
1416
import org.openapitools.codegen.TestUtils;
17+
import org.openapitools.codegen.config.CodegenConfigurator;
18+
import org.openapitools.codegen.java.assertions.JavaFileAssert;
1519
import org.openapitools.codegen.kotlin.KotlinTestUtils;
1620
import org.openapitools.codegen.languages.KotlinSpringServerCodegen;
1721
import org.openapitools.codegen.languages.features.CXFServerFeatures;
@@ -31,8 +35,10 @@
3135
import java.util.function.Function;
3236
import java.util.stream.Collectors;
3337

38+
import static org.assertj.core.api.Assertions.assertThat;
3439
import static org.openapitools.codegen.TestUtils.assertFileContains;
3540
import static org.openapitools.codegen.TestUtils.assertFileNotContains;
41+
import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT;
3642
import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.ANNOTATION_LIBRARY;
3743
import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DOCUMENTATION_PROVIDER;
3844

@@ -748,6 +754,40 @@ public void useBeanValidationGenerateAnnotationsForRequestBody() throws IOExcept
748754
);
749755
}
750756

757+
@Test
758+
public void contractWithoutEnumDoesNotContainEnumConverter() throws IOException {
759+
Map<String, File> output = generateFromContract("src/test/resources/3_0/generic.yaml");
760+
761+
assertThat(output).doesNotContainKey("EnumConverterConfiguration.kt");
762+
}
763+
764+
@Test
765+
public void contractWithEnumContainsEnumConverter() throws IOException {
766+
Map<String, File> output = generateFromContract("src/test/resources/3_0/enum.yaml");
767+
768+
File enumConverterFile = output.get("EnumConverterConfiguration.kt");
769+
assertThat(enumConverterFile).isNotNull();
770+
assertFileContains(enumConverterFile.toPath(), "fun typeConverter(): Converter<kotlin.String, Type> {");
771+
assertFileContains(enumConverterFile.toPath(), "return object: Converter<kotlin.String, Type> {");
772+
assertFileContains(enumConverterFile.toPath(), "override fun convert(source: kotlin.String): Type = Type.forValue(source)");
773+
}
774+
775+
@Test
776+
public void contractWithResolvedInnerEnumContainsEnumConverter() throws IOException {
777+
Map<String, File> files = generateFromContract(
778+
"src/test/resources/3_0/inner_enum.yaml",
779+
new HashMap<>(),
780+
new HashMap<>(),
781+
configurator -> configurator.addInlineSchemaOption("RESOLVE_INLINE_ENUMS", "true")
782+
);
783+
784+
File enumConverterFile = files.get("EnumConverterConfiguration.kt");
785+
assertThat(enumConverterFile).isNotNull();
786+
assertFileContains(enumConverterFile.toPath(), "fun ponyTypeConverter(): Converter<kotlin.String, PonyType> {");
787+
assertFileContains(enumConverterFile.toPath(), "return object: Converter<kotlin.String, PonyType> {");
788+
assertFileContains(enumConverterFile.toPath(), "override fun convert(source: kotlin.String): PonyType = PonyType.forValue(source)");
789+
}
790+
751791
@Test
752792
public void givenMultipartFormArray_whenGenerateDelegateAndService_thenParameterIsCreatedAsListOfMultipartFile() throws IOException {
753793
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
@@ -1192,4 +1232,54 @@ public void testValidationsInQueryParams_issue21238_Api_Delegate() throws IOExce
11921232
"@NotNull", "@Valid", "@Pattern(regexp=\"^[a-zA-Z0-9]+[a-zA-Z0-9\\\\.\\\\-_]*[a-zA-Z0-9]+$\")");
11931233
}
11941234

1235+
private Map<String, File> generateFromContract(String url) throws IOException {
1236+
return generateFromContract(url, new HashMap<>(), new HashMap<>());
1237+
}
1238+
1239+
private Map<String, File> generateFromContract(String url, Map<String, Object> additionalProperties) throws IOException {
1240+
return generateFromContract(url, additionalProperties, new HashMap<>());
1241+
}
1242+
1243+
private Map<String, File> generateFromContract(
1244+
String url,
1245+
Map<String, Object> additionalProperties,
1246+
Map<String, String> generatorPropertyDefaults
1247+
) throws IOException {
1248+
return generateFromContract(url, additionalProperties, generatorPropertyDefaults, codegen -> {
1249+
});
1250+
}
1251+
1252+
/**
1253+
* Generate the contract with additional configuration.
1254+
* <p>
1255+
* use CodegenConfigurator instead of CodegenConfig for easier configuration like in JavaClientCodeGenTest
1256+
*/
1257+
private Map<String, File> generateFromContract(
1258+
String url,
1259+
Map<String, Object> additionalProperties,
1260+
Map<String, String> generatorPropertyDefaults,
1261+
Consumer<CodegenConfigurator> consumer
1262+
) throws IOException {
1263+
1264+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
1265+
output.deleteOnExit();
1266+
1267+
final CodegenConfigurator configurator = new CodegenConfigurator()
1268+
.setGeneratorName("kotlin-spring")
1269+
.setAdditionalProperties(additionalProperties)
1270+
.setValidateSpec(false)
1271+
.setInputSpec(url)
1272+
.setLibrary(SPRING_BOOT)
1273+
.setOutputDir(output.getAbsolutePath());
1274+
1275+
consumer.accept(configurator);
1276+
1277+
ClientOptInput input = configurator.toClientOptInput();
1278+
DefaultGenerator generator = new DefaultGenerator();
1279+
generator.setGenerateMetadata(false);
1280+
generatorPropertyDefaults.forEach(generator::setGeneratorPropertyDefault);
1281+
1282+
return generator.opts(input).generate().stream()
1283+
.collect(Collectors.toMap(File::getName, Function.identity()));
1284+
}
11951285
}

samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ settings.gradle
99
src/main/kotlin/org/openapitools/api/ApiUtil.kt
1010
src/main/kotlin/org/openapitools/api/DefaultApi.kt
1111
src/main/kotlin/org/openapitools/api/Exceptions.kt
12+
src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
1213
src/main/kotlin/org/openapitools/model/ApiError.kt
1314
src/main/kotlin/org/openapitools/model/ReasonCode.kt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.openapitools.configuration
2+
3+
import org.openapitools.model.ReasonCode
4+
5+
import org.springframework.context.annotation.Bean
6+
import org.springframework.context.annotation.Configuration
7+
import org.springframework.core.convert.converter.Converter
8+
9+
/**
10+
* This class provides Spring Converter beans for the enum models in the OpenAPI specification.
11+
*
12+
* By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
13+
* correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
14+
* `original` or the specification has an integer enum.
15+
*/
16+
@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration")
17+
class EnumConverterConfiguration {
18+
19+
@Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.reasonCodeConverter"])
20+
fun reasonCodeConverter(): Converter<kotlin.Int, ReasonCode> {
21+
return object: Converter<kotlin.Int, ReasonCode> {
22+
override fun convert(source: kotlin.Int): ReasonCode = ReasonCode.forValue(source)
23+
}
24+
}
25+
26+
}

samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ src/main/kotlin/org/openapitools/SpringDocConfiguration.kt
1212
src/main/kotlin/org/openapitools/api/ApiUtil.kt
1313
src/main/kotlin/org/openapitools/api/Exceptions.kt
1414
src/main/kotlin/org/openapitools/api/MultipartMixedApiController.kt
15+
src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
1516
src/main/kotlin/org/openapitools/model/MultipartMixedRequestMarker.kt
1617
src/main/kotlin/org/openapitools/model/MultipartMixedStatus.kt
1718
src/main/resources/application.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.openapitools.configuration
2+
3+
import org.openapitools.model.MultipartMixedStatus
4+
5+
import org.springframework.context.annotation.Bean
6+
import org.springframework.context.annotation.Configuration
7+
import org.springframework.core.convert.converter.Converter
8+
9+
/**
10+
* This class provides Spring Converter beans for the enum models in the OpenAPI specification.
11+
*
12+
* By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
13+
* correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
14+
* `original` or the specification has an integer enum.
15+
*/
16+
@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration")
17+
class EnumConverterConfiguration {
18+
19+
@Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.multipartMixedStatusConverter"])
20+
fun multipartMixedStatusConverter(): Converter<kotlin.String, MultipartMixedStatus> {
21+
return object: Converter<kotlin.String, MultipartMixedStatus> {
22+
override fun convert(source: kotlin.String): MultipartMixedStatus = MultipartMixedStatus.forValue(source)
23+
}
24+
}
25+
26+
}

0 commit comments

Comments
 (0)