diff --git a/docs/generators/kotlin-server.md b/docs/generators/kotlin-server.md index 08125378d996..b5f7b5a74766 100644 --- a/docs/generators/kotlin-server.md +++ b/docs/generators/kotlin-server.md @@ -34,6 +34,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools| |implicitHeaders|Skip header parameters in the generated API methods.| |false| |interfaceOnly|Whether to generate only API interface stubs without the server files. This option is currently supported only when using jaxrs-spec library.| |false| +|inheritanceMode|Strategy for model inheritance generation. Values: `none`, `sealed`, `abstract`, `composition`.|
**none**
Flatten inheritance metadata (no discriminator/parent shaping).
**sealed**
Use sealed/interface-oriented shaping (supported for `ktor`, `javalin5`, and `javalin6`; rejected for `jaxrs-spec`).
**abstract**
Use abstract base class shaping.
**composition**
Flatten inheritance and generate discriminator owners as composition wrappers (`value: Any`).
|sealed (ktor, javalin5/javalin6), abstract (jaxrs-spec)| |library|library template (sub-template)|
**ktor**
ktor framework
**ktor2**
ktor (2.x) framework
**jaxrs-spec**
JAX-RS spec only
**javalin5**
Javalin 5
**javalin6**
Javalin 6
|ktor| |modelMutable|Create mutable models| |false| |omitGradleWrapper|Whether to omit Gradle wrapper for creating a sub project.| |false| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index ec6cfeb9f787..831523ce5044 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -109,6 +110,7 @@ public class KotlinServerCodegen extends AbstractKotlinCodegen implements BeanVa @Getter @Setter private Boolean delegatePatternEnabled = false; + private String inheritanceMode; /** * Constructs an instance of `KotlinServerCodegen`. @@ -184,6 +186,17 @@ public KotlinServerCodegen() { addSwitch(USE_JAKARTA_EE, Constants.USE_JAKARTA_EE_DESC, useJakartaEe); addSwitch(Constants.FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE, Constants.FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE_DESC, fixJacksonJsonTypeInfoInheritance); addSwitch(Constants.DELEGATE_PATTERN, Constants.DELEGATE_PATTERN_DESC, getDelegatePatternEnabled()); + addOption(Constants.INHERITANCE_MODE, Constants.INHERITANCE_MODE_DESC, + defaultInheritanceModeForLibrary(DEFAULT_LIBRARY), buildInheritanceModeOptions()); + } + + private Map buildInheritanceModeOptions() { + Map options = new HashMap<>(); + options.put(Constants.INHERITANCE_MODE_NONE, "Flat models (no inheritance-aware shaping)"); + options.put(Constants.INHERITANCE_MODE_SEALED, "Sealed/interface oriented inheritance"); + options.put(Constants.INHERITANCE_MODE_ABSTRACT, "Abstract base classes and concrete children"); + options.put(Constants.INHERITANCE_MODE_COMPOSITION, "Composition wrappers for polymorphism"); + return options; } @Override @@ -271,6 +284,37 @@ public void processOpts() { LOGGER.info("`library` option is empty. Default to {}", DEFAULT_LIBRARY); } + inheritanceMode = defaultInheritanceModeForLibrary(library); + if (additionalProperties.containsKey(Constants.INHERITANCE_MODE)) { + String configuredInheritanceMode = String.valueOf(additionalProperties.get(Constants.INHERITANCE_MODE)); + if (!isValidInheritanceMode(configuredInheritanceMode)) { + throw new IllegalArgumentException(String.format(Locale.ROOT, + "Invalid %s value '%s'. Supported values are: %s, %s, %s, %s", + Constants.INHERITANCE_MODE, + configuredInheritanceMode, + Constants.INHERITANCE_MODE_NONE, + Constants.INHERITANCE_MODE_SEALED, + Constants.INHERITANCE_MODE_ABSTRACT, + Constants.INHERITANCE_MODE_COMPOSITION)); + } + inheritanceMode = configuredInheritanceMode; + } + + if (Constants.JAXRS_SPEC.equals(library) && Constants.INHERITANCE_MODE_SEALED.equals(inheritanceMode)) { + throw new IllegalArgumentException(String.format(Locale.ROOT, + "Library '%s' does not support inheritanceMode '%s'. Use '%s', '%s', or '%s'.", + Constants.JAXRS_SPEC, + Constants.INHERITANCE_MODE_SEALED, + Constants.INHERITANCE_MODE_NONE, + Constants.INHERITANCE_MODE_ABSTRACT, + Constants.INHERITANCE_MODE_COMPOSITION)); + } + additionalProperties.put(Constants.INHERITANCE_MODE, inheritanceMode); + additionalProperties.put(Constants.X_INHERITANCE_MODE_NONE, Constants.INHERITANCE_MODE_NONE.equals(inheritanceMode)); + additionalProperties.put(Constants.X_INHERITANCE_MODE_SEALED, Constants.INHERITANCE_MODE_SEALED.equals(inheritanceMode)); + additionalProperties.put(Constants.X_INHERITANCE_MODE_ABSTRACT, Constants.INHERITANCE_MODE_ABSTRACT.equals(inheritanceMode)); + additionalProperties.put(Constants.X_INHERITANCE_MODE_COMPOSITION, Constants.INHERITANCE_MODE_COMPOSITION.equals(inheritanceMode)); + if (isKtor()) { typeMapping.put("date-time", "kotlin.String"); typeMapping.put("DateTime", "kotlin.String"); @@ -436,12 +480,36 @@ public static class Constants { public static final String USE_TAGS_DESC = "use tags for creating interface and controller classnames."; public static final String DELEGATE_PATTERN = "delegatePattern"; public static final String DELEGATE_PATTERN_DESC = "Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library."; + public static final String INHERITANCE_MODE = "inheritanceMode"; + public static final String INHERITANCE_MODE_DESC = "Strategy for model inheritance generation. Values: none, sealed, abstract, composition."; + public static final String INHERITANCE_MODE_NONE = "none"; + public static final String INHERITANCE_MODE_SEALED = "sealed"; + public static final String INHERITANCE_MODE_ABSTRACT = "abstract"; + public static final String INHERITANCE_MODE_COMPOSITION = "composition"; + public static final String X_INHERITANCE_MODE_NONE = "x-inheritance-mode-none"; + public static final String X_INHERITANCE_MODE_SEALED = "x-inheritance-mode-sealed"; + public static final String X_INHERITANCE_MODE_ABSTRACT = "x-inheritance-mode-abstract"; + public static final String X_INHERITANCE_MODE_COMPOSITION = "x-inheritance-mode-composition"; } @Override public Map postProcessAllModels(Map objs) { objs = super.postProcessAllModels(objs); + if (Constants.INHERITANCE_MODE_NONE.equals(inheritanceMode)) { + flattenInheritanceForNoneMode(objs); + return objs; + } + + if (Constants.INHERITANCE_MODE_COMPOSITION.equals(inheritanceMode)) { + applyCompositionMode(objs); + return objs; + } + + if (!isInheritancePostProcessingEnabled()) { + return objs; + } + // For libraries that use Jackson, set up parent-child relationships for discriminator children // This enables proper polymorphism support with @JsonTypeInfo and @JsonSubTypes annotations if (usesJacksonSerialization()) { @@ -920,4 +988,112 @@ private static String getCommonPath(String path1, String path2) { } return builder.toString(); } + + private static String defaultInheritanceModeForLibrary(String currentLibrary) { + return Constants.JAXRS_SPEC.equals(currentLibrary) + ? Constants.INHERITANCE_MODE_ABSTRACT + : Constants.INHERITANCE_MODE_SEALED; + } + + private static boolean isValidInheritanceMode(String value) { + return Constants.INHERITANCE_MODE_NONE.equals(value) + || Constants.INHERITANCE_MODE_SEALED.equals(value) + || Constants.INHERITANCE_MODE_ABSTRACT.equals(value) + || Constants.INHERITANCE_MODE_COMPOSITION.equals(value); + } + + private static void flattenInheritanceForNoneMode(Map objs) { + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel model = modelMap.getModel(); + + model.setDiscriminator(null); + model.setParent(null); + model.getVendorExtensions().remove("x-parent-ctor-args"); + model.getVendorExtensions().remove("x-discriminator-has-parent-properties"); + model.getVendorExtensions().remove("x-discriminator-visible-true"); + + if (model.interfaces != null) { + model.interfaces.clear(); + } + + for (CodegenProperty prop : model.getAllVars()) { + prop.isInherited = false; + } + for (CodegenProperty prop : model.getVars()) { + prop.isInherited = false; + } + for (CodegenProperty prop : model.getRequiredVars()) { + prop.isInherited = false; + } + for (CodegenProperty prop : model.getOptionalVars()) { + prop.isInherited = false; + } + } + } + } + + private static void applyCompositionMode(Map objs) { + Set discriminatorOwners = new HashSet<>(); + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel model = modelMap.getModel(); + if (model.getDiscriminator() != null + && model.getDiscriminator().getMappedModels() != null + && !model.getDiscriminator().getMappedModels().isEmpty()) { + discriminatorOwners.add(model.getClassname()); + } + } + } + + flattenInheritanceForNoneMode(objs); + + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel model = modelMap.getModel(); + if (discriminatorOwners.contains(model.getClassname())) { + replaceWithCompositionWrapper(model); + } + } + } + } + + private static void replaceWithCompositionWrapper(CodegenModel model) { + CodegenProperty wrapperValue = new CodegenProperty(); + wrapperValue.baseName = "value"; + wrapperValue.name = "value"; + wrapperValue.dataType = "kotlin.Any"; + wrapperValue.datatypeWithEnum = "kotlin.Any"; + wrapperValue.required = true; + wrapperValue.isNullable = false; + wrapperValue.isReadOnly = false; + + model.getVars().clear(); + model.getAllVars().clear(); + model.getRequiredVars().clear(); + model.getOptionalVars().clear(); + model.getMandatory().clear(); + model.getAllMandatory().clear(); + model.oneOf.clear(); + model.anyOf.clear(); + model.allOf.clear(); + + model.getVars().add(wrapperValue); + model.getAllVars().add(wrapperValue); + model.getRequiredVars().add(wrapperValue); + model.getMandatory().add("value"); + model.getAllMandatory().add("value"); + + model.hasVars = true; + model.hasRequired = true; + model.hasOptional = false; + model.emptyVars = false; + model.getVendorExtensions().put("x-composition-wrapper", true); + } + + private boolean isInheritancePostProcessingEnabled() { + // `none` and `composition` are handled in dedicated short-circuit paths. + return !Constants.INHERITANCE_MODE_NONE.equals(inheritanceMode) + && !Constants.INHERITANCE_MODE_COMPOSITION.equals(inheritanceMode); + } } diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache index 12fa7fceef56..4fecc4a53900 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache @@ -85,7 +85,7 @@ import java.io.Serializable {{#discriminator}} {{>typeInfoAnnotation}} {{#vendorExtensions.x-discriminator-has-parent-properties}} -sealed class {{classname}}( +{{#x-inheritance-mode-abstract}}abstract class{{/x-inheritance-mode-abstract}}{{^x-inheritance-mode-abstract}}sealed class{{/x-inheritance-mode-abstract}} {{classname}}( {{#requiredVars}} {{>data_class_sealed_var}}{{^-last}}, {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, @@ -94,7 +94,7 @@ sealed class {{classname}}( ) {{/vendorExtensions.x-discriminator-has-parent-properties}} {{^vendorExtensions.x-discriminator-has-parent-properties}} -sealed class {{classname}} +{{#x-inheritance-mode-abstract}}abstract class{{/x-inheritance-mode-abstract}}{{^x-inheritance-mode-abstract}}sealed class{{/x-inheritance-mode-abstract}} {{classname}} {{/vendorExtensions.x-discriminator-has-parent-properties}} {{/discriminator}} {{^discriminator}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class.mustache index 4c38bf64dccc..8384544057fb 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class.mustache @@ -29,22 +29,28 @@ import java.io.Serializable {{/isDeprecated}} {{>additionalModelTypeAnnotations}} -{{#nonPublicApi}}internal {{/nonPublicApi}}{{#discriminator}}interface{{/discriminator}}{{^discriminator}}data class{{/discriminator}} {{classname}}{{^discriminator}} ( +{{#nonPublicApi}}internal {{/nonPublicApi}}{{#discriminator}}{{#x-inheritance-mode-abstract}}abstract class{{/x-inheritance-mode-abstract}}{{^x-inheritance-mode-abstract}}interface{{/x-inheritance-mode-abstract}}{{/discriminator}}{{^discriminator}}data class{{/discriminator}} {{classname}}{{^discriminator}} ( {{#allVars}} {{#required}}{{>data_class_req_var}}{{/required}}{{^required}}{{>data_class_opt_var}}{{/required}}{{^-last}},{{/-last}} {{/allVars}} -){{/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}}{{#vendorExtensions.x-has-data-class-body}} { +){{/discriminator}}{{#discriminator}}{{#x-inheritance-mode-abstract}}( + +{{#allVars}} +{{#required}}{{>data_class_sealed_var}}{{/required}}{{^required}}{{>data_class_sealed_var}}{{/required}}{{^-last}},{{/-last}} + +{{/allVars}} +){{/x-inheritance-mode-abstract}}{{/discriminator}}{{#parent}}{{#vendorExtensions.x-parent-ctor-args}}{{#serializableModel}}{{#parcelizeModels}} : {{{parent}}}({{{.}}}), Serializable, Parcelable{{/parcelizeModels}}{{^parcelizeModels}} : {{{parent}}}({{{.}}}), Serializable{{/parcelizeModels}}{{/serializableModel}}{{^serializableModel}}{{#parcelizeModels}} : {{{parent}}}({{{.}}}), Parcelable{{/parcelizeModels}}{{^parcelizeModels}} : {{{parent}}}({{{.}}}){{/parcelizeModels}}{{/serializableModel}}{{/vendorExtensions.x-parent-ctor-args}}{{^vendorExtensions.x-parent-ctor-args}}{{#serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable, Parcelable{{/parcelizeModels}}{{^parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable{{/parcelizeModels}}{{/serializableModel}}{{^serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Parcelable{{/parcelizeModels}}{{^parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}{{/parcelizeModels}}{{/serializableModel}}{{/vendorExtensions.x-parent-ctor-args}}{{/parent}}{{^parent}}{{#serializableModel}}{{^parcelizeModels}} : Serializable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}{{#parcelizeModels}} : Serializable, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#vendorExtensions.x-has-data-class-body}} { {{/vendorExtensions.x-has-data-class-body}} {{#serializableModel}} {{#nonPublicApi}}internal {{/nonPublicApi}}companion object { private const val serialVersionUID: Long = 123 } {{/serializableModel}} -{{#discriminator}}{{#vars}}{{#required}} +{{#discriminator}}{{^x-inheritance-mode-abstract}}{{#vars}}{{#required}} {{>interface_req_var}}{{/required}}{{^required}} -{{>interface_opt_var}}{{/required}}{{/vars}}{{/discriminator}} +{{>interface_opt_var}}{{/required}}{{/vars}}{{/x-inheritance-mode-abstract}}{{/discriminator}} {{#hasEnums}} {{#vars}} {{#isEnum}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_opt_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_opt_var.mustache index f277f71da441..e263fe331119 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_opt_var.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_opt_var.mustache @@ -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}} \ No newline at end of file +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_req_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_req_var.mustache index 6f4647d16e98..7b495b55ef54 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_req_var.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_req_var.mustache @@ -3,4 +3,4 @@ {{/description}} @JsonProperty("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") -{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}} \ No newline at end of file +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index 87176dc4eaa3..8f54206c5e5b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -36,6 +36,11 @@ import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.INTERFACE_ONLY; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.JAVALIN5; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.JAVALIN6; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.INHERITANCE_MODE; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.INHERITANCE_MODE_ABSTRACT; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.INHERITANCE_MODE_COMPOSITION; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.INHERITANCE_MODE_NONE; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.INHERITANCE_MODE_SEALED; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.JAXRS_SPEC; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.RETURN_RESPONSE; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.USE_TAGS; @@ -43,6 +48,10 @@ import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.DELEGATE_PATTERN; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.KTOR; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.RESOURCES; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.X_INHERITANCE_MODE_ABSTRACT; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.X_INHERITANCE_MODE_COMPOSITION; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.X_INHERITANCE_MODE_NONE; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.X_INHERITANCE_MODE_SEALED; public class KotlinServerCodegenTest { @@ -465,6 +474,142 @@ public void allOfWithDiscriminator_generatesChildrenWithOverrideProperties() thr ); } + @Test + public void allOfWithDiscriminator_javalinAbstractMode_generatesAbstractParent() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, JAVALIN6); + codegen.additionalProperties().put(INHERITANCE_MODE, INHERITANCE_MODE_ABSTRACT); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism-allof-and-discriminator.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; + Path petModel = Paths.get(outputPath + "/models/Pet.kt"); + + assertFileContains( + petModel, + "abstract class Pet(", + "open val name: kotlin.String", + "open val petType: kotlin.String" + ); + assertFileNotContains( + petModel, + "sealed class Pet(" + ); + } + + @Test + public void allOfWithDiscriminator_javalinNoneMode_generatesFlatModels() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, JAVALIN6); + codegen.additionalProperties().put(INHERITANCE_MODE, INHERITANCE_MODE_NONE); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism-allof-and-discriminator.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; + Path petModel = Paths.get(outputPath + "/models/Pet.kt"); + Path catModel = Paths.get(outputPath + "/models/Cat.kt"); + + assertFileContains(petModel, "data class Pet("); + assertFileNotContains( + petModel, + "sealed class Pet(", + "abstract class Pet(", + "@com.fasterxml.jackson.annotation.JsonTypeInfo", + "@com.fasterxml.jackson.annotation.JsonSubTypes" + ); + + assertFileContains(catModel, "data class Cat("); + assertFileNotContains(catModel, ") : Pet("); + } + + @Test + public void oneOfWithDiscriminator_javalinCompositionMode_generatesWrapperParent() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, JAVALIN6); + codegen.additionalProperties().put(INHERITANCE_MODE, INHERITANCE_MODE_COMPOSITION); + + 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"; + Path petModel = Paths.get(outputPath + "/models/Pet.kt"); + Path catModel = Paths.get(outputPath + "/models/Cat.kt"); + + assertFileContains( + petModel, + "data class Pet(", + "val value: kotlin.Any" + ); + assertFileNotContains( + petModel, + "sealed class Pet", + "abstract class Pet", + "@com.fasterxml.jackson.annotation.JsonTypeInfo", + "@com.fasterxml.jackson.annotation.JsonSubTypes" + ); + + assertFileContains(catModel, "data class Cat("); + assertFileNotContains(catModel, ") : Pet("); + } + + @Test + public void allOfWithDiscriminator_jaxrsAbstractMode_generatesAbstractParentAndOverrideChildren() 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); + codegen.additionalProperties().put(INHERITANCE_MODE, INHERITANCE_MODE_ABSTRACT); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism-allof-and-discriminator.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; + + Path petModel = Paths.get(outputPath + "/models/Pet.kt"); + assertFileContains( + petModel, + "abstract class Pet(", + "open val name: kotlin.String", + "open val petType: kotlin.String" + ); + assertFileNotContains( + petModel, + "interface Pet" + ); + + Path catModel = Paths.get(outputPath + "/models/Cat.kt"); + assertFileContains( + catModel, + "override val name: kotlin.String", + "override val petType: kotlin.String", + ") : Pet(name = name, petType = petType)" + ); + } + @Test public void polymorphismWithoutDiscriminator_generatesRegularDataClass() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); @@ -671,6 +816,78 @@ public void delegatePattern_canBeEnabled() throws IOException { Assert.assertTrue(Files.exists(Paths.get(infraPath + "/APINotImplementedException.kt"))); } + @Test + public void inheritanceMode_defaultIsSealedForKtor() { + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.additionalProperties().put(LIBRARY, KTOR); + + codegen.processOpts(); + + Assert.assertEquals(codegen.additionalProperties().get(INHERITANCE_MODE), INHERITANCE_MODE_SEALED); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_SEALED), Boolean.TRUE); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_ABSTRACT), Boolean.FALSE); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_COMPOSITION), Boolean.FALSE); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_NONE), Boolean.FALSE); + } + + @Test + public void inheritanceMode_defaultIsAbstractForJaxrsSpec() { + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC); + + codegen.processOpts(); + + Assert.assertEquals(codegen.additionalProperties().get(INHERITANCE_MODE), INHERITANCE_MODE_ABSTRACT); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_ABSTRACT), Boolean.TRUE); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_SEALED), Boolean.FALSE); + } + + @Test + public void inheritanceMode_rejectsInvalidValue() { + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.additionalProperties().put(LIBRARY, KTOR); + codegen.additionalProperties().put(INHERITANCE_MODE, "invalid-mode"); + + IllegalArgumentException ex = Assert.expectThrows(IllegalArgumentException.class, codegen::processOpts); + Assert.assertTrue(ex.getMessage().contains("Invalid inheritanceMode value 'invalid-mode'")); + } + + @Test + public void inheritanceMode_rejectsSealedForJaxrsSpec() { + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC); + codegen.additionalProperties().put(INHERITANCE_MODE, INHERITANCE_MODE_SEALED); + + IllegalArgumentException ex = Assert.expectThrows(IllegalArgumentException.class, codegen::processOpts); + Assert.assertTrue(ex.getMessage().contains("does not support inheritanceMode 'sealed'")); + } + + @Test + public void inheritanceMode_acceptsCompositionForJaxrsSpec() { + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC); + codegen.additionalProperties().put(INHERITANCE_MODE, INHERITANCE_MODE_COMPOSITION); + + codegen.processOpts(); + + Assert.assertEquals(codegen.additionalProperties().get(INHERITANCE_MODE), INHERITANCE_MODE_COMPOSITION); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_COMPOSITION), Boolean.TRUE); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_ABSTRACT), Boolean.FALSE); + } + + @Test + public void inheritanceMode_acceptsNoneForJaxrsSpec() { + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC); + codegen.additionalProperties().put(INHERITANCE_MODE, INHERITANCE_MODE_NONE); + + codegen.processOpts(); + + Assert.assertEquals(codegen.additionalProperties().get(INHERITANCE_MODE), INHERITANCE_MODE_NONE); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_NONE), Boolean.TRUE); + Assert.assertEquals(codegen.additionalProperties().get(X_INHERITANCE_MODE_ABSTRACT), Boolean.FALSE); + } + @Test public void delegatePattern_enumWireValue() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile();