Skip to content

Commit 2ffb4ee

Browse files
authored
Feature/add kotlin enum unknown default case support (#23936)
* update docs for generators * only apply flag to kotlin spring generator * add unknown default value to moustache, and make tests to assert correctness * address review comments * fix typo in cliOption to CliOption... * oneline the override in the moustache file * add unknown default enum case to the dataclass moustache file * test the inclusion of the enum default for the dataclass files * create resources file for testing enum dataclass default * fallback is non nullable, resolves to 11184809 * update kotlin spring docs so that enumUnknownDefaultCase docs is equal to spring docs version * alter test assertions to only assert on UNKNOWN_DEFAULT_OPEN_API and also include it in existing integration test * enum is correctly generated, but one of the cases uses incorrect capitalisation? * alter whitespace in enum class moustache, and generate dataclass moustache enums based on enumvars * alter white space in moustache files, remove useless it->it.value==... * also make testcase for enum integers in kotlin spring :) * tests now assert based on correctly, originally, capitalized unknown_default_open_api * test based on the lowercased value of unknown_default_open_api, and also alter the way temp files are checked.. * dont test on the enum vars themselves, but rely on the ci workflow for correct files, and test presence/absence of the unknown_default_open_api * update docs based on provided script * add extra line after main merge... very much esthetics :) * fix compilation error from errornous merge in gh webui * fix whitespace formatting after review * alter whitespaces, and include the altered generated sample files * alter whitespaces in moustache files, and also in generated petstore files :) * alter whitespaces in enumClass moustache file, to remove empty newlines * alter whitespaces in dataclass moustache file, to remove weird tabs and spaces.. * add newline after enum class definition, and before enum listings! * fix tab/space formatting issues * final tab alignments.. * bump openapi version to 3_0_1 * generate samples, after merging main and some other hassles..
1 parent 3fdd2ce commit 2ffb4ee

73 files changed

Lines changed: 400 additions & 179 deletions

File tree

Some content is hidden

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

‎bin/configs/kotlin-spring-boot-integer-enum.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ additionalProperties:
1313
annotationLibrary: none
1414
documentationProvider: none
1515
enumPropertyNaming: UPPERCASE
16+
enumUnknownDefaultCase: "true"
1617
useEnumValueInterface: "true"

‎bin/configs/kotlin-spring-boot-oneof-enum-discriminator.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ additionalProperties:
1111
useSwaggerUI: "false"
1212
interfaceOnly: "true"
1313
useSpringBoot3: "true"
14+
enumUnknownDefaultCase: "true"

‎docs/generators/kotlin-spring.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
3434
|delegatePattern|Whether to generate the server files using the delegate pattern| |false|
3535
|documentationProvider|Select the OpenAPI documentation provider.|<dl><dt>**none**</dt><dd>Do not publish an OpenAPI specification.</dd><dt>**source**</dt><dd>Publish the original input OpenAPI specification.</dd><dt>**springdoc**</dt><dd>Generate an OpenAPI 3 specification using SpringDoc.</dd></dl>|springdoc|
3636
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original|
37+
|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response. With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false|
3738
|exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true|
3839
|generateJsonIncludeAnnotations|Whether to generate policy @JsonInclude annotations on model properties. When true, emits spec-honest annotations (required-field protection and the optional non-nullable policy from optionalNonNullPropertyJsonInclude). When false, none are generated and the global ObjectMapper owns inclusion. When left unset it defaults to false (7.23.0-equivalent output) and logs a warning; set it explicitly to silence the warning. A per-property override set via the `x-jackson-json-include-policy` vendor extension is always honored regardless of this flag.| |false|
3940
|generateJsonSetterNullsAnnotations|Whether to generate @JsonSetter(nulls = ...) annotations on optional non-nullable model properties. When true, emits @JsonSetter (Nulls.FAIL when openApiNullable is true, otherwise Nulls.SKIP) so an explicit null in the payload is handled explicitly. When false, none are generated and deserialization null-handling defers to the global ObjectMapper. When left unset it defaults to false (7.23.0-equivalent output) and logs a warning; set it explicitly to silence the warning.| |false|

‎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
@@ -388,6 +388,8 @@ public KotlinSpringServerCodegen() {
388388
+ "deserialization null-handling defers to the global ObjectMapper. When left unset it defaults to "
389389
+ "false (7.23.0-equivalent output) and logs a warning; set it explicitly to silence the warning.", false);
390390
cliOptions.add(CliOption.newBoolean(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces));
391+
cliOptions.add(CliOption.newBoolean(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE_DESC).defaultValue("false"));
392+
391393
addOption(CodegenConstants.TYPE_INFO_DEFAULT_IMPLS, CodegenConstants.TYPE_INFO_DEFAULT_IMPLS_DESC, "empty map");
392394
addSwitch(CodegenConstants.USE_ENUM_VALUE_INTERFACE, CodegenConstants.USE_ENUM_VALUE_INTERFACE_DESC, useEnumValueInterface);
393395
addSwitch(CodegenConstants.OPENAPI_NULLABLE,
@@ -762,6 +764,11 @@ public void processOpts() {
762764
}
763765
writePropertyBack(SKIP_DEFAULT_INTERFACE, skipDefaultInterface);
764766

767+
768+
if (additionalProperties.containsKey(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE)) {
769+
setEnumUnknownDefaultCase(Boolean.parseBoolean(additionalProperties.get(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE).toString()));
770+
}
771+
765772
if (additionalProperties.containsKey(REACTIVE)) {
766773
if (SPRING_CLOUD_LIBRARY.equals(library)) {
767774
throw new IllegalArgumentException("Currently, reactive option doesn't supported by Spring Cloud");

‎modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,26 @@
7676
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
7777
*/
7878
enum class {{{nameInPascalCase}}}(@get:JsonValue {{#useEnumValueInterface}}{{^isContainer}}override {{/isContainer}}{{/useEnumValueInterface}}val value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}) {{#vendorExtensions.x-kotlin-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}} {{/vendorExtensions.x-kotlin-implements}}{
79-
{{#allowableValues}}{{#enumVars}}
80-
{{{name}}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}};
8179
80+
{{#allowableValues}}{{#enumVars}} {{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}}
81+
{{/enumVars}}{{/allowableValues}}
8282
companion object {
8383
@JvmStatic
8484
@JsonCreator
8585
fun forValue(value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}): {{{nameInPascalCase}}} {
86-
return values().firstOrNull{it -> it.value == value}
86+
return values().firstOrNull{ it.value == value }
87+
{{#enumUnknownDefaultCase}}
88+
{{#allowableValues}}
89+
{{#enumVars}}
90+
{{#-last}}
91+
?: {{{name}}}
92+
{{/-last}}
93+
{{/enumVars}}
94+
{{/allowableValues}}
95+
{{/enumUnknownDefaultCase}}
96+
{{^enumUnknownDefaultCase}}
8797
?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{nameInPascalCase}}'")
98+
{{/enumUnknownDefaultCase}}
8899
}
89100
}
90101
}
Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
/**
2-
* {{{description}}}
3-
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
4-
*/
5-
enum class {{classname}}(@get:JsonValue {{#useEnumValueInterface}}override {{/useEnumValueInterface}}val value: {{dataType}}) {{#vendorExtensions.x-kotlin-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}} {{/vendorExtensions.x-kotlin-implements}}{
6-
{{#allowableValues}}{{#enumVars}}
7-
{{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}};
2+
* {{{description}}}
3+
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
4+
*/
5+
enum class {{classname}}(@get:JsonValue {{#useEnumValueInterface}}override {{/useEnumValueInterface}}val value: {{dataType}}){{#vendorExtensions.x-kotlin-implements}}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-kotlin-implements}} {
86
7+
{{#allowableValues}}{{#enumVars}} {{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}}
8+
{{/enumVars}}{{/allowableValues}}
99
companion object {
1010
@JvmStatic
1111
@JsonCreator
1212
fun forValue(value: {{dataType}}): {{classname}} {
13-
return values().firstOrNull{it -> it.value == value}
14-
?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{classname}}'")
13+
return values().firstOrNull{ it.value == value }
14+
{{#enumUnknownDefaultCase}}
15+
{{#allowableValues}}
16+
{{#enumVars}}
17+
{{#-last}}
18+
?: {{&name}}
19+
{{/-last}}
20+
{{/enumVars}}
21+
{{/allowableValues}}
22+
{{/enumUnknownDefaultCase}}
23+
{{^enumUnknownDefaultCase}}
24+
?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{classname}}'")
25+
{{/enumUnknownDefaultCase}}
1526
}
1627
}
17-
}
28+
}

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.swagger.parser.OpenAPIParser;
55
import io.swagger.v3.oas.models.OpenAPI;
66
import io.swagger.v3.oas.models.info.Info;
7+
import io.swagger.v3.oas.models.media.Schema;
78
import io.swagger.v3.oas.models.servers.Server;
89
import io.swagger.v3.oas.models.Operation;
910
import io.swagger.v3.parser.core.models.ParseOptions;
@@ -12,6 +13,8 @@
1213
import org.jetbrains.annotations.NotNull;
1314
import org.openapitools.codegen.ClientOptInput;
1415
import org.openapitools.codegen.CodegenConstants;
16+
import org.openapitools.codegen.CodegenModel;
17+
import org.openapitools.codegen.CodegenProperty;
1518
import org.openapitools.codegen.DefaultGenerator;
1619
import org.openapitools.codegen.TestUtils;
1720
import org.openapitools.codegen.config.CodegenConfigurator;
@@ -25,6 +28,8 @@
2528
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures.AnnotationLibrary;
2629
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DocumentationProvider;
2730
import org.openapitools.codegen.languages.features.SwaggerUIFeatures;
31+
import org.openapitools.codegen.model.ModelsMap;
32+
import org.openapitools.codegen.model.ModelMap;
2833
import org.testng.Assert;
2934
import org.testng.annotations.DataProvider;
3035
import org.testng.annotations.Test;
@@ -7715,6 +7720,100 @@ public void schemaMappingWithNullableAllOfRendersNullableKotlinProperty() throws
77157720
assertThat(content).contains("com.example.ExternalModel?");
77167721
}
77177722

7723+
@Test(description = "test enumUnknownDefaultCase option")
7724+
public void testEnumUnknownDefaultCaseOption() {
7725+
final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
7726+
7727+
// Test default value is false
7728+
codegen.processOpts();
7729+
Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.FALSE);
7730+
7731+
// Test setting via additionalProperties
7732+
codegen.additionalProperties().put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true");
7733+
codegen.processOpts();
7734+
Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.TRUE);
7735+
}
7736+
7737+
@Test(description = "test enum generation with enumUnknownDefaultCase enabled")
7738+
public void testEnumGenerationWithUnknownDefaultCase() throws IOException {
7739+
Map<String, File> files = generateFromContract(
7740+
"src/test/resources/3_1/enum_unknown_default_case.yaml",
7741+
Map.of(
7742+
CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, true
7743+
),
7744+
new HashMap<>(),
7745+
configurator -> {}
7746+
);
7747+
7748+
File colorFile = files.get("ColorEnum.kt");
7749+
assertThat(colorFile).isNotNull();
7750+
7751+
String content = Files.readString(colorFile.toPath());
7752+
7753+
assertThat(content).contains("unknown_default_open_api(\"unknown_default_open_api\")");
7754+
assertThat(content).contains("?: unknown_default_open_api");
7755+
}
7756+
7757+
@Test(description = "test enum generation with enumUnknownDefaultCase disabled")
7758+
public void testEnumGenerationWithoutUnknownDefaultCase() throws IOException {
7759+
Map<String, File> files = generateFromContract(
7760+
"src/test/resources/3_1/enum_unknown_default_case.yaml",
7761+
Map.of(
7762+
CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, false
7763+
),
7764+
new HashMap<>(),
7765+
configurator -> {}
7766+
);
7767+
7768+
File colorFile = files.get("ColorEnum.kt");
7769+
assertThat(colorFile).isNotNull();
7770+
7771+
String content = Files.readString(colorFile.toPath());
7772+
7773+
assertThat(content).doesNotContain("unknown_default_open_api(\"unknown_default_open_api\")");
7774+
assertThat(content).doesNotContain("?: unknown_default_open_api");
7775+
}
7776+
7777+
@Test(description = "test data class generation containing inline enum with enumUnknownDefaultCase enabled")
7778+
public void testDataClassGenerationWithUnknownDefaultCase() throws IOException {
7779+
Map<String, File> files = generateFromContract(
7780+
"src/test/resources/3_1/dataclass_unknown_default_case.yaml",
7781+
Map.of(
7782+
CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, true
7783+
),
7784+
new HashMap<>(),
7785+
configurator -> {}
7786+
);
7787+
7788+
File colorResponseFile = files.get("ColorResponse.kt");
7789+
assertThat(colorResponseFile).isNotNull();
7790+
7791+
String content = Files.readString(colorResponseFile.toPath());
7792+
7793+
assertThat(content).contains("unknown_default_open_api(\"unknown_default_open_api\")");
7794+
assertThat(content).contains("?: unknown_default_open_api");
7795+
}
7796+
7797+
@Test(description = "test data class generation containing inline enum with enumUnknownDefaultCase disabled")
7798+
public void testDataClassGenerationWithoutUnknownDefaultCase() throws IOException {
7799+
Map<String, File> files = generateFromContract(
7800+
"src/test/resources/3_1/dataclass_unknown_default_case.yaml",
7801+
Map.of(
7802+
CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, false
7803+
),
7804+
new HashMap<>(),
7805+
configurator -> {}
7806+
);
7807+
7808+
File colorResponseFile = files.get("ColorResponse.kt");
7809+
assertThat(colorResponseFile).isNotNull();
7810+
7811+
String content = Files.readString(colorResponseFile.toPath());
7812+
7813+
assertThat(content).doesNotContain("unknown_default_open_api(\"unknown_default_open_api\")");
7814+
assertThat(content).doesNotContain("?: unknown_default_open_api");
7815+
}
7816+
77187817
// ========== x-jackson-default-impl / typeInfoDefaultImpls tests ==========
77197818

77207819
@Test(description = "x-jackson-default-impl on deduction schema emits defaultImpl in @JsonTypeInfo")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Dataclass Nested Enum Test API
4+
description: API for testing inline/nested enum generation in data classes with enumUnknownDefaultCase
5+
version: 1.0.0
6+
paths:
7+
/colors:
8+
get:
9+
summary: Get color
10+
operationId: getColor
11+
responses:
12+
'200':
13+
description: Successful response
14+
content:
15+
application/json:
16+
schema:
17+
$ref: '#/components/schemas/ColorResponse'
18+
components:
19+
schemas:
20+
ColorResponse:
21+
type: object
22+
required:
23+
- color
24+
properties:
25+
color:
26+
type: string
27+
description: Available colors
28+
enum:
29+
- RED
30+
- GREEN
31+
- BLUE
32+
- YELLOW
33+
priority:
34+
type: integer
35+
description: Priority levels
36+
enum:
37+
- 1
38+
- 2
39+
- 3
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Enum Test API
4+
description: API for testing enum generation with enumUnknownDefaultCase
5+
version: 1.0.0
6+
paths:
7+
/colors:
8+
get:
9+
summary: Get color
10+
operationId: getColor
11+
responses:
12+
'200':
13+
description: Successful response
14+
content:
15+
application/json:
16+
schema:
17+
$ref: '#/components/schemas/ColorResponse'
18+
components:
19+
schemas:
20+
ColorResponse:
21+
type: object
22+
required:
23+
- color
24+
- status
25+
properties:
26+
color:
27+
$ref: '#/components/schemas/ColorEnum'
28+
status:
29+
$ref: '#/components/schemas/StatusEnum'
30+
priority:
31+
$ref: '#/components/schemas/PriorityEnum'
32+
ColorEnum:
33+
type: string
34+
description: Available colors
35+
enum:
36+
- RED
37+
- GREEN
38+
- BLUE
39+
- YELLOW
40+
StatusEnum:
41+
type: string
42+
description: Status values
43+
enum:
44+
- PENDING
45+
- APPROVED
46+
- REJECTED
47+
- IN_PROGRESS
48+
PriorityEnum:
49+
type: integer
50+
description: Priority levels
51+
enum:
52+
- 1
53+
- 2
54+
- 3
55+
- 4
56+
- 5

‎samples/server/others/kotlin-springboot/oneOf-discriminator-const/src/main/kotlin/org/openapitools/model/Cat.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ data class Cat(
4444
lazy("lazy"),
4545
adventurous("adventurous"),
4646
aggressive("aggressive");
47-
47+
4848
companion object {
4949
@JvmStatic
5050
@JsonCreator
5151
fun forValue(value: kotlin.String): HuntingSkill {
52-
return values().firstOrNull{it -> it.value == value}
52+
return values().firstOrNull{ it.value == value }
5353
?: throw IllegalArgumentException("Unexpected value '$value' for enum 'HuntingSkill'")
5454
}
5555
}

0 commit comments

Comments
 (0)