Skip to content

Commit 6278512

Browse files
authored
[kotlin-spring][server] Feat: Allow implementation of arbitrary interface in DTOs (similar to x-implements from java-spring) (#21950)
* add basic implementation and tests * improve test a bit * modify kotlin-spring.md * add x-kotlin-implements also to enum * update samples & properly define implemented vendor extension * use enum.getName() instead of hardcoded string as key in vendor extension map * fix docs * fix test openapi spec and test * add samples for x-kotlin-implements * add samples for x-kotlin-implements to proper output folder * fix * revert unwanted changes * move to correct place * fix mustache template * add to samples-kotlin-server.yaml * reuse 1 open api schema for everything. Add also case where interface extends interface. * add warn logs when x-kotlin-implements-fields is used without x-kotlin-implements to improve usability * remove unnecessary generated files * remove unnecessary generated files * remove "status" inner enum from Pet as it fails to properly import as Pet.Status in implementations. This is a separate bug - not caused by x-kotlin-implements
1 parent 5c04b75 commit 6278512

File tree

136 files changed

+2650
-97
lines changed

Some content is hidden

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

136 files changed

+2650
-97
lines changed

.github/workflows/samples-kotlin-server.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
- samples/server/petstore/kotlin-springboot-source-swagger1
3939
- samples/server/petstore/kotlin-springboot-source-swagger2
4040
- samples/server/petstore/kotlin-springboot-springfox
41+
- samples/server/petstore/kotlin-springboot-x-kotlin-implements
4142
- samples/server/petstore/kotlin-server/ktor
4243
- samples/server/petstore/kotlin-server/ktor2
4344
- samples/server/petstore/kotlin-server/jaxrs-spec
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
generatorName: kotlin-spring
2+
outputDir: samples/server/petstore/kotlin-springboot-x-kotlin-implements
3+
library: spring-boot
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-x-kotlin-implements.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
6+
additionalProperties:
7+
documentationProvider: none
8+
annotationLibrary: none
9+
useSwaggerUI: "false"
10+
serviceImplementation: "false"
11+
skipDefaultInterface: "true"
12+
interfaceOnly: "true"
13+
serializableModel: "true"
14+
beanValidations: "true"

docs/generators/kotlin-spring.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
6767
|x-discriminator-value|Used with model inheritance to specify value for discriminator that identifies current model|MODEL|
6868
|x-field-extra-annotation|List of custom annotations to be added to property|FIELD, OPERATION_PARAMETER|null
6969
|x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null
70+
|x-kotlin-implements|Ability to specify interfaces that model must implement|MODEL|empty array
71+
|x-kotlin-implements-fields|Specify attributes that are implemented by the interface(s) added via `x-kotlin-implements`|MODEL|empty array
7072

7173

7274
## IMPORT MAPPING

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
public enum VendorExtension {
1111

1212
X_IMPLEMENTS("x-implements", ExtensionLevel.MODEL, "Ability to specify interfaces that model must implements", "empty array"),
13+
X_KOTLIN_IMPLEMENTS("x-kotlin-implements", ExtensionLevel.MODEL, "Ability to specify interfaces that model must implement", "empty array"),
14+
X_KOTLIN_IMPLEMENTS_FIELDS("x-kotlin-implements-fields", ExtensionLevel.MODEL, "Specify attributes that are implemented by the interface(s) added via `x-kotlin-implements`", "empty array"),
1315
X_SPRING_PAGINATED("x-spring-paginated", ExtensionLevel.OPERATION, "Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object.", "false"),
1416
X_SPRING_PROVIDE_ARGS("x-spring-provide-args", ExtensionLevel.OPERATION, "Allows adding additional hidden parameters in the API specification to allow access to content such as header values or properties", "empty array"),
1517
X_DISCRIMINATOR_VALUE("x-discriminator-value", ExtensionLevel.MODEL, "Used with model inheritance to specify value for discriminator that identifies current model", ""),

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,16 @@ protected boolean needToImport(String type) {
835835
@Override
836836
public CodegenModel fromModel(String name, Schema schema) {
837837
CodegenModel m = super.fromModel(name, schema);
838+
List<String> implementedInterfacesClasses = (List<String>) m.getVendorExtensions().getOrDefault(VendorExtension.X_KOTLIN_IMPLEMENTS.getName(), List.of());
839+
List<String> implementedInterfacesFields = Optional.ofNullable((List<String>) m.getVendorExtensions().get(VendorExtension.X_KOTLIN_IMPLEMENTS_FIELDS.getName()))
840+
.map(xKotlinImplementsFields -> {
841+
if (implementedInterfacesClasses.isEmpty() && !xKotlinImplementsFields.isEmpty()) {
842+
LOGGER.warn("Annotating {} with {} without {} is not supported. {} will be ignored.",
843+
name, VendorExtension.X_KOTLIN_IMPLEMENTS_FIELDS.getName(), VendorExtension.X_KOTLIN_IMPLEMENTS.getName(),
844+
VendorExtension.X_KOTLIN_IMPLEMENTS_FIELDS.getName());
845+
}
846+
return xKotlinImplementsFields;
847+
}).orElse(List.of());
838848
m.optionalVars = m.optionalVars.stream().distinct().collect(Collectors.toList());
839849
// Update allVars/requiredVars/optionalVars with isInherited
840850
// Each of these lists contains elements that are similar, but they are all cloned
@@ -850,7 +860,9 @@ public CodegenModel fromModel(String name, Schema schema) {
850860
// Update any other vars (requiredVars, optionalVars)
851861
Stream.of(m.requiredVars, m.optionalVars)
852862
.flatMap(List::stream)
853-
.filter(p -> allVarsMap.containsKey(p.baseName))
863+
.filter(p -> allVarsMap.containsKey(p.baseName)
864+
|| implementedInterfacesFields.contains(p.baseName)
865+
)
854866
.forEach(p -> p.isInherited = true);
855867
return m;
856868
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,8 @@ public List<VendorExtension> getSupportedVendorExtensions() {
10121012
extensions.add(VendorExtension.X_DISCRIMINATOR_VALUE);
10131013
extensions.add(VendorExtension.X_FIELD_EXTRA_ANNOTATION);
10141014
extensions.add(VendorExtension.X_PATTERN_MESSAGE);
1015+
extensions.add(VendorExtension.X_KOTLIN_IMPLEMENTS);
1016+
extensions.add(VendorExtension.X_KOTLIN_IMPLEMENTS_FIELDS);
10151017
return extensions;
10161018
}
10171019

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,34 @@
1717
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
1818
{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>dataClassOptVar}}{{^-last}},
1919
{{/-last}}{{/optionalVars}}
20-
) {{/discriminator}}{{#parent}}: {{{.}}}{{#serializableModel}}, Serializable{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}: Serializable{{/serializableModel}}{{/parent}}{
20+
){{/discriminator}}{{! no newline
21+
}}{{#parent}} : {{{.}}}{{! no newline
22+
}}{{#serializableModel}}{{! no newline
23+
}}{{^vendorExtensions.x-kotlin-implements}}, Serializable{{/vendorExtensions.x-kotlin-implements}}{{! no newline
24+
}}{{#vendorExtensions.x-kotlin-implements}}, Serializable, {{! no newline
25+
}}{{#-first}} {{{.}}}{{/-first}}{{! no newline
26+
}}{{^-first}}, {{{.}}}{{/-first}}{{! no newline
27+
}}{{/vendorExtensions.x-kotlin-implements}}{{! no newline
28+
}}{{/serializableModel}}{{! no newline
29+
}}{{^serializableModel}}{{! no newline
30+
}}{{#vendorExtensions.x-kotlin-implements}}, {{{.}}}{{/vendorExtensions.x-kotlin-implements}}{{! no newline
31+
}}{{/serializableModel}}{{! no newline
32+
}}{{/parent}}{{! no newline
33+
}}{{^parent}}{{! no newline
34+
}}{{#serializableModel}}{{! no newline
35+
}}{{^vendorExtensions.x-kotlin-implements}} : Serializable{{/vendorExtensions.x-kotlin-implements}}{{! no newline
36+
}}{{#vendorExtensions.x-kotlin-implements}}{{! no newline
37+
}}{{#-first}} : Serializable, {{{.}}}{{/-first}}{{! no newline
38+
}}{{^-first}}, {{{.}}}{{/-first}}{{! no newline
39+
}}{{/vendorExtensions.x-kotlin-implements}}{{! no newline
40+
}}{{/serializableModel}}{{! no newline
41+
}}{{^serializableModel}}{{! no newline
42+
}}{{#vendorExtensions.x-kotlin-implements}}{{! no newline
43+
}}{{#-first}} : {{{.}}}{{/-first}}{{! no newline
44+
}}{{^-first}}, {{{.}}}{{/-first}}{{! no newline
45+
}}{{/vendorExtensions.x-kotlin-implements}}{{! no newline
46+
}}{{/serializableModel}}{{! no newline
47+
}}{{/parent}} {
2148
{{#discriminator}}
2249
{{#requiredVars}}
2350
{{>interfaceReqVar}}{{! prevent indent}}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* {{{description}}}
33
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
44
*/
5-
enum class {{classname}}(@get:JsonValue val value: {{dataType}}) {
5+
enum class {{classname}}(@get:JsonValue val value: {{dataType}}) {{#vendorExtensions.x-kotlin-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}} {{/vendorExtensions.x-kotlin-implements}}{
66
{{#allowableValues}}{{#enumVars}}
77
{{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}};
88

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

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,10 +946,80 @@ public void generateSerializableModel() throws Exception {
946946

947947
generator.opts(input).generate();
948948

949+
Path path = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/model/Pet.kt");
949950
assertFileContains(
950-
Paths.get(outputPath + "/src/main/kotlin/org/openapitools/model/Pet.kt"),
951+
path,
951952
"import java.io.Serializable",
952-
") : Serializable{",
953+
") : Serializable {",
954+
"private const val serialVersionUID: kotlin.Long = 1"
955+
);
956+
}
957+
@Test
958+
public void generateSerializableModelWithXimplements() throws Exception {
959+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
960+
output.deleteOnExit();
961+
String outputPath = output.getAbsolutePath().replace('\\', '/');
962+
963+
KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
964+
codegen.setOutputDir(output.getAbsolutePath());
965+
codegen.additionalProperties().put(CodegenConstants.SERIALIZABLE_MODEL, true);
966+
967+
ClientOptInput input = new ClientOptInput()
968+
.openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/petstore-with-x-kotlin-implements.yaml"))
969+
.config(codegen);
970+
DefaultGenerator generator = new DefaultGenerator();
971+
972+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
973+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
974+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
975+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
976+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
977+
978+
generator.opts(input).generate();
979+
980+
Path path = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/model/Dog.kt");
981+
assertFileContains(
982+
path,
983+
"import java.io.Serializable",
984+
"@get:JsonProperty(\"likesFetch\", required = true) override val likesFetch: kotlin.Boolean,",
985+
") : Pet, Serializable, com.some.pack.Fetchable {",
986+
"private const val serialVersionUID: kotlin.Long = 1"
987+
);
988+
}
989+
990+
@Test
991+
public void generateNonSerializableModelWithXimplements() throws Exception {
992+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
993+
output.deleteOnExit();
994+
String outputPath = output.getAbsolutePath().replace('\\', '/');
995+
996+
KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
997+
codegen.setOutputDir(output.getAbsolutePath());
998+
999+
ClientOptInput input = new ClientOptInput()
1000+
.openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/petstore-with-x-kotlin-implements.yaml"))
1001+
.config(codegen);
1002+
DefaultGenerator generator = new DefaultGenerator();
1003+
1004+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
1005+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
1006+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
1007+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
1008+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
1009+
1010+
generator.opts(input).generate();
1011+
1012+
Path path = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/model/Dog.kt");
1013+
assertFileContains(
1014+
path,
1015+
"@get:JsonProperty(\"likesFetch\", required = true) override val likesFetch: kotlin.Boolean,",
1016+
") : Pet, com.some.pack.Fetchable {"
1017+
);
1018+
assertFileNotContains(
1019+
path,
1020+
"import java.io.Serializable",
1021+
") : Pet, Serializable, com.some.pack.Fetchable {",
1022+
") : Pet, Serializable {",
9531023
"private const val serialVersionUID: kotlin.Long = 1"
9541024
);
9551025
}

0 commit comments

Comments
 (0)