diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45a08d37..9b818855 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/prelude-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,35 @@ jobs: - name: Build SDK run: ./scripts/build + - name: Get GitHub OIDC Token + if: |- + github.repository == 'stainless-sdks/prelude-java' && + !startsWith(github.ref, 'refs/heads/stl/') + 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/prelude-java' && + !startsWith(github.ref, 'refs/heads/stl/') + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + PROJECT: prelude-java + run: ./scripts/upload-artifacts test: timeout-minutes: 15 name: test runs-on: ${{ github.repository == 'stainless-sdks/prelude-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 7279cb1e..fdbed593 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 f5b43959..326496d4 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'prelude-so/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 091cfb12..f7014c35 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.10.0" + ".": "0.11.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 3559e655..d3333fdd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-fe14bed3c1b6245444e1a53e7b0b172301c207ef9500dc68f4d8e58a7bfec436.yml -openapi_spec_hash: 4acdc9dd487011b2391f6fe02812a6bd -config_hash: 55380048fe5cf686421acf7eeaae21be +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-c9124c257dd54dd0728cb57306b9082007439bfefac11642542605edb3e7606d.yml +openapi_spec_hash: 61dc64cc814d10975a8825ec88fd9c1c +config_hash: 107ae5754168e80c4ad2cd779a75bc36 diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c22f7f..42fc7ae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,60 @@ # Changelog +## 0.11.0 (2026-03-07) + +Full Changelog: [v0.10.0...v0.11.0](https://github.com/prelude-so/java-sdk/compare/v0.10.0...v0.11.0) + +### Features + +* **api:** api update ([ca9ee11](https://github.com/prelude-so/java-sdk/commit/ca9ee118f3800cfde6765103742c06a015f47111)) +* **api:** api update ([d683d44](https://github.com/prelude-so/java-sdk/commit/d683d441c3d8d89015ee9ab971ad883fc8f81b32)) +* **api:** api update ([01e0e53](https://github.com/prelude-so/java-sdk/commit/01e0e5369605017ae1ef44d473d593584ef07a71)) +* **api:** api update ([53cbdac](https://github.com/prelude-so/java-sdk/commit/53cbdac9a2e4b4cffaab963be9edeb77f4a07cf3)) +* **api:** api update ([7b287a1](https://github.com/prelude-so/java-sdk/commit/7b287a1a1f2bae3f1185bb8650a49169947bbeb0)) +* **api:** api update ([aabe63c](https://github.com/prelude-so/java-sdk/commit/aabe63c8899e9177d6d053cdd998806302c9c111)) +* **api:** api update ([74b758c](https://github.com/prelude-so/java-sdk/commit/74b758c2e80672f942000e1201edd081d67b0d18)) +* **api:** api update ([dc0fc24](https://github.com/prelude-so/java-sdk/commit/dc0fc245ce976922680aaa766216f76b039f3d46)) +* **client:** add `HttpRequest#url()` method ([6b5a7b3](https://github.com/prelude-so/java-sdk/commit/6b5a7b3018b374bfc0c8337b82d9ae2cc8ca20ac)) +* **client:** add connection pooling option ([dabf164](https://github.com/prelude-so/java-sdk/commit/dabf164c493a424cd9542d1355af0113cc16c6be)) +* **client:** allow configuring dispatcher executor service ([960cc19](https://github.com/prelude-so/java-sdk/commit/960cc193330b9e3b9fb29d48283430029846c3aa)) +* **client:** send `X-Stainless-Kotlin-Version` header ([32f4d72](https://github.com/prelude-so/java-sdk/commit/32f4d72f068581e4257c5934d3c58b75128f0289)) + + +### Bug Fixes + +* **client:** disallow coercion from float to int ([a498a2f](https://github.com/prelude-so/java-sdk/commit/a498a2f53afeaef5b2dc34b4ade246af2fec6167)) +* **client:** fully respect max retries ([db20e56](https://github.com/prelude-so/java-sdk/commit/db20e56fa9a8b373305bff3ae10afea7bbd0ff53)) +* **client:** preserve time zone in lenient date-time parsing ([62d4ba4](https://github.com/prelude-so/java-sdk/commit/62d4ba43268ead94f6f1ab811138bea8053a906e)) +* **client:** send retry count header for max retries 0 ([db20e56](https://github.com/prelude-so/java-sdk/commit/db20e56fa9a8b373305bff3ae10afea7bbd0ff53)) +* date time deserialization leniency ([b53df43](https://github.com/prelude-so/java-sdk/commit/b53df4388bfd39591bc9952d8039ea5e76bc6a99)) +* fix request delays for retrying to be more respectful of high requested delays ([249bfd4](https://github.com/prelude-so/java-sdk/commit/249bfd4b8dc6f64ff54c9cab37a66ab27ca258b5)) + + +### Chores + +* **ci:** skip uploading artifacts on stainless-internal branches ([2737021](https://github.com/prelude-so/java-sdk/commit/27370217d777fa1ebf591100e2be408388575241)) +* **ci:** upgrade `actions/github-script` ([47e61f7](https://github.com/prelude-so/java-sdk/commit/47e61f7b26522ad8336e16e76149d6aeb34ad4e5)) +* **ci:** upgrade `actions/setup-java` ([e3a09ff](https://github.com/prelude-so/java-sdk/commit/e3a09ffbe9d651ef627986ab8665898ddbdaf089)) +* **docs:** add missing descriptions ([edc87cc](https://github.com/prelude-so/java-sdk/commit/edc87cc96ccd7f8788b4394d4d6f8274f501ad91)) +* drop apache dependency ([ea51709](https://github.com/prelude-so/java-sdk/commit/ea517099a868cfbc5692b303c02f3f26078442b3)) +* **internal:** allow passing args to `./scripts/test` ([68d4da0](https://github.com/prelude-so/java-sdk/commit/68d4da01ab6ef8c673849405051b98d5d876841c)) +* **internal:** bump palantir-java-format ([4460207](https://github.com/prelude-so/java-sdk/commit/4460207c304c03b09598c8a2b1cfa7c86a4a6190)) +* **internal:** clean up maven repo artifact script and add html documentation to repo root ([88364fc](https://github.com/prelude-so/java-sdk/commit/88364fca4159730830a8d2a41b93d7684c04c01f)) +* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([27772a9](https://github.com/prelude-so/java-sdk/commit/27772a95464ad4ca072493d67655fb09cfb0ab14)) +* **internal:** depend on packages directly in example ([db20e56](https://github.com/prelude-so/java-sdk/commit/db20e56fa9a8b373305bff3ae10afea7bbd0ff53)) +* **internal:** expand imports ([ee924bd](https://github.com/prelude-so/java-sdk/commit/ee924bd9b90bfd9454c4622c6ba891fb21a0fd5c)) +* **internal:** improve maven repo docs ([d891959](https://github.com/prelude-so/java-sdk/commit/d8919592a63c3c766cc07a682294c0918a896aea)) +* **internal:** make `OkHttp` constructor internal ([4d0d7a8](https://github.com/prelude-so/java-sdk/commit/4d0d7a8469d9a062be145775327227919a816340)) +* **internal:** support uploading Maven repo artifacts to stainless package server ([141a5d6](https://github.com/prelude-so/java-sdk/commit/141a5d618305ea5cb1d17eb2da4f3b578727fd02)) +* **internal:** update `actions/checkout` version ([bcb84ae](https://github.com/prelude-so/java-sdk/commit/bcb84ae66e0256dc8509d802e905bb73610f85cb)) +* **internal:** update `TestServerExtension` comment ([2f1209d](https://github.com/prelude-so/java-sdk/commit/2f1209d3d5703a3565f11f4d3b08b09da524335b)) +* **internal:** update maven repo doc to include authentication ([95ce4a8](https://github.com/prelude-so/java-sdk/commit/95ce4a8a1c321fe80673d83ce10289ece6dc1895)) +* **internal:** upgrade AssertJ ([626ff7d](https://github.com/prelude-so/java-sdk/commit/626ff7d2113f7346c428e92a824ec50c38aec643)) +* make `Properties` more resilient to `null` ([7e517d6](https://github.com/prelude-so/java-sdk/commit/7e517d6ca2f9ed57d2263b98cd0c7ac8cd1b60b9)) +* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/prelude-so/java-sdk/issues/3240) in tests ([b53df43](https://github.com/prelude-so/java-sdk/commit/b53df4388bfd39591bc9952d8039ea5e76bc6a99)) +* **test:** do not count install time for mock server timeout ([d631a4b](https://github.com/prelude-so/java-sdk/commit/d631a4b7a630579d45fa2330c9c2a87c22d34b2c)) +* update mock server docs ([9ab611b](https://github.com/prelude-so/java-sdk/commit/9ab611b92520a6f17ad9c8ad4017bfe34ddf1c5e)) + ## 0.10.0 (2025-12-05) Full Changelog: [v0.9.0...v0.10.0](https://github.com/prelude-so/java-sdk/compare/v0.9.0...v0.10.0) diff --git a/LICENSE b/LICENSE index 69f0a677..ed784599 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 Prelude + Copyright 2026 Prelude 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 c825dd98..dfe6ae52 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/so.prelude.sdk/prelude-java)](https://central.sonatype.com/artifact/so.prelude.sdk/prelude-java/0.10.0) -[![javadoc](https://javadoc.io/badge2/so.prelude.sdk/prelude-java/0.10.0/javadoc.svg)](https://javadoc.io/doc/so.prelude.sdk/prelude-java/0.10.0) +[![Maven Central](https://img.shields.io/maven-central/v/so.prelude.sdk/prelude-java)](https://central.sonatype.com/artifact/so.prelude.sdk/prelude-java/0.11.0) +[![javadoc](https://javadoc.io/badge2/so.prelude.sdk/prelude-java/0.11.0/javadoc.svg)](https://javadoc.io/doc/so.prelude.sdk/prelude-java/0.11.0) @@ -13,7 +13,7 @@ It is generated with [Stainless](https://www.stainless.com/). -The REST API documentation can be found on [docs.prelude.so](https://docs.prelude.so). Javadocs are available on [javadoc.io](https://javadoc.io/doc/so.prelude.sdk/prelude-java/0.10.0). +The REST API documentation can be found on [docs.prelude.so](https://docs.prelude.so). Javadocs are available on [javadoc.io](https://javadoc.io/doc/so.prelude.sdk/prelude-java/0.11.0). @@ -24,7 +24,7 @@ The REST API documentation can be found on [docs.prelude.so](https://docs.prelud ### Gradle ```kotlin -implementation("so.prelude.sdk:prelude-java:0.10.0") +implementation("so.prelude.sdk:prelude-java:0.11.0") ``` ### Maven @@ -33,7 +33,7 @@ implementation("so.prelude.sdk:prelude-java:0.10.0") so.prelude.sdk prelude-java - 0.10.0 + 0.11.0 ``` @@ -282,6 +282,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 @@ -357,6 +359,25 @@ PreludeClient client = PreludeOkHttpClient.builder() .build(); ``` +### Connection pooling + +To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods: + +```java +import java.time.Duration; +import so.prelude.sdk.client.PreludeClient; +import so.prelude.sdk.client.okhttp.PreludeOkHttpClient; + +PreludeClient client = PreludeOkHttpClient.builder() + .fromEnv() + // If `maxIdleConnections` is set, then `keepAliveDuration` must be set, and vice versa. + .maxIdleConnections(10) + .keepAliveDuration(Duration.ofMinutes(2)) + .build(); +``` + +If both options are unset, OkHttp's default connection pool settings are used. + ### HTTPS > [!NOTE] diff --git a/build.gradle.kts b/build.gradle.kts index 7627e51d..1e231a8c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "so.prelude.sdk" - version = "0.10.0" // x-release-please-version + version = "0.11.0" // x-release-please-version } subprojects { diff --git a/buildSrc/src/main/kotlin/prelude.java.gradle.kts b/buildSrc/src/main/kotlin/prelude.java.gradle.kts index 70fc33f4..8f4f902a 100644 --- a/buildSrc/src/main/kotlin/prelude.java.gradle.kts +++ b/buildSrc/src/main/kotlin/prelude.java.gradle.kts @@ -45,7 +45,7 @@ tasks.withType().configureEach { val palantir by configurations.creating dependencies { - palantir("com.palantir.javaformat:palantir-java-format:2.73.0") + palantir("com.palantir.javaformat:palantir-java-format:2.89.0") } fun registerPalantir( diff --git a/buildSrc/src/main/kotlin/prelude.kotlin.gradle.kts b/buildSrc/src/main/kotlin/prelude.kotlin.gradle.kts index 08157260..d3d6ee7e 100644 --- a/buildSrc/src/main/kotlin/prelude.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/prelude.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/prelude.publish.gradle.kts b/buildSrc/src/main/kotlin/prelude.publish.gradle.kts index 4c60cca0..4aae353d 100644 --- a/buildSrc/src/main/kotlin/prelude.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/prelude.publish.gradle.kts @@ -7,6 +7,17 @@ plugins { id("com.vanniktech.maven.publish") } +publishing { + repositories { + if (project.hasProperty("publishLocal")) { + maven { + name = "LocalFileSystem" + url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo") + } + } + } +} + repositories { gradlePluginPortal() mavenCentral() @@ -17,8 +28,10 @@ extra["signingInMemoryKeyId"] = System.getenv("GPG_SIGNING_KEY_ID") extra["signingInMemoryKeyPassword"] = System.getenv("GPG_SIGNING_PASSWORD") configure { - signAllPublications() - publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + if (!project.hasProperty("publishLocal")) { + signAllPublications() + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + } coordinates(project.group.toString(), project.name, project.version.toString()) configure( diff --git a/prelude-java-client-okhttp/build.gradle.kts b/prelude-java-client-okhttp/build.gradle.kts index d5d05715..a3b01928 100644 --- a/prelude-java-client-okhttp/build.gradle.kts +++ b/prelude-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/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/OkHttpClient.kt b/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/OkHttpClient.kt index 8ab0eba3..5c5772fd 100644 --- a/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/OkHttpClient.kt +++ b/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/OkHttpClient.kt @@ -6,11 +6,15 @@ import java.net.Proxy import java.time.Duration import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService +import java.util.concurrent.TimeUnit import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Call import okhttp3.Callback +import okhttp3.ConnectionPool +import okhttp3.Dispatcher import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType @@ -31,7 +35,7 @@ import so.prelude.sdk.core.http.HttpResponse import so.prelude.sdk.errors.PreludeIoException class OkHttpClient -private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient { +internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient { override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { val call = newCall(request, requestOptions) @@ -198,6 +202,9 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien private var timeout: Timeout = Timeout.default() private var proxy: Proxy? = null + private var maxIdleConnections: Int? = null + private var keepAliveDuration: Duration? = null + private var dispatcherExecutorService: ExecutorService? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null @@ -208,6 +215,32 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + /** + * Sets the maximum number of idle connections kept by the underlying [ConnectionPool]. + * + * If this is set, then [keepAliveDuration] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun maxIdleConnections(maxIdleConnections: Int?) = apply { + this.maxIdleConnections = maxIdleConnections + } + + /** + * Sets the keep-alive duration for idle connections in the underlying [ConnectionPool]. + * + * If this is set, then [maxIdleConnections] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun keepAliveDuration(keepAliveDuration: Duration?) = apply { + this.keepAliveDuration = keepAliveDuration + } + + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { this.sslSocketFactory = sslSocketFactory } @@ -223,12 +256,32 @@ 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 maxIdleConnections = maxIdleConnections + val keepAliveDuration = keepAliveDuration + if (maxIdleConnections != null && keepAliveDuration != null) { + connectionPool( + ConnectionPool( + maxIdleConnections, + keepAliveDuration.toNanos(), + TimeUnit.NANOSECONDS, + ) + ) + } else { + check((maxIdleConnections != null) == (keepAliveDuration != null)) { + "Both or none of `maxIdleConnections` and `keepAliveDuration` must be set, but only one was set" + } + } + val sslSocketFactory = sslSocketFactory val trustManager = trustManager if (sslSocketFactory != null && trustManager != null) { diff --git a/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/PreludeOkHttpClient.kt b/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/PreludeOkHttpClient.kt index 17a2890f..efeb2c4b 100644 --- a/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/PreludeOkHttpClient.kt +++ b/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/PreludeOkHttpClient.kt @@ -7,6 +7,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,16 +45,78 @@ class PreludeOkHttpClient 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 maxIdleConnections: Int? = null + private var keepAliveDuration: Duration? = 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)`. */ fun proxy(proxy: Optional) = proxy(proxy.getOrNull()) + /** + * The maximum number of idle connections kept by the underlying OkHttp connection pool. + * + * If this is set, then [keepAliveDuration] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun maxIdleConnections(maxIdleConnections: Int?) = apply { + this.maxIdleConnections = maxIdleConnections + } + + /** + * Alias for [Builder.maxIdleConnections]. + * + * This unboxed primitive overload exists for backwards compatibility. + */ + fun maxIdleConnections(maxIdleConnections: Int) = + maxIdleConnections(maxIdleConnections as Int?) + + /** + * Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`. + */ + fun maxIdleConnections(maxIdleConnections: Optional) = + maxIdleConnections(maxIdleConnections.getOrNull()) + + /** + * The keep-alive duration for idle connections in the underlying OkHttp connection pool. + * + * If this is set, then [maxIdleConnections] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun keepAliveDuration(keepAliveDuration: Duration?) = apply { + this.keepAliveDuration = keepAliveDuration + } + + /** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */ + fun keepAliveDuration(keepAliveDuration: Optional) = + keepAliveDuration(keepAliveDuration.getOrNull()) + /** * The socket factory used to secure HTTPS connections. * @@ -297,6 +360,9 @@ class PreludeOkHttpClient private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .maxIdleConnections(maxIdleConnections) + .keepAliveDuration(keepAliveDuration) + .dispatcherExecutorService(dispatcherExecutorService) .sslSocketFactory(sslSocketFactory) .trustManager(trustManager) .hostnameVerifier(hostnameVerifier) diff --git a/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/PreludeOkHttpClientAsync.kt b/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/PreludeOkHttpClientAsync.kt index b49e17e6..6263f2f7 100644 --- a/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/PreludeOkHttpClientAsync.kt +++ b/prelude-java-client-okhttp/src/main/kotlin/so/prelude/sdk/client/okhttp/PreludeOkHttpClientAsync.kt @@ -7,6 +7,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,16 +45,78 @@ class PreludeOkHttpClientAsync 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 maxIdleConnections: Int? = null + private var keepAliveDuration: Duration? = 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)`. */ fun proxy(proxy: Optional) = proxy(proxy.getOrNull()) + /** + * The maximum number of idle connections kept by the underlying OkHttp connection pool. + * + * If this is set, then [keepAliveDuration] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun maxIdleConnections(maxIdleConnections: Int?) = apply { + this.maxIdleConnections = maxIdleConnections + } + + /** + * Alias for [Builder.maxIdleConnections]. + * + * This unboxed primitive overload exists for backwards compatibility. + */ + fun maxIdleConnections(maxIdleConnections: Int) = + maxIdleConnections(maxIdleConnections as Int?) + + /** + * Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`. + */ + fun maxIdleConnections(maxIdleConnections: Optional) = + maxIdleConnections(maxIdleConnections.getOrNull()) + + /** + * The keep-alive duration for idle connections in the underlying OkHttp connection pool. + * + * If this is set, then [maxIdleConnections] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun keepAliveDuration(keepAliveDuration: Duration?) = apply { + this.keepAliveDuration = keepAliveDuration + } + + /** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */ + fun keepAliveDuration(keepAliveDuration: Optional) = + keepAliveDuration(keepAliveDuration.getOrNull()) + /** * The socket factory used to secure HTTPS connections. * @@ -297,6 +360,9 @@ class PreludeOkHttpClientAsync private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .maxIdleConnections(maxIdleConnections) + .keepAliveDuration(keepAliveDuration) + .dispatcherExecutorService(dispatcherExecutorService) .sslSocketFactory(sslSocketFactory) .trustManager(trustManager) .hostnameVerifier(hostnameVerifier) diff --git a/prelude-java-core/build.gradle.kts b/prelude-java-core/build.gradle.kts index a880fda5..31f51449 100644 --- a/prelude-java-core/build.gradle.kts +++ b/prelude-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") } } @@ -25,13 +27,11 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2") - implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4") - implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1") testImplementation(kotlin("test")) testImplementation(project(":prelude-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/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClient.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClient.kt index 5e094710..278c33cf 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClient.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClient.kt @@ -47,16 +47,25 @@ interface PreludeClient { */ fun withOptions(modifier: Consumer): PreludeClient + /** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ fun lookup(): LookupService + /** Send transactional and marketing messages with compliance enforcement. */ fun notify(): NotifyService + /** Send transactional messages (deprecated - use Notify API instead). */ fun transactional(): TransactionalService + /** Verify phone numbers. */ fun verification(): VerificationService + /** Verify phone numbers. */ fun verificationManagement(): VerificationManagementService + /** Evaluate email addresses and phone numbers for trustworthiness. */ fun watch(): WatchService /** @@ -82,16 +91,25 @@ interface PreludeClient { */ fun withOptions(modifier: Consumer): PreludeClient.WithRawResponse + /** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ fun lookup(): LookupService.WithRawResponse + /** Send transactional and marketing messages with compliance enforcement. */ fun notify(): NotifyService.WithRawResponse + /** Send transactional messages (deprecated - use Notify API instead). */ fun transactional(): TransactionalService.WithRawResponse + /** Verify phone numbers. */ fun verification(): VerificationService.WithRawResponse + /** Verify phone numbers. */ fun verificationManagement(): VerificationManagementService.WithRawResponse + /** Evaluate email addresses and phone numbers for trustworthiness. */ fun watch(): WatchService.WithRawResponse } } diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientAsync.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientAsync.kt index 634a2518..2fd36276 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientAsync.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientAsync.kt @@ -47,16 +47,25 @@ interface PreludeClientAsync { */ fun withOptions(modifier: Consumer): PreludeClientAsync + /** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ fun lookup(): LookupServiceAsync + /** Send transactional and marketing messages with compliance enforcement. */ fun notify(): NotifyServiceAsync + /** Send transactional messages (deprecated - use Notify API instead). */ fun transactional(): TransactionalServiceAsync + /** Verify phone numbers. */ fun verification(): VerificationServiceAsync + /** Verify phone numbers. */ fun verificationManagement(): VerificationManagementServiceAsync + /** Evaluate email addresses and phone numbers for trustworthiness. */ fun watch(): WatchServiceAsync /** @@ -86,16 +95,25 @@ interface PreludeClientAsync { modifier: Consumer ): PreludeClientAsync.WithRawResponse + /** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ fun lookup(): LookupServiceAsync.WithRawResponse + /** Send transactional and marketing messages with compliance enforcement. */ fun notify(): NotifyServiceAsync.WithRawResponse + /** Send transactional messages (deprecated - use Notify API instead). */ fun transactional(): TransactionalServiceAsync.WithRawResponse + /** Verify phone numbers. */ fun verification(): VerificationServiceAsync.WithRawResponse + /** Verify phone numbers. */ fun verificationManagement(): VerificationManagementServiceAsync.WithRawResponse + /** Evaluate email addresses and phone numbers for trustworthiness. */ fun watch(): WatchServiceAsync.WithRawResponse } } diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientAsyncImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientAsyncImpl.kt index b28b3eff..2fab033f 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientAsyncImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientAsyncImpl.kt @@ -66,17 +66,26 @@ class PreludeClientAsyncImpl(private val clientOptions: ClientOptions) : Prelude override fun withOptions(modifier: Consumer): PreludeClientAsync = PreludeClientAsyncImpl(clientOptions.toBuilder().apply(modifier::accept).build()) + /** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ override fun lookup(): LookupServiceAsync = lookup + /** Send transactional and marketing messages with compliance enforcement. */ override fun notify(): NotifyServiceAsync = notify + /** Send transactional messages (deprecated - use Notify API instead). */ override fun transactional(): TransactionalServiceAsync = transactional + /** Verify phone numbers. */ override fun verification(): VerificationServiceAsync = verification + /** Verify phone numbers. */ override fun verificationManagement(): VerificationManagementServiceAsync = verificationManagement + /** Evaluate email addresses and phone numbers for trustworthiness. */ override fun watch(): WatchServiceAsync = watch override fun close() = clientOptions.close() @@ -116,17 +125,26 @@ class PreludeClientAsyncImpl(private val clientOptions: ClientOptions) : Prelude clientOptions.toBuilder().apply(modifier::accept).build() ) + /** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ override fun lookup(): LookupServiceAsync.WithRawResponse = lookup + /** Send transactional and marketing messages with compliance enforcement. */ override fun notify(): NotifyServiceAsync.WithRawResponse = notify + /** Send transactional messages (deprecated - use Notify API instead). */ override fun transactional(): TransactionalServiceAsync.WithRawResponse = transactional + /** Verify phone numbers. */ override fun verification(): VerificationServiceAsync.WithRawResponse = verification + /** Verify phone numbers. */ override fun verificationManagement(): VerificationManagementServiceAsync.WithRawResponse = verificationManagement + /** Evaluate email addresses and phone numbers for trustworthiness. */ override fun watch(): WatchServiceAsync.WithRawResponse = watch } } diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientImpl.kt index 23d7e0b3..f3848303 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/client/PreludeClientImpl.kt @@ -60,16 +60,25 @@ class PreludeClientImpl(private val clientOptions: ClientOptions) : PreludeClien override fun withOptions(modifier: Consumer): PreludeClient = PreludeClientImpl(clientOptions.toBuilder().apply(modifier::accept).build()) + /** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ override fun lookup(): LookupService = lookup + /** Send transactional and marketing messages with compliance enforcement. */ override fun notify(): NotifyService = notify + /** Send transactional messages (deprecated - use Notify API instead). */ override fun transactional(): TransactionalService = transactional + /** Verify phone numbers. */ override fun verification(): VerificationService = verification + /** Verify phone numbers. */ override fun verificationManagement(): VerificationManagementService = verificationManagement + /** Evaluate email addresses and phone numbers for trustworthiness. */ override fun watch(): WatchService = watch override fun close() = clientOptions.close() @@ -108,17 +117,26 @@ class PreludeClientImpl(private val clientOptions: ClientOptions) : PreludeClien clientOptions.toBuilder().apply(modifier::accept).build() ) + /** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ override fun lookup(): LookupService.WithRawResponse = lookup + /** Send transactional and marketing messages with compliance enforcement. */ override fun notify(): NotifyService.WithRawResponse = notify + /** Send transactional messages (deprecated - use Notify API instead). */ override fun transactional(): TransactionalService.WithRawResponse = transactional + /** Verify phone numbers. */ override fun verification(): VerificationService.WithRawResponse = verification + /** Verify phone numbers. */ override fun verificationManagement(): VerificationManagementService.WithRawResponse = verificationManagement + /** Evaluate email addresses and phone numbers for trustworthiness. */ override fun watch(): WatchService.WithRawResponse = watch } } diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/ClientOptions.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/ClientOptions.kt index a4ffe36f..3c3879ef 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/ClientOptions.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/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()) apiToken.let { if (!it.isEmpty()) { headers.put("Authorization", "Bearer $it") diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/ObjectMappers.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/ObjectMappers.kt index ac9f9608..4a638086 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/ObjectMappers.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/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/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/Properties.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/Properties.kt index 66c3c20c..63a9b65f 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/Properties.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/Properties.kt @@ -34,9 +34,9 @@ fun getOsName(): String { } } -fun getOsVersion(): String = System.getProperty("os.version", "unknown") +fun getOsVersion(): String = System.getProperty("os.version", "unknown") ?: "unknown" fun getPackageVersion(): String = - PreludeClient::class.java.`package`.implementationVersion ?: "unknown" + PreludeClient::class.java.`package`?.implementationVersion ?: "unknown" -fun getJavaVersion(): String = System.getProperty("java.version", "unknown") +fun getJavaVersion(): String = System.getProperty("java.version", "unknown") ?: "unknown" diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/HttpRequest.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/HttpRequest.kt index cf5ac118..071b534c 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/HttpRequest.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/HttpRequest.kt @@ -1,5 +1,6 @@ package so.prelude.sdk.core.http +import java.net.URLEncoder import so.prelude.sdk.core.checkRequired import so.prelude.sdk.core.toImmutable @@ -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/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/HttpRequestBodies.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/HttpRequestBodies.kt index 2e17bc28..8347737c 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/HttpRequestBodies.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/HttpRequestBodies.kt @@ -7,13 +7,13 @@ package so.prelude.sdk.core.http import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.node.JsonNodeType +import java.io.ByteArrayInputStream import java.io.InputStream import java.io.OutputStream +import java.util.UUID import kotlin.jvm.optionals.getOrNull -import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder -import org.apache.hc.core5.http.ContentType -import org.apache.hc.core5.http.HttpEntity import so.prelude.sdk.core.MultipartField +import so.prelude.sdk.core.toImmutable import so.prelude.sdk.errors.PreludeInvalidDataException @JvmSynthetic @@ -37,92 +37,231 @@ internal fun multipartFormData( jsonMapper: JsonMapper, fields: Map>, ): HttpRequestBody = - object : HttpRequestBody { - private val entity: HttpEntity by lazy { - MultipartEntityBuilder.create() - .apply { - fields.forEach { (name, field) -> - val knownValue = field.value.asKnown().getOrNull() - val parts = - if (knownValue is InputStream) { - // Read directly from the `InputStream` instead of reading it all - // into memory due to the `jsonMapper` serialization below. - sequenceOf(name to knownValue) - } else { - val node = jsonMapper.valueToTree(field.value) - serializePart(name, node) + MultipartBody.Builder() + .apply { + fields.forEach { (name, field) -> + val knownValue = field.value.asKnown().getOrNull() + val parts = + if (knownValue is InputStream) { + // Read directly from the `InputStream` instead of reading it all + // into memory due to the `jsonMapper` serialization below. + sequenceOf(name to knownValue) + } else { + val node = jsonMapper.valueToTree(field.value) + serializePart(name, node) + } + + parts.forEach { (name, bytes) -> + val partBody = + if (bytes is ByteArrayInputStream) { + val byteArray = bytes.readBytes() + + object : HttpRequestBody { + + override fun writeTo(outputStream: OutputStream) { + outputStream.write(byteArray) + } + + override fun contentType(): String = field.contentType + + override fun contentLength(): Long = byteArray.size.toLong() + + override fun repeatable(): Boolean = true + + override fun close() {} } + } else { + object : HttpRequestBody { + + override fun writeTo(outputStream: OutputStream) { + bytes.copyTo(outputStream) + } + + override fun contentType(): String = field.contentType + + override fun contentLength(): Long = -1L - parts.forEach { (name, bytes) -> - addBinaryBody( - name, - bytes, - ContentType.parseLenient(field.contentType), - field.filename().getOrNull(), - ) + override fun repeatable(): Boolean = false + + override fun close() = bytes.close() + } } - } + + addPart( + MultipartBody.Part.create( + name, + field.filename().getOrNull(), + field.contentType, + partBody, + ) + ) } - .build() + } } + .build() - private fun serializePart( - name: String, - node: JsonNode, - ): Sequence> = - when (node.nodeType) { - JsonNodeType.MISSING, - JsonNodeType.NULL -> emptySequence() - JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream()) - JsonNodeType.STRING -> sequenceOf(name to node.textValue().inputStream()) - JsonNodeType.BOOLEAN -> - sequenceOf(name to node.booleanValue().toString().inputStream()) - JsonNodeType.NUMBER -> - sequenceOf(name to node.numberValue().toString().inputStream()) - JsonNodeType.ARRAY -> - sequenceOf( - name to - node - .elements() - .asSequence() - .mapNotNull { element -> - when (element.nodeType) { - JsonNodeType.MISSING, - JsonNodeType.NULL -> null - JsonNodeType.STRING -> node.textValue() - JsonNodeType.BOOLEAN -> node.booleanValue().toString() - JsonNodeType.NUMBER -> node.numberValue().toString() - null, - JsonNodeType.BINARY, - JsonNodeType.ARRAY, - JsonNodeType.OBJECT, - JsonNodeType.POJO -> - throw PreludeInvalidDataException( - "Unexpected JsonNode type in array: ${node.nodeType}" - ) - } - } - .joinToString(",") - .inputStream() - ) - JsonNodeType.OBJECT -> - node.fields().asSequence().flatMap { (key, value) -> - serializePart("$name[$key]", value) - } - JsonNodeType.POJO, - null -> - throw PreludeInvalidDataException("Unexpected JsonNode type: ${node.nodeType}") +private fun serializePart(name: String, node: JsonNode): Sequence> = + when (node.nodeType) { + JsonNodeType.MISSING, + JsonNodeType.NULL -> emptySequence() + JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream()) + JsonNodeType.STRING -> sequenceOf(name to node.textValue().byteInputStream()) + JsonNodeType.BOOLEAN -> sequenceOf(name to node.booleanValue().toString().byteInputStream()) + JsonNodeType.NUMBER -> sequenceOf(name to node.numberValue().toString().byteInputStream()) + JsonNodeType.ARRAY -> + sequenceOf( + name to + node + .elements() + .asSequence() + .mapNotNull { element -> + when (element.nodeType) { + JsonNodeType.MISSING, + JsonNodeType.NULL -> null + JsonNodeType.STRING -> element.textValue() + JsonNodeType.BOOLEAN -> element.booleanValue().toString() + JsonNodeType.NUMBER -> element.numberValue().toString() + null, + JsonNodeType.BINARY, + JsonNodeType.ARRAY, + JsonNodeType.OBJECT, + JsonNodeType.POJO -> + throw PreludeInvalidDataException( + "Unexpected JsonNode type in array: ${element.nodeType}" + ) + } + } + .joinToString(",") + .byteInputStream() + ) + JsonNodeType.OBJECT -> + node.fields().asSequence().flatMap { (key, value) -> + serializePart("$name[$key]", value) + } + JsonNodeType.POJO, + null -> throw PreludeInvalidDataException("Unexpected JsonNode type: ${node.nodeType}") + } + +private class MultipartBody +private constructor(private val boundary: String, private val parts: List) : HttpRequestBody { + private val boundaryBytes: ByteArray = boundary.toByteArray() + private val contentType = "multipart/form-data; boundary=$boundary" + + // This must remain in sync with `contentLength`. + override fun writeTo(outputStream: OutputStream) { + parts.forEach { part -> + outputStream.write(DASHDASH) + outputStream.write(boundaryBytes) + outputStream.write(CRLF) + + outputStream.write(CONTENT_DISPOSITION) + outputStream.write(part.contentDisposition.toByteArray()) + outputStream.write(CRLF) + + outputStream.write(CONTENT_TYPE) + outputStream.write(part.contentType.toByteArray()) + outputStream.write(CRLF) + + outputStream.write(CRLF) + part.body.writeTo(outputStream) + outputStream.write(CRLF) + } + + outputStream.write(DASHDASH) + outputStream.write(boundaryBytes) + outputStream.write(DASHDASH) + outputStream.write(CRLF) + } + + override fun contentType(): String = contentType + + // This must remain in sync with `writeTo`. + override fun contentLength(): Long { + var byteCount = 0L + + parts.forEach { part -> + val contentLength = part.body.contentLength() + if (contentLength == -1L) { + return -1L } - private fun String.inputStream(): InputStream = toByteArray().inputStream() + byteCount += + DASHDASH.size + + boundaryBytes.size + + CRLF.size + + CONTENT_DISPOSITION.size + + part.contentDisposition.toByteArray().size + + CRLF.size + + CONTENT_TYPE.size + + part.contentType.toByteArray().size + + CRLF.size + + CRLF.size + + contentLength + + CRLF.size + } - override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream) + byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size + return byteCount + } - override fun contentType(): String = entity.contentType + override fun repeatable(): Boolean = parts.all { it.body.repeatable() } - override fun contentLength(): Long = entity.contentLength + override fun close() { + parts.forEach { it.body.close() } + } - override fun repeatable(): Boolean = entity.isRepeatable + class Builder { + private val boundary = UUID.randomUUID().toString() + private val parts: MutableList = mutableListOf() - override fun close() = entity.close() + fun addPart(part: Part) = apply { parts.add(part) } + + fun build() = MultipartBody(boundary, parts.toImmutable()) + } + + class Part + private constructor( + val contentDisposition: String, + val contentType: String, + val body: HttpRequestBody, + ) { + companion object { + fun create( + name: String, + filename: String?, + contentType: String, + body: HttpRequestBody, + ): Part { + val disposition = buildString { + append("form-data; name=") + appendQuotedString(name) + if (filename != null) { + append("; filename=") + appendQuotedString(filename) + } + } + return Part(disposition, contentType, body) + } + } + } + + companion object { + private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte()) + private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte()) + private val CONTENT_DISPOSITION = "Content-Disposition: ".toByteArray() + private val CONTENT_TYPE = "Content-Type: ".toByteArray() + + private fun StringBuilder.appendQuotedString(key: String) { + append('"') + for (ch in key) { + when (ch) { + '\n' -> append("%0A") + '\r' -> append("%0D") + '"' -> append("%22") + else -> append(ch) + } + } + append('"') + } } +} diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/RetryingHttpClient.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/RetryingHttpClient.kt index bced2b28..e1b8e20b 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/RetryingHttpClient.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/core/http/RetryingHttpClient.kt @@ -1,3 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. + package so.prelude.sdk.core.http import java.io.IOException @@ -31,10 +33,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 +46,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 +77,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 +92,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?, @@ -212,13 +214,8 @@ private constructor( } } ?.let { retryAfterNanos -> - // If the API asks us to wait a certain amount of time (and it's a reasonable - // amount), just - // do what it says. - val retryAfter = Duration.ofNanos(retryAfterNanos.toLong()) - if (retryAfter in Duration.ofNanos(0)..Duration.ofMinutes(1)) { - return retryAfter - } + // If the API asks us to wait a certain amount of time, do what it says. + return Duration.ofNanos(retryAfterNanos.toLong()) } // Apply exponential backoff, but not more than the max. diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendBatchParams.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendBatchParams.kt index ff7a3895..d742433f 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendBatchParams.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendBatchParams.kt @@ -64,6 +64,15 @@ private constructor( */ fun correlationId(): Optional = body.correlationId() + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun document(): Optional = body.document() + /** * The message expiration date in RFC3339 format. Messages will not be sent after this time. * @@ -141,6 +150,13 @@ private constructor( */ fun _correlationId(): JsonField = body._correlationId() + /** + * Returns the raw JSON value of [document]. + * + * Unlike [document], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _document(): JsonField = body._document() + /** * Returns the raw JSON value of [expiresAt]. * @@ -231,7 +247,7 @@ private constructor( * - [to] * - [callbackUrl] * - [correlationId] - * - [expiresAt] + * - [document] * - etc. */ fun body(body: Body) = apply { this.body = body.toBuilder() } @@ -292,6 +308,21 @@ private constructor( body.correlationId(correlationId) } + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + fun document(document: Document) = apply { body.document(document) } + + /** + * Sets [Builder.document] to an arbitrary JSON value. + * + * You should usually call [Builder.document] with a well-typed [Document] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun document(document: JsonField) = apply { body.document(document) } + /** * The message expiration date in RFC3339 format. Messages will not be sent after this time. */ @@ -524,6 +555,7 @@ private constructor( private val to: JsonField>, private val callbackUrl: JsonField, private val correlationId: JsonField, + private val document: JsonField, private val expiresAt: JsonField, private val from: JsonField, private val locale: JsonField, @@ -545,6 +577,9 @@ private constructor( @JsonProperty("correlation_id") @ExcludeMissing correlationId: JsonField = JsonMissing.of(), + @JsonProperty("document") + @ExcludeMissing + document: JsonField = JsonMissing.of(), @JsonProperty("expires_at") @ExcludeMissing expiresAt: JsonField = JsonMissing.of(), @@ -564,6 +599,7 @@ private constructor( to, callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -605,6 +641,15 @@ private constructor( */ fun correlationId(): Optional = correlationId.getOptional("correlation_id") + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun document(): Optional = document.getOptional("document") + /** * The message expiration date in RFC3339 format. Messages will not be sent after this time. * @@ -690,6 +735,13 @@ private constructor( @ExcludeMissing fun _correlationId(): JsonField = correlationId + /** + * Returns the raw JSON value of [document]. + * + * Unlike [document], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("document") @ExcludeMissing fun _document(): JsonField = document + /** * Returns the raw JSON value of [expiresAt]. * @@ -774,6 +826,7 @@ private constructor( private var to: JsonField>? = null private var callbackUrl: JsonField = JsonMissing.of() private var correlationId: JsonField = JsonMissing.of() + private var document: JsonField = JsonMissing.of() private var expiresAt: JsonField = JsonMissing.of() private var from: JsonField = JsonMissing.of() private var locale: JsonField = JsonMissing.of() @@ -788,6 +841,7 @@ private constructor( to = body.to.map { it.toMutableList() } callbackUrl = body.callbackUrl correlationId = body.correlationId + document = body.document expiresAt = body.expiresAt from = body.from locale = body.locale @@ -859,6 +913,21 @@ private constructor( this.correlationId = correlationId } + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + fun document(document: Document) = document(JsonField.of(document)) + + /** + * Sets [Builder.document] to an arbitrary JSON value. + * + * You should usually call [Builder.document] with a well-typed [Document] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun document(document: JsonField) = apply { this.document = document } + /** * The message expiration date in RFC3339 format. Messages will not be sent after this * time. @@ -982,6 +1051,7 @@ private constructor( checkRequired("to", to).map { it.toImmutable() }, callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -1003,6 +1073,7 @@ private constructor( to() callbackUrl() correlationId() + document().ifPresent { it.validate() } expiresAt() from() locale() @@ -1032,6 +1103,7 @@ private constructor( (to.asKnown().getOrNull()?.size ?: 0) + (if (callbackUrl.asKnown().isPresent) 1 else 0) + (if (correlationId.asKnown().isPresent) 1 else 0) + + (document.asKnown().getOrNull()?.validity() ?: 0) + (if (expiresAt.asKnown().isPresent) 1 else 0) + (if (from.asKnown().isPresent) 1 else 0) + (if (locale.asKnown().isPresent) 1 else 0) + @@ -1049,6 +1121,7 @@ private constructor( to == other.to && callbackUrl == other.callbackUrl && correlationId == other.correlationId && + document == other.document && expiresAt == other.expiresAt && from == other.from && locale == other.locale && @@ -1064,6 +1137,7 @@ private constructor( to, callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -1077,7 +1151,210 @@ private constructor( override fun hashCode(): Int = hashCode override fun toString() = - "Body{templateId=$templateId, to=$to, callbackUrl=$callbackUrl, correlationId=$correlationId, expiresAt=$expiresAt, from=$from, locale=$locale, preferredChannel=$preferredChannel, scheduleAt=$scheduleAt, variables=$variables, additionalProperties=$additionalProperties}" + "Body{templateId=$templateId, to=$to, callbackUrl=$callbackUrl, correlationId=$correlationId, document=$document, expiresAt=$expiresAt, from=$from, locale=$locale, preferredChannel=$preferredChannel, scheduleAt=$scheduleAt, variables=$variables, additionalProperties=$additionalProperties}" + } + + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + class Document + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val filename: JsonField, + private val url: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("filename") + @ExcludeMissing + filename: JsonField = JsonMissing.of(), + @JsonProperty("url") @ExcludeMissing url: JsonField = JsonMissing.of(), + ) : this(filename, url, mutableMapOf()) + + /** + * The filename to display for the document. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun filename(): String = filename.getRequired("filename") + + /** + * The URL of the document to attach. Must be a valid HTTP or HTTPS URL. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun url(): String = url.getRequired("url") + + /** + * Returns the raw JSON value of [filename]. + * + * Unlike [filename], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("filename") @ExcludeMissing fun _filename(): JsonField = filename + + /** + * Returns the raw JSON value of [url]. + * + * Unlike [url], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("url") @ExcludeMissing fun _url(): JsonField = url + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [Document]. + * + * The following fields are required: + * ```java + * .filename() + * .url() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Document]. */ + class Builder internal constructor() { + + private var filename: JsonField? = null + private var url: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(document: Document) = apply { + filename = document.filename + url = document.url + additionalProperties = document.additionalProperties.toMutableMap() + } + + /** The filename to display for the document. */ + fun filename(filename: String) = filename(JsonField.of(filename)) + + /** + * Sets [Builder.filename] to an arbitrary JSON value. + * + * You should usually call [Builder.filename] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun filename(filename: JsonField) = apply { this.filename = filename } + + /** The URL of the document to attach. Must be a valid HTTP or HTTPS URL. */ + fun url(url: String) = url(JsonField.of(url)) + + /** + * Sets [Builder.url] to an arbitrary JSON value. + * + * You should usually call [Builder.url] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun url(url: JsonField) = apply { this.url = url } + + fun additionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.clear() + putAllAdditionalProperties(additionalProperties) + } + + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + additionalProperties.put(key, value) + } + + fun putAllAdditionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.putAll(additionalProperties) + } + + fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) } + + fun removeAllAdditionalProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalProperty) + } + + /** + * Returns an immutable instance of [Document]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .filename() + * .url() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Document = + Document( + checkRequired("filename", filename), + checkRequired("url", url), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + fun validate(): Document = apply { + if (validated) { + return@apply + } + + filename() + url() + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: PreludeInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic + internal fun validity(): Int = + (if (filename.asKnown().isPresent) 1 else 0) + (if (url.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Document && + filename == other.filename && + url == other.url && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(filename, url, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Document{filename=$filename, url=$url, additionalProperties=$additionalProperties}" } /** Preferred channel for delivery. If unavailable, automatic fallback applies. */ diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendBatchResponse.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendBatchResponse.kt index 3cd0ea0c..9249a3eb 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendBatchResponse.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendBatchResponse.kt @@ -11,6 +11,7 @@ import java.util.Collections import java.util.Objects import java.util.Optional import kotlin.jvm.optionals.getOrNull +import so.prelude.sdk.core.Enum import so.prelude.sdk.core.ExcludeMissing import so.prelude.sdk.core.JsonField import so.prelude.sdk.core.JsonMissing @@ -885,6 +886,8 @@ private constructor( private val id: JsonField, private val correlationId: JsonField, private val createdAt: JsonField, + private val encoding: JsonField, + private val estimatedSegmentCount: JsonField, private val expiresAt: JsonField, private val from: JsonField, private val locale: JsonField, @@ -902,6 +905,12 @@ private constructor( @JsonProperty("created_at") @ExcludeMissing createdAt: JsonField = JsonMissing.of(), + @JsonProperty("encoding") + @ExcludeMissing + encoding: JsonField = JsonMissing.of(), + @JsonProperty("estimated_segment_count") + @ExcludeMissing + estimatedSegmentCount: JsonField = JsonMissing.of(), @JsonProperty("expires_at") @ExcludeMissing expiresAt: JsonField = JsonMissing.of(), @@ -917,6 +926,8 @@ private constructor( id, correlationId, createdAt, + encoding, + estimatedSegmentCount, expiresAt, from, locale, @@ -949,6 +960,27 @@ private constructor( */ fun createdAt(): Optional = createdAt.getOptional("created_at") + /** + * The SMS encoding type based on message content. GSM-7 supports standard characters + * (up to 160 chars per segment), while UCS-2 supports Unicode including emoji (up to 70 + * chars per segment). Only present for SMS messages. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun encoding(): Optional = encoding.getOptional("encoding") + + /** + * The estimated number of SMS segments for this message. This value is not contractual; + * the actual segment count will be determined after the SMS is sent by the provider. + * Only present for SMS messages. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun estimatedSegmentCount(): Optional = + estimatedSegmentCount.getOptional("estimated_segment_count") + /** * The message expiration date in RFC3339 format. * @@ -1016,6 +1048,26 @@ private constructor( @ExcludeMissing fun _createdAt(): JsonField = createdAt + /** + * Returns the raw JSON value of [encoding]. + * + * Unlike [encoding], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("encoding") + @ExcludeMissing + fun _encoding(): JsonField = encoding + + /** + * Returns the raw JSON value of [estimatedSegmentCount]. + * + * Unlike [estimatedSegmentCount], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("estimated_segment_count") + @ExcludeMissing + fun _estimatedSegmentCount(): JsonField = estimatedSegmentCount + /** * Returns the raw JSON value of [expiresAt]. * @@ -1081,6 +1133,8 @@ private constructor( private var id: JsonField = JsonMissing.of() private var correlationId: JsonField = JsonMissing.of() private var createdAt: JsonField = JsonMissing.of() + private var encoding: JsonField = JsonMissing.of() + private var estimatedSegmentCount: JsonField = JsonMissing.of() private var expiresAt: JsonField = JsonMissing.of() private var from: JsonField = JsonMissing.of() private var locale: JsonField = JsonMissing.of() @@ -1093,6 +1147,8 @@ private constructor( id = message.id correlationId = message.correlationId createdAt = message.createdAt + encoding = message.encoding + estimatedSegmentCount = message.estimatedSegmentCount expiresAt = message.expiresAt from = message.from locale = message.locale @@ -1142,6 +1198,41 @@ private constructor( this.createdAt = createdAt } + /** + * The SMS encoding type based on message content. GSM-7 supports standard + * characters (up to 160 chars per segment), while UCS-2 supports Unicode including + * emoji (up to 70 chars per segment). Only present for SMS messages. + */ + fun encoding(encoding: Encoding) = encoding(JsonField.of(encoding)) + + /** + * Sets [Builder.encoding] to an arbitrary JSON value. + * + * You should usually call [Builder.encoding] with a well-typed [Encoding] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun encoding(encoding: JsonField) = apply { this.encoding = encoding } + + /** + * The estimated number of SMS segments for this message. This value is not + * contractual; the actual segment count will be determined after the SMS is sent by + * the provider. Only present for SMS messages. + */ + fun estimatedSegmentCount(estimatedSegmentCount: Long) = + estimatedSegmentCount(JsonField.of(estimatedSegmentCount)) + + /** + * Sets [Builder.estimatedSegmentCount] to an arbitrary JSON value. + * + * You should usually call [Builder.estimatedSegmentCount] with a well-typed [Long] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun estimatedSegmentCount(estimatedSegmentCount: JsonField) = apply { + this.estimatedSegmentCount = estimatedSegmentCount + } + /** The message expiration date in RFC3339 format. */ fun expiresAt(expiresAt: OffsetDateTime) = expiresAt(JsonField.of(expiresAt)) @@ -1240,6 +1331,8 @@ private constructor( id, correlationId, createdAt, + encoding, + estimatedSegmentCount, expiresAt, from, locale, @@ -1259,6 +1352,8 @@ private constructor( id() correlationId() createdAt() + encoding().ifPresent { it.validate() } + estimatedSegmentCount() expiresAt() from() locale() @@ -1286,12 +1381,150 @@ private constructor( (if (id.asKnown().isPresent) 1 else 0) + (if (correlationId.asKnown().isPresent) 1 else 0) + (if (createdAt.asKnown().isPresent) 1 else 0) + + (encoding.asKnown().getOrNull()?.validity() ?: 0) + + (if (estimatedSegmentCount.asKnown().isPresent) 1 else 0) + (if (expiresAt.asKnown().isPresent) 1 else 0) + (if (from.asKnown().isPresent) 1 else 0) + (if (locale.asKnown().isPresent) 1 else 0) + (if (scheduleAt.asKnown().isPresent) 1 else 0) + (if (to.asKnown().isPresent) 1 else 0) + /** + * The SMS encoding type based on message content. GSM-7 supports standard characters + * (up to 160 chars per segment), while UCS-2 supports Unicode including emoji (up to 70 + * chars per segment). Only present for SMS messages. + */ + class Encoding @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that + * doesn't match any known member, and you want to know that value. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val GSM_7 = of("GSM-7") + + @JvmField val UCS_2 = of("UCS-2") + + @JvmStatic fun of(value: String) = Encoding(JsonField.of(value)) + } + + /** An enum containing [Encoding]'s known values. */ + enum class Known { + GSM_7, + UCS_2, + } + + /** + * An enum containing [Encoding]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Encoding] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, + * if the SDK is on an older version than the API, then the API may respond with + * new members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + GSM_7, + UCS_2, + /** + * An enum member indicating that [Encoding] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if + * you want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + GSM_7 -> Value.GSM_7 + UCS_2 -> Value.UCS_2 + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws PreludeInvalidDataException if this class instance's value is a not a + * known member. + */ + fun known(): Known = + when (this) { + GSM_7 -> Known.GSM_7 + UCS_2 -> Known.UCS_2 + else -> throw PreludeInvalidDataException("Unknown Encoding: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws PreludeInvalidDataException if this class instance's value does not have + * the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + PreludeInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + fun validate(): Encoding = apply { + if (validated) { + return@apply + } + + known() + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: PreludeInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Encoding && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -1301,6 +1534,8 @@ private constructor( id == other.id && correlationId == other.correlationId && createdAt == other.createdAt && + encoding == other.encoding && + estimatedSegmentCount == other.estimatedSegmentCount && expiresAt == other.expiresAt && from == other.from && locale == other.locale && @@ -1314,6 +1549,8 @@ private constructor( id, correlationId, createdAt, + encoding, + estimatedSegmentCount, expiresAt, from, locale, @@ -1326,7 +1563,7 @@ private constructor( override fun hashCode(): Int = hashCode override fun toString() = - "Message{id=$id, correlationId=$correlationId, createdAt=$createdAt, expiresAt=$expiresAt, from=$from, locale=$locale, scheduleAt=$scheduleAt, to=$to, additionalProperties=$additionalProperties}" + "Message{id=$id, correlationId=$correlationId, createdAt=$createdAt, encoding=$encoding, estimatedSegmentCount=$estimatedSegmentCount, expiresAt=$expiresAt, from=$from, locale=$locale, scheduleAt=$scheduleAt, to=$to, additionalProperties=$additionalProperties}" } override fun equals(other: Any?): Boolean { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendParams.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendParams.kt index f62667b8..c0907faf 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendParams.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendParams.kt @@ -67,6 +67,15 @@ private constructor( */ fun correlationId(): Optional = body.correlationId() + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun document(): Optional = body.document() + /** * The message expiration date in RFC3339 format. The message will not be sent if this time is * reached. @@ -150,6 +159,13 @@ private constructor( */ fun _correlationId(): JsonField = body._correlationId() + /** + * Returns the raw JSON value of [document]. + * + * Unlike [document], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _document(): JsonField = body._document() + /** * Returns the raw JSON value of [expiresAt]. * @@ -240,7 +256,7 @@ private constructor( * - [to] * - [callbackUrl] * - [correlationId] - * - [expiresAt] + * - [document] * - etc. */ fun body(body: Body) = apply { this.body = body.toBuilder() } @@ -297,6 +313,21 @@ private constructor( body.correlationId(correlationId) } + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + fun document(document: Document) = apply { body.document(document) } + + /** + * Sets [Builder.document] to an arbitrary JSON value. + * + * You should usually call [Builder.document] with a well-typed [Document] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun document(document: JsonField) = apply { body.document(document) } + /** * The message expiration date in RFC3339 format. The message will not be sent if this time * is reached. @@ -535,6 +566,7 @@ private constructor( private val to: JsonField, private val callbackUrl: JsonField, private val correlationId: JsonField, + private val document: JsonField, private val expiresAt: JsonField, private val from: JsonField, private val locale: JsonField, @@ -556,6 +588,9 @@ private constructor( @JsonProperty("correlation_id") @ExcludeMissing correlationId: JsonField = JsonMissing.of(), + @JsonProperty("document") + @ExcludeMissing + document: JsonField = JsonMissing.of(), @JsonProperty("expires_at") @ExcludeMissing expiresAt: JsonField = JsonMissing.of(), @@ -575,6 +610,7 @@ private constructor( to, callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -617,6 +653,15 @@ private constructor( */ fun correlationId(): Optional = correlationId.getOptional("correlation_id") + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun document(): Optional = document.getOptional("document") + /** * The message expiration date in RFC3339 format. The message will not be sent if this time * is reached. @@ -708,6 +753,13 @@ private constructor( @ExcludeMissing fun _correlationId(): JsonField = correlationId + /** + * Returns the raw JSON value of [document]. + * + * Unlike [document], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("document") @ExcludeMissing fun _document(): JsonField = document + /** * Returns the raw JSON value of [expiresAt]. * @@ -792,6 +844,7 @@ private constructor( private var to: JsonField? = null private var callbackUrl: JsonField = JsonMissing.of() private var correlationId: JsonField = JsonMissing.of() + private var document: JsonField = JsonMissing.of() private var expiresAt: JsonField = JsonMissing.of() private var from: JsonField = JsonMissing.of() private var locale: JsonField = JsonMissing.of() @@ -806,6 +859,7 @@ private constructor( to = body.to callbackUrl = body.callbackUrl correlationId = body.correlationId + document = body.document expiresAt = body.expiresAt from = body.from locale = body.locale @@ -870,6 +924,21 @@ private constructor( this.correlationId = correlationId } + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + fun document(document: Document) = document(JsonField.of(document)) + + /** + * Sets [Builder.document] to an arbitrary JSON value. + * + * You should usually call [Builder.document] with a well-typed [Document] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun document(document: JsonField) = apply { this.document = document } + /** * The message expiration date in RFC3339 format. The message will not be sent if this * time is reached. @@ -1002,6 +1071,7 @@ private constructor( checkRequired("to", to), callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -1023,6 +1093,7 @@ private constructor( to() callbackUrl() correlationId() + document().ifPresent { it.validate() } expiresAt() from() locale() @@ -1052,6 +1123,7 @@ private constructor( (if (to.asKnown().isPresent) 1 else 0) + (if (callbackUrl.asKnown().isPresent) 1 else 0) + (if (correlationId.asKnown().isPresent) 1 else 0) + + (document.asKnown().getOrNull()?.validity() ?: 0) + (if (expiresAt.asKnown().isPresent) 1 else 0) + (if (from.asKnown().isPresent) 1 else 0) + (if (locale.asKnown().isPresent) 1 else 0) + @@ -1069,6 +1141,7 @@ private constructor( to == other.to && callbackUrl == other.callbackUrl && correlationId == other.correlationId && + document == other.document && expiresAt == other.expiresAt && from == other.from && locale == other.locale && @@ -1084,6 +1157,7 @@ private constructor( to, callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -1097,7 +1171,210 @@ private constructor( override fun hashCode(): Int = hashCode override fun toString() = - "Body{templateId=$templateId, to=$to, callbackUrl=$callbackUrl, correlationId=$correlationId, expiresAt=$expiresAt, from=$from, locale=$locale, preferredChannel=$preferredChannel, scheduleAt=$scheduleAt, variables=$variables, additionalProperties=$additionalProperties}" + "Body{templateId=$templateId, to=$to, callbackUrl=$callbackUrl, correlationId=$correlationId, document=$document, expiresAt=$expiresAt, from=$from, locale=$locale, preferredChannel=$preferredChannel, scheduleAt=$scheduleAt, variables=$variables, additionalProperties=$additionalProperties}" + } + + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + class Document + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val filename: JsonField, + private val url: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("filename") + @ExcludeMissing + filename: JsonField = JsonMissing.of(), + @JsonProperty("url") @ExcludeMissing url: JsonField = JsonMissing.of(), + ) : this(filename, url, mutableMapOf()) + + /** + * The filename to display for the document. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun filename(): String = filename.getRequired("filename") + + /** + * The URL of the document to attach. Must be a valid HTTP or HTTPS URL. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun url(): String = url.getRequired("url") + + /** + * Returns the raw JSON value of [filename]. + * + * Unlike [filename], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("filename") @ExcludeMissing fun _filename(): JsonField = filename + + /** + * Returns the raw JSON value of [url]. + * + * Unlike [url], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("url") @ExcludeMissing fun _url(): JsonField = url + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [Document]. + * + * The following fields are required: + * ```java + * .filename() + * .url() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Document]. */ + class Builder internal constructor() { + + private var filename: JsonField? = null + private var url: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(document: Document) = apply { + filename = document.filename + url = document.url + additionalProperties = document.additionalProperties.toMutableMap() + } + + /** The filename to display for the document. */ + fun filename(filename: String) = filename(JsonField.of(filename)) + + /** + * Sets [Builder.filename] to an arbitrary JSON value. + * + * You should usually call [Builder.filename] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun filename(filename: JsonField) = apply { this.filename = filename } + + /** The URL of the document to attach. Must be a valid HTTP or HTTPS URL. */ + fun url(url: String) = url(JsonField.of(url)) + + /** + * Sets [Builder.url] to an arbitrary JSON value. + * + * You should usually call [Builder.url] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun url(url: JsonField) = apply { this.url = url } + + fun additionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.clear() + putAllAdditionalProperties(additionalProperties) + } + + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + additionalProperties.put(key, value) + } + + fun putAllAdditionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.putAll(additionalProperties) + } + + fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) } + + fun removeAllAdditionalProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalProperty) + } + + /** + * Returns an immutable instance of [Document]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .filename() + * .url() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Document = + Document( + checkRequired("filename", filename), + checkRequired("url", url), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + fun validate(): Document = apply { + if (validated) { + return@apply + } + + filename() + url() + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: PreludeInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic + internal fun validity(): Int = + (if (filename.asKnown().isPresent) 1 else 0) + (if (url.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Document && + filename == other.filename && + url == other.url && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(filename, url, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Document{filename=$filename, url=$url, additionalProperties=$additionalProperties}" } /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendResponse.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendResponse.kt index c6cb2997..e3b5c37c 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendResponse.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/NotifySendResponse.kt @@ -11,6 +11,7 @@ import java.util.Collections import java.util.Objects import java.util.Optional import kotlin.jvm.optionals.getOrNull +import so.prelude.sdk.core.Enum import so.prelude.sdk.core.ExcludeMissing import so.prelude.sdk.core.JsonField import so.prelude.sdk.core.JsonMissing @@ -30,6 +31,8 @@ private constructor( private val variables: JsonField, private val callbackUrl: JsonField, private val correlationId: JsonField, + private val encoding: JsonField, + private val estimatedSegmentCount: JsonField, private val from: JsonField, private val scheduleAt: JsonField, private val additionalProperties: MutableMap, @@ -57,6 +60,10 @@ private constructor( @JsonProperty("correlation_id") @ExcludeMissing correlationId: JsonField = JsonMissing.of(), + @JsonProperty("encoding") @ExcludeMissing encoding: JsonField = JsonMissing.of(), + @JsonProperty("estimated_segment_count") + @ExcludeMissing + estimatedSegmentCount: JsonField = JsonMissing.of(), @JsonProperty("from") @ExcludeMissing from: JsonField = JsonMissing.of(), @JsonProperty("schedule_at") @ExcludeMissing @@ -70,6 +77,8 @@ private constructor( variables, callbackUrl, correlationId, + encoding, + estimatedSegmentCount, from, scheduleAt, mutableMapOf(), @@ -139,6 +148,27 @@ private constructor( */ fun correlationId(): Optional = correlationId.getOptional("correlation_id") + /** + * The SMS encoding type based on message content. GSM-7 supports standard characters (up to 160 + * chars per segment), while UCS-2 supports Unicode including emoji (up to 70 chars per + * segment). Only present for SMS messages. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun encoding(): Optional = encoding.getOptional("encoding") + + /** + * The estimated number of SMS segments for this message. This value is not contractual; the + * actual segment count will be determined after the SMS is sent by the provider. Only present + * for SMS messages. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun estimatedSegmentCount(): Optional = + estimatedSegmentCount.getOptional("estimated_segment_count") + /** * The Sender ID used for this message. * @@ -221,6 +251,23 @@ private constructor( @ExcludeMissing fun _correlationId(): JsonField = correlationId + /** + * Returns the raw JSON value of [encoding]. + * + * Unlike [encoding], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("encoding") @ExcludeMissing fun _encoding(): JsonField = encoding + + /** + * Returns the raw JSON value of [estimatedSegmentCount]. + * + * Unlike [estimatedSegmentCount], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("estimated_segment_count") + @ExcludeMissing + fun _estimatedSegmentCount(): JsonField = estimatedSegmentCount + /** * Returns the raw JSON value of [from]. * @@ -278,6 +325,8 @@ private constructor( private var variables: JsonField? = null private var callbackUrl: JsonField = JsonMissing.of() private var correlationId: JsonField = JsonMissing.of() + private var encoding: JsonField = JsonMissing.of() + private var estimatedSegmentCount: JsonField = JsonMissing.of() private var from: JsonField = JsonMissing.of() private var scheduleAt: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @@ -292,6 +341,8 @@ private constructor( variables = notifySendResponse.variables callbackUrl = notifySendResponse.callbackUrl correlationId = notifySendResponse.correlationId + encoding = notifySendResponse.encoding + estimatedSegmentCount = notifySendResponse.estimatedSegmentCount from = notifySendResponse.from scheduleAt = notifySendResponse.scheduleAt additionalProperties = notifySendResponse.additionalProperties.toMutableMap() @@ -393,6 +444,41 @@ private constructor( this.correlationId = correlationId } + /** + * The SMS encoding type based on message content. GSM-7 supports standard characters (up to + * 160 chars per segment), while UCS-2 supports Unicode including emoji (up to 70 chars per + * segment). Only present for SMS messages. + */ + fun encoding(encoding: Encoding) = encoding(JsonField.of(encoding)) + + /** + * Sets [Builder.encoding] to an arbitrary JSON value. + * + * You should usually call [Builder.encoding] with a well-typed [Encoding] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun encoding(encoding: JsonField) = apply { this.encoding = encoding } + + /** + * The estimated number of SMS segments for this message. This value is not contractual; the + * actual segment count will be determined after the SMS is sent by the provider. Only + * present for SMS messages. + */ + fun estimatedSegmentCount(estimatedSegmentCount: Long) = + estimatedSegmentCount(JsonField.of(estimatedSegmentCount)) + + /** + * Sets [Builder.estimatedSegmentCount] to an arbitrary JSON value. + * + * You should usually call [Builder.estimatedSegmentCount] with a well-typed [Long] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun estimatedSegmentCount(estimatedSegmentCount: JsonField) = apply { + this.estimatedSegmentCount = estimatedSegmentCount + } + /** The Sender ID used for this message. */ fun from(from: String) = from(JsonField.of(from)) @@ -468,6 +554,8 @@ private constructor( checkRequired("variables", variables), callbackUrl, correlationId, + encoding, + estimatedSegmentCount, from, scheduleAt, additionalProperties.toMutableMap(), @@ -489,6 +577,8 @@ private constructor( variables().validate() callbackUrl() correlationId() + encoding().ifPresent { it.validate() } + estimatedSegmentCount() from() scheduleAt() validated = true @@ -517,6 +607,8 @@ private constructor( (variables.asKnown().getOrNull()?.validity() ?: 0) + (if (callbackUrl.asKnown().isPresent) 1 else 0) + (if (correlationId.asKnown().isPresent) 1 else 0) + + (encoding.asKnown().getOrNull()?.validity() ?: 0) + + (if (estimatedSegmentCount.asKnown().isPresent) 1 else 0) + (if (from.asKnown().isPresent) 1 else 0) + (if (scheduleAt.asKnown().isPresent) 1 else 0) @@ -620,6 +712,136 @@ private constructor( override fun toString() = "Variables{additionalProperties=$additionalProperties}" } + /** + * The SMS encoding type based on message content. GSM-7 supports standard characters (up to 160 + * chars per segment), while UCS-2 supports Unicode including emoji (up to 70 chars per + * segment). Only present for SMS messages. + */ + class Encoding @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val GSM_7 = of("GSM-7") + + @JvmField val UCS_2 = of("UCS-2") + + @JvmStatic fun of(value: String) = Encoding(JsonField.of(value)) + } + + /** An enum containing [Encoding]'s known values. */ + enum class Known { + GSM_7, + UCS_2, + } + + /** + * An enum containing [Encoding]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Encoding] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + GSM_7, + UCS_2, + /** An enum member indicating that [Encoding] was instantiated with an unknown value. */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + GSM_7 -> Value.GSM_7 + UCS_2 -> Value.UCS_2 + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws PreludeInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + GSM_7 -> Known.GSM_7 + UCS_2 -> Known.UCS_2 + else -> throw PreludeInvalidDataException("Unknown Encoding: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws PreludeInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { PreludeInvalidDataException("Value is not a String") } + + private var validated: Boolean = false + + fun validate(): Encoding = apply { + if (validated) { + return@apply + } + + known() + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: PreludeInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Encoding && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -634,6 +856,8 @@ private constructor( variables == other.variables && callbackUrl == other.callbackUrl && correlationId == other.correlationId && + encoding == other.encoding && + estimatedSegmentCount == other.estimatedSegmentCount && from == other.from && scheduleAt == other.scheduleAt && additionalProperties == other.additionalProperties @@ -649,6 +873,8 @@ private constructor( variables, callbackUrl, correlationId, + encoding, + estimatedSegmentCount, from, scheduleAt, additionalProperties, @@ -658,5 +884,5 @@ private constructor( override fun hashCode(): Int = hashCode override fun toString() = - "NotifySendResponse{id=$id, createdAt=$createdAt, expiresAt=$expiresAt, templateId=$templateId, to=$to, variables=$variables, callbackUrl=$callbackUrl, correlationId=$correlationId, from=$from, scheduleAt=$scheduleAt, additionalProperties=$additionalProperties}" + "NotifySendResponse{id=$id, createdAt=$createdAt, expiresAt=$expiresAt, templateId=$templateId, to=$to, variables=$variables, callbackUrl=$callbackUrl, correlationId=$correlationId, encoding=$encoding, estimatedSegmentCount=$estimatedSegmentCount, from=$from, scheduleAt=$scheduleAt, additionalProperties=$additionalProperties}" } diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/TransactionalSendParams.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/TransactionalSendParams.kt index a54e10b9..25157c5e 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/TransactionalSendParams.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/TransactionalSendParams.kt @@ -64,6 +64,15 @@ private constructor( */ fun correlationId(): Optional = body.correlationId() + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun document(): Optional = body.document() + /** * The message expiration date. * @@ -141,6 +150,13 @@ private constructor( */ fun _correlationId(): JsonField = body._correlationId() + /** + * Returns the raw JSON value of [document]. + * + * Unlike [document], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _document(): JsonField = body._document() + /** * Returns the raw JSON value of [expiresAt]. * @@ -224,7 +240,7 @@ private constructor( * - [to] * - [callbackUrl] * - [correlationId] - * - [expiresAt] + * - [document] * - etc. */ fun body(body: Body) = apply { this.body = body.toBuilder() } @@ -281,6 +297,21 @@ private constructor( body.correlationId(correlationId) } + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + fun document(document: Document) = apply { body.document(document) } + + /** + * Sets [Builder.document] to an arbitrary JSON value. + * + * You should usually call [Builder.document] with a well-typed [Document] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun document(document: JsonField) = apply { body.document(document) } + /** The message expiration date. */ fun expiresAt(expiresAt: String) = apply { body.expiresAt(expiresAt) } @@ -507,6 +538,7 @@ private constructor( private val to: JsonField, private val callbackUrl: JsonField, private val correlationId: JsonField, + private val document: JsonField, private val expiresAt: JsonField, private val from: JsonField, private val locale: JsonField, @@ -527,6 +559,9 @@ private constructor( @JsonProperty("correlation_id") @ExcludeMissing correlationId: JsonField = JsonMissing.of(), + @JsonProperty("document") + @ExcludeMissing + document: JsonField = JsonMissing.of(), @JsonProperty("expires_at") @ExcludeMissing expiresAt: JsonField = JsonMissing.of(), @@ -543,6 +578,7 @@ private constructor( to, callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -584,6 +620,15 @@ private constructor( */ fun correlationId(): Optional = correlationId.getOptional("correlation_id") + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun document(): Optional = document.getOptional("document") + /** * The message expiration date. * @@ -669,6 +714,13 @@ private constructor( @ExcludeMissing fun _correlationId(): JsonField = correlationId + /** + * Returns the raw JSON value of [document]. + * + * Unlike [document], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("document") @ExcludeMissing fun _document(): JsonField = document + /** * Returns the raw JSON value of [expiresAt]. * @@ -742,6 +794,7 @@ private constructor( private var to: JsonField? = null private var callbackUrl: JsonField = JsonMissing.of() private var correlationId: JsonField = JsonMissing.of() + private var document: JsonField = JsonMissing.of() private var expiresAt: JsonField = JsonMissing.of() private var from: JsonField = JsonMissing.of() private var locale: JsonField = JsonMissing.of() @@ -755,6 +808,7 @@ private constructor( to = body.to callbackUrl = body.callbackUrl correlationId = body.correlationId + document = body.document expiresAt = body.expiresAt from = body.from locale = body.locale @@ -819,6 +873,21 @@ private constructor( this.correlationId = correlationId } + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + fun document(document: Document) = document(JsonField.of(document)) + + /** + * Sets [Builder.document] to an arbitrary JSON value. + * + * You should usually call [Builder.document] with a well-typed [Document] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun document(document: JsonField) = apply { this.document = document } + /** The message expiration date. */ fun expiresAt(expiresAt: String) = expiresAt(JsonField.of(expiresAt)) @@ -934,6 +1003,7 @@ private constructor( checkRequired("to", to), callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -954,6 +1024,7 @@ private constructor( to() callbackUrl() correlationId() + document().ifPresent { it.validate() } expiresAt() from() locale() @@ -982,6 +1053,7 @@ private constructor( (if (to.asKnown().isPresent) 1 else 0) + (if (callbackUrl.asKnown().isPresent) 1 else 0) + (if (correlationId.asKnown().isPresent) 1 else 0) + + (document.asKnown().getOrNull()?.validity() ?: 0) + (if (expiresAt.asKnown().isPresent) 1 else 0) + (if (from.asKnown().isPresent) 1 else 0) + (if (locale.asKnown().isPresent) 1 else 0) + @@ -998,6 +1070,7 @@ private constructor( to == other.to && callbackUrl == other.callbackUrl && correlationId == other.correlationId && + document == other.document && expiresAt == other.expiresAt && from == other.from && locale == other.locale && @@ -1012,6 +1085,7 @@ private constructor( to, callbackUrl, correlationId, + document, expiresAt, from, locale, @@ -1024,7 +1098,210 @@ private constructor( override fun hashCode(): Int = hashCode override fun toString() = - "Body{templateId=$templateId, to=$to, callbackUrl=$callbackUrl, correlationId=$correlationId, expiresAt=$expiresAt, from=$from, locale=$locale, preferredChannel=$preferredChannel, variables=$variables, additionalProperties=$additionalProperties}" + "Body{templateId=$templateId, to=$to, callbackUrl=$callbackUrl, correlationId=$correlationId, document=$document, expiresAt=$expiresAt, from=$from, locale=$locale, preferredChannel=$preferredChannel, variables=$variables, additionalProperties=$additionalProperties}" + } + + /** + * A document to attach to the message. Only supported on WhatsApp templates that have a + * document header. + */ + class Document + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val filename: JsonField, + private val url: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("filename") + @ExcludeMissing + filename: JsonField = JsonMissing.of(), + @JsonProperty("url") @ExcludeMissing url: JsonField = JsonMissing.of(), + ) : this(filename, url, mutableMapOf()) + + /** + * The filename to display for the document. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun filename(): String = filename.getRequired("filename") + + /** + * The URL of the document to attach. Must be a valid HTTP or HTTPS URL. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun url(): String = url.getRequired("url") + + /** + * Returns the raw JSON value of [filename]. + * + * Unlike [filename], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("filename") @ExcludeMissing fun _filename(): JsonField = filename + + /** + * Returns the raw JSON value of [url]. + * + * Unlike [url], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("url") @ExcludeMissing fun _url(): JsonField = url + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [Document]. + * + * The following fields are required: + * ```java + * .filename() + * .url() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Document]. */ + class Builder internal constructor() { + + private var filename: JsonField? = null + private var url: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(document: Document) = apply { + filename = document.filename + url = document.url + additionalProperties = document.additionalProperties.toMutableMap() + } + + /** The filename to display for the document. */ + fun filename(filename: String) = filename(JsonField.of(filename)) + + /** + * Sets [Builder.filename] to an arbitrary JSON value. + * + * You should usually call [Builder.filename] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun filename(filename: JsonField) = apply { this.filename = filename } + + /** The URL of the document to attach. Must be a valid HTTP or HTTPS URL. */ + fun url(url: String) = url(JsonField.of(url)) + + /** + * Sets [Builder.url] to an arbitrary JSON value. + * + * You should usually call [Builder.url] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun url(url: JsonField) = apply { this.url = url } + + fun additionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.clear() + putAllAdditionalProperties(additionalProperties) + } + + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + additionalProperties.put(key, value) + } + + fun putAllAdditionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.putAll(additionalProperties) + } + + fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) } + + fun removeAllAdditionalProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalProperty) + } + + /** + * Returns an immutable instance of [Document]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .filename() + * .url() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Document = + Document( + checkRequired("filename", filename), + checkRequired("url", url), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + fun validate(): Document = apply { + if (validated) { + return@apply + } + + filename() + url() + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: PreludeInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic + internal fun validity(): Int = + (if (filename.asKnown().isPresent) 1 else 0) + (if (url.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Document && + filename == other.filename && + url == other.url && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(filename, url, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Document{filename=$filename, url=$url, additionalProperties=$additionalProperties}" } /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/VerificationCreateParams.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/VerificationCreateParams.kt index b2b88ae7..b83074d3 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/VerificationCreateParams.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/VerificationCreateParams.kt @@ -1188,7 +1188,6 @@ private constructor( private val callbackUrl: JsonField, private val codeSize: JsonField, private val customCode: JsonField, - private val integration: JsonField, private val locale: JsonField, private val method: JsonField, private val preferredChannel: JsonField, @@ -1210,9 +1209,6 @@ private constructor( @JsonProperty("custom_code") @ExcludeMissing customCode: JsonField = JsonMissing.of(), - @JsonProperty("integration") - @ExcludeMissing - integration: JsonField = JsonMissing.of(), @JsonProperty("locale") @ExcludeMissing locale: JsonField = JsonMissing.of(), @JsonProperty("method") @ExcludeMissing method: JsonField = JsonMissing.of(), @JsonProperty("preferred_channel") @@ -1232,7 +1228,6 @@ private constructor( callbackUrl, codeSize, customCode, - integration, locale, method, preferredChannel, @@ -1243,8 +1238,8 @@ private constructor( ) /** - * This allows you to automatically retrieve and fill the OTP code on mobile apps. Currently - * only Android devices are supported. + * This allows automatic OTP retrieval on mobile apps and web browsers. Supported platforms + * are Android (SMS Retriever API) and Web (WebOTP API). * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). @@ -1280,14 +1275,6 @@ private constructor( */ fun customCode(): Optional = customCode.getOptional("custom_code") - /** - * The integration that triggered the verification. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). - */ - fun integration(): Optional = integration.getOptional("integration") - /** * A BCP-47 formatted locale string with the language the text message will be sent to. If * there's no locale set, the language will be determined by the country code of the phone @@ -1377,15 +1364,6 @@ private constructor( @ExcludeMissing fun _customCode(): JsonField = customCode - /** - * Returns the raw JSON value of [integration]. - * - * Unlike [integration], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("integration") - @ExcludeMissing - fun _integration(): JsonField = integration - /** * Returns the raw JSON value of [locale]. * @@ -1460,7 +1438,6 @@ private constructor( private var callbackUrl: JsonField = JsonMissing.of() private var codeSize: JsonField = JsonMissing.of() private var customCode: JsonField = JsonMissing.of() - private var integration: JsonField = JsonMissing.of() private var locale: JsonField = JsonMissing.of() private var method: JsonField = JsonMissing.of() private var preferredChannel: JsonField = JsonMissing.of() @@ -1475,7 +1452,6 @@ private constructor( callbackUrl = options.callbackUrl codeSize = options.codeSize customCode = options.customCode - integration = options.integration locale = options.locale method = options.method preferredChannel = options.preferredChannel @@ -1486,8 +1462,8 @@ private constructor( } /** - * This allows you to automatically retrieve and fill the OTP code on mobile apps. - * Currently only Android devices are supported. + * This allows automatic OTP retrieval on mobile apps and web browsers. Supported + * platforms are Android (SMS Retriever API) and Web (WebOTP API). */ fun appRealm(appRealm: AppRealm) = appRealm(JsonField.of(appRealm)) @@ -1549,20 +1525,6 @@ private constructor( */ fun customCode(customCode: JsonField) = apply { this.customCode = customCode } - /** The integration that triggered the verification. */ - fun integration(integration: Integration) = integration(JsonField.of(integration)) - - /** - * Sets [Builder.integration] to an arbitrary JSON value. - * - * You should usually call [Builder.integration] with a well-typed [Integration] value - * instead. This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun integration(integration: JsonField) = apply { - this.integration = integration - } - /** * A BCP-47 formatted locale string with the language the text message will be sent to. * If there's no locale set, the language will be determined by the country code of the @@ -1684,7 +1646,6 @@ private constructor( callbackUrl, codeSize, customCode, - integration, locale, method, preferredChannel, @@ -1706,7 +1667,6 @@ private constructor( callbackUrl() codeSize() customCode() - integration().ifPresent { it.validate() } locale() method().ifPresent { it.validate() } preferredChannel().ifPresent { it.validate() } @@ -1736,7 +1696,6 @@ private constructor( (if (callbackUrl.asKnown().isPresent) 1 else 0) + (if (codeSize.asKnown().isPresent) 1 else 0) + (if (customCode.asKnown().isPresent) 1 else 0) + - (integration.asKnown().getOrNull()?.validity() ?: 0) + (if (locale.asKnown().isPresent) 1 else 0) + (method.asKnown().getOrNull()?.validity() ?: 0) + (preferredChannel.asKnown().getOrNull()?.validity() ?: 0) + @@ -1745,8 +1704,8 @@ private constructor( (variables.asKnown().getOrNull()?.validity() ?: 0) /** - * This allows you to automatically retrieve and fill the OTP code on mobile apps. Currently - * only Android devices are supported. + * This allows automatic OTP retrieval on mobile apps and web browsers. Supported platforms + * are Android (SMS Retriever API) and Web (WebOTP API). */ class AppRealm @JsonCreator(mode = JsonCreator.Mode.DISABLED) @@ -1765,7 +1724,8 @@ private constructor( ) : this(platform, value, mutableMapOf()) /** - * The platform the SMS will be sent to. We are currently only supporting "android". + * The platform for automatic OTP retrieval. Use "android" for the SMS Retriever API or + * "web" for the WebOTP API. * * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is * unexpectedly missing or null (e.g. if the server responded with an unexpected @@ -1774,9 +1734,11 @@ private constructor( fun platform(): Platform = platform.getRequired("platform") /** - * The Android SMS Retriever API hash code that identifies your app. For more - * information, see - * [Google documentation](https://developers.google.com/identity/sms-retriever/verify#computing_your_apps_hash_string). + * The value depends on the platform: + * - For Android: The SMS Retriever API hash code (11 characters). See + * [Google documentation](https://developers.google.com/identity/sms-retriever/verify#computing_your_apps_hash_string). + * - For Web: The origin domain (e.g., "example.com" or "www.example.com"). See + * [WebOTP API documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebOTP_API). * * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is * unexpectedly missing or null (e.g. if the server responded with an unexpected @@ -1842,7 +1804,8 @@ private constructor( } /** - * The platform the SMS will be sent to. We are currently only supporting "android". + * The platform for automatic OTP retrieval. Use "android" for the SMS Retriever API + * or "web" for the WebOTP API. */ fun platform(platform: Platform) = platform(JsonField.of(platform)) @@ -1856,9 +1819,11 @@ private constructor( fun platform(platform: JsonField) = apply { this.platform = platform } /** - * The Android SMS Retriever API hash code that identifies your app. For more - * information, see - * [Google documentation](https://developers.google.com/identity/sms-retriever/verify#computing_your_apps_hash_string). + * The value depends on the platform: + * - For Android: The SMS Retriever API hash code (11 characters). See + * [Google documentation](https://developers.google.com/identity/sms-retriever/verify#computing_your_apps_hash_string). + * - For Web: The origin domain (e.g., "example.com" or "www.example.com"). See + * [WebOTP API documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebOTP_API). */ fun value(value: String) = value(JsonField.of(value)) @@ -1945,7 +1910,10 @@ private constructor( (platform.asKnown().getOrNull()?.validity() ?: 0) + (if (value.asKnown().isPresent) 1 else 0) - /** The platform the SMS will be sent to. We are currently only supporting "android". */ + /** + * The platform for automatic OTP retrieval. Use "android" for the SMS Retriever API or + * "web" for the WebOTP API. + */ class Platform @JsonCreator private constructor(private val value: JsonField) : Enum { @@ -1963,12 +1931,15 @@ private constructor( @JvmField val ANDROID = of("android") + @JvmField val WEB = of("web") + @JvmStatic fun of(value: String) = Platform(JsonField.of(value)) } /** An enum containing [Platform]'s known values. */ enum class Known { - ANDROID + ANDROID, + WEB, } /** @@ -1982,6 +1953,7 @@ private constructor( */ enum class Value { ANDROID, + WEB, /** * An enum member indicating that [Platform] was instantiated with an unknown * value. @@ -1999,6 +1971,7 @@ private constructor( fun value(): Value = when (this) { ANDROID -> Value.ANDROID + WEB -> Value.WEB else -> Value._UNKNOWN } @@ -2014,6 +1987,7 @@ private constructor( fun known(): Known = when (this) { ANDROID -> Known.ANDROID + WEB -> Known.WEB else -> throw PreludeInvalidDataException("Unknown Platform: $value") } @@ -2092,138 +2066,6 @@ private constructor( "AppRealm{platform=$platform, value=$value, additionalProperties=$additionalProperties}" } - /** The integration that triggered the verification. */ - class Integration @JsonCreator private constructor(private val value: JsonField) : - Enum { - - /** - * Returns this class instance's raw value. - * - * This is usually only useful if this instance was deserialized from data that doesn't - * match any known member, and you want to know that value. For example, if the SDK is - * on an older version than the API, then the API may respond with new members that the - * SDK is unaware of. - */ - @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value - - companion object { - - @JvmField val AUTH0 = of("auth0") - - @JvmField val SUPABASE = of("supabase") - - @JvmStatic fun of(value: String) = Integration(JsonField.of(value)) - } - - /** An enum containing [Integration]'s known values. */ - enum class Known { - AUTH0, - SUPABASE, - } - - /** - * An enum containing [Integration]'s known values, as well as an [_UNKNOWN] member. - * - * An instance of [Integration] can contain an unknown value in a couple of cases: - * - It was deserialized from data that doesn't match any known member. For example, if - * the SDK is on an older version than the API, then the API may respond with new - * members that the SDK is unaware of. - * - It was constructed with an arbitrary value using the [of] method. - */ - enum class Value { - AUTH0, - SUPABASE, - /** - * An enum member indicating that [Integration] was instantiated with an unknown - * value. - */ - _UNKNOWN, - } - - /** - * Returns an enum member corresponding to this class instance's value, or - * [Value._UNKNOWN] if the class was instantiated with an unknown value. - * - * Use the [known] method instead if you're certain the value is always known or if you - * want to throw for the unknown case. - */ - fun value(): Value = - when (this) { - AUTH0 -> Value.AUTH0 - SUPABASE -> Value.SUPABASE - else -> Value._UNKNOWN - } - - /** - * Returns an enum member corresponding to this class instance's value. - * - * Use the [value] method instead if you're uncertain the value is always known and - * don't want to throw for the unknown case. - * - * @throws PreludeInvalidDataException if this class instance's value is a not a known - * member. - */ - fun known(): Known = - when (this) { - AUTH0 -> Known.AUTH0 - SUPABASE -> Known.SUPABASE - else -> throw PreludeInvalidDataException("Unknown Integration: $value") - } - - /** - * Returns this class instance's primitive wire representation. - * - * This differs from the [toString] method because that method is primarily for - * debugging and generally doesn't throw. - * - * @throws PreludeInvalidDataException if this class instance's value does not have the - * expected primitive type. - */ - fun asString(): String = - _value().asString().orElseThrow { - PreludeInvalidDataException("Value is not a String") - } - - private var validated: Boolean = false - - fun validate(): Integration = apply { - if (validated) { - return@apply - } - - known() - validated = true - } - - fun isValid(): Boolean = - try { - validate() - true - } catch (e: PreludeInvalidDataException) { - false - } - - /** - * Returns a score indicating how many valid values are contained in this object - * recursively. - * - * Used for best match union deserialization. - */ - @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1 - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - return other is Integration && value == other.value - } - - override fun hashCode() = value.hashCode() - - override fun toString() = value.toString() - } - /** * The method used for verifying this phone number. The 'voice' option provides an * accessible alternative for visually impaired users by delivering the verification code @@ -2638,7 +2480,6 @@ private constructor( callbackUrl == other.callbackUrl && codeSize == other.codeSize && customCode == other.customCode && - integration == other.integration && locale == other.locale && method == other.method && preferredChannel == other.preferredChannel && @@ -2654,7 +2495,6 @@ private constructor( callbackUrl, codeSize, customCode, - integration, locale, method, preferredChannel, @@ -2668,7 +2508,7 @@ private constructor( override fun hashCode(): Int = hashCode override fun toString() = - "Options{appRealm=$appRealm, callbackUrl=$callbackUrl, codeSize=$codeSize, customCode=$customCode, integration=$integration, locale=$locale, method=$method, preferredChannel=$preferredChannel, senderId=$senderId, templateId=$templateId, variables=$variables, additionalProperties=$additionalProperties}" + "Options{appRealm=$appRealm, callbackUrl=$callbackUrl, codeSize=$codeSize, customCode=$customCode, locale=$locale, method=$method, preferredChannel=$preferredChannel, senderId=$senderId, templateId=$templateId, variables=$variables, additionalProperties=$additionalProperties}" } /** @@ -2739,8 +2579,9 @@ private constructor( fun appVersion(): Optional = appVersion.getOptional("app_version") /** - * The unique identifier for the user's device. For Android, this corresponds to the - * `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. + * A unique ID for the user's device. You should ensure that each user device has a unique + * `device_id` value. Ideally, for Android, this corresponds to the `ANDROID_ID` and for + * iOS, this corresponds to the `identifierForVendor`. * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). @@ -2765,7 +2606,10 @@ private constructor( devicePlatform.getOptional("device_platform") /** - * The IP address of the user's device. + * The public IP v4 or v6 address of the end-user's device. You should collect this from + * your backend. If your backend is behind a proxy, use the `X-Forwarded-For`, `Forwarded`, + * `True-Client-IP`, `CF-Connecting-IP` or an equivalent header to get the actual public IP + * of the end-user's device. * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). @@ -2773,8 +2617,8 @@ private constructor( fun ip(): Optional = ip.getOptional("ip") /** - * This signal should provide a higher level of trust, indicating that the user is genuine. - * Contact us to discuss your use case. For more details, refer to + * This signal should indicate a higher level of trust, explicitly stating that the user is + * genuine. Contact us to discuss your use case. For more details, refer to * [Signals](/verify/v2/documentation/prevent-fraud#signals). * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the @@ -2783,9 +2627,9 @@ private constructor( fun isTrustedUser(): Optional = isTrustedUser.getOptional("is_trusted_user") /** - * The JA4 fingerprint observed for the connection. Prelude will infer it automatically when - * requests go through our client SDK (which uses Prelude's edge), but you can also provide - * it explicitly if you terminate TLS yourself. + * The JA4 fingerprint observed for the end-user's connection. Prelude will infer it + * automatically when you use our Frontend SDKs (which use Prelude's edge network), but you + * can also forward the value if you terminate TLS yourself. * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). @@ -2945,8 +2789,9 @@ private constructor( fun appVersion(appVersion: JsonField) = apply { this.appVersion = appVersion } /** - * The unique identifier for the user's device. For Android, this corresponds to the - * `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. + * A unique ID for the user's device. You should ensure that each user device has a + * unique `device_id` value. Ideally, for Android, this corresponds to the `ANDROID_ID` + * and for iOS, this corresponds to the `identifierForVendor`. */ fun deviceId(deviceId: String) = deviceId(JsonField.of(deviceId)) @@ -2988,7 +2833,12 @@ private constructor( this.devicePlatform = devicePlatform } - /** The IP address of the user's device. */ + /** + * The public IP v4 or v6 address of the end-user's device. You should collect this from + * your backend. If your backend is behind a proxy, use the `X-Forwarded-For`, + * `Forwarded`, `True-Client-IP`, `CF-Connecting-IP` or an equivalent header to get the + * actual public IP of the end-user's device. + */ fun ip(ip: String) = ip(JsonField.of(ip)) /** @@ -3001,8 +2851,8 @@ private constructor( fun ip(ip: JsonField) = apply { this.ip = ip } /** - * This signal should provide a higher level of trust, indicating that the user is - * genuine. Contact us to discuss your use case. For more details, refer to + * This signal should indicate a higher level of trust, explicitly stating that the user + * is genuine. Contact us to discuss your use case. For more details, refer to * [Signals](/verify/v2/documentation/prevent-fraud#signals). */ fun isTrustedUser(isTrustedUser: Boolean) = isTrustedUser(JsonField.of(isTrustedUser)) @@ -3019,9 +2869,9 @@ private constructor( } /** - * The JA4 fingerprint observed for the connection. Prelude will infer it automatically - * when requests go through our client SDK (which uses Prelude's edge), but you can also - * provide it explicitly if you terminate TLS yourself. + * The JA4 fingerprint observed for the end-user's connection. Prelude will infer it + * automatically when you use our Frontend SDKs (which use Prelude's edge network), but + * you can also forward the value if you terminate TLS yourself. */ fun ja4Fingerprint(ja4Fingerprint: String) = ja4Fingerprint(JsonField.of(ja4Fingerprint)) diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/VerificationCreateResponse.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/VerificationCreateResponse.kt index fce0bbd1..b1b13f68 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/VerificationCreateResponse.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/VerificationCreateResponse.kt @@ -66,6 +66,11 @@ private constructor( /** * The status of the verification. + * * `success` - A new verification window was created. + * * `retry` - A new attempt was created for an existing verification window. + * * `challenged` - The verification is suspicious and is restricted to non-SMS and non-voice + * channels only. This mode must be enabled for your customer account by Prelude support. + * * `blocked` - The verification was blocked. * * @throws PreludeInvalidDataException if the JSON field has an unexpected type or is * unexpectedly missing or null (e.g. if the server responded with an unexpected value). @@ -250,7 +255,15 @@ private constructor( */ fun method(method: JsonField) = apply { this.method = method } - /** The status of the verification. */ + /** + * The status of the verification. + * * `success` - A new verification window was created. + * * `retry` - A new attempt was created for an existing verification window. + * * `challenged` - The verification is suspicious and is restricted to non-SMS and + * non-voice channels only. This mode must be enabled for your customer account by Prelude + * support. + * * `blocked` - The verification was blocked. + */ fun status(status: Status) = status(JsonField.of(status)) /** @@ -570,7 +583,14 @@ private constructor( override fun toString() = value.toString() } - /** The status of the verification. */ + /** + * The status of the verification. + * * `success` - A new verification window was created. + * * `retry` - A new attempt was created for an existing verification window. + * * `challenged` - The verification is suspicious and is restricted to non-SMS and non-voice + * channels only. This mode must be enabled for your customer account by Prelude support. + * * `blocked` - The verification was blocked. + */ class Status @JsonCreator private constructor(private val value: JsonField) : Enum { /** @@ -589,6 +609,8 @@ private constructor( @JvmField val RETRY = of("retry") + @JvmField val CHALLENGED = of("challenged") + @JvmField val BLOCKED = of("blocked") @JvmStatic fun of(value: String) = Status(JsonField.of(value)) @@ -598,6 +620,7 @@ private constructor( enum class Known { SUCCESS, RETRY, + CHALLENGED, BLOCKED, } @@ -613,6 +636,7 @@ private constructor( enum class Value { SUCCESS, RETRY, + CHALLENGED, BLOCKED, /** An enum member indicating that [Status] was instantiated with an unknown value. */ _UNKNOWN, @@ -629,6 +653,7 @@ private constructor( when (this) { SUCCESS -> Value.SUCCESS RETRY -> Value.RETRY + CHALLENGED -> Value.CHALLENGED BLOCKED -> Value.BLOCKED else -> Value._UNKNOWN } @@ -646,6 +671,7 @@ private constructor( when (this) { SUCCESS -> Known.SUCCESS RETRY -> Known.RETRY + CHALLENGED -> Known.CHALLENGED BLOCKED -> Known.BLOCKED else -> throw PreludeInvalidDataException("Unknown Status: $value") } diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchPredictParams.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchPredictParams.kt index 09f75807..ae6a1e37 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchPredictParams.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchPredictParams.kt @@ -1158,8 +1158,9 @@ private constructor( fun appVersion(): Optional = appVersion.getOptional("app_version") /** - * The unique identifier for the user's device. For Android, this corresponds to the - * `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. + * A unique ID for the user's device. You should ensure that each user device has a unique + * `device_id` value. Ideally, for Android, this corresponds to the `ANDROID_ID` and for + * iOS, this corresponds to the `identifierForVendor`. * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). @@ -1184,7 +1185,10 @@ private constructor( devicePlatform.getOptional("device_platform") /** - * The IP address of the user's device. + * The public IP v4 or v6 address of the end-user's device. You should collect this from + * your backend. If your backend is behind a proxy, use the `X-Forwarded-For`, `Forwarded`, + * `True-Client-IP`, `CF-Connecting-IP` or an equivalent header to get the actual public IP + * of the end-user's device. * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). @@ -1192,8 +1196,8 @@ private constructor( fun ip(): Optional = ip.getOptional("ip") /** - * This signal should provide a higher level of trust, indicating that the user is genuine. - * Contact us to discuss your use case. For more details, refer to + * This signal should indicate a higher level of trust, explicitly stating that the user is + * genuine. Contact us to discuss your use case. For more details, refer to * [Signals](/verify/v2/documentation/prevent-fraud#signals). * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the @@ -1202,9 +1206,9 @@ private constructor( fun isTrustedUser(): Optional = isTrustedUser.getOptional("is_trusted_user") /** - * The JA4 fingerprint observed for the connection. Prelude will infer it automatically when - * requests go through our client SDK (which uses Prelude's edge), but you can also provide - * it explicitly if you terminate TLS yourself. + * The JA4 fingerprint observed for the end-user's connection. Prelude will infer it + * automatically when you use our Frontend SDKs (which use Prelude's edge network), but you + * can also forward the value if you terminate TLS yourself. * * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). @@ -1364,8 +1368,9 @@ private constructor( fun appVersion(appVersion: JsonField) = apply { this.appVersion = appVersion } /** - * The unique identifier for the user's device. For Android, this corresponds to the - * `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. + * A unique ID for the user's device. You should ensure that each user device has a + * unique `device_id` value. Ideally, for Android, this corresponds to the `ANDROID_ID` + * and for iOS, this corresponds to the `identifierForVendor`. */ fun deviceId(deviceId: String) = deviceId(JsonField.of(deviceId)) @@ -1407,7 +1412,12 @@ private constructor( this.devicePlatform = devicePlatform } - /** The IP address of the user's device. */ + /** + * The public IP v4 or v6 address of the end-user's device. You should collect this from + * your backend. If your backend is behind a proxy, use the `X-Forwarded-For`, + * `Forwarded`, `True-Client-IP`, `CF-Connecting-IP` or an equivalent header to get the + * actual public IP of the end-user's device. + */ fun ip(ip: String) = ip(JsonField.of(ip)) /** @@ -1420,8 +1430,8 @@ private constructor( fun ip(ip: JsonField) = apply { this.ip = ip } /** - * This signal should provide a higher level of trust, indicating that the user is - * genuine. Contact us to discuss your use case. For more details, refer to + * This signal should indicate a higher level of trust, explicitly stating that the user + * is genuine. Contact us to discuss your use case. For more details, refer to * [Signals](/verify/v2/documentation/prevent-fraud#signals). */ fun isTrustedUser(isTrustedUser: Boolean) = isTrustedUser(JsonField.of(isTrustedUser)) @@ -1438,9 +1448,9 @@ private constructor( } /** - * The JA4 fingerprint observed for the connection. Prelude will infer it automatically - * when requests go through our client SDK (which uses Prelude's edge), but you can also - * provide it explicitly if you terminate TLS yourself. + * The JA4 fingerprint observed for the end-user's connection. Prelude will infer it + * automatically when you use our Frontend SDKs (which use Prelude's edge network), but + * you can also forward the value if you terminate TLS yourself. */ fun ja4Fingerprint(ja4Fingerprint: String) = ja4Fingerprint(JsonField.of(ja4Fingerprint)) diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchPredictResponse.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchPredictResponse.kt index 2981c266..5b046375 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchPredictResponse.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchPredictResponse.kt @@ -8,13 +8,16 @@ import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import java.util.Collections import java.util.Objects +import java.util.Optional import kotlin.jvm.optionals.getOrNull import so.prelude.sdk.core.Enum import so.prelude.sdk.core.ExcludeMissing import so.prelude.sdk.core.JsonField import so.prelude.sdk.core.JsonMissing import so.prelude.sdk.core.JsonValue +import so.prelude.sdk.core.checkKnown import so.prelude.sdk.core.checkRequired +import so.prelude.sdk.core.toImmutable import so.prelude.sdk.errors.PreludeInvalidDataException class WatchPredictResponse @@ -23,6 +26,7 @@ private constructor( private val id: JsonField, private val prediction: JsonField, private val requestId: JsonField, + private val riskFactors: JsonField>, private val additionalProperties: MutableMap, ) { @@ -33,7 +37,10 @@ private constructor( @ExcludeMissing prediction: JsonField = JsonMissing.of(), @JsonProperty("request_id") @ExcludeMissing requestId: JsonField = JsonMissing.of(), - ) : this(id, prediction, requestId, mutableMapOf()) + @JsonProperty("risk_factors") + @ExcludeMissing + riskFactors: JsonField> = JsonMissing.of(), + ) : this(id, prediction, requestId, riskFactors, mutableMapOf()) /** * The prediction identifier. @@ -60,6 +67,35 @@ private constructor( */ fun requestId(): String = requestId.getRequired("request_id") + /** + * The risk factors that contributed to the suspicious prediction. Only present when prediction + * is "suspicious" and the anti-fraud system detected specific risk signals. + * * `behavioral_pattern` - The phone number past behavior during verification flows exhibits + * suspicious patterns. + * * `device_attribute` - The device exhibits characteristics associated with suspicious + * activity patterns. + * * `fraud_database` - The phone number has been flagged as suspicious in one or more of our + * fraud databases. + * * `location_discrepancy` - The phone number prefix and IP address discrepancy indicates + * potential fraud. + * * `network_fingerprint` - The network connection exhibits characteristics associated with + * suspicious activity patterns. + * * `poor_conversion_history` - The phone number has a history of poorly converting to a + * verified phone number. + * * `prefix_concentration` - The phone number is part of a range known to be associated with + * suspicious activity patterns. + * * `suspected_request_tampering` - The SDK signature is invalid and the request is considered + * to be tampered with. + * * `suspicious_ip_address` - The IP address is deemed to be associated with suspicious + * activity patterns. + * * `temporary_phone_number` - The phone number is known to be a temporary or disposable + * number. + * + * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun riskFactors(): Optional> = riskFactors.getOptional("risk_factors") + /** * Returns the raw JSON value of [id]. * @@ -83,6 +119,15 @@ private constructor( */ @JsonProperty("request_id") @ExcludeMissing fun _requestId(): JsonField = requestId + /** + * Returns the raw JSON value of [riskFactors]. + * + * Unlike [riskFactors], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("risk_factors") + @ExcludeMissing + fun _riskFactors(): JsonField> = riskFactors + @JsonAnySetter private fun putAdditionalProperty(key: String, value: JsonValue) { additionalProperties.put(key, value) @@ -116,6 +161,7 @@ private constructor( private var id: JsonField? = null private var prediction: JsonField? = null private var requestId: JsonField? = null + private var riskFactors: JsonField>? = null private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic @@ -123,6 +169,7 @@ private constructor( id = watchPredictResponse.id prediction = watchPredictResponse.prediction requestId = watchPredictResponse.requestId + riskFactors = watchPredictResponse.riskFactors.map { it.toMutableList() } additionalProperties = watchPredictResponse.additionalProperties.toMutableMap() } @@ -164,6 +211,55 @@ private constructor( */ fun requestId(requestId: JsonField) = apply { this.requestId = requestId } + /** + * The risk factors that contributed to the suspicious prediction. Only present when + * prediction is "suspicious" and the anti-fraud system detected specific risk signals. + * * `behavioral_pattern` - The phone number past behavior during verification flows + * exhibits suspicious patterns. + * * `device_attribute` - The device exhibits characteristics associated with suspicious + * activity patterns. + * * `fraud_database` - The phone number has been flagged as suspicious in one or more of + * our fraud databases. + * * `location_discrepancy` - The phone number prefix and IP address discrepancy indicates + * potential fraud. + * * `network_fingerprint` - The network connection exhibits characteristics associated with + * suspicious activity patterns. + * * `poor_conversion_history` - The phone number has a history of poorly converting to a + * verified phone number. + * * `prefix_concentration` - The phone number is part of a range known to be associated + * with suspicious activity patterns. + * * `suspected_request_tampering` - The SDK signature is invalid and the request is + * considered to be tampered with. + * * `suspicious_ip_address` - The IP address is deemed to be associated with suspicious + * activity patterns. + * * `temporary_phone_number` - The phone number is known to be a temporary or disposable + * number. + */ + fun riskFactors(riskFactors: List) = riskFactors(JsonField.of(riskFactors)) + + /** + * Sets [Builder.riskFactors] to an arbitrary JSON value. + * + * You should usually call [Builder.riskFactors] with a well-typed `List` value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun riskFactors(riskFactors: JsonField>) = apply { + this.riskFactors = riskFactors.map { it.toMutableList() } + } + + /** + * Adds a single [RiskFactor] to [riskFactors]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addRiskFactor(riskFactor: RiskFactor) = apply { + riskFactors = + (riskFactors ?: JsonField.of(mutableListOf())).also { + checkKnown("riskFactors", it).add(riskFactor) + } + } + fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() putAllAdditionalProperties(additionalProperties) @@ -202,6 +298,7 @@ private constructor( checkRequired("id", id), checkRequired("prediction", prediction), checkRequired("requestId", requestId), + (riskFactors ?: JsonMissing.of()).map { it.toImmutable() }, additionalProperties.toMutableMap(), ) } @@ -216,6 +313,7 @@ private constructor( id() prediction().validate() requestId() + riskFactors().ifPresent { it.forEach { it.validate() } } validated = true } @@ -236,7 +334,8 @@ private constructor( internal fun validity(): Int = (if (id.asKnown().isPresent) 1 else 0) + (prediction.asKnown().getOrNull()?.validity() ?: 0) + - (if (requestId.asKnown().isPresent) 1 else 0) + (if (requestId.asKnown().isPresent) 1 else 0) + + (riskFactors.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) /** The prediction outcome. */ class Prediction @JsonCreator private constructor(private val value: JsonField) : Enum { @@ -366,6 +465,181 @@ private constructor( override fun toString() = value.toString() } + class RiskFactor @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val BEHAVIORAL_PATTERN = of("behavioral_pattern") + + @JvmField val DEVICE_ATTRIBUTE = of("device_attribute") + + @JvmField val FRAUD_DATABASE = of("fraud_database") + + @JvmField val LOCATION_DISCREPANCY = of("location_discrepancy") + + @JvmField val NETWORK_FINGERPRINT = of("network_fingerprint") + + @JvmField val POOR_CONVERSION_HISTORY = of("poor_conversion_history") + + @JvmField val PREFIX_CONCENTRATION = of("prefix_concentration") + + @JvmField val SUSPECTED_REQUEST_TAMPERING = of("suspected_request_tampering") + + @JvmField val SUSPICIOUS_IP_ADDRESS = of("suspicious_ip_address") + + @JvmField val TEMPORARY_PHONE_NUMBER = of("temporary_phone_number") + + @JvmStatic fun of(value: String) = RiskFactor(JsonField.of(value)) + } + + /** An enum containing [RiskFactor]'s known values. */ + enum class Known { + BEHAVIORAL_PATTERN, + DEVICE_ATTRIBUTE, + FRAUD_DATABASE, + LOCATION_DISCREPANCY, + NETWORK_FINGERPRINT, + POOR_CONVERSION_HISTORY, + PREFIX_CONCENTRATION, + SUSPECTED_REQUEST_TAMPERING, + SUSPICIOUS_IP_ADDRESS, + TEMPORARY_PHONE_NUMBER, + } + + /** + * An enum containing [RiskFactor]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [RiskFactor] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + BEHAVIORAL_PATTERN, + DEVICE_ATTRIBUTE, + FRAUD_DATABASE, + LOCATION_DISCREPANCY, + NETWORK_FINGERPRINT, + POOR_CONVERSION_HISTORY, + PREFIX_CONCENTRATION, + SUSPECTED_REQUEST_TAMPERING, + SUSPICIOUS_IP_ADDRESS, + TEMPORARY_PHONE_NUMBER, + /** + * An enum member indicating that [RiskFactor] was instantiated with an unknown value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + BEHAVIORAL_PATTERN -> Value.BEHAVIORAL_PATTERN + DEVICE_ATTRIBUTE -> Value.DEVICE_ATTRIBUTE + FRAUD_DATABASE -> Value.FRAUD_DATABASE + LOCATION_DISCREPANCY -> Value.LOCATION_DISCREPANCY + NETWORK_FINGERPRINT -> Value.NETWORK_FINGERPRINT + POOR_CONVERSION_HISTORY -> Value.POOR_CONVERSION_HISTORY + PREFIX_CONCENTRATION -> Value.PREFIX_CONCENTRATION + SUSPECTED_REQUEST_TAMPERING -> Value.SUSPECTED_REQUEST_TAMPERING + SUSPICIOUS_IP_ADDRESS -> Value.SUSPICIOUS_IP_ADDRESS + TEMPORARY_PHONE_NUMBER -> Value.TEMPORARY_PHONE_NUMBER + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws PreludeInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + BEHAVIORAL_PATTERN -> Known.BEHAVIORAL_PATTERN + DEVICE_ATTRIBUTE -> Known.DEVICE_ATTRIBUTE + FRAUD_DATABASE -> Known.FRAUD_DATABASE + LOCATION_DISCREPANCY -> Known.LOCATION_DISCREPANCY + NETWORK_FINGERPRINT -> Known.NETWORK_FINGERPRINT + POOR_CONVERSION_HISTORY -> Known.POOR_CONVERSION_HISTORY + PREFIX_CONCENTRATION -> Known.PREFIX_CONCENTRATION + SUSPECTED_REQUEST_TAMPERING -> Known.SUSPECTED_REQUEST_TAMPERING + SUSPICIOUS_IP_ADDRESS -> Known.SUSPICIOUS_IP_ADDRESS + TEMPORARY_PHONE_NUMBER -> Known.TEMPORARY_PHONE_NUMBER + else -> throw PreludeInvalidDataException("Unknown RiskFactor: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws PreludeInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { PreludeInvalidDataException("Value is not a String") } + + private var validated: Boolean = false + + fun validate(): RiskFactor = apply { + if (validated) { + return@apply + } + + known() + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: PreludeInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is RiskFactor && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -375,15 +649,16 @@ private constructor( id == other.id && prediction == other.prediction && requestId == other.requestId && + riskFactors == other.riskFactors && additionalProperties == other.additionalProperties } private val hashCode: Int by lazy { - Objects.hash(id, prediction, requestId, additionalProperties) + Objects.hash(id, prediction, requestId, riskFactors, additionalProperties) } override fun hashCode(): Int = hashCode override fun toString() = - "WatchPredictResponse{id=$id, prediction=$prediction, requestId=$requestId, additionalProperties=$additionalProperties}" + "WatchPredictResponse{id=$id, prediction=$prediction, requestId=$requestId, riskFactors=$riskFactors, additionalProperties=$additionalProperties}" } diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchSendFeedbacksParams.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchSendFeedbacksParams.kt index 56bcc629..0bc6414e 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchSendFeedbacksParams.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/models/WatchSendFeedbacksParams.kt @@ -441,9 +441,7 @@ private constructor( private constructor( private val target: JsonField, private val type: JsonField, - private val dispatchId: JsonField, private val metadata: JsonField, - private val signals: JsonField, private val additionalProperties: MutableMap, ) { @@ -451,14 +449,10 @@ private constructor( private constructor( @JsonProperty("target") @ExcludeMissing target: JsonField = JsonMissing.of(), @JsonProperty("type") @ExcludeMissing type: JsonField = JsonMissing.of(), - @JsonProperty("dispatch_id") - @ExcludeMissing - dispatchId: JsonField = JsonMissing.of(), @JsonProperty("metadata") @ExcludeMissing metadata: JsonField = JsonMissing.of(), - @JsonProperty("signals") @ExcludeMissing signals: JsonField = JsonMissing.of(), - ) : this(target, type, dispatchId, metadata, signals, mutableMapOf()) + ) : this(target, type, metadata, mutableMapOf()) /** * The feedback target. Only supports phone numbers for now. @@ -476,14 +470,6 @@ private constructor( */ fun type(): Type = type.getRequired("type") - /** - * The identifier of the dispatch that came from the front-end SDK. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). - */ - fun dispatchId(): Optional = dispatchId.getOptional("dispatch_id") - /** * The metadata for this feedback. * @@ -492,15 +478,6 @@ private constructor( */ fun metadata(): Optional = metadata.getOptional("metadata") - /** - * The signals used for anti-fraud. For more details, refer to - * [Signals](/verify/v2/documentation/prevent-fraud#signals). - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). - */ - fun signals(): Optional = signals.getOptional("signals") - /** * Returns the raw JSON value of [target]. * @@ -515,15 +492,6 @@ private constructor( */ @JsonProperty("type") @ExcludeMissing fun _type(): JsonField = type - /** - * Returns the raw JSON value of [dispatchId]. - * - * Unlike [dispatchId], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("dispatch_id") - @ExcludeMissing - fun _dispatchId(): JsonField = dispatchId - /** * Returns the raw JSON value of [metadata]. * @@ -531,13 +499,6 @@ private constructor( */ @JsonProperty("metadata") @ExcludeMissing fun _metadata(): JsonField = metadata - /** - * Returns the raw JSON value of [signals]. - * - * Unlike [signals], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("signals") @ExcludeMissing fun _signals(): JsonField = signals - @JsonAnySetter private fun putAdditionalProperty(key: String, value: JsonValue) { additionalProperties.put(key, value) @@ -569,18 +530,14 @@ private constructor( private var target: JsonField? = null private var type: JsonField? = null - private var dispatchId: JsonField = JsonMissing.of() private var metadata: JsonField = JsonMissing.of() - private var signals: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic internal fun from(feedback: Feedback) = apply { target = feedback.target type = feedback.type - dispatchId = feedback.dispatchId metadata = feedback.metadata - signals = feedback.signals additionalProperties = feedback.additionalProperties.toMutableMap() } @@ -608,18 +565,6 @@ private constructor( */ fun type(type: JsonField) = apply { this.type = type } - /** The identifier of the dispatch that came from the front-end SDK. */ - fun dispatchId(dispatchId: String) = dispatchId(JsonField.of(dispatchId)) - - /** - * Sets [Builder.dispatchId] to an arbitrary JSON value. - * - * You should usually call [Builder.dispatchId] with a well-typed [String] value - * instead. This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun dispatchId(dispatchId: JsonField) = apply { this.dispatchId = dispatchId } - /** The metadata for this feedback. */ fun metadata(metadata: Metadata) = metadata(JsonField.of(metadata)) @@ -632,21 +577,6 @@ private constructor( */ fun metadata(metadata: JsonField) = apply { this.metadata = metadata } - /** - * The signals used for anti-fraud. For more details, refer to - * [Signals](/verify/v2/documentation/prevent-fraud#signals). - */ - fun signals(signals: Signals) = signals(JsonField.of(signals)) - - /** - * Sets [Builder.signals] to an arbitrary JSON value. - * - * You should usually call [Builder.signals] with a well-typed [Signals] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun signals(signals: JsonField) = apply { this.signals = signals } - fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() putAllAdditionalProperties(additionalProperties) @@ -683,9 +613,7 @@ private constructor( Feedback( checkRequired("target", target), checkRequired("type", type), - dispatchId, metadata, - signals, additionalProperties.toMutableMap(), ) } @@ -699,9 +627,7 @@ private constructor( target().validate() type().validate() - dispatchId() metadata().ifPresent { it.validate() } - signals().ifPresent { it.validate() } validated = true } @@ -723,9 +649,7 @@ private constructor( internal fun validity(): Int = (target.asKnown().getOrNull()?.validity() ?: 0) + (type.asKnown().getOrNull()?.validity() ?: 0) + - (if (dispatchId.asKnown().isPresent) 1 else 0) + - (metadata.asKnown().getOrNull()?.validity() ?: 0) + - (signals.asKnown().getOrNull()?.validity() ?: 0) + (metadata.asKnown().getOrNull()?.validity() ?: 0) /** The feedback target. Only supports phone numbers for now. */ class Target @@ -1348,691 +1272,6 @@ private constructor( "Metadata{correlationId=$correlationId, additionalProperties=$additionalProperties}" } - /** - * The signals used for anti-fraud. For more details, refer to - * [Signals](/verify/v2/documentation/prevent-fraud#signals). - */ - class Signals - @JsonCreator(mode = JsonCreator.Mode.DISABLED) - private constructor( - private val appVersion: JsonField, - private val deviceId: JsonField, - private val deviceModel: JsonField, - private val devicePlatform: JsonField, - private val ip: JsonField, - private val isTrustedUser: JsonField, - private val ja4Fingerprint: JsonField, - private val osVersion: JsonField, - private val userAgent: JsonField, - private val additionalProperties: MutableMap, - ) { - - @JsonCreator - private constructor( - @JsonProperty("app_version") - @ExcludeMissing - appVersion: JsonField = JsonMissing.of(), - @JsonProperty("device_id") - @ExcludeMissing - deviceId: JsonField = JsonMissing.of(), - @JsonProperty("device_model") - @ExcludeMissing - deviceModel: JsonField = JsonMissing.of(), - @JsonProperty("device_platform") - @ExcludeMissing - devicePlatform: JsonField = JsonMissing.of(), - @JsonProperty("ip") @ExcludeMissing ip: JsonField = JsonMissing.of(), - @JsonProperty("is_trusted_user") - @ExcludeMissing - isTrustedUser: JsonField = JsonMissing.of(), - @JsonProperty("ja4_fingerprint") - @ExcludeMissing - ja4Fingerprint: JsonField = JsonMissing.of(), - @JsonProperty("os_version") - @ExcludeMissing - osVersion: JsonField = JsonMissing.of(), - @JsonProperty("user_agent") - @ExcludeMissing - userAgent: JsonField = JsonMissing.of(), - ) : this( - appVersion, - deviceId, - deviceModel, - devicePlatform, - ip, - isTrustedUser, - ja4Fingerprint, - osVersion, - userAgent, - mutableMapOf(), - ) - - /** - * The version of your application. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun appVersion(): Optional = appVersion.getOptional("app_version") - - /** - * The unique identifier for the user's device. For Android, this corresponds to the - * `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun deviceId(): Optional = deviceId.getOptional("device_id") - - /** - * The model of the user's device. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun deviceModel(): Optional = deviceModel.getOptional("device_model") - - /** - * The type of the user's device. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun devicePlatform(): Optional = - devicePlatform.getOptional("device_platform") - - /** - * The IP address of the user's device. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun ip(): Optional = ip.getOptional("ip") - - /** - * This signal should provide a higher level of trust, indicating that the user is - * genuine. Contact us to discuss your use case. For more details, refer to - * [Signals](/verify/v2/documentation/prevent-fraud#signals). - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun isTrustedUser(): Optional = isTrustedUser.getOptional("is_trusted_user") - - /** - * The JA4 fingerprint observed for the connection. Prelude will infer it automatically - * when requests go through our client SDK (which uses Prelude's edge), but you can also - * provide it explicitly if you terminate TLS yourself. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun ja4Fingerprint(): Optional = ja4Fingerprint.getOptional("ja4_fingerprint") - - /** - * The version of the user's device operating system. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun osVersion(): Optional = osVersion.getOptional("os_version") - - /** - * The user agent of the user's device. If the individual fields (os_version, - * device_platform, device_model) are provided, we will prioritize those values instead - * of parsing them from the user agent string. - * - * @throws PreludeInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun userAgent(): Optional = userAgent.getOptional("user_agent") - - /** - * Returns the raw JSON value of [appVersion]. - * - * Unlike [appVersion], this method doesn't throw if the JSON field has an unexpected - * type. - */ - @JsonProperty("app_version") - @ExcludeMissing - fun _appVersion(): JsonField = appVersion - - /** - * Returns the raw JSON value of [deviceId]. - * - * Unlike [deviceId], this method doesn't throw if the JSON field has an unexpected - * type. - */ - @JsonProperty("device_id") @ExcludeMissing fun _deviceId(): JsonField = deviceId - - /** - * Returns the raw JSON value of [deviceModel]. - * - * Unlike [deviceModel], this method doesn't throw if the JSON field has an unexpected - * type. - */ - @JsonProperty("device_model") - @ExcludeMissing - fun _deviceModel(): JsonField = deviceModel - - /** - * Returns the raw JSON value of [devicePlatform]. - * - * Unlike [devicePlatform], this method doesn't throw if the JSON field has an - * unexpected type. - */ - @JsonProperty("device_platform") - @ExcludeMissing - fun _devicePlatform(): JsonField = devicePlatform - - /** - * Returns the raw JSON value of [ip]. - * - * Unlike [ip], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("ip") @ExcludeMissing fun _ip(): JsonField = ip - - /** - * Returns the raw JSON value of [isTrustedUser]. - * - * Unlike [isTrustedUser], this method doesn't throw if the JSON field has an unexpected - * type. - */ - @JsonProperty("is_trusted_user") - @ExcludeMissing - fun _isTrustedUser(): JsonField = isTrustedUser - - /** - * Returns the raw JSON value of [ja4Fingerprint]. - * - * Unlike [ja4Fingerprint], this method doesn't throw if the JSON field has an - * unexpected type. - */ - @JsonProperty("ja4_fingerprint") - @ExcludeMissing - fun _ja4Fingerprint(): JsonField = ja4Fingerprint - - /** - * Returns the raw JSON value of [osVersion]. - * - * Unlike [osVersion], this method doesn't throw if the JSON field has an unexpected - * type. - */ - @JsonProperty("os_version") - @ExcludeMissing - fun _osVersion(): JsonField = osVersion - - /** - * Returns the raw JSON value of [userAgent]. - * - * Unlike [userAgent], this method doesn't throw if the JSON field has an unexpected - * type. - */ - @JsonProperty("user_agent") - @ExcludeMissing - fun _userAgent(): JsonField = userAgent - - @JsonAnySetter - private fun putAdditionalProperty(key: String, value: JsonValue) { - additionalProperties.put(key, value) - } - - @JsonAnyGetter - @ExcludeMissing - fun _additionalProperties(): Map = - Collections.unmodifiableMap(additionalProperties) - - fun toBuilder() = Builder().from(this) - - companion object { - - /** Returns a mutable builder for constructing an instance of [Signals]. */ - @JvmStatic fun builder() = Builder() - } - - /** A builder for [Signals]. */ - class Builder internal constructor() { - - private var appVersion: JsonField = JsonMissing.of() - private var deviceId: JsonField = JsonMissing.of() - private var deviceModel: JsonField = JsonMissing.of() - private var devicePlatform: JsonField = JsonMissing.of() - private var ip: JsonField = JsonMissing.of() - private var isTrustedUser: JsonField = JsonMissing.of() - private var ja4Fingerprint: JsonField = JsonMissing.of() - private var osVersion: JsonField = JsonMissing.of() - private var userAgent: JsonField = JsonMissing.of() - private var additionalProperties: MutableMap = mutableMapOf() - - @JvmSynthetic - internal fun from(signals: Signals) = apply { - appVersion = signals.appVersion - deviceId = signals.deviceId - deviceModel = signals.deviceModel - devicePlatform = signals.devicePlatform - ip = signals.ip - isTrustedUser = signals.isTrustedUser - ja4Fingerprint = signals.ja4Fingerprint - osVersion = signals.osVersion - userAgent = signals.userAgent - additionalProperties = signals.additionalProperties.toMutableMap() - } - - /** The version of your application. */ - fun appVersion(appVersion: String) = appVersion(JsonField.of(appVersion)) - - /** - * Sets [Builder.appVersion] to an arbitrary JSON value. - * - * You should usually call [Builder.appVersion] with a well-typed [String] value - * instead. This method is primarily for setting the field to an undocumented or not - * yet supported value. - */ - fun appVersion(appVersion: JsonField) = apply { - this.appVersion = appVersion - } - - /** - * The unique identifier for the user's device. For Android, this corresponds to the - * `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. - */ - fun deviceId(deviceId: String) = deviceId(JsonField.of(deviceId)) - - /** - * Sets [Builder.deviceId] to an arbitrary JSON value. - * - * You should usually call [Builder.deviceId] with a well-typed [String] value - * instead. This method is primarily for setting the field to an undocumented or not - * yet supported value. - */ - fun deviceId(deviceId: JsonField) = apply { this.deviceId = deviceId } - - /** The model of the user's device. */ - fun deviceModel(deviceModel: String) = deviceModel(JsonField.of(deviceModel)) - - /** - * Sets [Builder.deviceModel] to an arbitrary JSON value. - * - * You should usually call [Builder.deviceModel] with a well-typed [String] value - * instead. This method is primarily for setting the field to an undocumented or not - * yet supported value. - */ - fun deviceModel(deviceModel: JsonField) = apply { - this.deviceModel = deviceModel - } - - /** The type of the user's device. */ - fun devicePlatform(devicePlatform: DevicePlatform) = - devicePlatform(JsonField.of(devicePlatform)) - - /** - * Sets [Builder.devicePlatform] to an arbitrary JSON value. - * - * You should usually call [Builder.devicePlatform] with a well-typed - * [DevicePlatform] value instead. This method is primarily for setting the field to - * an undocumented or not yet supported value. - */ - fun devicePlatform(devicePlatform: JsonField) = apply { - this.devicePlatform = devicePlatform - } - - /** The IP address of the user's device. */ - fun ip(ip: String) = ip(JsonField.of(ip)) - - /** - * Sets [Builder.ip] to an arbitrary JSON value. - * - * You should usually call [Builder.ip] with a well-typed [String] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun ip(ip: JsonField) = apply { this.ip = ip } - - /** - * This signal should provide a higher level of trust, indicating that the user is - * genuine. Contact us to discuss your use case. For more details, refer to - * [Signals](/verify/v2/documentation/prevent-fraud#signals). - */ - fun isTrustedUser(isTrustedUser: Boolean) = - isTrustedUser(JsonField.of(isTrustedUser)) - - /** - * Sets [Builder.isTrustedUser] to an arbitrary JSON value. - * - * You should usually call [Builder.isTrustedUser] with a well-typed [Boolean] value - * instead. This method is primarily for setting the field to an undocumented or not - * yet supported value. - */ - fun isTrustedUser(isTrustedUser: JsonField) = apply { - this.isTrustedUser = isTrustedUser - } - - /** - * The JA4 fingerprint observed for the connection. Prelude will infer it - * automatically when requests go through our client SDK (which uses Prelude's - * edge), but you can also provide it explicitly if you terminate TLS yourself. - */ - fun ja4Fingerprint(ja4Fingerprint: String) = - ja4Fingerprint(JsonField.of(ja4Fingerprint)) - - /** - * Sets [Builder.ja4Fingerprint] to an arbitrary JSON value. - * - * You should usually call [Builder.ja4Fingerprint] with a well-typed [String] value - * instead. This method is primarily for setting the field to an undocumented or not - * yet supported value. - */ - fun ja4Fingerprint(ja4Fingerprint: JsonField) = apply { - this.ja4Fingerprint = ja4Fingerprint - } - - /** The version of the user's device operating system. */ - fun osVersion(osVersion: String) = osVersion(JsonField.of(osVersion)) - - /** - * Sets [Builder.osVersion] to an arbitrary JSON value. - * - * You should usually call [Builder.osVersion] with a well-typed [String] value - * instead. This method is primarily for setting the field to an undocumented or not - * yet supported value. - */ - fun osVersion(osVersion: JsonField) = apply { this.osVersion = osVersion } - - /** - * The user agent of the user's device. If the individual fields (os_version, - * device_platform, device_model) are provided, we will prioritize those values - * instead of parsing them from the user agent string. - */ - fun userAgent(userAgent: String) = userAgent(JsonField.of(userAgent)) - - /** - * Sets [Builder.userAgent] to an arbitrary JSON value. - * - * You should usually call [Builder.userAgent] with a well-typed [String] value - * instead. This method is primarily for setting the field to an undocumented or not - * yet supported value. - */ - fun userAgent(userAgent: JsonField) = apply { this.userAgent = userAgent } - - fun additionalProperties(additionalProperties: Map) = apply { - this.additionalProperties.clear() - putAllAdditionalProperties(additionalProperties) - } - - fun putAdditionalProperty(key: String, value: JsonValue) = apply { - additionalProperties.put(key, value) - } - - fun putAllAdditionalProperties(additionalProperties: Map) = - apply { - this.additionalProperties.putAll(additionalProperties) - } - - fun removeAdditionalProperty(key: String) = apply { - additionalProperties.remove(key) - } - - fun removeAllAdditionalProperties(keys: Set) = apply { - keys.forEach(::removeAdditionalProperty) - } - - /** - * Returns an immutable instance of [Signals]. - * - * Further updates to this [Builder] will not mutate the returned instance. - */ - fun build(): Signals = - Signals( - appVersion, - deviceId, - deviceModel, - devicePlatform, - ip, - isTrustedUser, - ja4Fingerprint, - osVersion, - userAgent, - additionalProperties.toMutableMap(), - ) - } - - private var validated: Boolean = false - - fun validate(): Signals = apply { - if (validated) { - return@apply - } - - appVersion() - deviceId() - deviceModel() - devicePlatform().ifPresent { it.validate() } - ip() - isTrustedUser() - ja4Fingerprint() - osVersion() - userAgent() - validated = true - } - - fun isValid(): Boolean = - try { - validate() - true - } catch (e: PreludeInvalidDataException) { - false - } - - /** - * Returns a score indicating how many valid values are contained in this object - * recursively. - * - * Used for best match union deserialization. - */ - @JvmSynthetic - internal fun validity(): Int = - (if (appVersion.asKnown().isPresent) 1 else 0) + - (if (deviceId.asKnown().isPresent) 1 else 0) + - (if (deviceModel.asKnown().isPresent) 1 else 0) + - (devicePlatform.asKnown().getOrNull()?.validity() ?: 0) + - (if (ip.asKnown().isPresent) 1 else 0) + - (if (isTrustedUser.asKnown().isPresent) 1 else 0) + - (if (ja4Fingerprint.asKnown().isPresent) 1 else 0) + - (if (osVersion.asKnown().isPresent) 1 else 0) + - (if (userAgent.asKnown().isPresent) 1 else 0) - - /** The type of the user's device. */ - class DevicePlatform - @JsonCreator - private constructor(private val value: JsonField) : Enum { - - /** - * Returns this class instance's raw value. - * - * This is usually only useful if this instance was deserialized from data that - * doesn't match any known member, and you want to know that value. For example, if - * the SDK is on an older version than the API, then the API may respond with new - * members that the SDK is unaware of. - */ - @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value - - companion object { - - @JvmField val ANDROID = of("android") - - @JvmField val IOS = of("ios") - - @JvmField val IPADOS = of("ipados") - - @JvmField val TVOS = of("tvos") - - @JvmField val WEB = of("web") - - @JvmStatic fun of(value: String) = DevicePlatform(JsonField.of(value)) - } - - /** An enum containing [DevicePlatform]'s known values. */ - enum class Known { - ANDROID, - IOS, - IPADOS, - TVOS, - WEB, - } - - /** - * An enum containing [DevicePlatform]'s known values, as well as an [_UNKNOWN] - * member. - * - * An instance of [DevicePlatform] can contain an unknown value in a couple of - * cases: - * - It was deserialized from data that doesn't match any known member. For example, - * if the SDK is on an older version than the API, then the API may respond with - * new members that the SDK is unaware of. - * - It was constructed with an arbitrary value using the [of] method. - */ - enum class Value { - ANDROID, - IOS, - IPADOS, - TVOS, - WEB, - /** - * An enum member indicating that [DevicePlatform] was instantiated with an - * unknown value. - */ - _UNKNOWN, - } - - /** - * Returns an enum member corresponding to this class instance's value, or - * [Value._UNKNOWN] if the class was instantiated with an unknown value. - * - * Use the [known] method instead if you're certain the value is always known or if - * you want to throw for the unknown case. - */ - fun value(): Value = - when (this) { - ANDROID -> Value.ANDROID - IOS -> Value.IOS - IPADOS -> Value.IPADOS - TVOS -> Value.TVOS - WEB -> Value.WEB - else -> Value._UNKNOWN - } - - /** - * Returns an enum member corresponding to this class instance's value. - * - * Use the [value] method instead if you're uncertain the value is always known and - * don't want to throw for the unknown case. - * - * @throws PreludeInvalidDataException if this class instance's value is a not a - * known member. - */ - fun known(): Known = - when (this) { - ANDROID -> Known.ANDROID - IOS -> Known.IOS - IPADOS -> Known.IPADOS - TVOS -> Known.TVOS - WEB -> Known.WEB - else -> throw PreludeInvalidDataException("Unknown DevicePlatform: $value") - } - - /** - * Returns this class instance's primitive wire representation. - * - * This differs from the [toString] method because that method is primarily for - * debugging and generally doesn't throw. - * - * @throws PreludeInvalidDataException if this class instance's value does not have - * the expected primitive type. - */ - fun asString(): String = - _value().asString().orElseThrow { - PreludeInvalidDataException("Value is not a String") - } - - private var validated: Boolean = false - - fun validate(): DevicePlatform = apply { - if (validated) { - return@apply - } - - known() - validated = true - } - - fun isValid(): Boolean = - try { - validate() - true - } catch (e: PreludeInvalidDataException) { - false - } - - /** - * Returns a score indicating how many valid values are contained in this object - * recursively. - * - * Used for best match union deserialization. - */ - @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1 - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - return other is DevicePlatform && value == other.value - } - - override fun hashCode() = value.hashCode() - - override fun toString() = value.toString() - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - return other is Signals && - appVersion == other.appVersion && - deviceId == other.deviceId && - deviceModel == other.deviceModel && - devicePlatform == other.devicePlatform && - ip == other.ip && - isTrustedUser == other.isTrustedUser && - ja4Fingerprint == other.ja4Fingerprint && - osVersion == other.osVersion && - userAgent == other.userAgent && - additionalProperties == other.additionalProperties - } - - private val hashCode: Int by lazy { - Objects.hash( - appVersion, - deviceId, - deviceModel, - devicePlatform, - ip, - isTrustedUser, - ja4Fingerprint, - osVersion, - userAgent, - additionalProperties, - ) - } - - override fun hashCode(): Int = hashCode - - override fun toString() = - "Signals{appVersion=$appVersion, deviceId=$deviceId, deviceModel=$deviceModel, devicePlatform=$devicePlatform, ip=$ip, isTrustedUser=$isTrustedUser, ja4Fingerprint=$ja4Fingerprint, osVersion=$osVersion, userAgent=$userAgent, additionalProperties=$additionalProperties}" - } - override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -2041,20 +1280,18 @@ private constructor( return other is Feedback && target == other.target && type == other.type && - dispatchId == other.dispatchId && metadata == other.metadata && - signals == other.signals && additionalProperties == other.additionalProperties } private val hashCode: Int by lazy { - Objects.hash(target, type, dispatchId, metadata, signals, additionalProperties) + Objects.hash(target, type, metadata, additionalProperties) } override fun hashCode(): Int = hashCode override fun toString() = - "Feedback{target=$target, type=$type, dispatchId=$dispatchId, metadata=$metadata, signals=$signals, additionalProperties=$additionalProperties}" + "Feedback{target=$target, type=$type, metadata=$metadata, additionalProperties=$additionalProperties}" } override fun equals(other: Any?): Boolean { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/LookupServiceAsync.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/LookupServiceAsync.kt index a4677874..45b3e96a 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/LookupServiceAsync.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/LookupServiceAsync.kt @@ -10,6 +10,10 @@ import so.prelude.sdk.core.http.HttpResponseFor import so.prelude.sdk.models.LookupLookupParams import so.prelude.sdk.models.LookupLookupResponse +/** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ interface LookupServiceAsync { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/LookupServiceAsyncImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/LookupServiceAsyncImpl.kt index 2d1049ee..b8b03e0c 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/LookupServiceAsyncImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/LookupServiceAsyncImpl.kt @@ -21,6 +21,10 @@ import so.prelude.sdk.core.prepareAsync import so.prelude.sdk.models.LookupLookupParams import so.prelude.sdk.models.LookupLookupResponse +/** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ class LookupServiceAsyncImpl internal constructor(private val clientOptions: ClientOptions) : LookupServiceAsync { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/NotifyServiceAsync.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/NotifyServiceAsync.kt index d9171a53..a5dc5fb9 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/NotifyServiceAsync.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/NotifyServiceAsync.kt @@ -22,6 +22,7 @@ import so.prelude.sdk.models.NotifySendBatchResponse import so.prelude.sdk.models.NotifySendParams import so.prelude.sdk.models.NotifySendResponse +/** Send transactional and marketing messages with compliance enforcement. */ interface NotifyServiceAsync { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/NotifyServiceAsyncImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/NotifyServiceAsyncImpl.kt index 2e330ccf..b6eedef3 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/NotifyServiceAsyncImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/NotifyServiceAsyncImpl.kt @@ -34,6 +34,7 @@ import so.prelude.sdk.models.NotifySendBatchResponse import so.prelude.sdk.models.NotifySendParams import so.prelude.sdk.models.NotifySendResponse +/** Send transactional and marketing messages with compliance enforcement. */ class NotifyServiceAsyncImpl internal constructor(private val clientOptions: ClientOptions) : NotifyServiceAsync { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsync.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsync.kt index 2e3e5680..205bb427 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsync.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsync.kt @@ -10,6 +10,7 @@ import so.prelude.sdk.core.http.HttpResponseFor import so.prelude.sdk.models.TransactionalSendParams import so.prelude.sdk.models.TransactionalSendResponse +/** Send transactional messages (deprecated - use Notify API instead). */ interface TransactionalServiceAsync { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsyncImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsyncImpl.kt index 48676901..72e6952d 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsyncImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsyncImpl.kt @@ -20,6 +20,7 @@ import so.prelude.sdk.core.prepareAsync import so.prelude.sdk.models.TransactionalSendParams import so.prelude.sdk.models.TransactionalSendResponse +/** Send transactional messages (deprecated - use Notify API instead). */ class TransactionalServiceAsyncImpl internal constructor(private val clientOptions: ClientOptions) : TransactionalServiceAsync { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationManagementServiceAsync.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationManagementServiceAsync.kt index 3b2b126e..9ffd8995 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationManagementServiceAsync.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationManagementServiceAsync.kt @@ -18,6 +18,7 @@ import so.prelude.sdk.models.VerificationManagementSetPhoneNumberResponse import so.prelude.sdk.models.VerificationManagementSubmitSenderIdParams import so.prelude.sdk.models.VerificationManagementSubmitSenderIdResponse +/** Verify phone numbers. */ interface VerificationManagementServiceAsync { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationManagementServiceAsyncImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationManagementServiceAsyncImpl.kt index da3d72e2..114fa82d 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationManagementServiceAsyncImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationManagementServiceAsyncImpl.kt @@ -30,6 +30,7 @@ import so.prelude.sdk.models.VerificationManagementSetPhoneNumberResponse import so.prelude.sdk.models.VerificationManagementSubmitSenderIdParams import so.prelude.sdk.models.VerificationManagementSubmitSenderIdResponse +/** Verify phone numbers. */ class VerificationManagementServiceAsyncImpl internal constructor(private val clientOptions: ClientOptions) : VerificationManagementServiceAsync { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationServiceAsync.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationServiceAsync.kt index 94490285..8fc8aa45 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationServiceAsync.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationServiceAsync.kt @@ -12,6 +12,7 @@ import so.prelude.sdk.models.VerificationCheckResponse import so.prelude.sdk.models.VerificationCreateParams import so.prelude.sdk.models.VerificationCreateResponse +/** Verify phone numbers. */ interface VerificationServiceAsync { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationServiceAsyncImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationServiceAsyncImpl.kt index c25e05dd..38f8fe50 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationServiceAsyncImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/VerificationServiceAsyncImpl.kt @@ -22,6 +22,7 @@ import so.prelude.sdk.models.VerificationCheckResponse import so.prelude.sdk.models.VerificationCreateParams import so.prelude.sdk.models.VerificationCreateResponse +/** Verify phone numbers. */ class VerificationServiceAsyncImpl internal constructor(private val clientOptions: ClientOptions) : VerificationServiceAsync { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/WatchServiceAsync.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/WatchServiceAsync.kt index d11b69ab..74fd6227 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/WatchServiceAsync.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/WatchServiceAsync.kt @@ -14,6 +14,7 @@ import so.prelude.sdk.models.WatchSendEventsResponse import so.prelude.sdk.models.WatchSendFeedbacksParams import so.prelude.sdk.models.WatchSendFeedbacksResponse +/** Evaluate email addresses and phone numbers for trustworthiness. */ interface WatchServiceAsync { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/WatchServiceAsyncImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/WatchServiceAsyncImpl.kt index 3279d5c0..10f44729 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/WatchServiceAsyncImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/async/WatchServiceAsyncImpl.kt @@ -24,6 +24,7 @@ import so.prelude.sdk.models.WatchSendEventsResponse import so.prelude.sdk.models.WatchSendFeedbacksParams import so.prelude.sdk.models.WatchSendFeedbacksResponse +/** Evaluate email addresses and phone numbers for trustworthiness. */ class WatchServiceAsyncImpl internal constructor(private val clientOptions: ClientOptions) : WatchServiceAsync { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/LookupService.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/LookupService.kt index 4e24ea9c..49936709 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/LookupService.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/LookupService.kt @@ -10,6 +10,10 @@ import so.prelude.sdk.core.http.HttpResponseFor import so.prelude.sdk.models.LookupLookupParams import so.prelude.sdk.models.LookupLookupResponse +/** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ interface LookupService { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/LookupServiceImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/LookupServiceImpl.kt index c0e99d0a..3de339ef 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/LookupServiceImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/LookupServiceImpl.kt @@ -20,6 +20,10 @@ import so.prelude.sdk.core.prepare import so.prelude.sdk.models.LookupLookupParams import so.prelude.sdk.models.LookupLookupResponse +/** + * Retrieve detailed information about a phone number including carrier data, line type, and + * portability status. + */ class LookupServiceImpl internal constructor(private val clientOptions: ClientOptions) : LookupService { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/NotifyService.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/NotifyService.kt index 4a1a2f8c..d0ad6728 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/NotifyService.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/NotifyService.kt @@ -22,6 +22,7 @@ import so.prelude.sdk.models.NotifySendBatchResponse import so.prelude.sdk.models.NotifySendParams import so.prelude.sdk.models.NotifySendResponse +/** Send transactional and marketing messages with compliance enforcement. */ interface NotifyService { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/NotifyServiceImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/NotifyServiceImpl.kt index 637ed679..7bb132ad 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/NotifyServiceImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/NotifyServiceImpl.kt @@ -33,6 +33,7 @@ import so.prelude.sdk.models.NotifySendBatchResponse import so.prelude.sdk.models.NotifySendParams import so.prelude.sdk.models.NotifySendResponse +/** Send transactional and marketing messages with compliance enforcement. */ class NotifyServiceImpl internal constructor(private val clientOptions: ClientOptions) : NotifyService { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/TransactionalService.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/TransactionalService.kt index a37facbc..b5d89299 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/TransactionalService.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/TransactionalService.kt @@ -10,6 +10,7 @@ import so.prelude.sdk.core.http.HttpResponseFor import so.prelude.sdk.models.TransactionalSendParams import so.prelude.sdk.models.TransactionalSendResponse +/** Send transactional messages (deprecated - use Notify API instead). */ interface TransactionalService { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/TransactionalServiceImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/TransactionalServiceImpl.kt index c63124f2..782bdca0 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/TransactionalServiceImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/TransactionalServiceImpl.kt @@ -19,6 +19,7 @@ import so.prelude.sdk.core.prepare import so.prelude.sdk.models.TransactionalSendParams import so.prelude.sdk.models.TransactionalSendResponse +/** Send transactional messages (deprecated - use Notify API instead). */ class TransactionalServiceImpl internal constructor(private val clientOptions: ClientOptions) : TransactionalService { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationManagementService.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationManagementService.kt index 3a018436..a0a546bc 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationManagementService.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationManagementService.kt @@ -18,6 +18,7 @@ import so.prelude.sdk.models.VerificationManagementSetPhoneNumberResponse import so.prelude.sdk.models.VerificationManagementSubmitSenderIdParams import so.prelude.sdk.models.VerificationManagementSubmitSenderIdResponse +/** Verify phone numbers. */ interface VerificationManagementService { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationManagementServiceImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationManagementServiceImpl.kt index 380ea4fb..9f359532 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationManagementServiceImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationManagementServiceImpl.kt @@ -29,6 +29,7 @@ import so.prelude.sdk.models.VerificationManagementSetPhoneNumberResponse import so.prelude.sdk.models.VerificationManagementSubmitSenderIdParams import so.prelude.sdk.models.VerificationManagementSubmitSenderIdResponse +/** Verify phone numbers. */ class VerificationManagementServiceImpl internal constructor(private val clientOptions: ClientOptions) : VerificationManagementService { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationService.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationService.kt index dbbfde74..f36b24c5 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationService.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationService.kt @@ -12,6 +12,7 @@ import so.prelude.sdk.models.VerificationCheckResponse import so.prelude.sdk.models.VerificationCreateParams import so.prelude.sdk.models.VerificationCreateResponse +/** Verify phone numbers. */ interface VerificationService { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationServiceImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationServiceImpl.kt index 1be4fd08..c9bc3c91 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationServiceImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/VerificationServiceImpl.kt @@ -21,6 +21,7 @@ import so.prelude.sdk.models.VerificationCheckResponse import so.prelude.sdk.models.VerificationCreateParams import so.prelude.sdk.models.VerificationCreateResponse +/** Verify phone numbers. */ class VerificationServiceImpl internal constructor(private val clientOptions: ClientOptions) : VerificationService { diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/WatchService.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/WatchService.kt index 0b7a2f70..442b2a2b 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/WatchService.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/WatchService.kt @@ -14,6 +14,7 @@ import so.prelude.sdk.models.WatchSendEventsResponse import so.prelude.sdk.models.WatchSendFeedbacksParams import so.prelude.sdk.models.WatchSendFeedbacksResponse +/** Evaluate email addresses and phone numbers for trustworthiness. */ interface WatchService { /** diff --git a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/WatchServiceImpl.kt b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/WatchServiceImpl.kt index 52801041..5476a500 100644 --- a/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/WatchServiceImpl.kt +++ b/prelude-java-core/src/main/kotlin/so/prelude/sdk/services/blocking/WatchServiceImpl.kt @@ -23,6 +23,7 @@ import so.prelude.sdk.models.WatchSendEventsResponse import so.prelude.sdk.models.WatchSendFeedbacksParams import so.prelude.sdk.models.WatchSendFeedbacksResponse +/** Evaluate email addresses and phone numbers for trustworthiness. */ class WatchServiceImpl internal constructor(private val clientOptions: ClientOptions) : WatchService { diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/TestServerExtension.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/TestServerExtension.kt index 81ad71e7..e799ee1b 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/TestServerExtension.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/TestServerExtension.kt @@ -15,25 +15,12 @@ class TestServerExtension : BeforeAllCallback, ExecutionCondition { } catch (e: Exception) { throw RuntimeException( """ - The test suite will not run without a mock Prism server running against your OpenAPI spec. + The test suite will not run without a mock server running against your OpenAPI spec. You can set the environment variable `SKIP_MOCK_TESTS` to `true` to skip running any tests that require the mock server. - To fix: - - 1. Install Prism (requires Node 16+): - - With npm: - $ npm install -g @stoplight/prism-cli - - With yarn: - $ yarn global add @stoplight/prism-cli - - 2. Run the mock server - - To run the server, pass in the path of your OpenAPI spec to the prism command: - $ prism mock path/to/your.openapi.yml + To fix run `./scripts/mock` in a separate terminal. """ .trimIndent(), e, diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/ObjectMappersTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/ObjectMappersTest.kt index 04dbd3e5..0cb6145d 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/ObjectMappersTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/ObjectMappersTest.kt @@ -3,12 +3,14 @@ package so.prelude.sdk.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/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/HttpRequestBodiesTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/HttpRequestBodiesTest.kt new file mode 100644 index 00000000..78082e7b --- /dev/null +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/HttpRequestBodiesTest.kt @@ -0,0 +1,729 @@ +// File generated from our OpenAPI spec by Stainless. + +package so.prelude.sdk.core.http + +import java.io.ByteArrayOutputStream +import java.io.InputStream +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import so.prelude.sdk.core.MultipartField +import so.prelude.sdk.core.jsonMapper + +internal class HttpRequestBodiesTest { + + @Test + fun multipartFormData_serializesFieldWithFilename() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "file" to + MultipartField.builder() + .value("hello") + .filename("hello.txt") + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(output.size().toLong()).isEqualTo(body.contentLength()) + val boundary = body.contentType()!!.substringAfter("multipart/form-data; boundary=") + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="file"; filename="hello.txt" + |Content-Type: text/plain + | + |hello + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesFieldWithoutFilename() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "field" to + MultipartField.builder() + .value("value") + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(output.size().toLong()).isEqualTo(body.contentLength()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="field" + |Content-Type: text/plain + | + |value + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesInputStream() { + // Use `.buffered()` to get a non-ByteArrayInputStream, which hits the non-repeatable code + // path. + val inputStream = "stream content".byteInputStream().buffered() + val body = + multipartFormData( + jsonMapper(), + mapOf( + "data" to + MultipartField.builder() + .value(inputStream) + .contentType("application/octet-stream") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isFalse() + assertThat(body.contentLength()).isEqualTo(-1L) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="data" + |Content-Type: application/octet-stream + | + |stream content + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesByteArray() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "binary" to + MultipartField.builder() + .value("abc".toByteArray()) + .contentType("application/octet-stream") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="binary" + |Content-Type: application/octet-stream + | + |abc + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesBooleanValue() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "flag" to + MultipartField.builder() + .value(true) + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="flag" + |Content-Type: text/plain + | + |true + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesNumberValue() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "count" to + MultipartField.builder().value(42).contentType("text/plain").build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="count" + |Content-Type: text/plain + | + |42 + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesNullValueAsNoParts() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "present" to + MultipartField.builder() + .value("yes") + .contentType("text/plain") + .build(), + "absent" to + MultipartField.builder() + .value(null as String?) + .contentType("text/plain") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="present" + |Content-Type: text/plain + | + |yes + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesArray() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "items" to + MultipartField.builder>() + .value(listOf("alpha", "beta", "gamma")) + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="items" + |Content-Type: text/plain + | + |alpha,beta,gamma + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesObjectAsNestedParts() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "meta" to + MultipartField.builder>() + .value(mapOf("key1" to "val1", "key2" to "val2")) + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="meta[key1]" + |Content-Type: text/plain + | + |val1 + |--$boundary + |Content-Disposition: form-data; name="meta[key2]" + |Content-Type: text/plain + | + |val2 + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesMultipleFields() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "name" to + MultipartField.builder() + .value("Alice") + .contentType("text/plain") + .build(), + "age" to + MultipartField.builder().value(30).contentType("text/plain").build(), + "file" to + MultipartField.builder() + .value("file contents") + .filename("doc.txt") + .contentType("text/plain") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="name" + |Content-Type: text/plain + | + |Alice + |--$boundary + |Content-Disposition: form-data; name="age" + |Content-Type: text/plain + | + |30 + |--$boundary + |Content-Disposition: form-data; name="file"; filename="doc.txt" + |Content-Type: text/plain + | + |file contents + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_quotesSpecialCharactersInNameAndFilename() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "field\nname" to + MultipartField.builder() + .value("value") + .filename("file\r\"name.txt") + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="field%0Aname"; filename="file%0D%22name.txt" + |Content-Type: text/plain + | + |value + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_writeIsRepeatable() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "field" to + MultipartField.builder() + .value("repeatable") + .contentType("text/plain") + .build() + ), + ) + + val output1 = ByteArrayOutputStream() + body.writeTo(output1) + val output2 = ByteArrayOutputStream() + body.writeTo(output2) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output1.size().toLong()) + val boundary = boundary(body) + val expected = + """ + |--$boundary + |Content-Disposition: form-data; name="field" + |Content-Type: text/plain + | + |repeatable + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + assertThat(output1.toString("UTF-8")).isEqualTo(expected) + assertThat(output2.toString("UTF-8")).isEqualTo(expected) + } + + @Test + fun multipartFormData_serializesByteArrayInputStream() { + // ByteArrayInputStream is specifically handled as repeatable with known content length. + val inputStream = "byte array stream".byteInputStream() + val body = + multipartFormData( + jsonMapper(), + mapOf( + "data" to + MultipartField.builder() + .value(inputStream) + .contentType("application/octet-stream") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="data" + |Content-Type: application/octet-stream + | + |byte array stream + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesInputStreamWithFilename() { + // Use `.buffered()` to get a non-ByteArrayInputStream, which hits the non-repeatable code + // path. + val inputStream = "file data".byteInputStream().buffered() + val body = + multipartFormData( + jsonMapper(), + mapOf( + "upload" to + MultipartField.builder() + .value(inputStream) + .filename("upload.bin") + .contentType("application/octet-stream") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isFalse() + assertThat(body.contentLength()).isEqualTo(-1L) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="upload"; filename="upload.bin" + |Content-Type: application/octet-stream + | + |file data + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesNestedArrayInObject() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "data" to + MultipartField.builder>>() + .value(mapOf("tags" to listOf("a", "b"))) + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="data[tags]" + |Content-Type: text/plain + | + |a,b + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_contentLengthIsUnknownWhenInputStreamPresent() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "text" to + MultipartField.builder() + .value("hello") + .contentType("text/plain") + .build(), + "stream" to + MultipartField.builder() + // Use `.buffered()` to get a non-ByteArrayInputStream, which hits the + // non-repeatable code path. + .value("data".byteInputStream().buffered()) + .contentType("application/octet-stream") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isFalse() + assertThat(body.contentLength()).isEqualTo(-1L) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="text" + |Content-Type: text/plain + | + |hello + |--$boundary + |Content-Disposition: form-data; name="stream" + |Content-Type: application/octet-stream + | + |data + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesEmptyArray() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "required" to + MultipartField.builder() + .value("present") + .contentType("text/plain") + .build(), + "items" to + MultipartField.builder>() + .value(emptyList()) + .contentType("text/plain") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="required" + |Content-Type: text/plain + | + |present + |--$boundary + |Content-Disposition: form-data; name="items" + |Content-Type: text/plain + | + | + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesEmptyObject() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "required" to + MultipartField.builder() + .value("present") + .contentType("text/plain") + .build(), + "meta" to + MultipartField.builder>() + .value(emptyMap()) + .contentType("text/plain") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="required" + |Content-Type: text/plain + | + |present + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + private fun boundary(body: HttpRequestBody): String = + body.contentType()!!.substringAfter("multipart/form-data; boundary=") +} diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/HttpRequestTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/HttpRequestTest.kt new file mode 100644 index 00000000..e96c6319 --- /dev/null +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/HttpRequestTest.kt @@ -0,0 +1,110 @@ +package so.prelude.sdk.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/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/RetryingHttpClientTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/RetryingHttpClientTest.kt index 7d331437..c08003d2 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/RetryingHttpClientTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/core/http/RetryingHttpClientTest.kt @@ -1,6 +1,17 @@ +// File generated from our OpenAPI spec by Stainless. + package so.prelude.sdk.core.http -import com.github.tomakehurst.wiremock.client.WireMock.* +import com.github.tomakehurst.wiremock.client.WireMock.equalTo +import com.github.tomakehurst.wiremock.client.WireMock.matching +import com.github.tomakehurst.wiremock.client.WireMock.ok +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.resetAllScenarios +import com.github.tomakehurst.wiremock.client.WireMock.serviceUnavailable +import com.github.tomakehurst.wiremock.client.WireMock.stubFor +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.github.tomakehurst.wiremock.client.WireMock.verify import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo import com.github.tomakehurst.wiremock.junit5.WireMockTest import com.github.tomakehurst.wiremock.stubbing.Scenario diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendBatchParamsTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendBatchParamsTest.kt index 13cefdb2..10a6f9c8 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendBatchParamsTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendBatchParamsTest.kt @@ -17,6 +17,12 @@ internal class NotifySendBatchParamsTest { .addTo("+15551234567") .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("campaign-12345") + .document( + NotifySendBatchParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt(OffsetDateTime.parse("2025-12-25T18:00:00Z")) .from("from") .locale("el-GR") @@ -40,6 +46,12 @@ internal class NotifySendBatchParamsTest { .addTo("+15551234567") .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("campaign-12345") + .document( + NotifySendBatchParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt(OffsetDateTime.parse("2025-12-25T18:00:00Z")) .from("from") .locale("el-GR") @@ -59,6 +71,13 @@ internal class NotifySendBatchParamsTest { assertThat(body.to()).containsExactly("+33612345678", "+15551234567") assertThat(body.callbackUrl()).contains("https://your-app.com/webhooks/notify") assertThat(body.correlationId()).contains("campaign-12345") + assertThat(body.document()) + .contains( + NotifySendBatchParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) assertThat(body.expiresAt()).contains(OffsetDateTime.parse("2025-12-25T18:00:00Z")) assertThat(body.from()).contains("from") assertThat(body.locale()).contains("el-GR") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendBatchResponseTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendBatchResponseTest.kt index 53911bc0..6f3c6aea 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendBatchResponseTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendBatchResponseTest.kt @@ -33,6 +33,8 @@ internal class NotifySendBatchResponseTest { .id("tx_01k8ap1btqf5r9fq2c8ax5fhc9") .correlationId("correlation_id") .createdAt(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) + .encoding(NotifySendBatchResponse.Result.Message.Encoding.GSM_7) + .estimatedSegmentCount(1L) .expiresAt(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) .from("YourBrand") .locale("locale") @@ -73,6 +75,8 @@ internal class NotifySendBatchResponseTest { .id("tx_01k8ap1btqf5r9fq2c8ax5fhc9") .correlationId("correlation_id") .createdAt(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) + .encoding(NotifySendBatchResponse.Result.Message.Encoding.GSM_7) + .estimatedSegmentCount(1L) .expiresAt(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) .from("YourBrand") .locale("locale") @@ -118,6 +122,8 @@ internal class NotifySendBatchResponseTest { .id("tx_01k8ap1btqf5r9fq2c8ax5fhc9") .correlationId("correlation_id") .createdAt(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) + .encoding(NotifySendBatchResponse.Result.Message.Encoding.GSM_7) + .estimatedSegmentCount(1L) .expiresAt(OffsetDateTime.parse("2019-12-27T18:11:19.117Z")) .from("YourBrand") .locale("locale") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendParamsTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendParamsTest.kt index 2289f5f9..4f6ce1f6 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendParamsTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendParamsTest.kt @@ -16,6 +16,12 @@ internal class NotifySendParamsTest { .to("+33612345678") .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("order-12345") + .document( + NotifySendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt(OffsetDateTime.parse("2025-12-25T18:00:00Z")) .from("from") .locale("el-GR") @@ -38,6 +44,12 @@ internal class NotifySendParamsTest { .to("+33612345678") .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("order-12345") + .document( + NotifySendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt(OffsetDateTime.parse("2025-12-25T18:00:00Z")) .from("from") .locale("el-GR") @@ -57,6 +69,13 @@ internal class NotifySendParamsTest { assertThat(body.to()).isEqualTo("+33612345678") assertThat(body.callbackUrl()).contains("https://your-app.com/webhooks/notify") assertThat(body.correlationId()).contains("order-12345") + assertThat(body.document()) + .contains( + NotifySendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) assertThat(body.expiresAt()).contains(OffsetDateTime.parse("2025-12-25T18:00:00Z")) assertThat(body.from()).contains("from") assertThat(body.locale()).contains("el-GR") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendResponseTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendResponseTest.kt index f67eb5a9..eb57d883 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendResponseTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/NotifySendResponseTest.kt @@ -28,6 +28,8 @@ internal class NotifySendResponseTest { ) .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("order-12345") + .encoding(NotifySendResponse.Encoding.GSM_7) + .estimatedSegmentCount(1L) .from("YourBrand") .scheduleAt(OffsetDateTime.parse("2025-12-25T08:00:00-05:00")) .build() @@ -49,6 +51,8 @@ internal class NotifySendResponseTest { assertThat(notifySendResponse.callbackUrl()) .contains("https://your-app.com/webhooks/notify") assertThat(notifySendResponse.correlationId()).contains("order-12345") + assertThat(notifySendResponse.encoding()).contains(NotifySendResponse.Encoding.GSM_7) + assertThat(notifySendResponse.estimatedSegmentCount()).contains(1L) assertThat(notifySendResponse.from()).contains("YourBrand") assertThat(notifySendResponse.scheduleAt()) .contains(OffsetDateTime.parse("2025-12-25T08:00:00-05:00")) @@ -72,6 +76,8 @@ internal class NotifySendResponseTest { ) .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("order-12345") + .encoding(NotifySendResponse.Encoding.GSM_7) + .estimatedSegmentCount(1L) .from("YourBrand") .scheduleAt(OffsetDateTime.parse("2025-12-25T08:00:00-05:00")) .build() diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/TransactionalSendParamsTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/TransactionalSendParamsTest.kt index d5d08754..f0e3c8e4 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/TransactionalSendParamsTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/TransactionalSendParamsTest.kt @@ -15,6 +15,12 @@ internal class TransactionalSendParamsTest { .to("+30123456789") .callbackUrl("callback_url") .correlationId("correlation_id") + .document( + TransactionalSendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt("expires_at") .from("from") .locale("el-GR") @@ -35,6 +41,12 @@ internal class TransactionalSendParamsTest { .to("+30123456789") .callbackUrl("callback_url") .correlationId("correlation_id") + .document( + TransactionalSendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt("expires_at") .from("from") .locale("el-GR") @@ -52,6 +64,13 @@ internal class TransactionalSendParamsTest { assertThat(body.to()).isEqualTo("+30123456789") assertThat(body.callbackUrl()).contains("callback_url") assertThat(body.correlationId()).contains("correlation_id") + assertThat(body.document()) + .contains( + TransactionalSendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) assertThat(body.expiresAt()).contains("expires_at") assertThat(body.from()).contains("from") assertThat(body.locale()).contains("el-GR") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/VerificationCreateParamsTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/VerificationCreateParamsTest.kt index e57e0770..64407b5f 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/VerificationCreateParamsTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/VerificationCreateParamsTest.kt @@ -32,7 +32,6 @@ internal class VerificationCreateParamsTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel(VerificationCreateParams.Options.PreferredChannel.SMS) @@ -51,7 +50,7 @@ internal class VerificationCreateParamsTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -92,7 +91,6 @@ internal class VerificationCreateParamsTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel(VerificationCreateParams.Options.PreferredChannel.SMS) @@ -111,7 +109,7 @@ internal class VerificationCreateParamsTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -148,7 +146,6 @@ internal class VerificationCreateParamsTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel(VerificationCreateParams.Options.PreferredChannel.SMS) @@ -168,7 +165,7 @@ internal class VerificationCreateParamsTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchPredictParamsTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchPredictParamsTest.kt index a3f23b37..52df9bbb 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchPredictParamsTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchPredictParamsTest.kt @@ -24,7 +24,7 @@ internal class WatchPredictParamsTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(WatchPredictParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -56,7 +56,7 @@ internal class WatchPredictParamsTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(WatchPredictParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -86,7 +86,7 @@ internal class WatchPredictParamsTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(WatchPredictParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchPredictResponseTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchPredictResponseTest.kt index 52cc6b26..a3ec651d 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchPredictResponseTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchPredictResponseTest.kt @@ -3,6 +3,7 @@ package so.prelude.sdk.models import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import kotlin.jvm.optionals.getOrNull import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import so.prelude.sdk.core.jsonMapper @@ -16,6 +17,8 @@ internal class WatchPredictResponseTest { .id("prd_01jc0t6fwwfgfsq1md24mhyztj") .prediction(WatchPredictResponse.Prediction.LEGITIMATE) .requestId("3d19215e-2991-4a05-a41a-527314e6ff6a") + .addRiskFactor(WatchPredictResponse.RiskFactor.SUSPICIOUS_IP_ADDRESS) + .addRiskFactor(WatchPredictResponse.RiskFactor.FRAUD_DATABASE) .build() assertThat(watchPredictResponse.id()).isEqualTo("prd_01jc0t6fwwfgfsq1md24mhyztj") @@ -23,6 +26,11 @@ internal class WatchPredictResponseTest { .isEqualTo(WatchPredictResponse.Prediction.LEGITIMATE) assertThat(watchPredictResponse.requestId()) .isEqualTo("3d19215e-2991-4a05-a41a-527314e6ff6a") + assertThat(watchPredictResponse.riskFactors().getOrNull()) + .containsExactly( + WatchPredictResponse.RiskFactor.SUSPICIOUS_IP_ADDRESS, + WatchPredictResponse.RiskFactor.FRAUD_DATABASE, + ) } @Test @@ -33,6 +41,8 @@ internal class WatchPredictResponseTest { .id("prd_01jc0t6fwwfgfsq1md24mhyztj") .prediction(WatchPredictResponse.Prediction.LEGITIMATE) .requestId("3d19215e-2991-4a05-a41a-527314e6ff6a") + .addRiskFactor(WatchPredictResponse.RiskFactor.SUSPICIOUS_IP_ADDRESS) + .addRiskFactor(WatchPredictResponse.RiskFactor.FRAUD_DATABASE) .build() val roundtrippedWatchPredictResponse = diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchSendFeedbacksParamsTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchSendFeedbacksParamsTest.kt index e8337885..ac936f2e 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchSendFeedbacksParamsTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/models/WatchSendFeedbacksParamsTest.kt @@ -19,29 +19,11 @@ internal class WatchSendFeedbacksParamsTest { .build() ) .type(WatchSendFeedbacksParams.Feedback.Type.VERIFICATION_STARTED) - .dispatchId("123e4567-e89b-12d3-a456-426614174000") .metadata( WatchSendFeedbacksParams.Feedback.Metadata.builder() .correlationId("correlation_id") .build() ) - .signals( - WatchSendFeedbacksParams.Feedback.Signals.builder() - .appVersion("1.2.34") - .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") - .deviceModel("iPhone17,2") - .devicePlatform( - WatchSendFeedbacksParams.Feedback.Signals.DevicePlatform.IOS - ) - .ip("192.0.2.1") - .isTrustedUser(false) - .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") - .osVersion("18.0.1") - .userAgent( - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" - ) - .build() - ) .build() ) .build() @@ -60,29 +42,11 @@ internal class WatchSendFeedbacksParamsTest { .build() ) .type(WatchSendFeedbacksParams.Feedback.Type.VERIFICATION_STARTED) - .dispatchId("123e4567-e89b-12d3-a456-426614174000") .metadata( WatchSendFeedbacksParams.Feedback.Metadata.builder() .correlationId("correlation_id") .build() ) - .signals( - WatchSendFeedbacksParams.Feedback.Signals.builder() - .appVersion("1.2.34") - .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") - .deviceModel("iPhone17,2") - .devicePlatform( - WatchSendFeedbacksParams.Feedback.Signals.DevicePlatform.IOS - ) - .ip("192.0.2.1") - .isTrustedUser(false) - .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") - .osVersion("18.0.1") - .userAgent( - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" - ) - .build() - ) .build() ) .build() @@ -99,29 +63,11 @@ internal class WatchSendFeedbacksParamsTest { .build() ) .type(WatchSendFeedbacksParams.Feedback.Type.VERIFICATION_STARTED) - .dispatchId("123e4567-e89b-12d3-a456-426614174000") .metadata( WatchSendFeedbacksParams.Feedback.Metadata.builder() .correlationId("correlation_id") .build() ) - .signals( - WatchSendFeedbacksParams.Feedback.Signals.builder() - .appVersion("1.2.34") - .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") - .deviceModel("iPhone17,2") - .devicePlatform( - WatchSendFeedbacksParams.Feedback.Signals.DevicePlatform.IOS - ) - .ip("192.0.2.1") - .isTrustedUser(false) - .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") - .osVersion("18.0.1") - .userAgent( - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" - ) - .build() - ) .build() ) } diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/ErrorHandlingTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/ErrorHandlingTest.kt index cc337ac3..3662bc36 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/ErrorHandlingTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/ErrorHandlingTest.kt @@ -98,7 +98,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -119,7 +118,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -177,7 +176,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -198,7 +196,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -256,7 +254,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -277,7 +274,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -335,7 +332,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -356,7 +352,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -414,7 +410,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -435,7 +430,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -493,7 +488,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -514,7 +508,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -572,7 +566,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -593,7 +586,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -651,7 +644,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -672,7 +664,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -730,7 +722,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -751,7 +742,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -809,7 +800,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -830,7 +820,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -888,7 +878,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -909,7 +898,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -967,7 +956,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -988,7 +976,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -1046,7 +1034,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -1067,7 +1054,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -1125,7 +1112,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -1146,7 +1132,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -1204,7 +1190,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -1225,7 +1210,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -1283,7 +1268,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -1304,7 +1288,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -1360,7 +1344,6 @@ internal class ErrorHandlingTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel( @@ -1381,7 +1364,7 @@ internal class ErrorHandlingTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/ServiceParamsTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/ServiceParamsTest.kt index 48abe370..6b0e902a 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/ServiceParamsTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/ServiceParamsTest.kt @@ -36,7 +36,7 @@ internal class ServiceParamsTest { .build() } - @Disabled("Prism doesn't support callbacks yet") + @Disabled("Mock server doesn't support callbacks yet") @Test fun create() { val verificationService = client.verification() @@ -69,7 +69,6 @@ internal class ServiceParamsTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel(VerificationCreateParams.Options.PreferredChannel.SMS) @@ -88,7 +87,7 @@ internal class ServiceParamsTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/NotifyServiceAsyncTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/NotifyServiceAsyncTest.kt index da5d2fe7..0bd60e2d 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/NotifyServiceAsyncTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/NotifyServiceAsyncTest.kt @@ -119,7 +119,7 @@ internal class NotifyServiceAsyncTest { response.validate() } - @Disabled("Prism doesn't support callbacks yet") + @Disabled("Mock server doesn't support callbacks yet") @Test fun send() { val client = @@ -136,6 +136,12 @@ internal class NotifyServiceAsyncTest { .to("+33612345678") .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("order-12345") + .document( + NotifySendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt(OffsetDateTime.parse("2025-12-25T18:00:00Z")) .from("from") .locale("el-GR") @@ -171,6 +177,12 @@ internal class NotifyServiceAsyncTest { .addTo("+15551234567") .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("campaign-12345") + .document( + NotifySendBatchParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt(OffsetDateTime.parse("2025-12-25T18:00:00Z")) .from("from") .locale("el-GR") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsyncTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsyncTest.kt index 414fbe9b..01349dff 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsyncTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/TransactionalServiceAsyncTest.kt @@ -13,7 +13,7 @@ import so.prelude.sdk.models.TransactionalSendParams @ExtendWith(TestServerExtension::class) internal class TransactionalServiceAsyncTest { - @Disabled("Prism doesn't support callbacks yet") + @Disabled("Mock server doesn't support callbacks yet") @Test fun send() { val client = @@ -30,6 +30,12 @@ internal class TransactionalServiceAsyncTest { .to("+30123456789") .callbackUrl("callback_url") .correlationId("correlation_id") + .document( + TransactionalSendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt("expires_at") .from("from") .locale("el-GR") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/VerificationServiceAsyncTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/VerificationServiceAsyncTest.kt index b241e64c..1f75d14b 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/VerificationServiceAsyncTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/VerificationServiceAsyncTest.kt @@ -14,7 +14,7 @@ import so.prelude.sdk.models.VerificationCreateParams @ExtendWith(TestServerExtension::class) internal class VerificationServiceAsyncTest { - @Disabled("Prism doesn't support callbacks yet") + @Disabled("Mock server doesn't support callbacks yet") @Test fun create() { val client = @@ -52,7 +52,6 @@ internal class VerificationServiceAsyncTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel(VerificationCreateParams.Options.PreferredChannel.SMS) @@ -71,7 +70,7 @@ internal class VerificationServiceAsyncTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/WatchServiceAsyncTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/WatchServiceAsyncTest.kt index 8e2388cd..52d14fe4 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/WatchServiceAsyncTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/async/WatchServiceAsyncTest.kt @@ -43,7 +43,7 @@ internal class WatchServiceAsyncTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(WatchPredictParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -113,29 +113,11 @@ internal class WatchServiceAsyncTest { .build() ) .type(WatchSendFeedbacksParams.Feedback.Type.VERIFICATION_STARTED) - .dispatchId("123e4567-e89b-12d3-a456-426614174000") .metadata( WatchSendFeedbacksParams.Feedback.Metadata.builder() .correlationId("correlation_id") .build() ) - .signals( - WatchSendFeedbacksParams.Feedback.Signals.builder() - .appVersion("1.2.34") - .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") - .deviceModel("iPhone17,2") - .devicePlatform( - WatchSendFeedbacksParams.Feedback.Signals.DevicePlatform.IOS - ) - .ip("192.0.2.1") - .isTrustedUser(false) - .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") - .osVersion("18.0.1") - .userAgent( - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" - ) - .build() - ) .build() ) .build() diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/NotifyServiceTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/NotifyServiceTest.kt index 714aba7d..d9e6d06e 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/NotifyServiceTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/NotifyServiceTest.kt @@ -114,7 +114,7 @@ internal class NotifyServiceTest { response.validate() } - @Disabled("Prism doesn't support callbacks yet") + @Disabled("Mock server doesn't support callbacks yet") @Test fun send() { val client = @@ -131,6 +131,12 @@ internal class NotifyServiceTest { .to("+33612345678") .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("order-12345") + .document( + NotifySendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt(OffsetDateTime.parse("2025-12-25T18:00:00Z")) .from("from") .locale("el-GR") @@ -165,6 +171,12 @@ internal class NotifyServiceTest { .addTo("+15551234567") .callbackUrl("https://your-app.com/webhooks/notify") .correlationId("campaign-12345") + .document( + NotifySendBatchParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt(OffsetDateTime.parse("2025-12-25T18:00:00Z")) .from("from") .locale("el-GR") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/TransactionalServiceTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/TransactionalServiceTest.kt index af239dfd..33dbd82b 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/TransactionalServiceTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/TransactionalServiceTest.kt @@ -13,7 +13,7 @@ import so.prelude.sdk.models.TransactionalSendParams @ExtendWith(TestServerExtension::class) internal class TransactionalServiceTest { - @Disabled("Prism doesn't support callbacks yet") + @Disabled("Mock server doesn't support callbacks yet") @Test fun send() { val client = @@ -30,6 +30,12 @@ internal class TransactionalServiceTest { .to("+30123456789") .callbackUrl("callback_url") .correlationId("correlation_id") + .document( + TransactionalSendParams.Document.builder() + .filename("invoice.pdf") + .url("https://example.com/invoice.pdf") + .build() + ) .expiresAt("expires_at") .from("from") .locale("el-GR") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/VerificationServiceTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/VerificationServiceTest.kt index 0ccf68c2..657fcd6a 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/VerificationServiceTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/VerificationServiceTest.kt @@ -14,7 +14,7 @@ import so.prelude.sdk.models.VerificationCreateParams @ExtendWith(TestServerExtension::class) internal class VerificationServiceTest { - @Disabled("Prism doesn't support callbacks yet") + @Disabled("Mock server doesn't support callbacks yet") @Test fun create() { val client = @@ -52,7 +52,6 @@ internal class VerificationServiceTest { .callbackUrl("callback_url") .codeSize(5L) .customCode("123456") - .integration(VerificationCreateParams.Options.Integration.AUTH0) .locale("el-GR") .method(VerificationCreateParams.Options.Method.AUTO) .preferredChannel(VerificationCreateParams.Options.PreferredChannel.SMS) @@ -71,7 +70,7 @@ internal class VerificationServiceTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(VerificationCreateParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") diff --git a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/WatchServiceTest.kt b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/WatchServiceTest.kt index d5e3c2bf..21dbbec3 100644 --- a/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/WatchServiceTest.kt +++ b/prelude-java-core/src/test/kotlin/so/prelude/sdk/services/blocking/WatchServiceTest.kt @@ -43,7 +43,7 @@ internal class WatchServiceTest { .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") .deviceModel("iPhone17,2") .devicePlatform(WatchPredictParams.Signals.DevicePlatform.IOS) - .ip("192.0.2.1") + .ip("203.0.113.123") .isTrustedUser(false) .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") .osVersion("18.0.1") @@ -111,29 +111,11 @@ internal class WatchServiceTest { .build() ) .type(WatchSendFeedbacksParams.Feedback.Type.VERIFICATION_STARTED) - .dispatchId("123e4567-e89b-12d3-a456-426614174000") .metadata( WatchSendFeedbacksParams.Feedback.Metadata.builder() .correlationId("correlation_id") .build() ) - .signals( - WatchSendFeedbacksParams.Feedback.Signals.builder() - .appVersion("1.2.34") - .deviceId("8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2") - .deviceModel("iPhone17,2") - .devicePlatform( - WatchSendFeedbacksParams.Feedback.Signals.DevicePlatform.IOS - ) - .ip("192.0.2.1") - .isTrustedUser(false) - .ja4Fingerprint("t13d1516h2_8daaf6152771_e5627efa2ab1") - .osVersion("18.0.1") - .userAgent( - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" - ) - .build() - ) .build() ) .build() diff --git a/prelude-java-proguard-test/build.gradle.kts b/prelude-java-proguard-test/build.gradle.kts index 397f015b..575d6982 100644 --- a/prelude-java-proguard-test/build.gradle.kts +++ b/prelude-java-proguard-test/build.gradle.kts @@ -18,8 +18,8 @@ dependencies { testImplementation(project(":prelude-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/scripts/build b/scripts/build index f4063482..16a2b00d 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/mock b/scripts/mock index 0b28f6ea..bcf3b392 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,11 +21,22 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - # Wait for server to come online + # Wait for server to come online (max 30s) echo -n "Waiting for server" + attempts=0 while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Prism server to start" + cat .prism.log + exit 1 + fi echo -n "." sleep 0.1 done diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts new file mode 100755 index 00000000..10f3c705 --- /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::"