diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a7202d..2915cd2 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: | @@ -40,14 +40,17 @@ jobs: build: timeout-minutes: 15 name: build + permissions: + contents: read + id-token: write runs-on: ${{ github.repository == 'stainless-sdks/blooio-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: | @@ -61,16 +64,31 @@ jobs: - name: Build SDK run: ./scripts/build + - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/blooio-java' + id: github-oidc + uses: actions/github-script@v8 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Build and upload Maven artifacts + if: github.repository == 'stainless-sdks/blooio-java' + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + PROJECT: blooio-java + run: ./scripts/upload-artifacts test: timeout-minutes: 15 name: test runs-on: ${{ github.repository == 'stainless-sdks/blooio-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 11f4929..2d81e3a 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 53c616f..2d36b26 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'Blooio/blooio-java-sdk' && (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 2ed3b71..3d2ac0b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.4" + ".": "0.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index af9651d..3daf3d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 0.1.0 (2026-02-07) + +Full Changelog: [v0.0.4...v0.1.0](https://github.com/Blooio/blooio-java-sdk/compare/v0.0.4...v0.1.0) + +### Features + +* **client:** add `HttpRequest#url()` method ([dd2bca3](https://github.com/Blooio/blooio-java-sdk/commit/dd2bca38596e4ef7626ef23d1a73ce7678a4375e)) +* **client:** allow configuring dispatcher executor service ([4f0db30](https://github.com/Blooio/blooio-java-sdk/commit/4f0db3097b9b0c1876887115146216c0b3a50c30)) +* **client:** send `X-Stainless-Kotlin-Version` header ([f6d45cd](https://github.com/Blooio/blooio-java-sdk/commit/f6d45cddc950b7443fb661d4804c4986ef89c187)) + + +### Bug Fixes + +* **client:** disallow coercion from float to int ([3ee28c5](https://github.com/Blooio/blooio-java-sdk/commit/3ee28c5e8bd033c3d96134fb13903941de2e8735)) +* **client:** fully respect max retries ([2ddb812](https://github.com/Blooio/blooio-java-sdk/commit/2ddb8126d441b260c80b9ef3b87a24f9ce20b5be)) +* **client:** preserve time zone in lenient date-time parsing ([573fcaf](https://github.com/Blooio/blooio-java-sdk/commit/573fcaf71930c387314fb18231334b542df8a47f)) +* **client:** send retry count header for max retries 0 ([2ddb812](https://github.com/Blooio/blooio-java-sdk/commit/2ddb8126d441b260c80b9ef3b87a24f9ce20b5be)) +* date time deserialization leniency ([b2ac60a](https://github.com/Blooio/blooio-java-sdk/commit/b2ac60a271022b4f389feee2e73467927b2c7a51)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([a32a19d](https://github.com/Blooio/blooio-java-sdk/commit/a32a19dcfbeff710c7c6c0b4daaa8ed98107294a)) +* **ci:** upgrade `actions/setup-java` ([67eb04e](https://github.com/Blooio/blooio-java-sdk/commit/67eb04e66d48b47097af4e8d66f093688ff1f8ef)) +* **internal:** allow passing args to `./scripts/test` ([a8da670](https://github.com/Blooio/blooio-java-sdk/commit/a8da67050906f36094fdf4cf4fb278d6abe01f58)) +* **internal:** clean up maven repo artifact script and add html documentation to repo root ([d441da9](https://github.com/Blooio/blooio-java-sdk/commit/d441da9aa8607881a63e2ed14b9037fcccf1600d)) +* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([d3c3b9c](https://github.com/Blooio/blooio-java-sdk/commit/d3c3b9c5e26275edbc5897780f4338d88e65eb7d)) +* **internal:** depend on packages directly in example ([2ddb812](https://github.com/Blooio/blooio-java-sdk/commit/2ddb8126d441b260c80b9ef3b87a24f9ce20b5be)) +* **internal:** improve maven repo docs ([24c2d4b](https://github.com/Blooio/blooio-java-sdk/commit/24c2d4b5b8e3295a5218d2f999fd796fef269b88)) +* **internal:** support uploading Maven repo artifacts to stainless package server ([d790509](https://github.com/Blooio/blooio-java-sdk/commit/d7905093b659e696e05478ee514fbbad370cc9d0)) +* **internal:** update `actions/checkout` version ([8cfab13](https://github.com/Blooio/blooio-java-sdk/commit/8cfab13629687564f829968a161b50843d8ddc73)) +* **internal:** update maven repo doc to include authentication ([0259720](https://github.com/Blooio/blooio-java-sdk/commit/025972007906b16f60bb8dc09ff3433c752f4be0)) +* **internal:** upgrade AssertJ ([0c278a7](https://github.com/Blooio/blooio-java-sdk/commit/0c278a7a62ba05270a09d4ad6d4e0c1eadebd280)) +* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/Blooio/blooio-java-sdk/issues/3240) in tests ([b2ac60a](https://github.com/Blooio/blooio-java-sdk/commit/b2ac60a271022b4f389feee2e73467927b2c7a51)) + + +### Documentation + +* add comment for arbitrary value fields ([a636226](https://github.com/Blooio/blooio-java-sdk/commit/a636226690a94e032e7b7d6e8fcf9d2f373bcfc3)) + ## 0.0.4 (2025-12-03) Full Changelog: [v0.0.3...v0.0.4](https://github.com/Blooio/blooio-java-sdk/compare/v0.0.3...v0.0.4) diff --git a/LICENSE b/LICENSE index 789f1c0..ce4ae4d 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Blooio + Copyright 2026 Blooio Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 0ec93d0..7b7b1f3 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.blooio.api/blooio-java)](https://central.sonatype.com/artifact/com.blooio.api/blooio-java/0.0.4) -[![javadoc](https://javadoc.io/badge2/com.blooio.api/blooio-java/0.0.4/javadoc.svg)](https://javadoc.io/doc/com.blooio.api/blooio-java/0.0.4) +[![Maven Central](https://img.shields.io/maven-central/v/com.blooio.api/blooio-java)](https://central.sonatype.com/artifact/com.blooio.api/blooio-java/0.1.0) +[![javadoc](https://javadoc.io/badge2/com.blooio.api/blooio-java/0.1.0/javadoc.svg)](https://javadoc.io/doc/com.blooio.api/blooio-java/0.1.0) @@ -13,7 +13,7 @@ It is generated with [Stainless](https://www.stainless.com/). -Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.blooio.api/blooio-java/0.0.4). +Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.blooio.api/blooio-java/0.1.0). @@ -24,7 +24,7 @@ Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.blooio.api/blo ### Gradle ```kotlin -implementation("com.blooio.api:blooio-java:0.0.4") +implementation("com.blooio.api:blooio-java:0.1.0") ``` ### Maven @@ -33,7 +33,7 @@ implementation("com.blooio.api:blooio-java:0.0.4") com.blooio.api blooio-java - 0.0.4 + 0.1.0 ``` @@ -258,6 +258,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/blooio-java-client-okhttp/build.gradle.kts b/blooio-java-client-okhttp/build.gradle.kts index 3449d62..e556e32 100644 --- a/blooio-java-client-okhttp/build.gradle.kts +++ b/blooio-java-client-okhttp/build.gradle.kts @@ -10,6 +10,6 @@ dependencies { implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") testImplementation(kotlin("test")) - testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation("org.assertj:assertj-core:3.27.7") testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") } diff --git a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClient.kt b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClient.kt index 1d24917..e4722d3 100644 --- a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClient.kt +++ b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClient.kt @@ -16,6 +16,7 @@ import java.net.Proxy import java.time.Clock import java.time.Duration import java.util.Optional +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager @@ -44,11 +45,31 @@ class BlooioOkHttpClient private constructor() { class Builder internal constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() + private var dispatcherExecutorService: ExecutorService? = null private var proxy: Proxy? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null + /** + * The executor service to use for running HTTP requests. + * + * Defaults to OkHttp's + * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104). + * + * This class takes ownership of the executor service and shuts it down when closed. + */ + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + + /** + * Alias for calling [Builder.dispatcherExecutorService] with + * `dispatcherExecutorService.orElse(null)`. + */ + fun dispatcherExecutorService(dispatcherExecutorService: Optional) = + dispatcherExecutorService(dispatcherExecutorService.getOrNull()) + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ @@ -297,6 +318,7 @@ class BlooioOkHttpClient private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .dispatcherExecutorService(dispatcherExecutorService) .sslSocketFactory(sslSocketFactory) .trustManager(trustManager) .hostnameVerifier(hostnameVerifier) diff --git a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClientAsync.kt b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClientAsync.kt index d5e7916..3572e6a 100644 --- a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClientAsync.kt +++ b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClientAsync.kt @@ -16,6 +16,7 @@ import java.net.Proxy import java.time.Clock import java.time.Duration import java.util.Optional +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager @@ -44,11 +45,31 @@ class BlooioOkHttpClientAsync private constructor() { class Builder internal constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() + private var dispatcherExecutorService: ExecutorService? = null private var proxy: Proxy? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null + /** + * The executor service to use for running HTTP requests. + * + * Defaults to OkHttp's + * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104). + * + * This class takes ownership of the executor service and shuts it down when closed. + */ + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + + /** + * Alias for calling [Builder.dispatcherExecutorService] with + * `dispatcherExecutorService.orElse(null)`. + */ + fun dispatcherExecutorService(dispatcherExecutorService: Optional) = + dispatcherExecutorService(dispatcherExecutorService.getOrNull()) + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ @@ -297,6 +318,7 @@ class BlooioOkHttpClientAsync private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .dispatcherExecutorService(dispatcherExecutorService) .sslSocketFactory(sslSocketFactory) .trustManager(trustManager) .hostnameVerifier(hostnameVerifier) diff --git a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/OkHttpClient.kt b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/OkHttpClient.kt index a5565a5..f01ac88 100644 --- a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/OkHttpClient.kt +++ b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/OkHttpClient.kt @@ -15,11 +15,13 @@ import java.net.Proxy import java.time.Duration import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Call import okhttp3.Callback +import okhttp3.Dispatcher import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType @@ -198,6 +200,7 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien private var timeout: Timeout = Timeout.default() private var proxy: Proxy? = null + private var dispatcherExecutorService: ExecutorService? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null @@ -208,6 +211,10 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { this.sslSocketFactory = sslSocketFactory } @@ -223,12 +230,16 @@ 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()) .callTimeout(timeout.request()) .proxy(proxy) .apply { + dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) } + val sslSocketFactory = sslSocketFactory val trustManager = trustManager if (sslSocketFactory != null && trustManager != null) { diff --git a/blooio-java-core/build.gradle.kts b/blooio-java-core/build.gradle.kts index 813acf4..f647be8 100644 --- a/blooio-java-core/build.gradle.kts +++ b/blooio-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") } } @@ -31,7 +33,7 @@ dependencies { testImplementation(kotlin("test")) testImplementation(project(":blooio-java-client-okhttp")) testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") - testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation("org.assertj:assertj-core:3.27.7") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3") testImplementation("org.junit-pioneer:junit-pioneer:1.9.1") diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/ClientOptions.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/ClientOptions.kt index 815c3e2..5d2f3a9 100644 --- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/ClientOptions.kt +++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/ClientOptions.kt @@ -404,6 +404,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/blooio-java-core/src/main/kotlin/com/blooio/api/core/ObjectMappers.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/ObjectMappers.kt index 555296c..47b2466 100644 --- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/ObjectMappers.kt +++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/ObjectMappers.kt @@ -24,7 +24,8 @@ import java.io.InputStream import java.time.DateTimeException import java.time.LocalDate import java.time.LocalDateTime -import java.time.ZonedDateTime +import java.time.OffsetDateTime +import java.time.ZoneId 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) { @@ -150,17 +158,20 @@ private class LenientLocalDateTimeDeserializer : return when { !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> - LocalDate.from(temporal).atStartOfDay() + LocalDate.from(temporal) + .atStartOfDay() + .atZone(ZoneId.of("UTC")) + .toOffsetDateTime() !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> - LocalDateTime.from(temporal) - else -> ZonedDateTime.from(temporal).toLocalDateTime() + LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime() + else -> OffsetDateTime.from(temporal) } } 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/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequest.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequest.kt index 7c10228..5bb901b 100644 --- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequest.kt +++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequest.kt @@ -2,6 +2,7 @@ package com.blooio.api.core.http import com.blooio.api.core.checkRequired import com.blooio.api.core.toImmutable +import java.net.URLEncoder class HttpRequest private constructor( @@ -13,6 +14,35 @@ private constructor( @get:JvmName("body") val body: HttpRequestBody?, ) { + fun url(): String = buildString { + append(baseUrl) + + pathSegments.forEach { segment -> + if (!endsWith("/")) { + append("/") + } + append(URLEncoder.encode(segment, "UTF-8")) + } + + if (queryParams.isEmpty()) { + return@buildString + } + + append("?") + var isFirst = true + queryParams.keys().forEach { key -> + queryParams.values(key).forEach { value -> + if (!isFirst) { + append("&") + } + append(URLEncoder.encode(key, "UTF-8")) + append("=") + append(URLEncoder.encode(value, "UTF-8")) + isFirst = false + } + } + } + fun toBuilder(): Builder = Builder().from(this) override fun toString(): String = diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/RetryingHttpClient.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/RetryingHttpClient.kt index 6f34419..f261e5f 100644 --- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/RetryingHttpClient.kt +++ b/blooio-java-core/src/main/kotlin/com/blooio/api/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/blooio-java-core/src/main/kotlin/com/blooio/api/models/me/MeRetrieveResponse.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/models/me/MeRetrieveResponse.kt index 05b1a17..fb7c24c 100644 --- a/blooio-java-core/src/main/kotlin/com/blooio/api/models/me/MeRetrieveResponse.kt +++ b/blooio-java-core/src/main/kotlin/com/blooio/api/models/me/MeRetrieveResponse.kt @@ -71,7 +71,14 @@ private constructor( fun integrationDetails(): Optional = integrationDetails.getOptional("integration_details") - /** Custom metadata associated with the API key. */ + /** + * Custom metadata associated with the API key. + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = meRetrieveResponse.metadata().convert(MyClass.class); + * ``` + */ @JsonProperty("metadata") @ExcludeMissing fun _metadata(): JsonValue = metadata /** @@ -614,7 +621,14 @@ private constructor( fun customerWebhookUrl(): Optional = customerWebhookUrl.getOptional("customer_webhook_url") - /** Integration-specific metadata. */ + /** + * Integration-specific metadata. + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = integrationDetails.metadata().convert(MyClass.class); + * ``` + */ @JsonProperty("metadata") @ExcludeMissing fun _metadata(): JsonValue = metadata /** diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/models/messages/MessageRetrieveResponse.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/models/messages/MessageRetrieveResponse.kt index 38a65f1..59a17ee 100644 --- a/blooio-java-core/src/main/kotlin/com/blooio/api/models/messages/MessageRetrieveResponse.kt +++ b/blooio-java-core/src/main/kotlin/com/blooio/api/models/messages/MessageRetrieveResponse.kt @@ -97,7 +97,14 @@ private constructor( */ fun messageId(): Optional = messageId.getOptional("message_id") - /** Original metadata provided plus system generated fields. */ + /** + * Original metadata provided plus system generated fields. + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = messageRetrieveResponse.metadata().convert(MyClass.class); + * ``` + */ @JsonProperty("metadata") @ExcludeMissing fun _metadata(): JsonValue = metadata /** diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/models/messages/MessageSendParams.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/models/messages/MessageSendParams.kt index cf098d3..abccdd8 100644 --- a/blooio-java-core/src/main/kotlin/com/blooio/api/models/messages/MessageSendParams.kt +++ b/blooio-java-core/src/main/kotlin/com/blooio/api/models/messages/MessageSendParams.kt @@ -56,7 +56,14 @@ private constructor( */ fun attachments(): Optional> = body.attachments() - /** Arbitrary key/value pairs to associate with the message. */ + /** + * Arbitrary key/value pairs to associate with the message. + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = messageSendParams.metadata().convert(MyClass.class); + * ``` + */ fun _metadata(): JsonValue = body._metadata() /** @@ -377,7 +384,14 @@ private constructor( */ fun attachments(): Optional> = attachments.getOptional("attachments") - /** Arbitrary key/value pairs to associate with the message. */ + /** + * Arbitrary key/value pairs to associate with the message. + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = body.metadata().convert(MyClass.class); + * ``` + */ @JsonProperty("metadata") @ExcludeMissing fun _metadata(): JsonValue = metadata /** diff --git a/blooio-java-core/src/test/kotlin/com/blooio/api/core/ObjectMappersTest.kt b/blooio-java-core/src/test/kotlin/com/blooio/api/core/ObjectMappersTest.kt index a6a39a5..a787581 100644 --- a/blooio-java-core/src/test/kotlin/com/blooio/api/core/ObjectMappersTest.kt +++ b/blooio-java-core/src/test/kotlin/com/blooio/api/core/ObjectMappersTest.kt @@ -3,12 +3,14 @@ package com.blooio.api.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.LocalDate +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.ZoneOffset import kotlin.reflect.KClass import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource import org.junitpioneer.jupiter.cartesian.CartesianTest @@ -46,11 +48,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 +56,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,19 +74,44 @@ internal class ObjectMappersTest { } } - enum class LenientLocalDateTimeTestCase(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"), - ZONED_DATE_TIME_2("1998-04-21T04:00:00Z"), + enum class LenientOffsetDateTimeTestCase( + val string: String, + val expectedOffsetDateTime: OffsetDateTime, + ) { + DATE( + "1998-04-21", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(0, 0), ZoneOffset.UTC), + ), + DATE_TIME( + "1998-04-21T04:00:00", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC), + ), + ZONED_DATE_TIME_1( + "1998-04-21T04:00:00+03:00", + expectedOffsetDateTime = + OffsetDateTime.of( + LocalDate.of(1998, 4, 21), + LocalTime.of(4, 0), + ZoneOffset.ofHours(3), + ), + ), + ZONED_DATE_TIME_2( + "1998-04-21T04:00:00Z", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC), + ), } @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) } + val offsetDateTime = jsonMapper().readValue(json) + + assertThat(offsetDateTime).isEqualTo(testCase.expectedOffsetDateTime) } } diff --git a/blooio-java-core/src/test/kotlin/com/blooio/api/core/http/HttpRequestTest.kt b/blooio-java-core/src/test/kotlin/com/blooio/api/core/http/HttpRequestTest.kt new file mode 100644 index 0000000..57d1430 --- /dev/null +++ b/blooio-java-core/src/test/kotlin/com/blooio/api/core/http/HttpRequestTest.kt @@ -0,0 +1,110 @@ +package com.blooio.api.core.http + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +internal class HttpRequestTest { + + enum class UrlTestCase(val request: HttpRequest, val expectedUrl: String) { + BASE_URL_ONLY( + HttpRequest.builder().method(HttpMethod.GET).baseUrl("https://api.example.com").build(), + expectedUrl = "https://api.example.com", + ), + BASE_URL_WITH_TRAILING_SLASH( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com/") + .build(), + expectedUrl = "https://api.example.com/", + ), + SINGLE_PATH_SEGMENT( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .build(), + expectedUrl = "https://api.example.com/users", + ), + MULTIPLE_PATH_SEGMENTS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegments("users", "123", "profile") + .build(), + expectedUrl = "https://api.example.com/users/123/profile", + ), + PATH_SEGMENT_WITH_SPECIAL_CHARS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("user name") + .build(), + expectedUrl = "https://api.example.com/user+name", + ), + SINGLE_QUERY_PARAM( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParam("limit", "10") + .build(), + expectedUrl = "https://api.example.com/users?limit=10", + ), + MULTIPLE_QUERY_PARAMS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParam("limit", "10") + .putQueryParam("offset", "20") + .build(), + expectedUrl = "https://api.example.com/users?limit=10&offset=20", + ), + QUERY_PARAM_WITH_SPECIAL_CHARS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("search") + .putQueryParam("q", "hello world") + .build(), + expectedUrl = "https://api.example.com/search?q=hello+world", + ), + MULTIPLE_VALUES_SAME_PARAM( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParams("tags", listOf("admin", "user")) + .build(), + expectedUrl = "https://api.example.com/users?tags=admin&tags=user", + ), + BASE_URL_WITH_TRAILING_SLASH_AND_PATH( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com/") + .addPathSegment("users") + .build(), + expectedUrl = "https://api.example.com/users", + ), + COMPLEX_URL( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl("https://api.example.com") + .addPathSegments("v1", "users", "123") + .putQueryParams("include", listOf("profile", "settings")) + .putQueryParam("format", "json") + .build(), + expectedUrl = + "https://api.example.com/v1/users/123?include=profile&include=settings&format=json", + ), + } + + @ParameterizedTest + @EnumSource + fun url(testCase: UrlTestCase) { + val actualUrl = testCase.request.url() + + assertThat(actualUrl).isEqualTo(testCase.expectedUrl) + } +} diff --git a/blooio-java-proguard-test/build.gradle.kts b/blooio-java-proguard-test/build.gradle.kts index 720f493..34ff8aa 100644 --- a/blooio-java-proguard-test/build.gradle.kts +++ b/blooio-java-proguard-test/build.gradle.kts @@ -18,8 +18,8 @@ dependencies { testImplementation(project(":blooio-java")) 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("org.assertj:assertj-core:3.27.7") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } tasks.shadowJar { diff --git a/build.gradle.kts b/build.gradle.kts index 3773229..1f87ca0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ repositories { allprojects { group = "com.blooio.api" - version = "0.0.4" // x-release-please-version + version = "0.1.0" // x-release-please-version } subprojects { diff --git a/buildSrc/src/main/kotlin/blooio.kotlin.gradle.kts b/buildSrc/src/main/kotlin/blooio.kotlin.gradle.kts index 42465f2..2ae7785 100644 --- a/buildSrc/src/main/kotlin/blooio.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/blooio.kotlin.gradle.kts @@ -33,6 +33,9 @@ kotlin { tasks.withType().configureEach { systemProperty("junit.jupiter.execution.parallel.enabled", true) systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") + + // `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation. + inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true) } val ktfmt by configurations.creating diff --git a/buildSrc/src/main/kotlin/blooio.publish.gradle.kts b/buildSrc/src/main/kotlin/blooio.publish.gradle.kts index 89bd9b0..e83dae5 100644 --- a/buildSrc/src/main/kotlin/blooio.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/blooio.publish.gradle.kts @@ -39,6 +39,14 @@ configure { } } } + repositories { + if (project.hasProperty("publishLocal")) { + maven { + name = "LocalFileSystem" + url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo") + } + } + } } signing { diff --git a/scripts/build b/scripts/build index f406348..16a2b00 100755 --- a/scripts/build +++ b/scripts/build @@ -5,4 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Building classes" -./gradlew build testClasses -x test +./gradlew build testClasses "$@" -x test diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts new file mode 100755 index 0000000..10f3c70 --- /dev/null +++ b/scripts/upload-artifacts @@ -0,0 +1,193 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# ANSI Color Codes +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" + local body="$3" + echo -e "${RED}${msg}${NC}" + [[ -f "$headers" ]] && echo -e "${RED}Headers:$(cat "$headers")${NC}" + echo -e "${RED}Body: ${body}${NC}" + exit 1 +} + +upload_file() { + local file_name="$1" + local tmp_headers + tmp_headers=$(mktemp) + + if [ -f "$file_name" ]; then + echo -e "${GREEN}Processing file: $file_name${NC}" + pkg_file_name="mvn${file_name#"${MAVEN_REPO_PATH}"}" + + # Get signed URL for uploading artifact file + signed_url_response=$(curl -X POST -G "$URL" \ + -sS --retry 5 \ + -D "$tmp_headers" \ + --data-urlencode "filename=$pkg_file_name" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + + # Validate JSON and extract URL + if ! signed_url=$(echo "$signed_url_response" | jq -e -r '.url' 2>/dev/null) || [[ "$signed_url" == "null" ]]; then + log_error "Failed to get valid signed URL" "$tmp_headers" "$signed_url_response" + fi + + # Set content-type based on file extension + local extension="${file_name##*.}" + local content_type + case "$extension" in + jar) content_type="application/java-archive" ;; + 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 to upload artifact file" "$tmp_headers" "$upload_response" + fi + + # Insert small throttle to reduce rate limiting risk + sleep 0.1 + fi +} + +walk_tree() { + local current_dir="$1" + + for entry in "$current_dir"/*; do + # Check that entry is valid + [ -e "$entry" ] || [ -h "$entry" ] || continue + + if [ -d "$entry" ]; then + walk_tree "$entry" + else + upload_file "$entry" + fi + 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" +./gradlew publishMavenPublicationToLocalFileSystemRepository -PpublishLocal +echo "::endgroup::" + +echo "::group::Uploading to pkg.stainless.com" +walk_tree "$MAVEN_REPO_PATH" +echo "::endgroup::" + +echo "::group::Generating instructions" +generate_instructions +echo "::endgroup::"