From 569acbc5710c8e2922af8907b836f24e1526dfc1 Mon Sep 17 00:00:00 2001 From: Owen Nelson Date: Tue, 15 Oct 2024 12:37:49 -0700 Subject: [PATCH] Libs(Kotlin): reapply local patches to 7.9.0 templates Removes the enum_class mustache and custom serializer impl in the hopes that the 7.9.0 generator handles these correctly without our patches. Testing should confirm this. If they are still needed, we'll bring that stuff back with the tests. The ApiClient template had a block for running requests as a suspendable coroutine which _should be present_ since we configure the generator with `useCoroutines=true`, however I need to learn more kotlin before I'm confident about how to fit the retry loop around that. For now, I've left the blocking-style request we've always had in place (i.e. the block you'd get when `useCoroutines=false`). Parts of `build.gradle` have been updated to track the generator. The kotlin version, for example, was needed to get the build working. N.b. if the build starts hanging for you, clean out the `build` and `generated` directories since there will be stale artifacts in each that can interfere. --- kotlin/build.gradle | 6 +- .../openapi/.openapi-generator-ignore | 2 +- .../internal/infrastructure/Serializer.kt | 63 ----------------- kotlin/templates/enum_class.mustache | 67 ------------------- .../infrastructure/ApiClient.kt.mustache | 57 +++++++++++----- 5 files changed, 45 insertions(+), 150 deletions(-) delete mode 100644 kotlin/lib/generated/openapi/src/main/kotlin/com/svix/kotlin/internal/infrastructure/Serializer.kt delete mode 100644 kotlin/templates/enum_class.mustache diff --git a/kotlin/build.gradle b/kotlin/build.gradle index 5076072a7..88f04c0e4 100644 --- a/kotlin/build.gradle +++ b/kotlin/build.gradle @@ -2,12 +2,13 @@ group GROUP version VERSION_NAME wrapper { - gradleVersion = '6.8.3' + gradleVersion = '8.7' distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip" } buildscript { - ext.kotlin_version = '1.5.10' + ext.kotlin_version = '1.9.23' + ext.spotless_version = "6.25.0" repositories { maven { url "https://repo1.maven.org/maven2" } @@ -15,6 +16,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.diffplug.spotless:spotless-plugin-gradle:$spotless_version" classpath "io.github.gradle-nexus:publish-plugin:1.1.0" } } diff --git a/kotlin/lib/generated/openapi/.openapi-generator-ignore b/kotlin/lib/generated/openapi/.openapi-generator-ignore index 166ad6ce2..7707c18fa 100644 --- a/kotlin/lib/generated/openapi/.openapi-generator-ignore +++ b/kotlin/lib/generated/openapi/.openapi-generator-ignore @@ -22,4 +22,4 @@ # Then explicitly reverse the ignore rule for a single file: #!docs/README.md -src/main/kotlin/com/svix/kotlin/internal/infrastructure/Serializer.kt \ No newline at end of file +# src/main/kotlin/com/svix/kotlin/internal/infrastructure/Serializer.kt \ No newline at end of file diff --git a/kotlin/lib/generated/openapi/src/main/kotlin/com/svix/kotlin/internal/infrastructure/Serializer.kt b/kotlin/lib/generated/openapi/src/main/kotlin/com/svix/kotlin/internal/infrastructure/Serializer.kt deleted file mode 100644 index a12ade971..000000000 --- a/kotlin/lib/generated/openapi/src/main/kotlin/com/svix/kotlin/internal/infrastructure/Serializer.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.svix.kotlin.internal.infrastructure - -import com.squareup.moshi.FromJson -import com.squareup.moshi.Moshi -import com.squareup.moshi.ToJson -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import com.svix.kotlin.models.MessageAttemptTriggerType -import com.svix.kotlin.models.MessageStatus - -object Serializer { - @JvmStatic - val moshiBuilder: Moshi.Builder = Moshi.Builder() - .add(OffsetDateTimeAdapter()) - .add(LocalDateTimeAdapter()) - .add(LocalDateAdapter()) - .add(UUIDAdapter()) - .add(ByteArrayAdapter()) - .add(URIAdapter()) - .add(KotlinJsonAdapterFactory()) - .add(BigDecimalAdapter()) - .add(BigIntegerAdapter()) - .add(MessageStatusAdapter()) - .add(MessageAttemptTriggerType()) - - @JvmStatic - val moshi: Moshi by lazy { - moshiBuilder.build() - } -} - -class MessageStatusAdapter { - @ToJson - fun toJson(messageStatus: MessageStatus): Int { - return messageStatus.value - } - - @FromJson - fun fromJson(messageStatus: Int): MessageStatus { - MessageStatus.values().forEach { - if (it.value == messageStatus) { - return it - } - } - return MessageStatus.Unknown - } -} - -class MessageAttemptTriggerType { - @ToJson - fun toJson(triggerType: MessageAttemptTriggerType): Int { - return triggerType.value - } - - @FromJson - fun fromJson(triggerType: Int): MessageAttemptTriggerType { - MessageAttemptTriggerType.values().forEach { - if (it.value == triggerType) { - return it - } - } - return MessageAttemptTriggerType.Unknown - } -} diff --git a/kotlin/templates/enum_class.mustache b/kotlin/templates/enum_class.mustache deleted file mode 100644 index 6536d010b..000000000 --- a/kotlin/templates/enum_class.mustache +++ /dev/null @@ -1,67 +0,0 @@ -{{^multiplatform}} -{{#gson}} -import com.google.gson.annotations.SerializedName -{{/gson}} -{{#moshi}} -import com.squareup.moshi.Json -{{/moshi}} -{{#jackson}} -import com.fasterxml.jackson.annotation.JsonProperty -{{/jackson}} -{{#kotlinx_serialization}} -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -{{/kotlinx_serialization}} -{{/multiplatform}} -{{#multiplatform}} -import kotlinx.serialization.* -{{/multiplatform}} - -/** -* {{{description}}} -* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} -*/ - -{{#multiplatform}}@Serializable{{/multiplatform}}{{#kotlinx_serialization}}@Serializable{{/kotlinx_serialization}} -{{#nonPublicApi}}internal {{/nonPublicApi}}enum class {{classname}}(val value: {{{dataType}}}) { - - {{^isString}}Unknown(-1),{{/isString}} -{{#allowableValues}}{{#enumVars}} - {{^multiplatform}} - {{#moshi}} - @Json(name = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) - {{/moshi}} - {{#gson}} - @SerializedName(value = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) - {{/gson}} - {{#jackson}} - @JsonProperty(value = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) - {{/jackson}} - {{#kotlinx_serialization}} - @SerialName(value = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) - {{/kotlinx_serialization}} - {{/multiplatform}} - {{#multiplatform}} - @SerialName(value = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) - {{/multiplatform}} - {{#isArray}} - {{#isList}} - {{&name}}(listOf({{{value}}})){{^-last}},{{/-last}}{{#-last}};{{/-last}} - {{/isList}} - {{^isList}} - {{&name}}(arrayOf({{{value}}})){{^-last}},{{/-last}}{{#-last}};{{/-last}} - {{/isList}} - {{/isArray}} - {{^isArray}} - {{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} - {{/isArray}} -{{/enumVars}}{{/allowableValues}} - - /** - This override toString avoids using the enum var name and uses the actual api value instead. - In cases the var name and value are different, the client would send incorrect enums to the server. - **/ - override fun toString(): String { - return value{{^isString}}.toString(){{/isString}} - } -} diff --git a/kotlin/templates/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache b/kotlin/templates/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache index d60e54cd9..db3b3bd77 100644 --- a/kotlin/templates/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache +++ b/kotlin/templates/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache @@ -57,6 +57,8 @@ import com.fasterxml.jackson.core.type.TypeReference {{#moshi}} import com.squareup.moshi.adapter {{/moshi}} +import kotlinx.coroutines.delay +import kotlin.random.Random {{#nonPublicApi}}internal {{/nonPublicApi}}val EMPTY_REQUEST: RequestBody = ByteArray(0).toRequestBody() @@ -69,13 +71,13 @@ import com.squareup.moshi.adapter protected const val FormDataMediaType = "multipart/form-data" protected const val FormUrlEncMediaType = "application/x-www-form-urlencoded" protected const val XmlMediaType = "application/xml" + protected const val UserAgent = "User-Agent" protected const val OctetMediaType = "application/octet-stream" val apiKey: MutableMap = mutableMapOf() val apiKeyPrefix: MutableMap = mutableMapOf() var username: String? = null var password: String? = null - var accessToken: String? = null const val baseUrlKey = "{{packageName}}.baseUrl" @JvmStatic @@ -87,6 +89,11 @@ import com.squareup.moshi.adapter val builder: OkHttpClient.Builder = OkHttpClient.Builder() } + var accessToken: String? = null + var userAgent: String? = null + var initialRetryDelayMillis = 50L + var numRetries: Int = 3 + /** * Guess Content-Type header from the given file (defaults to "application/octet-stream"). * @@ -137,6 +144,9 @@ import com.squareup.moshi.adapter } mediaType == null || mediaType.startsWith("application/") && mediaType.endsWith("json") -> if (content == null) { + // FIXME(onelson): double check that this is where we land on DELETE requests + // We initially had to patch the client for responses without a body. + // Ref: https://github.com/svix/svix-webhooks/pull/1124 EMPTY_REQUEST } else { {{#moshi}} @@ -318,6 +328,13 @@ import com.squareup.moshi.adapter updateAuthParams(requestConfig) {{/hasAuthMethods}} + // add user agent + if (requestConfig.headers[UserAgent].isNullOrEmpty()) { + userAgent?.let { userAgent -> + requestConfig.headers[UserAgent] = userAgent + } + } + val url = httpUrl.newBuilder() .addEncodedPathSegments(requestConfig.path.trimStart('/')) .apply { @@ -332,6 +349,9 @@ import com.squareup.moshi.adapter if (requestConfig.body != null && requestConfig.headers[ContentType].isNullOrEmpty()) { requestConfig.headers[ContentType] = JsonMediaType } + if (requestConfig.headers["svix-req-id"].isNullOrEmpty()) { + requestConfig.headers["svix-req-id"] = Math.abs(Random.nextBits(32)).toString() + } if (requestConfig.headers[Accept].isNullOrEmpty()) { requestConfig.headers[Accept] = JsonMediaType } @@ -360,23 +380,26 @@ import com.squareup.moshi.adapter headers.forEach { header -> addHeader(header.key, header.value) } }.build() - {{#useCoroutines}} - val response: Response = suspendCancellableCoroutine { continuation -> - val call = client.newCall(request) - continuation.invokeOnCancellation { call.cancel() } - call.enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - continuation.resumeWithException(e) - } - override fun onResponse(call: Call, response: Response) { - continuation.resume(response) - } - }) + + // FIXME(onelson): the upstream generator template has the below block for the useCoroutines=false case. + // More reading up on kotlin required before it's clear how to do a retry loop over a suspendable. + // For now, we'll do the non-coroutine thing. + // Ref: https://github.com/OpenAPITools/openapi-generator/blob/4145000dfebe7a9edea4555c8515383da7602458/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache#L363-L376 + var response = client.newCall(request).execute() + + var sleepTime = initialRetryDelayMillis + var retryCount = 0 + for (i in 0 until numRetries-1) { + if (response.isSuccessful || response.code < 500) { + break + } + response.close() + retryCount = retryCount.inc() + delay(sleepTime) + sleepTime = sleepTime * 2 + var newRequest = request.newBuilder().header("svix-retry-count", retryCount.toString()).build() + response = client.newCall(newRequest).execute() } - {{/useCoroutines}} - {{^useCoroutines}} - val response = client.newCall(request).execute() - {{/useCoroutines}} val accept = response.header(ContentType)?.substringBefore(";")?.lowercase(Locale.US)