Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ public KotlinServerCodegen() {
// Enable proper oneOf/anyOf discriminator handling for polymorphism
legacyDiscriminatorBehavior = false;

// Generate sealed interfaces for oneOf schemas (mirrors KotlinSpringServerCodegen)
useOneOfInterfaces = true;

modifyFeatureSet(features -> features
.includeDocumentationFeatures(DocumentationFeature.Readme)
.wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML))
Expand Down Expand Up @@ -617,6 +620,12 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
owner.getRequiredVars().add(parentDiscriminatorProp);
owner.getAllVars().add(parentDiscriminatorProp);

// Children are retyped to kotlin.String and marked inherited below, so they can
// override an abstract declaration on the oneOf interface. Only then may the
// interface declare the discriminator - see oneof_interface.mustache.
owner.getVendorExtensions().put("x-one-of-interface-declares-discriminator", true);
owner.getDiscriminator().getVendorExtensions().put("x-one-of-interface-declares-discriminator", true);

// Parent now has properties (just the discriminator)
hasParentProperties = true;

Expand Down Expand Up @@ -698,9 +707,11 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
childModel.getAllVars().add(discriminatorProp);
}

// Set parent constructor args for the discriminator property
childModel.getVendorExtensions().put("x-parent-ctor-args",
discriminatorVarName + " = " + discriminatorVarName);
// Set parent constructor args only when parent is a sealed class (not interface)
if (!useOneOfInterfaces) {
childModel.getVendorExtensions().put("x-parent-ctor-args",
discriminatorVarName + " = " + discriminatorVarName);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,13 @@ sealed class {{classname}}
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}},
{{/-last}}{{/optionalVars}}
){{#parent}} : {{{.}}}({{#vendorExtensions.x-parent-ctor-args}}{{{.}}}{{/vendorExtensions.x-parent-ctor-args}}){{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{^parcelizeModels}}{{#serializableModel}}: Serializable {{/serializableModel}}{{/parcelizeModels}}{{#parcelizeModels}}{{#serializableModel}} : Parcelable, Serializable {{/serializableModel}}{{/parcelizeModels}}{{/parent}}
){{#parent}} : {{{.}}}{{#vendorExtensions.x-parent-ctor-args}}({{{.}}}){{/vendorExtensions.x-parent-ctor-args}}{{/parent}}{{^parent}}{{! no newline
}}{{#vendorExtensions.x-implements}}{{! <- oneOf sealed interfaces this model is a member of
}}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{! no newline
}}{{#-last}}{{#parcelizeModels}}, Parcelable{{/parcelizeModels}}{{#serializableModel}}, Serializable{{/serializableModel}}{{/-last}}{{! no newline
}}{{/vendorExtensions.x-implements}}{{! no newline
}}{{^vendorExtensions.x-implements}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{^parcelizeModels}}{{#serializableModel}}: Serializable {{/serializableModel}}{{/parcelizeModels}}{{#parcelizeModels}}{{#serializableModel}} : Parcelable, Serializable {{/serializableModel}}{{/parcelizeModels}}{{/vendorExtensions.x-implements}}{{! no newline
}}{{/parent}}
{{#vendorExtensions.x-has-data-class-body}}
{
{{/vendorExtensions.x-has-data-class-body}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
{{/description}}

@JsonProperty("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}")
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
{{/description}}

@JsonProperty("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}")
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import {{javaxPackage}}.validation.Valid
{{/useBeanValidation}}
{{#models}}
{{#model}}
{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{>oneof_model}}{{/oneOf}}{{^oneOf}}{{#isAlias}}typealias {{classname}} = {{{dataType}}}{{/isAlias}}{{^isAlias}}{{>data_class}}{{/isAlias}}{{/oneOf}}{{/isEnum}}
{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{#oneOf}}{{>oneof_model}}{{/oneOf}}{{^oneOf}}{{#isAlias}}typealias {{classname}} = {{{dataType}}}{{/isAlias}}{{^isAlias}}{{>data_class}}{{/isAlias}}{{/oneOf}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}}
{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ package {{modelPackage}}

{{#models}}
{{#model}}
{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{>data_class}}{{/isEnum}}
{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>data_class}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}}
{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* {{{description}}}
*/
{{#discriminator}}
{{>typeInfoAnnotation}}
{{/discriminator}}
{{#additionalModelTypeAnnotations}}
{{{.}}}
{{/additionalModelTypeAnnotations}}
{{#vendorExtensions.x-class-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-class-extra-annotation}}
sealed interface {{classname}}{{#vendorExtensions.x-kotlin-implements}}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-kotlin-implements}} {
{{#discriminator}}
{{#vendorExtensions.x-one-of-interface-declares-discriminator}}
val {{propertyName}}: {{{propertyType}}}
{{/vendorExtensions.x-one-of-interface-declares-discriminator}}
{{/discriminator}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ public void givenSchemaObjectPropertyNameContainsDollarSignWhenGenerateThenDolla
// ==================== Polymorphism and Discriminator Tests ====================

@Test
public void oneOfWithDiscriminator_generatesSealedClassWithDiscriminatorProperty() throws IOException {
public void oneOfWithDiscriminator_generatesSealedInterfaceWithDiscriminatorProperty() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Expand All @@ -341,20 +341,20 @@ public void oneOfWithDiscriminator_generatesSealedClassWithDiscriminatorProperty
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";
Path petModel = Paths.get(outputPath + "/models/Pet.kt");

// Pet should be a sealed class with Jackson polymorphism annotations and discriminator property
// Pet should be a sealed interface with Jackson polymorphism annotations and discriminator property
assertFileContains(
petModel,
"sealed class Pet(",
"open val petType: kotlin.String",
"sealed interface Pet",
"val petType:",
"@com.fasterxml.jackson.annotation.JsonTypeInfo",
"property = \"petType\"",
"visible = true",
"@com.fasterxml.jackson.annotation.JsonSubTypes"
);
assertFileNotContains(petModel, "sealed class", "typealias");
}

@Test
public void oneOfWithDiscriminator_generatesChildrenWithOverrideDiscriminatorProperty() throws IOException {
public void oneOfWithDiscriminator_generatesChildrenImplementingInterface() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Expand All @@ -369,35 +369,14 @@ public void oneOfWithDiscriminator_generatesChildrenWithOverrideDiscriminatorPro

String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";

// Cat should have petType as overridden non-nullable String with default value
// Cat and Dog should be data classes (not typealias)
Path catModel = Paths.get(outputPath + "/models/Cat.kt");
assertFileContains(
catModel,
"data class Cat(",
"override val petType: kotlin.String = \"cat\"",
") : Pet(petType = petType)"
);
// Should NOT be nullable
assertFileNotContains(
catModel,
"petType: kotlin.String?",
"petType: kotlin.Any"
);
assertFileContains(catModel, "data class Cat(");
assertFileNotContains(catModel, "typealias", "kotlin.Any");

// Dog should have petType as overridden non-nullable String with default value
Path dogModel = Paths.get(outputPath + "/models/Dog.kt");
assertFileContains(
dogModel,
"data class Dog(",
"override val petType: kotlin.String = \"dog\"",
") : Pet(petType = petType)"
);
// Should NOT be nullable
assertFileNotContains(
dogModel,
"petType: kotlin.String?",
"petType: kotlin.Any"
);
assertFileContains(dogModel, "data class Dog(");
assertFileNotContains(dogModel, "typealias", "kotlin.Any");
}

@Test
Expand Down Expand Up @@ -466,7 +445,7 @@ public void allOfWithDiscriminator_generatesChildrenWithOverrideProperties() thr
}

@Test
public void polymorphismWithoutDiscriminator_generatesRegularDataClass() throws IOException {
public void polymorphismWithoutDiscriminator_generatesSealedInterface() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Expand All @@ -482,14 +461,12 @@ public void polymorphismWithoutDiscriminator_generatesRegularDataClass() throws
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";
Path petModel = Paths.get(outputPath + "/models/Pet.kt");

// Without discriminator, Pet should be a regular data class (not sealed)
assertFileContains(
petModel,
"data class Pet("
);
// Without discriminator, Pet should be a sealed interface (oneOf) without Jackson annotations
assertFileContains(petModel, "sealed interface Pet");
assertFileNotContains(
petModel,
"sealed class",
"typealias",
"@com.fasterxml.jackson.annotation.JsonTypeInfo",
"@com.fasterxml.jackson.annotation.JsonSubTypes"
);
Expand All @@ -513,12 +490,17 @@ public void fixJacksonJsonTypeInfoInheritance_canBeDisabled() throws IOException
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";
Path petModel = Paths.get(outputPath + "/models/Pet.kt");

// When fixJacksonJsonTypeInfoInheritance is false and parent has no properties,
// visible should be false for oneOf pattern
assertFileContains(
petModel,
"visible = false"
);
// With useOneOfInterfaces, Pet is a sealed interface.
// When fixJacksonJsonTypeInfoInheritance is false, visible should be false.
assertFileContains(petModel, "sealed interface Pet", "visible = false");

// The interface must NOT declare the discriminator here: the children are only retyped to
// kotlin.String and marked inherited (i.e. rendered with `override`) when the Jackson fix is
// enabled. Declaring it anyway makes the subtypes fail to compile with
// "'petType' hides member of supertype 'Pet' and needs an 'override' modifier".
assertFileNotContains(petModel, "val petType");
assertFileNotContains(Paths.get(outputPath + "/models/Cat.kt"), "override val petType");
assertFileNotContains(Paths.get(outputPath + "/models/Dog.kt"), "override val petType");
}

// ==================== useTags for JAXRS-SPEC ====================
Expand Down Expand Up @@ -827,4 +809,59 @@ public void testCommonPathDoesNotShadowOtherTags_jaxrsSpec() throws IOException
assertFileContains(tagOneApi, "@Path(\"/foo/bar/one\")", "@Path(\"/foo/bar/two\")");
assertFileContains(tagTwoApi, "@Path(\"/foo/bar/three\")", "@Path(\"/baz/bar/four\")");
}

@Test(description = "oneOf with discriminator generates sealed interface, not typealias")
public void testOneOfWithDiscriminatorGeneratesSealedInterface() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

KotlinServerCodegen codegen = new KotlinServerCodegen();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC);

new DefaultGenerator().opts(new ClientOptInput()
.openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism-and-discriminator.yaml"))
.config(codegen))
.generate();

String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server/models";

// Pet must be a sealed interface with Jackson annotations, not a typealias
assertFileContains(Paths.get(outputPath + "/Pet.kt"),
"sealed interface Pet",
"@com.fasterxml.jackson.annotation.JsonTypeInfo",
"property = \"petType\"",
"@com.fasterxml.jackson.annotation.JsonSubTypes"
);
assertFileNotContains(Paths.get(outputPath + "/Pet.kt"), "typealias", "kotlin.Any");

// Subtypes must extend the sealed interface and override its discriminator
assertFileContains(Paths.get(outputPath + "/Cat.kt"), "data class Cat", ") : Pet", "override val petType");
assertFileNotContains(Paths.get(outputPath + "/Cat.kt"), "typealias", "kotlin.Any");
assertFileContains(Paths.get(outputPath + "/Dog.kt"), "data class Dog", ") : Pet", "override val petType");
assertFileNotContains(Paths.get(outputPath + "/Dog.kt"), "typealias", "kotlin.Any");
}

@Test(description = "oneOf without discriminator generates a sealed interface that subtypes implement")
public void testOneOfWithoutDiscriminatorSubtypesImplementInterface() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

KotlinServerCodegen codegen = new KotlinServerCodegen();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(LIBRARY, JAVALIN6);

new DefaultGenerator().opts(new ClientOptInput()
.openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism.yaml"))
.config(codegen))
.generate();

String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server/models";

// Without a discriminator the interface has no members, so it would be uninhabited unless the
// oneOf members declare it as a supertype via x-implements.
assertFileContains(Paths.get(outputPath + "/Pet.kt"), "sealed interface Pet");
assertFileContains(Paths.get(outputPath + "/Cat.kt"), "data class Cat", ") : Pet");
assertFileContains(Paths.get(outputPath + "/Dog.kt"), "data class Dog", ") : Pet");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package org.openapitools.server.models
* A pet cat
* @param huntingSkill The measured skill for hunting
* @param petType
* @param name
*/
data class Cat(
/* The measured skill for hunting */
Expand All @@ -25,7 +26,7 @@ data class Cat(

@field:com.fasterxml.jackson.annotation.JsonProperty("petType")
val petType: kotlin.Any? = null
) : Pet()
) : Pet
{
/**
* The measured skill for hunting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package org.openapitools.server.models
* A pet dog
* @param petType
* @param packSize the size of the pack the dog is from
* @param name
*/
data class Dog(

Expand All @@ -25,5 +26,5 @@ data class Dog(

@field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
val packSize: kotlin.Int = 0
) : Pet()
) : Pet

Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ import org.openapitools.server.models.Dog
com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"),
com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog")
)
sealed class Pet
sealed interface Pet {
}

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package org.openapitools.server.models
* A pet cat
* @param huntingSkill The measured skill for hunting
* @param petType
* @param name
*/
data class Cat(
/* The measured skill for hunting */
Expand All @@ -26,7 +27,7 @@ data class Cat(
@field:com.fasterxml.jackson.annotation.JsonProperty("petType")
override val petType: kotlin.String = "cat",

) : Pet(petType = petType)
) : Pet
{
/**
* The measured skill for hunting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package org.openapitools.server.models
* A pet dog
* @param petType
* @param packSize the size of the pack the dog is from
* @param name
*/
data class Dog(

Expand All @@ -25,5 +26,5 @@ data class Dog(

@field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
val packSize: kotlin.Int = 0
) : Pet(petType = petType)
) : Pet

Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,13 @@ import org.openapitools.server.models.Dog

/**
* A pet
* @param petType
*/
@com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "petType", visible = true)
@com.fasterxml.jackson.annotation.JsonSubTypes(
com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"),
com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog")
)
sealed class Pet(

@field:com.fasterxml.jackson.annotation.JsonProperty("petType")
open val petType: kotlin.String

)
sealed interface Pet {
val petType: kotlin.String
}

Loading