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)