diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0b5e4c3..57a12ad1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -47,10 +47,10 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -85,10 +85,10 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/courier-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml index 48bee9c5..800d5266 100644 --- a/.github/workflows/publish-sonatype.yml +++ b/.github/workflows/publish-sonatype.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index ba006aee..2cc67736 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'trycourier/courier-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f1a48d37..90eeef65 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.7.1" + ".": "4.8.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 92aa6915..98ae2b6d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 78 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-e3e54d99e2a73fd87519270f2685131050d342e86a4e96130247b854deae5c20.yml -openapi_spec_hash: 897a3fbee24f24d021d6af0df480220c -config_hash: 66a5c28bb74d78454456d9ce7d1c0a0c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-9a543994a29d199daec5228c34a90caf017db9a1084289f58645c6849b606940.yml +openapi_spec_hash: f2f64858daadb0a1978b0e4a4d8ed149 +config_hash: ba6cf7f4dbdf6b9d03c6962dd1770569 diff --git a/CHANGELOG.md b/CHANGELOG.md index 20855c1d..5ce2fad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## 4.8.0 (2026-01-22) + +Full Changelog: [v4.7.1...v4.8.0](https://github.com/trycourier/courier-java/compare/v4.7.1...v4.8.0) + +### Features + +* **client:** send `X-Stainless-Kotlin-Version` header ([11e5765](https://github.com/trycourier/courier-java/commit/11e5765a138fa78d8aaf0abc3df4dffd1493d3b4)) + + +### Bug Fixes + +* **client:** disallow coercion from float to int ([5263a13](https://github.com/trycourier/courier-java/commit/5263a1385622f70224b69681b7f75661d89107ef)) +* **client:** fully respect max retries ([33cdc16](https://github.com/trycourier/courier-java/commit/33cdc16ed453f885b62c996480751a45035e0afd)) +* **client:** send retry count header for max retries 0 ([33cdc16](https://github.com/trycourier/courier-java/commit/33cdc16ed453f885b62c996480751a45035e0afd)) +* date time deserialization leniency ([fa50329](https://github.com/trycourier/courier-java/commit/fa50329d5f5e1cd2c555d5deb71b4dd4d8a57c4a)) + + +### Chores + +* **ci:** upgrade `actions/setup-java` ([01049af](https://github.com/trycourier/courier-java/commit/01049af97f49ca7d1ad74d09017838263e24735f)) +* **internal:** clean up maven repo artifact script and add html documentation to repo root ([e1937ab](https://github.com/trycourier/courier-java/commit/e1937aba76551a3e486cb8961c3f1e0981d9a57f)) +* **internal:** depend on packages directly in example ([33cdc16](https://github.com/trycourier/courier-java/commit/33cdc16ed453f885b62c996480751a45035e0afd)) +* **internal:** improve maven repo docs ([af0c83b](https://github.com/trycourier/courier-java/commit/af0c83b582d3d26589a0f1176405a365df1e26b5)) +* **internal:** update `actions/checkout` version ([337fcd7](https://github.com/trycourier/courier-java/commit/337fcd7a6f5f7d5747ab6f48e74287e58d18eca0)) +* **internal:** update maven repo doc to include authentication ([dbf1829](https://github.com/trycourier/courier-java/commit/dbf182983acea01de5784fe9df85f1d43482de5a)) +* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/trycourier/courier-java/issues/3240) in tests ([fa50329](https://github.com/trycourier/courier-java/commit/fa50329d5f5e1cd2c555d5deb71b4dd4d8a57c4a)) + ## 4.7.1 (2026-01-14) Full Changelog: [v4.7.0...v4.7.1](https://github.com/trycourier/courier-java/compare/v4.7.0...v4.7.1) diff --git a/README.md b/README.md index 47bcf43a..a85bd3fe 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.courier/courier-java)](https://central.sonatype.com/artifact/com.courier/courier-java/4.7.1) -[![javadoc](https://javadoc.io/badge2/com.courier/courier-java/4.7.1/javadoc.svg)](https://javadoc.io/doc/com.courier/courier-java/4.7.1) +[![Maven Central](https://img.shields.io/maven-central/v/com.courier/courier-java)](https://central.sonatype.com/artifact/com.courier/courier-java/4.8.0) +[![javadoc](https://javadoc.io/badge2/com.courier/courier-java/4.8.0/javadoc.svg)](https://javadoc.io/doc/com.courier/courier-java/4.8.0) @@ -13,7 +13,7 @@ It is generated with [Stainless](https://www.stainless.com/). -The REST API documentation can be found on [www.courier.com](https://www.courier.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.courier/courier-java/4.7.1). +The REST API documentation can be found on [www.courier.com](https://www.courier.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.courier/courier-java/4.8.0). @@ -24,7 +24,7 @@ The REST API documentation can be found on [www.courier.com](https://www.courier ### Gradle ```kotlin -implementation("com.courier:courier-java:4.7.1") +implementation("com.courier:courier-java:4.8.0") ``` ### Maven @@ -33,7 +33,7 @@ implementation("com.courier:courier-java:4.7.1") com.courier courier-java - 4.7.1 + 4.8.0 ``` @@ -310,6 +310,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t > [!CAUTION] > We make no guarantee that the SDK works correctly when the Jackson version check is disabled. +Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead. + ## Network options ### Retries diff --git a/build.gradle.kts b/build.gradle.kts index 885270a0..3e741ebc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "com.courier" - version = "4.7.1" // x-release-please-version + version = "4.8.0" // x-release-please-version } subprojects { diff --git a/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/OkHttpClient.kt b/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/OkHttpClient.kt index 79595a1b..ca6ffdbe 100644 --- a/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/OkHttpClient.kt +++ b/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/OkHttpClient.kt @@ -230,6 +230,8 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien fun build(): OkHttpClient = OkHttpClient( okhttp3.OkHttpClient.Builder() + // `RetryingHttpClient` handles retries if the user enabled them. + .retryOnConnectionFailure(false) .connectTimeout(timeout.connect()) .readTimeout(timeout.read()) .writeTimeout(timeout.write()) diff --git a/courier-java-core/build.gradle.kts b/courier-java-core/build.gradle.kts index d31e4b11..3396f586 100644 --- a/courier-java-core/build.gradle.kts +++ b/courier-java-core/build.gradle.kts @@ -5,14 +5,16 @@ plugins { configurations.all { resolutionStrategy { - // Compile and test against a lower Jackson version to ensure we're compatible with it. - // We publish with a higher version (see below) to ensure users depend on a secure version by default. - force("com.fasterxml.jackson.core:jackson-core:2.13.4") - force("com.fasterxml.jackson.core:jackson-databind:2.13.4") - force("com.fasterxml.jackson.core:jackson-annotations:2.13.4") - force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4") - force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4") - force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + // Compile and test against a lower Jackson version to ensure we're compatible with it. Note that + // we generally support 2.13.4, but test against 2.14.0 because 2.13.4 has some annoying (but + // niche) bugs (users should upgrade if they encounter them). We publish with a higher version + // (see below) to ensure users depend on a secure version by default. + force("com.fasterxml.jackson.core:jackson-core:2.14.0") + force("com.fasterxml.jackson.core:jackson-databind:2.14.0") + force("com.fasterxml.jackson.core:jackson-annotations:2.14.0") + force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.0") + force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0") + force("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } } diff --git a/courier-java-core/src/main/kotlin/com/courier/core/ClientOptions.kt b/courier-java-core/src/main/kotlin/com/courier/core/ClientOptions.kt index f9dc8443..122fde64 100644 --- a/courier-java-core/src/main/kotlin/com/courier/core/ClientOptions.kt +++ b/courier-java-core/src/main/kotlin/com/courier/core/ClientOptions.kt @@ -402,6 +402,7 @@ private constructor( headers.put("X-Stainless-Package-Version", getPackageVersion()) headers.put("X-Stainless-Runtime", "JRE") headers.put("X-Stainless-Runtime-Version", getJavaVersion()) + headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString()) apiKey.let { if (!it.isEmpty()) { headers.put("Authorization", "Bearer $it") diff --git a/courier-java-core/src/main/kotlin/com/courier/core/ObjectMappers.kt b/courier-java-core/src/main/kotlin/com/courier/core/ObjectMappers.kt index a2cc05b6..ba88699c 100644 --- a/courier-java-core/src/main/kotlin/com/courier/core/ObjectMappers.kt +++ b/courier-java-core/src/main/kotlin/com/courier/core/ObjectMappers.kt @@ -24,6 +24,7 @@ import java.io.InputStream import java.time.DateTimeException import java.time.LocalDate import java.time.LocalDateTime +import java.time.OffsetDateTime import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField @@ -36,7 +37,7 @@ fun jsonMapper(): JsonMapper = .addModule( SimpleModule() .addSerializer(InputStreamSerializer) - .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer()) + .addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer()) ) .withCoercionConfig(LogicalType.Boolean) { it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) @@ -47,6 +48,7 @@ fun jsonMapper(): JsonMapper = } .withCoercionConfig(LogicalType.Integer) { it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) .setCoercion(CoercionInputShape.String, CoercionAction.Fail) .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) @@ -64,6 +66,12 @@ fun jsonMapper(): JsonMapper = .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) } + .withCoercionConfig(LogicalType.DateTime) { + it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } .withCoercionConfig(LogicalType.Array) { it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) @@ -124,10 +132,10 @@ private object InputStreamSerializer : BaseSerializer(InputStream:: } /** - * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes. + * A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes. */ -private class LenientLocalDateTimeDeserializer : - StdDeserializer(LocalDateTime::class.java) { +private class LenientOffsetDateTimeDeserializer : + StdDeserializer(OffsetDateTime::class.java) { companion object { @@ -141,7 +149,7 @@ private class LenientLocalDateTimeDeserializer : override fun logicalType(): LogicalType = LogicalType.DateTime - override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime { + override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime { val exceptions = mutableListOf() for (formatter in DATE_TIME_FORMATTERS) { @@ -149,18 +157,20 @@ private class LenientLocalDateTimeDeserializer : val temporal = formatter.parse(p.text) return when { - !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> - LocalDate.from(temporal).atStartOfDay() - !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> - LocalDateTime.from(temporal) - else -> ZonedDateTime.from(temporal).toLocalDateTime() - } + !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> + LocalDate.from(temporal).atStartOfDay() + !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> + LocalDateTime.from(temporal) + else -> ZonedDateTime.from(temporal).toLocalDateTime() + } + .atZone(context.timeZone.toZoneId()) + .toOffsetDateTime() } catch (e: DateTimeException) { exceptions.add(e) } } - throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply { + throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply { exceptions.forEach { addSuppressed(it) } } } diff --git a/courier-java-core/src/main/kotlin/com/courier/core/http/RetryingHttpClient.kt b/courier-java-core/src/main/kotlin/com/courier/core/http/RetryingHttpClient.kt index f2271eb8..91211328 100644 --- a/courier-java-core/src/main/kotlin/com/courier/core/http/RetryingHttpClient.kt +++ b/courier-java-core/src/main/kotlin/com/courier/core/http/RetryingHttpClient.kt @@ -31,10 +31,6 @@ private constructor( ) : HttpClient { override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { - if (!isRetryable(request) || maxRetries <= 0) { - return httpClient.execute(request, requestOptions) - } - var modifiedRequest = maybeAddIdempotencyHeader(request) // Don't send the current retry count in the headers if the caller set their own value. @@ -48,6 +44,10 @@ private constructor( modifiedRequest = setRetryCountHeader(modifiedRequest, retries) } + if (!isRetryable(modifiedRequest)) { + return httpClient.execute(modifiedRequest, requestOptions) + } + val response = try { val response = httpClient.execute(modifiedRequest, requestOptions) @@ -75,10 +75,6 @@ private constructor( request: HttpRequest, requestOptions: RequestOptions, ): CompletableFuture { - if (!isRetryable(request) || maxRetries <= 0) { - return httpClient.executeAsync(request, requestOptions) - } - val modifiedRequest = maybeAddIdempotencyHeader(request) // Don't send the current retry count in the headers if the caller set their own value. @@ -94,8 +90,12 @@ private constructor( val requestWithRetryCount = if (shouldSendRetryCount) setRetryCountHeader(request, retries) else request - return httpClient - .executeAsync(requestWithRetryCount, requestOptions) + val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions) + if (!isRetryable(requestWithRetryCount)) { + return responseFuture + } + + return responseFuture .handleAsync( fun( response: HttpResponse?, diff --git a/courier-java-core/src/main/kotlin/com/courier/models/MessageRoutingChannel.kt b/courier-java-core/src/main/kotlin/com/courier/models/MessageRoutingChannel.kt index d578d83f..1a6a9359 100644 --- a/courier-java-core/src/main/kotlin/com/courier/models/MessageRoutingChannel.kt +++ b/courier-java-core/src/main/kotlin/com/courier/models/MessageRoutingChannel.kt @@ -167,7 +167,7 @@ private constructor( .toList() return when (bestMatches.size) { // This can happen if what we're deserializing is completely incompatible with all - // the possible variants (e.g. deserializing from array). + // the possible variants (e.g. deserializing from boolean). 0 -> MessageRoutingChannel(_json = json) 1 -> bestMatches.single() // If there's more than one match with the highest validity, then use the first diff --git a/courier-java-core/src/main/kotlin/com/courier/models/UserProfileFirebaseToken.kt b/courier-java-core/src/main/kotlin/com/courier/models/UserProfileFirebaseToken.kt index 3f825320..0beef446 100644 --- a/courier-java-core/src/main/kotlin/com/courier/models/UserProfileFirebaseToken.kt +++ b/courier-java-core/src/main/kotlin/com/courier/models/UserProfileFirebaseToken.kt @@ -165,7 +165,7 @@ private constructor( .toList() return when (bestMatches.size) { // This can happen if what we're deserializing is completely incompatible with all - // the possible variants (e.g. deserializing from object). + // the possible variants (e.g. deserializing from boolean). 0 -> UserProfileFirebaseToken(_json = json) 1 -> bestMatches.single() // If there's more than one match with the highest validity, then use the first diff --git a/courier-java-core/src/main/kotlin/com/courier/models/notifications/NotificationGetContent.kt b/courier-java-core/src/main/kotlin/com/courier/models/notifications/NotificationGetContent.kt index d58e0f1e..d4365985 100644 --- a/courier-java-core/src/main/kotlin/com/courier/models/notifications/NotificationGetContent.kt +++ b/courier-java-core/src/main/kotlin/com/courier/models/notifications/NotificationGetContent.kt @@ -940,7 +940,7 @@ private constructor( .toList() return when (bestMatches.size) { // This can happen if what we're deserializing is completely incompatible - // with all the possible variants (e.g. deserializing from array). + // with all the possible variants (e.g. deserializing from boolean). 0 -> Content(_json = json) 1 -> bestMatches.single() // If there's more than one match with the highest validity, then use the diff --git a/courier-java-core/src/main/kotlin/com/courier/models/send/SendMessageParams.kt b/courier-java-core/src/main/kotlin/com/courier/models/send/SendMessageParams.kt index 23beed96..80bfc668 100644 --- a/courier-java-core/src/main/kotlin/com/courier/models/send/SendMessageParams.kt +++ b/courier-java-core/src/main/kotlin/com/courier/models/send/SendMessageParams.kt @@ -2089,7 +2089,7 @@ private constructor( return when (bestMatches.size) { // This can happen if what we're deserializing is completely // incompatible with all the possible variants (e.g. deserializing from - // object). + // boolean). 0 -> ExpiresIn(_json = json) 1 -> bestMatches.single() // If there's more than one match with the highest validity, then use diff --git a/courier-java-core/src/main/kotlin/com/courier/models/users/tokens/UserToken.kt b/courier-java-core/src/main/kotlin/com/courier/models/users/tokens/UserToken.kt index 038730e6..4df0674b 100644 --- a/courier-java-core/src/main/kotlin/com/courier/models/users/tokens/UserToken.kt +++ b/courier-java-core/src/main/kotlin/com/courier/models/users/tokens/UserToken.kt @@ -983,7 +983,7 @@ private constructor( .toList() return when (bestMatches.size) { // This can happen if what we're deserializing is completely incompatible with - // all the possible variants (e.g. deserializing from object). + // all the possible variants (e.g. deserializing from integer). 0 -> ExpiryDate(_json = json) 1 -> bestMatches.single() // If there's more than one match with the highest validity, then use the first diff --git a/courier-java-core/src/test/kotlin/com/courier/core/ObjectMappersTest.kt b/courier-java-core/src/test/kotlin/com/courier/core/ObjectMappersTest.kt index 027efee0..539939ef 100644 --- a/courier-java-core/src/test/kotlin/com/courier/core/ObjectMappersTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/core/ObjectMappersTest.kt @@ -3,7 +3,7 @@ package com.courier.core import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.fasterxml.jackson.module.kotlin.readValue -import java.time.LocalDateTime +import java.time.OffsetDateTime import kotlin.reflect.KClass import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable @@ -46,11 +46,7 @@ internal class ObjectMappersTest { val VALID_CONVERSIONS = listOf( FLOAT to DOUBLE, - FLOAT to INTEGER, - FLOAT to LONG, DOUBLE to FLOAT, - DOUBLE to INTEGER, - DOUBLE to LONG, INTEGER to FLOAT, INTEGER to DOUBLE, INTEGER to LONG, @@ -58,14 +54,6 @@ internal class ObjectMappersTest { LONG to DOUBLE, LONG to INTEGER, CLASS to MAP, - // These aren't actually valid, but coercion configs don't work for String until - // v2.14.0: https://github.com/FasterXML/jackson-databind/issues/3240 - // We currently test on v2.13.4. - BOOLEAN to STRING, - FLOAT to STRING, - DOUBLE to STRING, - INTEGER to STRING, - LONG to STRING, ) } } @@ -84,7 +72,7 @@ internal class ObjectMappersTest { } } - enum class LenientLocalDateTimeTestCase(val string: String) { + enum class LenientOffsetDateTimeTestCase(val string: String) { DATE("1998-04-21"), DATE_TIME("1998-04-21T04:00:00"), ZONED_DATE_TIME_1("1998-04-21T04:00:00+03:00"), @@ -93,10 +81,10 @@ internal class ObjectMappersTest { @ParameterizedTest @EnumSource - fun readLocalDateTime_lenient(testCase: LenientLocalDateTimeTestCase) { + fun readOffsetDateTime_lenient(testCase: LenientOffsetDateTimeTestCase) { val jsonMapper = jsonMapper() val json = jsonMapper.writeValueAsString(testCase.string) - assertDoesNotThrow { jsonMapper().readValue(json) } + assertDoesNotThrow { jsonMapper().readValue(json) } } } diff --git a/courier-java-core/src/test/kotlin/com/courier/models/MessageRoutingChannelTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/MessageRoutingChannelTest.kt index fcfc0fec..767ec93d 100644 --- a/courier-java-core/src/test/kotlin/com/courier/models/MessageRoutingChannelTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/models/MessageRoutingChannelTest.kt @@ -9,6 +9,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource internal class MessageRoutingChannelTest { @@ -67,11 +69,18 @@ internal class MessageRoutingChannelTest { assertThat(roundtrippedMessageRoutingChannel).isEqualTo(messageRoutingChannel) } - @Test - fun incompatibleJsonShapeDeserializesToUnknown() { - val value = JsonValue.from(listOf("invalid", "array")) + enum class IncompatibleJsonShapeTestCase(val value: JsonValue) { + BOOLEAN(JsonValue.from(false)), + INTEGER(JsonValue.from(-1)), + FLOAT(JsonValue.from(3.14)), + ARRAY(JsonValue.from(listOf("invalid", "array"))), + } + + @ParameterizedTest + @EnumSource + fun incompatibleJsonShapeDeserializesToUnknown(testCase: IncompatibleJsonShapeTestCase) { val messageRoutingChannel = - jsonMapper().convertValue(value, jacksonTypeRef()) + jsonMapper().convertValue(testCase.value, jacksonTypeRef()) val e = assertThrows { messageRoutingChannel.validate() } assertThat(e).hasMessageStartingWith("Unknown ") diff --git a/courier-java-core/src/test/kotlin/com/courier/models/UserProfileFirebaseTokenTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/UserProfileFirebaseTokenTest.kt index 99660664..61b8e987 100644 --- a/courier-java-core/src/test/kotlin/com/courier/models/UserProfileFirebaseTokenTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/models/UserProfileFirebaseTokenTest.kt @@ -9,6 +9,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource internal class UserProfileFirebaseTokenTest { @@ -60,11 +62,18 @@ internal class UserProfileFirebaseTokenTest { assertThat(roundtrippedUserProfileFirebaseToken).isEqualTo(userProfileFirebaseToken) } - @Test - fun incompatibleJsonShapeDeserializesToUnknown() { - val value = JsonValue.from(mapOf("invalid" to "object")) + enum class IncompatibleJsonShapeTestCase(val value: JsonValue) { + BOOLEAN(JsonValue.from(false)), + INTEGER(JsonValue.from(-1)), + FLOAT(JsonValue.from(3.14)), + OBJECT(JsonValue.from(mapOf("invalid" to "object"))), + } + + @ParameterizedTest + @EnumSource + fun incompatibleJsonShapeDeserializesToUnknown(testCase: IncompatibleJsonShapeTestCase) { val userProfileFirebaseToken = - jsonMapper().convertValue(value, jacksonTypeRef()) + jsonMapper().convertValue(testCase.value, jacksonTypeRef()) val e = assertThrows { userProfileFirebaseToken.validate() } assertThat(e).hasMessageStartingWith("Unknown ") diff --git a/courier-java-proguard-test/build.gradle.kts b/courier-java-proguard-test/build.gradle.kts index 9d263108..0d3c8c12 100644 --- a/courier-java-proguard-test/build.gradle.kts +++ b/courier-java-proguard-test/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } tasks.shadowJar { diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts index 729e6f22..10f3c705 100755 --- a/scripts/upload-artifacts +++ b/scripts/upload-artifacts @@ -7,6 +7,8 @@ GREEN='\033[32m' RED='\033[31m' NC='\033[0m' # No Color +MAVEN_REPO_PATH="./build/local-maven-repo" + log_error() { local msg="$1" local headers="$2" @@ -24,7 +26,7 @@ upload_file() { if [ -f "$file_name" ]; then echo -e "${GREEN}Processing file: $file_name${NC}" - pkg_file_name="mvn${file_name#./build/local-maven-repo}" + pkg_file_name="mvn${file_name#"${MAVEN_REPO_PATH}"}" # Get signed URL for uploading artifact file signed_url_response=$(curl -X POST -G "$URL" \ @@ -47,18 +49,20 @@ upload_file() { md5|sha1|sha256|sha512) content_type="text/plain" ;; module) content_type="application/json" ;; pom|xml) content_type="application/xml" ;; + html) content_type="text/html" ;; *) content_type="application/octet-stream" ;; esac # Upload file upload_response=$(curl -v -X PUT \ --retry 5 \ + --retry-all-errors \ -D "$tmp_headers" \ -H "Content-Type: $content_type" \ --data-binary "@${file_name}" "$signed_url" 2>&1) if ! echo "$upload_response" | grep -q "HTTP/[0-9.]* 200"; then - log_error "Failed upload artifact file" "$tmp_headers" "$upload_response" + log_error "Failed to upload artifact file" "$tmp_headers" "$upload_response" fi # Insert small throttle to reduce rate limiting risk @@ -81,6 +85,99 @@ walk_tree() { done } +generate_instructions() { + cat << EOF > "$MAVEN_REPO_PATH/index.html" + + + + Maven Repo + + +

Stainless SDK Maven Repository

+

This is the Maven repository for your Stainless Java SDK build.

+ +

Project configuration

+ +

The details depend on whether you're using Maven or Gradle as your build tool.

+ +

Maven

+ +

Add the following to your project's pom.xml:

+
<repositories>
+    <repository>
+        <id>stainless-sdk-repo</id>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

Gradle

+

Add the following to your build.gradle file:

+
repositories {
+    maven {
+        url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+    }
+}
+ +
+

Configuring authentication (if required)

+ +

Some accounts may require authentication to access the repository. If so, use the + following instructions, replacing YOUR_STAINLESS_API_TOKEN with your actual token.

+ +

Maven with authentication

+ +

First, ensure you have the following in your Maven settings.xml for repo authentication:

+
<servers>
+    <server>
+        <id>stainless-sdk-repo</id>
+        <configuration>
+            <httpHeaders>
+                <property>
+                    <name>Authorization</name>
+                    <value>Bearer YOUR_STAINLESS_API_TOKEN</value>
+                </property>
+            </httpHeaders>
+        </configuration>
+    </server>
+</servers>
+ +

Then, add the following to your project's pom.xml:

+
<repositories>
+    <repository>
+        <id>stainless-sdk-repo</id>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

Gradle with authentication

+

Add the following to your build.gradle file:

+
repositories {
+    maven {
+        url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+        credentials(HttpHeaderCredentials) {
+            name = "Authorization"
+            value = "Bearer YOUR_STAINLESS_API_TOKEN"
+        }
+        authentication {
+            header(HttpHeaderAuthentication)
+        }
+    }
+}
+
+ +

Using the repository

+

Once you've configured the repository, you can include dependencies from it as usual. See your + project README + for more details.

+ + +EOF + upload_file "${MAVEN_REPO_PATH}/index.html" + + echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'" + echo "For more details, see the directions in https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn/index.html" +} + cd "$(dirname "$0")/.." echo "::group::Creating local Maven content" @@ -88,9 +185,9 @@ echo "::group::Creating local Maven content" echo "::endgroup::" echo "::group::Uploading to pkg.stainless.com" -walk_tree "./build/local-maven-repo" +walk_tree "$MAVEN_REPO_PATH" echo "::endgroup::" echo "::group::Generating instructions" -echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'" +generate_instructions echo "::endgroup::"