Skip to content

Commit

Permalink
Kotlin: Fix kotlinx_serialization code generation
Browse files Browse the repository at this point in the history
And update to Kotlin 1.5.0 and kotlinx.serialization 1.2.1. Fix nested
enum annotation '@serializable' instead of '@KSerializable' when
'kotlinx_serialization' is used. Fix missing JsonMediaType in
ApiClient.kt (#9242). Add 'kotlinx_serialization' serialization library
to documentation. Use explicity type in RequestConfig to keep type
information for JSON serialization.

Resolves #9242
  • Loading branch information
saschpe committed May 31, 2021
1 parent c4df343 commit a20eca0
Show file tree
Hide file tree
Showing 82 changed files with 1,647 additions and 2,245 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ wrapper {
}

buildscript {
ext.kotlin_version = '1.4.30'
ext.kotlin_version = '1.5.0'
{{#jvm-retrofit2}}
ext.retrofitVersion = '2.7.2'
{{/jvm-retrofit2}}
Expand Down Expand Up @@ -72,7 +72,7 @@ dependencies {
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.1"
{{/jackson}}
{{#kotlinx_serialization}}
compile "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"
compile "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1"
{{/kotlinx_serialization}}
{{#jvm-okhttp3}}
compile "com.squareup.okhttp3:okhttp:3.12.13"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import java.io.Serializable
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
*/
{{#kotlinx_serialization}}
@KSerializable
{{#serializableModel}}@KSerializable{{/serializableModel}}{{^serializableModel}}@Serializable{{/serializableModel}}
{{/kotlinx_serialization}}
{{#nonPublicApi}}internal {{/nonPublicApi}}enum class {{{nameInCamelCase}}}(val value: {{^isContainer}}{{dataType}}{{/isContainer}}{{#isContainer}}kotlin.String{{/isContainer}}) {
{{#allowableValues}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import kotlinx.serialization.internal.CommonEnumSerializer
* {{{description}}}
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
*/
{{#multiplatform}}@Serializable(with = {{classname}}.Serializer::class){{/multiplatform}}{{#kotlinx_serialization}}@Serializable(with = {{classname}}.Serializer::class){{/kotlinx_serialization}}
{{#multiplatform}}@Serializable(with = {{classname}}.Serializer::class){{/multiplatform}}{{#kotlinx_serialization}}@Serializable{{/kotlinx_serialization}}
{{#nonPublicApi}}internal {{/nonPublicApi}}enum class {{classname}}(val value: {{{dataType}}}) {
{{#allowableValues}}{{#enumVars}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ 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(
{{#nonPublicApi}}internal {{/nonPublicApi}}data class RequestConfig<T>(
val method: RequestMethod,
val path: String,
val headers: MutableMap<String, String> = mutableMapOf(),
val query: MutableMap<String, List<String>> = mutableMapOf(),
val body: kotlin.Any? = null
val body: T? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {{packageName}}.infrastructure.toMultiValue
{{^doNotUseRxAndCoroutines}}{{#useCoroutines}}suspend {{/useCoroutines}}{{/doNotUseRxAndCoroutines}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}} {
val localVariableConfig = {{operationId}}RequestConfig({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})

val localVarResponse = request<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Any?{{/returnType}}>(
val localVarResponse = request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, Any?>{{/hasFormParams}}{{/hasBodyParam}}, {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}>(
localVariableConfig
)

Expand Down Expand Up @@ -71,8 +71,8 @@ import {{packageName}}.infrastructure.toMultiValue
{{#isDeprecated}}
@Deprecated(message = "This operation is deprecated.")
{{/isDeprecated}}
fun {{operationId}}RequestConfig({{#allParams}}{{{paramName}}}: {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : RequestConfig {
val localVariableBody: kotlin.Any? = {{#hasBodyParam}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to {{{paramName}}}{{^-last}}, {{/-last}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}}
fun {{operationId}}RequestConfig({{#allParams}}{{{paramName}}}: {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : RequestConfig<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, Any?>{{/hasFormParams}}{{/hasBodyParam}}> {
val localVariableBody = {{#hasBodyParam}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to {{{paramName}}}{{^-last}}, {{/-last}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}}
val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mutableMapOf()
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf<kotlin.String, List<kotlin.String>>()
.apply {
Expand All @@ -99,16 +99,14 @@ import {{packageName}}.infrastructure.toMultiValue
{{#headerParams}}
{{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
{{/headerParams}}
val localVariableConfig = RequestConfig(

return RequestConfig(
method = RequestMethod.{{httpMethod}},
path = "{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", "${{{paramName}}}"){{/pathParams}},
query = localVariableQuery,
headers = localVariableHeaders,
body = localVariableBody
)

return localVariableConfig
}

{{/operation}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,23 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.Headers
import okhttp3.MultipartBody
import java.io.File
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.net.URLConnection
import java.util.Date
{{^threetenbp}}
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.OffsetTime
{{/threetenbp}}
import java.util.Date
import java.util.Locale
{{#kotlinx_serialization}}
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
{{/kotlinx_serialization}}
{{#threetenbp}}
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
Expand Down Expand Up @@ -86,14 +91,10 @@ import org.threeten.bp.OffsetTime
protected inline fun <reified T> requestBody(content: T, mediaType: String = JsonMediaType): RequestBody =
when {
{{#jvm-okhttp3}}
content is File -> RequestBody.create(
MediaType.parse(mediaType), content
)
content is File -> RequestBody.create(MediaType.parse(mediaType), content)
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
content is File -> content.asRequestBody(
mediaType.toMediaTypeOrNull()
)
content is File -> content.asRequestBody(mediaType.toMediaTypeOrNull())
{{/jvm-okhttp4}}
mediaType == FormDataMediaType -> {
MultipartBody.Builder()
Expand Down Expand Up @@ -154,12 +155,12 @@ import org.threeten.bp.OffsetTime
MediaType.parse(mediaType), Serializer.jacksonObjectMapper.writeValueAsString(content)
{{/jackson}}
{{#kotlinx_serialization}}
MediaType.parse(mediaType), Serializer.jvmJson.decodeFromString<T>(content)
MediaType.parse(mediaType), Serializer.jvmJson.encodeToString(content)
{{/kotlinx_serialization}}
)
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
mediaType == JsonMediaType -> {{#moshi}}Serializer.moshi.adapter(T::class.java).toJson(content){{/moshi}}{{#gson}}Serializer.gson.toJson(content, T::class.java){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.writeValueAsString(content){{/jackson}}{{#kotlinx_serialization}}Serializer.jvmJson.decodeFromString<T>(content){{/kotlinx_serialization}}.toRequestBody(
mediaType == JsonMediaType -> {{#moshi}}Serializer.moshi.adapter(T::class.java).toJson(content){{/moshi}}{{#gson}}Serializer.gson.toJson(content, T::class.java){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.writeValueAsString(content){{/jackson}}{{#kotlinx_serialization}}Serializer.jvmJson.encodeToString(content){{/kotlinx_serialization}}.toRequestBody(
mediaType.toMediaTypeOrNull()
)
{{/jvm-okhttp4}}
Expand Down Expand Up @@ -196,13 +197,13 @@ import org.threeten.bp.OffsetTime
return f as T
}
return when(mediaType) {
JsonMediaType -> {{#moshi}}Serializer.moshi.adapter(T::class.java).fromJson(bodyContent){{/moshi}}{{#gson}}Serializer.gson.fromJson(bodyContent, T::class.java){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.readValue(bodyContent, T::class.java){{/jackson}}
JsonMediaType -> {{#moshi}}Serializer.moshi.adapter(T::class.java).fromJson(bodyContent){{/moshi}}{{#gson}}Serializer.gson.fromJson(bodyContent, T::class.java){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.readValue(bodyContent, T::class.java){{/jackson}}{{#kotlinx_serialization}}Serializer.jvmJson.decodeFromString<T>(bodyContent){{/kotlinx_serialization}}
else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.")
}
}

{{#hasAuthMethods}}
protected fun updateAuthParams(requestConfig: RequestConfig) {
protected fun <T> updateAuthParams(requestConfig: RequestConfig<T>) {
{{#authMethods}}
{{#isApiKey}}
{{#isKeyInHeader}}
Expand Down Expand Up @@ -259,7 +260,7 @@ import org.threeten.bp.OffsetTime
}
{{/hasAuthMethods}}

protected inline fun <reified T: Any?> request(requestConfig: RequestConfig): ApiInfrastructureResponse<T?> {
protected inline fun <reified I, reified T: Any?> request(requestConfig: RequestConfig<I>): ApiInfrastructureResponse<T?> {
{{#jvm-okhttp3}}
val httpUrl = HttpUrl.parse(baseUrl) ?: throw IllegalStateException("baseUrl is invalid.")
{{/jvm-okhttp3}}
Expand Down Expand Up @@ -300,7 +301,7 @@ import org.threeten.bp.OffsetTime
}

// TODO: support multiple contentType options here.
val contentType = (headers[ContentType] as String).substringBefore(";").toLowerCase()
val contentType = (headers[ContentType] as String).substringBefore(";").lowercase(Locale.getDefault())

val request = when (requestConfig.method) {
RequestMethod.DELETE -> Request.Builder().url(url).delete(requestBody(requestConfig.body, contentType))
Expand All @@ -315,57 +316,46 @@ import org.threeten.bp.OffsetTime
}.build()

val response = client.newCall(request).execute()
val accept = response.header(ContentType)?.substringBefore(";")?.toLowerCase()
val accept = response.header(ContentType)?.substringBefore(";")?.lowercase(Locale.getDefault())

// TODO: handle specific mapping types. e.g. Map<int, Class<?>>
when {
response.isRedirect -> return Redirection(
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
return when {
response.isRedirect -> Redirection(
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
response.isInformational -> return Informational(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
response.isInformational -> Informational(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
response.isSuccessful -> return Success(
responseBody(response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}, accept),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
response.isSuccessful -> Success(
responseBody(response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}, accept),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
response.isClientError -> return ClientError(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}?.string(),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
response.isClientError -> ClientError(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}?.string(),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
else -> return ServerError(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}?.string(),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
else -> ServerError(
response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}?.string(),
response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
)
}
}

protected fun parameterToString(value: Any?): String {
when (value) {
null -> {
return ""
}
is Array<*> -> {
return toMultiValue(value, "csv").toString()
}
is Iterable<*> -> {
return toMultiValue(value, "csv").toString()
}
is OffsetDateTime, is OffsetTime, is LocalDateTime, is LocalDate, is LocalTime, is Date -> {
return parseDateToQueryString<Any>(value)
}
else -> {
return value.toString()
}
}
protected fun parameterToString(value: Any?): String = when (value) {
null -> ""
is Array<*> -> toMultiValue(value, "csv").toString()
is Iterable<*> -> toMultiValue(value, "csv").toString()
is OffsetDateTime, is OffsetTime, is LocalDateTime, is LocalDate, is LocalTime, is Date ->
parseDateToQueryString(value)
else -> value.toString()
}

protected inline fun <reified T: Any> parseDateToQueryString(value : T): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ import {{packageName}}.auth.*
else request(requestConfig, authNames = authNames)
}

protected suspend fun request(requestConfig: RequestConfig, body: OutgoingContent = EmptyContent, authNames: kotlin.collections.List<String>): HttpResponse {
protected suspend inline fun <reified T: Any?> request(requestConfig: RequestConfig<T>, body: OutgoingContent = EmptyContent, authNames: kotlin.collections.List<String>): HttpResponse {
requestConfig.updateForAuth(authNames)
val headers = requestConfig.headers
Expand Down
2 changes: 1 addition & 1 deletion samples/client/petstore/kotlin-gson/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ wrapper {
}

buildscript {
ext.kotlin_version = '1.4.30'
ext.kotlin_version = '1.5.0'

repositories {
maven { url "https://repo1.maven.org/maven2" }
Expand Down
Loading

0 comments on commit a20eca0

Please sign in to comment.