Skip to content

Commit

Permalink
[Kotlin] Introduce support for Kotlin Explicit API Mode for Kotlin-Cl…
Browse files Browse the repository at this point in the history
…ient generator (resolve #16506) (#19999)

* [Kotlin] Properly document nonPublicApi CLI option

* [Kotlin] Respect parameter name of parent KSerializer to avoid miss-behavior

* [Kotlin] Introduce support for Kotlin Explicit API Mode for Kotlin-Client generator (resolves #16506)
  • Loading branch information
xsveda authored Oct 30, 2024
1 parent e9ea12f commit acb1641
Show file tree
Hide file tree
Showing 271 changed files with 5,526 additions and 723 deletions.
1 change: 1 addition & 0 deletions .github/workflows/samples-kotlin-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
sample:
# client
- samples/client/petstore/kotlin
- samples/client/petstore/kotlin-explicit
- samples/client/petstore/kotlin-gson
- samples/client/petstore/kotlin-jackson
- samples/client/petstore/kotlin-model-prefix-type-mappings
Expand Down
7 changes: 7 additions & 0 deletions bin/configs/kotlin-explicit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
generatorName: kotlin
outputDir: samples/client/petstore/kotlin-explicit
inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-client
additionalProperties:
artifactId: kotlin-petstore-explicit
explicitApi: "true"
2 changes: 2 additions & 0 deletions docs/generators/kotlin.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|collectionType|Option. Collection type to use|<dl><dt>**array**</dt><dd>kotlin.Array</dd><dt>**list**</dt><dd>kotlin.collections.List</dd></dl>|list|
|dateLibrary|Option. Date library to use|<dl><dt>**threetenbp-localdatetime**</dt><dd>Threetenbp - Backport of JSR310 (jvm only, for legacy app only)</dd><dt>**kotlinx-datetime**</dt><dd>kotlinx-datetime (preferred for multiplatform)</dd><dt>**string**</dt><dd>String</dd><dt>**java8-localdatetime**</dt><dd>Java 8 native JSR310 (jvm only, for legacy app only)</dd><dt>**java8**</dt><dd>Java 8 native JSR310 (jvm only, preferred for jdk 1.8+)</dd><dt>**threetenbp**</dt><dd>Threetenbp - Backport of JSR310 (jvm only, preferred for jdk &lt; 1.8)</dd></dl>|java8|
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |original|
|explicitApi|Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode.| |false|
|failOnUnknownProperties|Fail Jackson de-serialization on unknown properties| |false|
|generateOneOfAnyOfWrappers|Generate oneOf, anyOf schemas as wrappers.| |false|
|generateRoomModels|Generate Android Room database models in addition to API models (JVM Volley library only)| |false|
Expand All @@ -34,6 +35,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|mapFileBinaryToByteArray|Map File and Binary to ByteArray (default: false)| |false|
|modelMutable|Create mutable models| |false|
|moshiCodeGen|Whether to enable codegen with the Moshi library. Refer to the [official Moshi doc](https://github.com/square/moshi#codegen) for more info.| |false|
|nonPublicApi|Generates code with reduced access modifiers; allows embedding elsewhere without exposing non-public API calls to consumers.| |false|
|nullableReturnType|Nullable return type| |false|
|omitGradlePluginVersions|Whether to declare Gradle plugin versions in build files.| |false|
|omitGradleWrapper|Whether to omit Gradle wrapper for creating a sub project.| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {

public static final String MOSHI_CODE_GEN = "moshiCodeGen";

public static final String EXPLICIT_API = "explicitApi";
public static final String NULLABLE_RETURN_TYPE = "nullableReturnType";

public static final String SUPPORT_ANDROID_API_LEVEL_25_AND_BELLOW = "supportAndroidApiLevel25AndBelow";
Expand Down Expand Up @@ -268,6 +269,8 @@ public KotlinClientCodegen() {
cliOptions.add(CliOption.newBoolean(MOSHI_CODE_GEN, "Whether to enable codegen with the Moshi library. Refer to the [official Moshi doc](https://github.com/square/moshi#codegen) for more info."));
cliOptions.add(CliOption.newBoolean(FAIL_ON_UNKNOWN_PROPERTIES, "Fail Jackson de-serialization on unknown properties", false));

cliOptions.add(CliOption.newBoolean(EXPLICIT_API, "Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode."));
cliOptions.add(CliOption.newBoolean(CodegenConstants.NON_PUBLIC_API, CodegenConstants.NON_PUBLIC_API_DESC));
cliOptions.add(CliOption.newBoolean(NULLABLE_RETURN_TYPE, "Nullable return type"));

cliOptions.add(CliOption.newBoolean(GENERATE_ROOM_MODELS, "Generate Android Room database models in addition to API models (JVM Volley library only)", false));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import java.io.IOException
@Deprecated(message = "This schema is deprecated.")
{{/isDeprecated}}
{{>additionalModelTypeAnnotations}}
{{#nonPublicApi}}internal {{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {
class CustomTypeAdapterFactory : TypeAdapterFactory {
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ idea {
}
}
{{/idea}}
{{#explicitApi}}
kotlin {
explicitApi()
}
{{/explicitApi}}
{{#jvm-spring-webclient}}{{#useSpringBoot3}}
kotlin {
jvmToolchain {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ import {{packageName}}.infrastructure.ITransformForStorage
{{#vendorExtensions.x-class-extra-annotation}}
{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
{{#nonPublicApi}}internal {{/nonPublicApi}}{{#discriminator}}interface{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class{{/discriminator}} {{classname}}{{^discriminator}} (
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}{{#discriminator}}interface{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class{{/discriminator}} {{classname}}{{^discriminator}} (

{{#allVars}}
{{#required}}{{>data_class_req_var}}{{/required}}{{^required}}{{>data_class_opt_var}}{{/required}}{{^-last}},{{/-last}}
Expand All @@ -94,7 +94,7 @@ import {{packageName}}.infrastructure.ITransformForStorage
){{/discriminator}}
{{/generateRoomModels}}
{{#serializableModel}}
{{#nonPublicApi}}internal {{/nonPublicApi}}companion object {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}companion object {
private const val serialVersionUID: Long = 123
}
{{/serializableModel}}
Expand All @@ -120,7 +120,7 @@ import {{packageName}}.infrastructure.ITransformForStorage
{{#multiplatform}}
@Serializable
{{/multiplatform}}
{{#nonPublicApi}}internal {{/nonPublicApi}}enum class {{{nameInPascalCase}}}(val value: {{^isContainer}}{{dataType}}{{/isContainer}}{{#isContainer}}kotlin.String{{/isContainer}}) {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}enum class {{{nameInPascalCase}}}({{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val value: {{^isContainer}}{{dataType}}{{/isContainer}}{{#isContainer}}kotlin.String{{/isContainer}}) {
{{#allowableValues}}
{{#enumVars}}
{{^multiplatform}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import kotlinx.serialization.*
@JsonClass(generateAdapter = false)
{{/moshi}}
{{/multiplatform}}
{{#nonPublicApi}}internal {{/nonPublicApi}}enum class {{classname}}(val value: {{{dataType}}}) {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}enum class {{classname}}({{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val value: {{{dataType}}}) {
{{#allowableValues}}{{#enumVars}}
{{^multiplatform}}
{{#moshi}}
Expand Down Expand Up @@ -79,16 +79,16 @@ import kotlinx.serialization.*
*/
override fun toString(): kotlin.String = value{{^isString}}.toString(){{/isString}}

companion object {
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}companion object {
/**
* Converts the provided [data] to a [String] on success, null otherwise.
*/
fun encode(data: kotlin.Any?): kotlin.String? = if (data is {{classname}}) "$data" else null
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun encode(data: kotlin.Any?): kotlin.String? = if (data is {{classname}}) "$data" else null

/**
* Returns a valid [{{classname}}] for [data], null otherwise.
*/
fun decode(data: kotlin.Any?): {{classname}}? = data?.let {
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun decode(data: kotlin.Any?): {{classname}}? = data?.let {
val normalizedData = "$it".lowercase()
values().firstOrNull { value ->
it == value || normalizedData == "$value".lowercase()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package {{packageName}}.infrastructure

{{#nonPublicApi}}internal {{/nonPublicApi}}typealias MultiValueMap = MutableMap<String,List<String>>
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}typealias MultiValueMap = MutableMap<String,List<String>>

{{#nonPublicApi}}internal {{/nonPublicApi}}fun collectionDelimiter(collectionFormat: String) = when(collectionFormat) {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun collectionDelimiter(collectionFormat: String): String = when(collectionFormat) {
"csv" -> ","
"tsv" -> "\t"
"pipe" -> "|"
"space" -> " "
else -> ""
}

{{#nonPublicApi}}internal {{/nonPublicApi}}val defaultMultiValueConverter: (item: Any?) -> String = { item -> "$item" }
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val defaultMultiValueConverter: (item: Any?) -> String = { item -> "$item" }

{{#nonPublicApi}}internal {{/nonPublicApi}}fun <T : Any?> toMultiValue(items: Array<T>, collectionFormat: String, map: (item: T) -> String = defaultMultiValueConverter)
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun <T : Any?> toMultiValue(items: Array<T>, collectionFormat: String, map: (item: T) -> String = defaultMultiValueConverter): List<String>
= toMultiValue(items.asIterable(), collectionFormat, map)

{{#nonPublicApi}}internal {{/nonPublicApi}}fun <T : Any?> toMultiValue(items: Iterable<T>, collectionFormat: String, map: (item: T) -> String = defaultMultiValueConverter): List<String> {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun <T : Any?> toMultiValue(items: Iterable<T>, collectionFormat: String, map: (item: T) -> String = defaultMultiValueConverter): List<String> {
return when(collectionFormat) {
"multi" -> items.map(map)
else -> listOf(items.joinToString(separator = collectionDelimiter(collectionFormat), transform = map))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package {{packageName}}.infrastructure
* NOTE: Headers is a Map<String,String> because rfc2616 defines
* multi-valued headers as csv-only.
*/
{{#nonPublicApi}}internal {{/nonPublicApi}}data class PartConfig<T>(
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class PartConfig<T>(
val headers: MutableMap<String, String> = mutableMapOf(),
val body: T? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package {{packageName}}.infrastructure
* NOTE: Headers is a Map<String,String> because rfc2616 defines
* multi-valued headers as csv-only.
*/
{{#nonPublicApi}}internal {{/nonPublicApi}}data class RequestConfig<T>(
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class RequestConfig<T>(
val method: RequestMethod,
val path: String,
val headers: MutableMap<String, String> = mutableMapOf(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package {{packageName}}.infrastructure
/**
* Provides enumerated HTTP verbs
*/
{{#nonPublicApi}}internal {{/nonPublicApi}}enum class RequestMethod {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}enum class RequestMethod {
GET, DELETE, HEAD, OPTIONS, PATCH, POST, PUT
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import java.util.concurrent.atomic.AtomicBoolean

@Serializer(forClass = AtomicBoolean::class)
{{#nonPublicApi}}internal {{/nonPublicApi}}object AtomicBooleanAdapter : KSerializer<AtomicBoolean> {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object AtomicBooleanAdapter : KSerializer<AtomicBoolean> {
override fun serialize(encoder: Encoder, value: AtomicBoolean) {
encoder.encodeBoolean(value.get())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import java.util.concurrent.atomic.AtomicInteger

@Serializer(forClass = AtomicInteger::class)
{{#nonPublicApi}}internal {{/nonPublicApi}}object AtomicIntegerAdapter : KSerializer<AtomicInteger> {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object AtomicIntegerAdapter : KSerializer<AtomicInteger> {
override fun serialize(encoder: Encoder, value: AtomicInteger) {
encoder.encodeInt(value.get())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import java.util.concurrent.atomic.AtomicLong

@Serializer(forClass = AtomicLong::class)
{{#nonPublicApi}}internal {{/nonPublicApi}}object AtomicLongAdapter : KSerializer<AtomicLong> {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object AtomicLongAdapter : KSerializer<AtomicLong> {
override fun serialize(encoder: Encoder, value: AtomicLong) {
encoder.encodeLong(value.get())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ import java.math.BigDecimal

{{#kotlinx_serialization}}
@Serializer(forClass = BigDecimal::class)
{{#nonPublicApi}}internal {{/nonPublicApi}}object BigDecimalAdapter : KSerializer<BigDecimal> {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object BigDecimalAdapter : KSerializer<BigDecimal> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): BigDecimal = BigDecimal(decoder.decodeString())
override fun serialize(encoder: Encoder, value: BigDecimal) = encoder.encodeString(value.toPlainString())
}
{{/kotlinx_serialization}}
{{#moshi}}
{{#nonPublicApi}}internal {{/nonPublicApi}}class BigDecimalAdapter {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class BigDecimalAdapter {
@ToJson
fun toJson(value: BigDecimal): String {
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun toJson(value: BigDecimal): String {
return value.toPlainString()
}

@FromJson
fun fromJson(value: String): BigDecimal {
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun fromJson(value: String): BigDecimal {
return BigDecimal(value)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import java.math.BigInteger

{{#kotlinx_serialization}}
@Serializer(forClass = BigInteger::class)
{{#nonPublicApi}}internal {{/nonPublicApi}}object BigIntegerAdapter : KSerializer<BigInteger> {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object BigIntegerAdapter : KSerializer<BigInteger> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BigInteger", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): BigInteger {
return BigInteger(decoder.decodeString())
Expand All @@ -29,14 +29,14 @@ import java.math.BigInteger
}
{{/kotlinx_serialization}}
{{#moshi}}
{{#nonPublicApi}}internal {{/nonPublicApi}}class BigIntegerAdapter {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class BigIntegerAdapter {
@ToJson
fun toJson(value: BigInteger): String {
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun toJson(value: BigInteger): String {
return value.toString()
}

@FromJson
fun fromJson(value: String): BigInteger {
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun fromJson(value: String): BigInteger {
return BigInteger(value)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ import java.io.IOException
{{/gson}}

{{#moshi}}
{{#nonPublicApi}}internal {{/nonPublicApi}}class ByteArrayAdapter {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class ByteArrayAdapter {
@ToJson
fun toJson(data: ByteArray): String = String(data)
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun toJson(data: ByteArray): String = String(data)

@FromJson
fun fromJson(data: String): ByteArray = data.toByteArray()
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun fromJson(data: String): ByteArray = data.toByteArray()
}
{{/moshi}}
{{#gson}}
{{#nonPublicApi}}internal {{/nonPublicApi}}class ByteArrayAdapter : TypeAdapter<ByteArray>() {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class ByteArrayAdapter : TypeAdapter<ByteArray>() {
@Throws(IOException::class)
override fun write(out: JsonWriter?, value: ByteArray?) {
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ import java.io.IOException
import kotlinx.datetime.Instant

{{#moshi}}
{{#nonPublicApi}}internal {{/nonPublicApi}}class InstantAdapter {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class InstantAdapter {
@ToJson
fun toJson(value: Instant): String {
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun toJson(value: Instant): String {
return value.toString()
}

@FromJson
fun fromJson(value: String): Instant {
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun fromJson(value: String): Instant {
return Instant.parse(value)
}

}
{{/moshi}}
{{#gson}}
{{#nonPublicApi}}internal {{/nonPublicApi}}class InstantAdapter : TypeAdapter<Instant>() {
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class InstantAdapter : TypeAdapter<Instant>() {
@Throws(IOException::class)
override fun write(out: JsonWriter?, value: Instant?) {
if (value == null) {
Expand Down
Loading

0 comments on commit acb1641

Please sign in to comment.