Skip to content

Commit 0115d78

Browse files
committed
Implement inheritance and oneOf support in kotlin-client
1 parent 9586099 commit 0115d78

File tree

74 files changed

+3052
-106
lines changed

Some content is hidden

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

74 files changed

+3052
-106
lines changed

bin/configs/kotlin-client-oneOf.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
generatorName: kotlin
2+
outputDir: samples/client/petstore/kotlin-client-oneOf
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/oneof_polymorphism_and_inheritance.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/kotlin-client
5+
additionalProperties:
6+
artifactId: kotlin-client-oneOf
7+
library: jvm-ktor
8+
serializationLibrary: jackson
9+
sortParamsByRequiredFlag: false

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2623,7 +2623,7 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
26232623

26242624
// TODO revise the logic below to set discriminator, xml attributes
26252625
if (supportsInheritance || supportsMixins) {
2626-
m.allVars = new ArrayList<>();
2626+
m.allVars = m.allVars == null ? new ArrayList<>() : m.allVars;
26272627
if (composed.getAllOf() != null) {
26282628
int modelImplCnt = 0; // only one inline object allowed in a ComposedModel
26292629
int modelDiscriminators = 0; // only one discriminator allowed in a ComposedModel

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

Lines changed: 137 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.io.File;
3535
import java.io.IOException;
3636
import java.util.*;
37+
import java.util.function.BiConsumer;
3738
import java.util.function.Function;
3839
import java.util.regex.Pattern;
3940
import java.util.stream.Collectors;
@@ -407,10 +408,40 @@ public String modelFileFolder() {
407408
return outputFolder + File.separator + sourceFolder + File.separator + modelPackage().replace('.', File.separatorChar);
408409
}
409410

411+
private enum KotlinModelType {
412+
INTERFACE("interface", "x-kotlin-interface"), OPEN_CLASS("open class", "x-kotlin-open-class"), DATA_CLASS(
413+
"data class", "x-kotlin-data-class");
414+
415+
final String literal;
416+
final String flag;
417+
418+
KotlinModelType(String literal, String flag) {
419+
this.literal = literal;
420+
this.flag = flag;
421+
}
422+
}
423+
424+
private enum CombinationType {
425+
ALL_OF(it -> it.allOf), ONE_OF(it -> it.oneOf);
426+
final Function<CodegenModel, Set<String>> modelNamesFunc;
427+
428+
CombinationType(Function<CodegenModel, Set<String>> modelNamesFunc) {
429+
this.modelNamesFunc = modelNamesFunc;
430+
}
431+
}
432+
433+
private List<CodegenModel> getModels(CodegenModel cm, CombinationType ct) {
434+
List<CodegenModel> interfaceModels = cm.getInterfaceModels();
435+
Set<String> names = ct.modelNamesFunc.apply(cm);
436+
if (names == null || interfaceModels == null) {
437+
return Collections.emptyList();
438+
}
439+
return interfaceModels.stream().filter(it -> names.contains(it.classname)).collect(Collectors.toList());
440+
}
441+
410442
@Override
411443
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
412444
objs = super.postProcessAllModels(objs);
413-
objs = super.updateAllModels(objs);
414445

415446
if (!additionalModelTypeAnnotations.isEmpty()) {
416447
for (String modelName : objs.keySet()) {
@@ -419,26 +450,118 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
419450
}
420451
}
421452

422-
return objs;
423-
}
453+
final BiConsumer<CodegenModel, CodegenModel> markAsExtends = (childModel, superModel) -> {
454+
((Map<CodegenModel, CodegenModel>) childModel.vendorExtensions.computeIfAbsent("x-implements",
455+
(k) -> new IdentityHashMap<>())).put(superModel, superModel);
456+
superModel.vendorExtensions.put("x-has-implementors", Boolean.TRUE);
457+
};
424458

425-
@Override
426-
public ModelsMap postProcessModels(ModelsMap objs) {
427-
objs = super.postProcessModelsEnum(objs);
428-
for (ModelMap mo : objs.getModels()) {
429-
CodegenModel cm = mo.getModel();
430-
if (cm.getDiscriminator() != null) {
431-
cm.vendorExtensions.put("x-has-data-class-body", true);
432-
break;
459+
for (ModelsMap map : objs.values()) {
460+
for (ModelMap mo : map.getModels()) {
461+
CodegenModel codegenModel = mo.getModel();
462+
for (CodegenModel referencedModel : getModels(codegenModel, CombinationType.ALL_OF)) {
463+
if (!referencedModel.name.equals(codegenModel.parent)) {
464+
markAsExtends.accept(codegenModel, referencedModel);
465+
}
466+
}
467+
for (CodegenModel interfaceModel : getModels(codegenModel, CombinationType.ONE_OF)) {
468+
markAsExtends.accept(interfaceModel, codegenModel);
469+
}
433470
}
471+
}
472+
473+
for (ModelsMap map : objs.values()) {
474+
for (ModelMap mo : map.getModels()) {
475+
CodegenModel cm = mo.getModel();
476+
477+
KotlinModelType kotlinType;
478+
if (cm.getDiscriminator() != null || cm.vendorExtensions.get("x-has-implementors") == Boolean.TRUE) {
479+
kotlinType = KotlinModelType.INTERFACE;
480+
} else if (cm.hasChildren) {
481+
kotlinType = KotlinModelType.OPEN_CLASS;
482+
} else {
483+
kotlinType = KotlinModelType.DATA_CLASS;
484+
}
485+
cm.vendorExtensions.put("x-kotlin-model-type", kotlinType.literal);
486+
cm.vendorExtensions.put(kotlinType.flag, true);
487+
cm.vendorExtensions.put("x-parent-kotlin-interface",
488+
cm.parentModel != null && (cm.parentModel.getDiscriminator() != null || cm.vendorExtensions.get("x-has-implementors") == Boolean.TRUE));
434489

435-
for (CodegenProperty var : cm.vars) {
436-
if (var.isEnum || isSerializableModel()) {
490+
if (kotlinType != KotlinModelType.DATA_CLASS || isSerializableModel() || cm.vars.stream().anyMatch(var -> var.isEnum)) {
437491
cm.vendorExtensions.put("x-has-data-class-body", true);
438-
break;
492+
}
493+
494+
final Map<CodegenModel, CodegenModel> xImplements =
495+
(Map<CodegenModel, CodegenModel>) cm.vendorExtensions.getOrDefault("x-implements",
496+
Collections.emptyMap());
497+
if (!xImplements.isEmpty()) {
498+
cm.vendorExtensions.put("x-kotlin-interfaces-list",
499+
xImplements.keySet().stream().filter(im -> im != cm.parentModel).map(CodegenModel::getClassname).sorted().collect(Collectors.joining(", ")));
500+
}
501+
}
502+
}
503+
504+
// we need to add variables from parent to current model if current is data class
505+
for (ModelsMap map : objs.values()) {
506+
for (ModelMap mo : map.getModels()) {
507+
final CodegenModel cm = mo.getModel();
508+
final CodegenModel parentModel = cm.getParentModel();
509+
final boolean selfIsInterface = Objects.equals(cm.getVendorExtensions().get("x-kotlin-model-type"),
510+
KotlinModelType.INTERFACE.literal);
511+
512+
if (!selfIsInterface && parentModel != null) {
513+
// TODO: handle other interfaces as well, not only parent? need test case
514+
Set<String> alreadyDefined =
515+
cm.getAllVars().stream().map(CodegenProperty::getName).collect(Collectors.toSet());
516+
for (CodegenProperty prop : parentModel.getVars()) {
517+
if (!alreadyDefined.contains(prop.name)) {
518+
cm.allVars.add(prop.clone());
519+
alreadyDefined.add(prop.name);
520+
}
521+
}
522+
}
523+
}
524+
}
525+
526+
// mark all override variables as such
527+
for (ModelsMap map : objs.values()) {
528+
for (ModelMap mo : map.getModels()) {
529+
CodegenModel cm = mo.getModel();
530+
Set<String> inheritedPropertyNames = new TreeSet<>();
531+
532+
Collection<CodegenModel> interfaceModels =
533+
((Map<CodegenModel, CodegenModel>) cm.getVendorExtensions().getOrDefault("x-implements",
534+
Collections.emptyMap())).values();
535+
for (CodegenModel interfaceModel : interfaceModels) {
536+
interfaceModel.vars.stream().map(it -> it.name).forEach(inheritedPropertyNames::add);
537+
interfaceModel.allVars.stream().map(it -> it.name).forEach(inheritedPropertyNames::add);
538+
}
539+
540+
final CodegenModel parentModel = cm.getParentModel();
541+
if (parentModel != null) {
542+
parentModel.vars.stream().map(it -> it.name).forEach(inheritedPropertyNames::add);
543+
parentModel.allVars.stream().map(it -> it.name).forEach(inheritedPropertyNames::add);
544+
}
545+
546+
for (CodegenProperty prop : cm.vars) {
547+
if (inheritedPropertyNames.contains(prop.name)) {
548+
prop.vendorExtensions.put("x-kotlin-override", Boolean.TRUE);
549+
}
550+
}
551+
for (CodegenProperty prop : cm.allVars) {
552+
if (inheritedPropertyNames.contains(prop.name)) {
553+
prop.vendorExtensions.put("x-kotlin-override", Boolean.TRUE);
554+
}
439555
}
440556
}
441557
}
558+
559+
return objs;
560+
}
561+
562+
@Override
563+
public ModelsMap postProcessModels(ModelsMap objs) {
564+
objs = super.postProcessModels(objs);
442565
return postProcessModelsEnum(objs);
443566
}
444567

@@ -890,29 +1013,6 @@ protected boolean needToImport(String type) {
8901013
!type.contains(".");
8911014
}
8921015

893-
@Override
894-
public CodegenModel fromModel(String name, Schema schema) {
895-
CodegenModel m = super.fromModel(name, schema);
896-
m.optionalVars = m.optionalVars.stream().distinct().collect(Collectors.toList());
897-
// Update allVars/requiredVars/optionalVars with isInherited
898-
// Each of these lists contains elements that are similar, but they are all cloned
899-
// via CodegenModel.removeAllDuplicatedProperty and therefore need to be updated
900-
// separately.
901-
// First find only the parent vars via baseName matching
902-
Map<String, CodegenProperty> allVarsMap = m.allVars.stream()
903-
.collect(Collectors.toMap(CodegenProperty::getBaseName, Function.identity()));
904-
allVarsMap.keySet()
905-
.removeAll(m.vars.stream().map(CodegenProperty::getBaseName).collect(Collectors.toSet()));
906-
// Update the allVars
907-
allVarsMap.values().forEach(p -> p.isInherited = true);
908-
// Update any other vars (requiredVars, optionalVars)
909-
Stream.of(m.requiredVars, m.optionalVars)
910-
.flatMap(List::stream)
911-
.filter(p -> allVarsMap.containsKey(p.baseName))
912-
.forEach(p -> p.isInherited = true);
913-
return m;
914-
}
915-
9161016
@Override
9171017
public String toEnumValue(String value, String datatype) {
9181018
if ("kotlin.Int".equals(datatype) || "kotlin.Long".equals(datatype)) {

modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ import {{packageName}}.infrastructure.ITransformForStorage
6262
@Deprecated(message = "This schema is deprecated.")
6363
{{/isDeprecated}}
6464
{{>additionalModelTypeAnnotations}}
65-
{{#nonPublicApi}}internal {{/nonPublicApi}}{{#discriminator}}interface{{/discriminator}}{{^discriminator}}data class{{/discriminator}} {{classname}}{{^discriminator}} (
65+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{vendorExtensions.x-kotlin-model-type}} {{classname}}{{^vendorExtensions.x-kotlin-interface}} (
6666

6767
{{#allVars}}
6868
{{#required}}{{>data_class_req_var}}{{/required}}{{^required}}{{>data_class_opt_var}}{{/required}}{{^-last}},{{/-last}}
6969

7070
{{/allVars}}
71-
){{/discriminator}}{{#parent}}{{^serializableModel}}{{^parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{#serializableModel}}{{^parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{^serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{#serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}{{^parcelizeModels}} : Serializable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}{{#parcelizeModels}} : Serializable, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#generateRoomModels}}{{#parent}}, {{/parent}}{{^discriminator}}{{^parent}}:{{/parent}} ITransformForStorage<{{classname}}RoomModel>{{/discriminator}}{{/generateRoomModels}}{{#vendorExtensions.x-has-data-class-body}} {
71+
){{/vendorExtensions.x-kotlin-interface}}{{#parent}}{{^serializableModel}}{{^parcelizeModels}} : {{{parent}}}{{^isMap}}{{^isArray}}{{>data_class_parent_args}}{{/isArray}}{{/isMap}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{#serializableModel}}{{^parcelizeModels}} : {{{parent}}}{{^isMap}}{{^isArray}}{{>data_class_parent_args}}{{/isArray}}{{/isMap}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{^serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{^isMap}}{{^isArray}}{{>data_class_parent_args}}{{/isArray}}{{/isMap}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{#serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{^isMap}}{{^isArray}}{{>data_class_parent_args}}{{/isArray}}{{/isMap}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}{{^parcelizeModels}} : Serializable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}{{#parcelizeModels}} : Serializable, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#generateRoomModels}}{{#parent}}, {{/parent}}{{^discriminator}}{{^parent}}:{{/parent}} ITransformForStorage<{{classname}}RoomModel>{{/discriminator}}{{/generateRoomModels}}{{#vendorExtensions.x-kotlin-interfaces-list}}{{#parent}},{{/parent}}{{^parent}} : {{/parent}}{{vendorExtensions.x-kotlin-interfaces-list}}{{/vendorExtensions.x-kotlin-interfaces-list}}{{#vendorExtensions.x-has-data-class-body}} {
7272
{{/vendorExtensions.x-has-data-class-body}}
7373
{{#generateRoomModels}}
7474
companion object { }
@@ -83,9 +83,9 @@ import {{packageName}}.infrastructure.ITransformForStorage
8383
private const val serialVersionUID: Long = 123
8484
}
8585
{{/serializableModel}}
86-
{{#discriminator}}{{#vars}}{{#required}}
86+
{{#vendorExtensions.x-kotlin-interface}}{{#vars}}{{#required}}
8787
{{>interface_req_var}}{{/required}}{{^required}}
88-
{{>interface_opt_var}}{{/required}}{{/vars}}{{/discriminator}}
88+
{{>interface_opt_var}}{{/required}}{{/vars}}{{/vendorExtensions.x-kotlin-interface}}
8989
{{#hasEnums}}
9090
{{#vars}}
9191
{{#isEnum}}

modules/openapi-generator/src/main/resources/kotlin-client/data_class_opt_var.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818
{{#deprecated}}
1919
@Deprecated(message = "This property is deprecated.")
2020
{{/deprecated}}
21-
{{#multiplatform}}@SerialName(value = "{{{vendorExtensions.x-base-name-literal}}}") {{/multiplatform}}{{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isArray}}{{#isList}}{{#uniqueItems}}kotlin.collections.{{#modelMutable}}Mutable{{/modelMutable}}Set{{/uniqueItems}}{{^uniqueItems}}kotlin.collections.{{#modelMutable}}Mutable{{/modelMutable}}List{{/uniqueItems}}{{/isList}}{{^isList}}kotlin.Array{{/isList}}<{{^items.isEnum}}{{^items.isPrimitiveType}}{{^items.isModel}}{{#kotlinx_serialization}}@Contextual {{/kotlinx_serialization}}{{/items.isModel}}{{/items.isPrimitiveType}}{{{items.dataType}}}{{/items.isEnum}}{{#items.isEnum}}{{classname}}.{{{nameInCamelCase}}}{{/items.isEnum}}>{{/isArray}}{{^isEnum}}{{^isArray}}{{{dataType}}}{{/isArray}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}.{{{nameInCamelCase}}}{{/isArray}}{{/isEnum}}? = {{^defaultValue}}null{{/defaultValue}}{{#defaultValue}}{{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{^multiplatform}}{{{dataType}}}("{{{defaultValue}}}"){{/multiplatform}}{{#multiplatform}}({{{defaultValue}}}).toDouble(){{/multiplatform}}{{/isNumber}}{{/defaultValue}}
21+
{{#multiplatform}}@SerialName(value = "{{{vendorExtensions.x-base-name-literal}}}") {{/multiplatform}}{{>model_override}}{{>model_open}}{{>modelMutable}} {{{name}}}: {{#isArray}}{{#isList}}{{#uniqueItems}}kotlin.collections.{{#modelMutable}}Mutable{{/modelMutable}}Set{{/uniqueItems}}{{^uniqueItems}}kotlin.collections.{{#modelMutable}}Mutable{{/modelMutable}}List{{/uniqueItems}}{{/isList}}{{^isList}}kotlin.Array{{/isList}}<{{^items.isEnum}}{{^items.isPrimitiveType}}{{^items.isModel}}{{#kotlinx_serialization}}@Contextual {{/kotlinx_serialization}}{{/items.isModel}}{{/items.isPrimitiveType}}{{{items.dataType}}}{{/items.isEnum}}{{#items.isEnum}}{{classname}}.{{{nameInCamelCase}}}{{/items.isEnum}}>{{/isArray}}{{^isEnum}}{{^isArray}}{{{dataType}}}{{/isArray}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}.{{{nameInCamelCase}}}{{/isArray}}{{/isEnum}}? = {{^defaultValue}}null{{/defaultValue}}{{#defaultValue}}{{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{^multiplatform}}{{{dataType}}}("{{{defaultValue}}}"){{/multiplatform}}{{#multiplatform}}({{{defaultValue}}}).toDouble(){{/multiplatform}}{{/isNumber}}{{/defaultValue}}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{{^vendorExtensions.x-parent-kotlin-interface}}({{#parentModel.allVars}}
2+
{{name}} = {{name}}{{^-last}},{{/-last}}{{/parentModel.allVars}}){{/vendorExtensions.x-parent-kotlin-interface}}

0 commit comments

Comments
 (0)