diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 0056f0ab..ad56a78c 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "4.9.1"
+ ".": "4.10.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 17749e26..bb69a634 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 81
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-3fc1c86b4a83a16393aaf17d1fb3ac6098d30dd057ba872973b57285a7a3f0d0.yml
-openapi_spec_hash: 02a545d217b13399f311e99561f9de1d
-config_hash: 0789c3cddc625bb9712b3bded274ab6c
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-44c2e612f5d40e03f56712a4e123a193e6ea03cc64a91d0c34ee094563dafa1c.yml
+openapi_spec_hash: 40bf6b3f7992d55f1bd543f32564ea86
+config_hash: b1f6d0f43161b66d201043fcbe5c5695
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d183d7d5..39df0b76 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,35 @@
# Changelog
+## 4.10.0 (2026-03-03)
+
+Full Changelog: [v4.9.1...v4.10.0](https://github.com/trycourier/courier-java/compare/v4.9.1...v4.10.0)
+
+### Features
+
+* **client:** add connection pooling option ([b2e1bcb](https://github.com/trycourier/courier-java/commit/b2e1bcbd856bbe7d247497b2296dd8f368fdc6da))
+
+
+### Bug Fixes
+
+* **api:** restructure token add single request body, rename pathToken to token ([9577dec](https://github.com/trycourier/courier-java/commit/9577decaf5da849b9c35d2c7a6302ef2b105193e))
+* **types:** remove brand field from ElementalContent model ([cb336a3](https://github.com/trycourier/courier-java/commit/cb336a3bb81b5e2bd25483be06c45bdfd518fe2b))
+
+
+### Chores
+
+* drop apache dependency ([90094b9](https://github.com/trycourier/courier-java/commit/90094b91bb84ab5f9b01d351031431990495e2fa))
+* **internal:** expand imports ([b1b8ef3](https://github.com/trycourier/courier-java/commit/b1b8ef384a3acddc0ce6cd79cfca840e6da4c748))
+* **internal:** make `OkHttp` constructor internal ([4e39320](https://github.com/trycourier/courier-java/commit/4e393200781dd186e45a32ff8d18be7083e83b31))
+* **internal:** remove mock server code ([b63945e](https://github.com/trycourier/courier-java/commit/b63945ee56673ba3e736de9d2170b0a27e7dcd82))
+* **internal:** update `TestServerExtension` comment ([26b5aed](https://github.com/trycourier/courier-java/commit/26b5aede00f47e7a0bae77e058e36a00361060b0))
+* make `Properties` more resilient to `null` ([401bc43](https://github.com/trycourier/courier-java/commit/401bc435edc5a6798afb1c1619a0de37cdb7a9fe))
+* update mock server docs ([ff62ef0](https://github.com/trycourier/courier-java/commit/ff62ef0c2c8c232f96a3b3e5ade9c6ac22a1b248))
+
+
+### Documentation
+
+* add AUTO-GENERATED-OVERVIEW markers for README sync ([#92](https://github.com/trycourier/courier-java/issues/92)) ([4bc2f46](https://github.com/trycourier/courier-java/commit/4bc2f4661c8c7d35d72234c1f5520eec71a39bcc))
+
## 4.9.1 (2026-02-07)
Full Changelog: [v4.9.0...v4.9.1](https://github.com/trycourier/courier-java/compare/v4.9.0...v4.9.1)
diff --git a/README.md b/README.md
index 1572f135..804ce850 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@
-[](https://central.sonatype.com/artifact/com.courier/courier-java/4.9.1)
-[](https://javadoc.io/doc/com.courier/courier-java/4.9.1)
+[](https://central.sonatype.com/artifact/com.courier/courier-java/4.10.0)
+[](https://javadoc.io/doc/com.courier/courier-java/4.10.0)
@@ -14,7 +14,7 @@ It is generated with [Stainless](https://www.stainless.com/).
-The REST API documentation can be found on [www.courier.com](https://www.courier.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.courier/courier-java/4.9.1).
+The REST API documentation can be found on [www.courier.com](https://www.courier.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.courier/courier-java/4.10.0).
@@ -25,7 +25,7 @@ The REST API documentation can be found on [www.courier.com](https://www.courier
### Gradle
```kotlin
-implementation("com.courier:courier-java:4.9.1")
+implementation("com.courier:courier-java:4.10.0")
```
### Maven
@@ -34,7 +34,7 @@ implementation("com.courier:courier-java:4.9.1")
com.courier
courier-java
- 4.9.1
+ 4.10.0
```
@@ -388,6 +388,25 @@ CourierClient client = CourierOkHttpClient.builder()
.build();
```
+### Connection pooling
+
+To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods:
+
+```java
+import com.courier.client.CourierClient;
+import com.courier.client.okhttp.CourierOkHttpClient;
+import java.time.Duration;
+
+CourierClient client = CourierOkHttpClient.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 aae0be02..653fe829 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,7 @@ repositories {
allprojects {
group = "com.courier"
- version = "4.9.1" // x-release-please-version
+ version = "4.10.0" // x-release-please-version
}
subprojects {
diff --git a/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/CourierOkHttpClient.kt b/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/CourierOkHttpClient.kt
index 429e095e..73be7c55 100644
--- a/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/CourierOkHttpClient.kt
+++ b/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/CourierOkHttpClient.kt
@@ -47,6 +47,8 @@ class CourierOkHttpClient private 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
@@ -75,6 +77,46 @@ class CourierOkHttpClient private constructor() {
/** 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.
*
@@ -317,6 +359,8 @@ class CourierOkHttpClient private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
+ .maxIdleConnections(maxIdleConnections)
+ .keepAliveDuration(keepAliveDuration)
.dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
diff --git a/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/CourierOkHttpClientAsync.kt b/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/CourierOkHttpClientAsync.kt
index 2e2adb16..8a1baa2a 100644
--- a/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/CourierOkHttpClientAsync.kt
+++ b/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/CourierOkHttpClientAsync.kt
@@ -47,6 +47,8 @@ class CourierOkHttpClientAsync private 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
@@ -75,6 +77,46 @@ class CourierOkHttpClientAsync private constructor() {
/** 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.
*
@@ -317,6 +359,8 @@ class CourierOkHttpClientAsync private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
+ .maxIdleConnections(maxIdleConnections)
+ .keepAliveDuration(keepAliveDuration)
.dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
diff --git a/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/OkHttpClient.kt b/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/OkHttpClient.kt
index ca6ffdbe..86340f02 100644
--- a/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/OkHttpClient.kt
+++ b/courier-java-client-okhttp/src/main/kotlin/com/courier/client/okhttp/OkHttpClient.kt
@@ -16,11 +16,13 @@ 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
@@ -33,7 +35,7 @@ import okhttp3.logging.HttpLoggingInterceptor
import okio.BufferedSink
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)
@@ -200,6 +202,8 @@ 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
@@ -211,6 +215,28 @@ 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
}
@@ -240,6 +266,22 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
.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/courier-java-core/build.gradle.kts b/courier-java-core/build.gradle.kts
index 15e9f371..d4f0b6c8 100644
--- a/courier-java-core/build.gradle.kts
+++ b/courier-java-core/build.gradle.kts
@@ -27,8 +27,6 @@ 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(":courier-java-client-okhttp"))
diff --git a/courier-java-core/src/main/kotlin/com/courier/core/Properties.kt b/courier-java-core/src/main/kotlin/com/courier/core/Properties.kt
index f1fb24f6..20b78089 100644
--- a/courier-java-core/src/main/kotlin/com/courier/core/Properties.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/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 =
- CourierClient::class.java.`package`.implementationVersion ?: "unknown"
+ CourierClient::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/courier-java-core/src/main/kotlin/com/courier/core/http/HttpRequestBodies.kt b/courier-java-core/src/main/kotlin/com/courier/core/http/HttpRequestBodies.kt
index 5736754c..c2ddbd06 100644
--- a/courier-java-core/src/main/kotlin/com/courier/core/http/HttpRequestBodies.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/core/http/HttpRequestBodies.kt
@@ -5,16 +5,16 @@
package com.courier.core.http
import com.courier.core.MultipartField
+import com.courier.core.toImmutable
import com.courier.errors.CourierInvalidDataException
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
@JvmSynthetic
internal inline fun json(jsonMapper: JsonMapper, value: T): HttpRequestBody =
@@ -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 CourierInvalidDataException(
- "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 CourierInvalidDataException("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 CourierInvalidDataException(
+ "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 CourierInvalidDataException("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/courier-java-core/src/main/kotlin/com/courier/core/http/RetryingHttpClient.kt b/courier-java-core/src/main/kotlin/com/courier/core/http/RetryingHttpClient.kt
index 91211328..b41448f2 100644
--- a/courier-java-core/src/main/kotlin/com/courier/core/http/RetryingHttpClient.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/core/http/RetryingHttpClient.kt
@@ -1,3 +1,5 @@
+// File generated from our OpenAPI spec by Stainless.
+
package com.courier.core.http
import com.courier.core.DefaultSleeper
diff --git a/courier-java-core/src/main/kotlin/com/courier/models/ElementalContent.kt b/courier-java-core/src/main/kotlin/com/courier/models/ElementalContent.kt
index ec80329c..e3659f80 100644
--- a/courier-java-core/src/main/kotlin/com/courier/models/ElementalContent.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/models/ElementalContent.kt
@@ -16,7 +16,6 @@ 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
class ElementalContent
@@ -24,7 +23,6 @@ class ElementalContent
private constructor(
private val elements: JsonField>,
private val version: JsonField,
- private val brand: JsonField,
private val additionalProperties: MutableMap,
) {
@@ -34,8 +32,7 @@ private constructor(
@ExcludeMissing
elements: JsonField> = JsonMissing.of(),
@JsonProperty("version") @ExcludeMissing version: JsonField = JsonMissing.of(),
- @JsonProperty("brand") @ExcludeMissing brand: JsonField = JsonMissing.of(),
- ) : this(elements, version, brand, mutableMapOf())
+ ) : this(elements, version, mutableMapOf())
/**
* @throws CourierInvalidDataException if the JSON field has an unexpected type or is
@@ -51,12 +48,6 @@ private constructor(
*/
fun version(): String = version.getRequired("version")
- /**
- * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
- * server responded with an unexpected value).
- */
- fun brand(): Optional = brand.getOptional("brand")
-
/**
* Returns the raw JSON value of [elements].
*
@@ -73,13 +64,6 @@ private constructor(
*/
@JsonProperty("version") @ExcludeMissing fun _version(): JsonField = version
- /**
- * Returns the raw JSON value of [brand].
- *
- * Unlike [brand], this method doesn't throw if the JSON field has an unexpected type.
- */
- @JsonProperty("brand") @ExcludeMissing fun _brand(): JsonField = brand
-
@JsonAnySetter
private fun putAdditionalProperty(key: String, value: JsonValue) {
additionalProperties.put(key, value)
@@ -111,14 +95,12 @@ private constructor(
private var elements: JsonField>? = null
private var version: JsonField? = null
- private var brand: JsonField = JsonMissing.of()
private var additionalProperties: MutableMap = mutableMapOf()
@JvmSynthetic
internal fun from(elementalContent: ElementalContent) = apply {
elements = elementalContent.elements.map { it.toMutableList() }
version = elementalContent.version
- brand = elementalContent.brand
additionalProperties = elementalContent.additionalProperties.toMutableMap()
}
@@ -205,19 +187,6 @@ private constructor(
*/
fun version(version: JsonField) = apply { this.version = version }
- fun brand(brand: String?) = brand(JsonField.ofNullable(brand))
-
- /** Alias for calling [Builder.brand] with `brand.orElse(null)`. */
- fun brand(brand: Optional) = brand(brand.getOrNull())
-
- /**
- * Sets [Builder.brand] to an arbitrary JSON value.
- *
- * You should usually call [Builder.brand] with a well-typed [String] value instead. This
- * method is primarily for setting the field to an undocumented or not yet supported value.
- */
- fun brand(brand: JsonField) = apply { this.brand = brand }
-
fun additionalProperties(additionalProperties: Map) = apply {
this.additionalProperties.clear()
putAllAdditionalProperties(additionalProperties)
@@ -254,7 +223,6 @@ private constructor(
ElementalContent(
checkRequired("elements", elements).map { it.toImmutable() },
checkRequired("version", version),
- brand,
additionalProperties.toMutableMap(),
)
}
@@ -268,7 +236,6 @@ private constructor(
elements().forEach { it.validate() }
version()
- brand()
validated = true
}
@@ -288,8 +255,7 @@ private constructor(
@JvmSynthetic
internal fun validity(): Int =
(elements.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
- (if (version.asKnown().isPresent) 1 else 0) +
- (if (brand.asKnown().isPresent) 1 else 0)
+ (if (version.asKnown().isPresent) 1 else 0)
override fun equals(other: Any?): Boolean {
if (this === other) {
@@ -299,16 +265,13 @@ private constructor(
return other is ElementalContent &&
elements == other.elements &&
version == other.version &&
- brand == other.brand &&
additionalProperties == other.additionalProperties
}
- private val hashCode: Int by lazy {
- Objects.hash(elements, version, brand, additionalProperties)
- }
+ private val hashCode: Int by lazy { Objects.hash(elements, version, additionalProperties) }
override fun hashCode(): Int = hashCode
override fun toString() =
- "ElementalContent{elements=$elements, version=$version, brand=$brand, additionalProperties=$additionalProperties}"
+ "ElementalContent{elements=$elements, version=$version, additionalProperties=$additionalProperties}"
}
diff --git a/courier-java-core/src/main/kotlin/com/courier/models/users/tokens/TokenAddSingleParams.kt b/courier-java-core/src/main/kotlin/com/courier/models/users/tokens/TokenAddSingleParams.kt
index 35140b34..3d80f81f 100644
--- a/courier-java-core/src/main/kotlin/com/courier/models/users/tokens/TokenAddSingleParams.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/models/users/tokens/TokenAddSingleParams.kt
@@ -2,11 +2,32 @@
package com.courier.models.users.tokens
+import com.courier.core.BaseDeserializer
+import com.courier.core.BaseSerializer
+import com.courier.core.Enum
+import com.courier.core.ExcludeMissing
+import com.courier.core.JsonField
+import com.courier.core.JsonMissing
import com.courier.core.JsonValue
import com.courier.core.Params
+import com.courier.core.allMaxBy
import com.courier.core.checkRequired
+import com.courier.core.getOrThrow
import com.courier.core.http.Headers
import com.courier.core.http.QueryParams
+import com.courier.errors.CourierInvalidDataException
+import com.fasterxml.jackson.annotation.JsonAnyGetter
+import com.fasterxml.jackson.annotation.JsonAnySetter
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.ObjectCodec
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
+import java.util.Collections
import java.util.Objects
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@@ -15,19 +36,86 @@ import kotlin.jvm.optionals.getOrNull
class TokenAddSingleParams
private constructor(
private val userId: String,
- private val pathToken: String?,
- private val userToken: UserToken,
+ private val token: String?,
+ private val body: Body,
private val additionalHeaders: Headers,
private val additionalQueryParams: QueryParams,
) : Params {
fun userId(): String = userId
- fun pathToken(): Optional = Optional.ofNullable(pathToken)
+ fun token(): Optional = Optional.ofNullable(token)
- fun userToken(): UserToken = userToken
+ /**
+ * @throws CourierInvalidDataException 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 providerKey(): ProviderKey = body.providerKey()
- fun _additionalBodyProperties(): Map = userToken._additionalProperties()
+ /**
+ * Information about the device the token came from.
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun device(): Optional = body.device()
+
+ /**
+ * ISO 8601 formatted date the token expires. Defaults to 2 months. Set to false to disable
+ * expiration.
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun expiryDate(): Optional = body.expiryDate()
+
+ /**
+ * Properties about the token.
+ *
+ * This arbitrary value can be deserialized into a custom type using the `convert` method:
+ * ```java
+ * MyClass myObject = tokenAddSingleParams.properties().convert(MyClass.class);
+ * ```
+ */
+ fun _properties(): JsonValue = body._properties()
+
+ /**
+ * Tracking information about the device the token came from.
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun tracking(): Optional = body.tracking()
+
+ /**
+ * Returns the raw JSON value of [providerKey].
+ *
+ * Unlike [providerKey], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ fun _providerKey(): JsonField = body._providerKey()
+
+ /**
+ * Returns the raw JSON value of [device].
+ *
+ * Unlike [device], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ fun _device(): JsonField = body._device()
+
+ /**
+ * Returns the raw JSON value of [expiryDate].
+ *
+ * Unlike [expiryDate], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ fun _expiryDate(): JsonField = body._expiryDate()
+
+ /**
+ * Returns the raw JSON value of [tracking].
+ *
+ * Unlike [tracking], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ fun _tracking(): JsonField = body._tracking()
+
+ fun _additionalBodyProperties(): Map = body._additionalProperties()
/** Additional headers to send with the request. */
fun _additionalHeaders(): Headers = additionalHeaders
@@ -45,7 +133,7 @@ private constructor(
* The following fields are required:
* ```java
* .userId()
- * .userToken()
+ * .providerKey()
* ```
*/
@JvmStatic fun builder() = Builder()
@@ -55,28 +143,128 @@ private constructor(
class Builder internal constructor() {
private var userId: String? = null
- private var pathToken: String? = null
- private var userToken: UserToken? = null
+ private var token: String? = null
+ private var body: Body.Builder = Body.builder()
private var additionalHeaders: Headers.Builder = Headers.builder()
private var additionalQueryParams: QueryParams.Builder = QueryParams.builder()
@JvmSynthetic
internal fun from(tokenAddSingleParams: TokenAddSingleParams) = apply {
userId = tokenAddSingleParams.userId
- pathToken = tokenAddSingleParams.pathToken
- userToken = tokenAddSingleParams.userToken
+ token = tokenAddSingleParams.token
+ body = tokenAddSingleParams.body.toBuilder()
additionalHeaders = tokenAddSingleParams.additionalHeaders.toBuilder()
additionalQueryParams = tokenAddSingleParams.additionalQueryParams.toBuilder()
}
fun userId(userId: String) = apply { this.userId = userId }
- fun pathToken(pathToken: String?) = apply { this.pathToken = pathToken }
+ fun token(token: String?) = apply { this.token = token }
+
+ /** Alias for calling [Builder.token] with `token.orElse(null)`. */
+ fun token(token: Optional) = token(token.getOrNull())
+
+ /**
+ * Sets the entire request body.
+ *
+ * This is generally only useful if you are already constructing the body separately.
+ * Otherwise, it's more convenient to use the top-level setters instead:
+ * - [providerKey]
+ * - [device]
+ * - [expiryDate]
+ * - [properties]
+ * - [tracking]
+ * - etc.
+ */
+ fun body(body: Body) = apply { this.body = body.toBuilder() }
+
+ fun providerKey(providerKey: ProviderKey) = apply { body.providerKey(providerKey) }
+
+ /**
+ * Sets [Builder.providerKey] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.providerKey] with a well-typed [ProviderKey] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun providerKey(providerKey: JsonField) = apply {
+ body.providerKey(providerKey)
+ }
+
+ /** Information about the device the token came from. */
+ fun device(device: Device?) = apply { body.device(device) }
+
+ /** Alias for calling [Builder.device] with `device.orElse(null)`. */
+ fun device(device: Optional) = device(device.getOrNull())
+
+ /**
+ * Sets [Builder.device] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.device] with a well-typed [Device] value instead. This
+ * method is primarily for setting the field to an undocumented or not yet supported value.
+ */
+ fun device(device: JsonField) = apply { body.device(device) }
+
+ /**
+ * ISO 8601 formatted date the token expires. Defaults to 2 months. Set to false to disable
+ * expiration.
+ */
+ fun expiryDate(expiryDate: ExpiryDate?) = apply { body.expiryDate(expiryDate) }
+
+ /** Alias for calling [Builder.expiryDate] with `expiryDate.orElse(null)`. */
+ fun expiryDate(expiryDate: Optional) = expiryDate(expiryDate.getOrNull())
- /** Alias for calling [Builder.pathToken] with `pathToken.orElse(null)`. */
- fun pathToken(pathToken: Optional) = pathToken(pathToken.getOrNull())
+ /**
+ * Sets [Builder.expiryDate] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.expiryDate] with a well-typed [ExpiryDate] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun expiryDate(expiryDate: JsonField) = apply { body.expiryDate(expiryDate) }
+
+ /** Alias for calling [expiryDate] with `ExpiryDate.ofString(string)`. */
+ fun expiryDate(string: String) = apply { body.expiryDate(string) }
+
+ /** Alias for calling [expiryDate] with `ExpiryDate.ofBool(bool)`. */
+ fun expiryDate(bool: Boolean) = apply { body.expiryDate(bool) }
+
+ /** Properties about the token. */
+ fun properties(properties: JsonValue) = apply { body.properties(properties) }
+
+ /** Tracking information about the device the token came from. */
+ fun tracking(tracking: Tracking?) = apply { body.tracking(tracking) }
+
+ /** Alias for calling [Builder.tracking] with `tracking.orElse(null)`. */
+ fun tracking(tracking: Optional) = tracking(tracking.getOrNull())
- fun userToken(userToken: UserToken) = apply { this.userToken = userToken }
+ /**
+ * Sets [Builder.tracking] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.tracking] with a well-typed [Tracking] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet supported
+ * value.
+ */
+ fun tracking(tracking: JsonField) = apply { body.tracking(tracking) }
+
+ fun additionalBodyProperties(additionalBodyProperties: Map) = apply {
+ body.additionalProperties(additionalBodyProperties)
+ }
+
+ fun putAdditionalBodyProperty(key: String, value: JsonValue) = apply {
+ body.putAdditionalProperty(key, value)
+ }
+
+ fun putAllAdditionalBodyProperties(additionalBodyProperties: Map) =
+ apply {
+ body.putAllAdditionalProperties(additionalBodyProperties)
+ }
+
+ fun removeAdditionalBodyProperty(key: String) = apply { body.removeAdditionalProperty(key) }
+
+ fun removeAllAdditionalBodyProperties(keys: Set) = apply {
+ body.removeAllAdditionalProperties(keys)
+ }
fun additionalHeaders(additionalHeaders: Headers) = apply {
this.additionalHeaders.clear()
@@ -184,7 +372,7 @@ private constructor(
* The following fields are required:
* ```java
* .userId()
- * .userToken()
+ * .providerKey()
* ```
*
* @throws IllegalStateException if any required field is unset.
@@ -192,19 +380,19 @@ private constructor(
fun build(): TokenAddSingleParams =
TokenAddSingleParams(
checkRequired("userId", userId),
- pathToken,
- checkRequired("userToken", userToken),
+ token,
+ body.build(),
additionalHeaders.build(),
additionalQueryParams.build(),
)
}
- fun _body(): UserToken = userToken
+ fun _body(): Body = body
fun _pathParam(index: Int): String =
when (index) {
0 -> userId
- 1 -> pathToken ?: ""
+ 1 -> token ?: ""
else -> ""
}
@@ -212,6 +400,1264 @@ private constructor(
override fun _queryParams(): QueryParams = additionalQueryParams
+ /**
+ * Request body for adding a single token. The token value itself is provided via the path
+ * parameter, so it is omitted from the body.
+ */
+ class Body
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val providerKey: JsonField,
+ private val device: JsonField,
+ private val expiryDate: JsonField,
+ private val properties: JsonValue,
+ private val tracking: JsonField,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("provider_key")
+ @ExcludeMissing
+ providerKey: JsonField = JsonMissing.of(),
+ @JsonProperty("device") @ExcludeMissing device: JsonField = JsonMissing.of(),
+ @JsonProperty("expiry_date")
+ @ExcludeMissing
+ expiryDate: JsonField = JsonMissing.of(),
+ @JsonProperty("properties") @ExcludeMissing properties: JsonValue = JsonMissing.of(),
+ @JsonProperty("tracking")
+ @ExcludeMissing
+ tracking: JsonField = JsonMissing.of(),
+ ) : this(providerKey, device, expiryDate, properties, tracking, mutableMapOf())
+
+ /**
+ * @throws CourierInvalidDataException 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 providerKey(): ProviderKey = providerKey.getRequired("provider_key")
+
+ /**
+ * Information about the device the token came from.
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun device(): Optional = device.getOptional("device")
+
+ /**
+ * ISO 8601 formatted date the token expires. Defaults to 2 months. Set to false to disable
+ * expiration.
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun expiryDate(): Optional = expiryDate.getOptional("expiry_date")
+
+ /**
+ * Properties about the token.
+ *
+ * This arbitrary value can be deserialized into a custom type using the `convert` method:
+ * ```java
+ * MyClass myObject = body.properties().convert(MyClass.class);
+ * ```
+ */
+ @JsonProperty("properties") @ExcludeMissing fun _properties(): JsonValue = properties
+
+ /**
+ * Tracking information about the device the token came from.
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun tracking(): Optional = tracking.getOptional("tracking")
+
+ /**
+ * Returns the raw JSON value of [providerKey].
+ *
+ * Unlike [providerKey], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("provider_key")
+ @ExcludeMissing
+ fun _providerKey(): JsonField = providerKey
+
+ /**
+ * Returns the raw JSON value of [device].
+ *
+ * Unlike [device], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("device") @ExcludeMissing fun _device(): JsonField = device
+
+ /**
+ * Returns the raw JSON value of [expiryDate].
+ *
+ * Unlike [expiryDate], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("expiry_date")
+ @ExcludeMissing
+ fun _expiryDate(): JsonField = expiryDate
+
+ /**
+ * Returns the raw JSON value of [tracking].
+ *
+ * Unlike [tracking], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("tracking") @ExcludeMissing fun _tracking(): JsonField = tracking
+
+ @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 [Body].
+ *
+ * The following fields are required:
+ * ```java
+ * .providerKey()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Body]. */
+ class Builder internal constructor() {
+
+ private var providerKey: JsonField? = null
+ private var device: JsonField = JsonMissing.of()
+ private var expiryDate: JsonField = JsonMissing.of()
+ private var properties: JsonValue = JsonMissing.of()
+ private var tracking: JsonField = JsonMissing.of()
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(body: Body) = apply {
+ providerKey = body.providerKey
+ device = body.device
+ expiryDate = body.expiryDate
+ properties = body.properties
+ tracking = body.tracking
+ additionalProperties = body.additionalProperties.toMutableMap()
+ }
+
+ fun providerKey(providerKey: ProviderKey) = providerKey(JsonField.of(providerKey))
+
+ /**
+ * Sets [Builder.providerKey] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.providerKey] with a well-typed [ProviderKey] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun providerKey(providerKey: JsonField) = apply {
+ this.providerKey = providerKey
+ }
+
+ /** Information about the device the token came from. */
+ fun device(device: Device?) = device(JsonField.ofNullable(device))
+
+ /** Alias for calling [Builder.device] with `device.orElse(null)`. */
+ fun device(device: Optional) = device(device.getOrNull())
+
+ /**
+ * Sets [Builder.device] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.device] with a well-typed [Device] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun device(device: JsonField) = apply { this.device = device }
+
+ /**
+ * ISO 8601 formatted date the token expires. Defaults to 2 months. Set to false to
+ * disable expiration.
+ */
+ fun expiryDate(expiryDate: ExpiryDate?) = expiryDate(JsonField.ofNullable(expiryDate))
+
+ /** Alias for calling [Builder.expiryDate] with `expiryDate.orElse(null)`. */
+ fun expiryDate(expiryDate: Optional) = expiryDate(expiryDate.getOrNull())
+
+ /**
+ * Sets [Builder.expiryDate] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.expiryDate] with a well-typed [ExpiryDate] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun expiryDate(expiryDate: JsonField) = apply {
+ this.expiryDate = expiryDate
+ }
+
+ /** Alias for calling [expiryDate] with `ExpiryDate.ofString(string)`. */
+ fun expiryDate(string: String) = expiryDate(ExpiryDate.ofString(string))
+
+ /** Alias for calling [expiryDate] with `ExpiryDate.ofBool(bool)`. */
+ fun expiryDate(bool: Boolean) = expiryDate(ExpiryDate.ofBool(bool))
+
+ /** Properties about the token. */
+ fun properties(properties: JsonValue) = apply { this.properties = properties }
+
+ /** Tracking information about the device the token came from. */
+ fun tracking(tracking: Tracking?) = tracking(JsonField.ofNullable(tracking))
+
+ /** Alias for calling [Builder.tracking] with `tracking.orElse(null)`. */
+ fun tracking(tracking: Optional) = tracking(tracking.getOrNull())
+
+ /**
+ * Sets [Builder.tracking] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.tracking] with a well-typed [Tracking] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun tracking(tracking: JsonField) = apply { this.tracking = tracking }
+
+ 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 [Body].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .providerKey()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): Body =
+ Body(
+ checkRequired("providerKey", providerKey),
+ device,
+ expiryDate,
+ properties,
+ tracking,
+ additionalProperties.toMutableMap(),
+ )
+ }
+
+ private var validated: Boolean = false
+
+ fun validate(): Body = apply {
+ if (validated) {
+ return@apply
+ }
+
+ providerKey().validate()
+ device().ifPresent { it.validate() }
+ expiryDate().ifPresent { it.validate() }
+ tracking().ifPresent { it.validate() }
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: CourierInvalidDataException) {
+ 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 =
+ (providerKey.asKnown().getOrNull()?.validity() ?: 0) +
+ (device.asKnown().getOrNull()?.validity() ?: 0) +
+ (expiryDate.asKnown().getOrNull()?.validity() ?: 0) +
+ (tracking.asKnown().getOrNull()?.validity() ?: 0)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Body &&
+ providerKey == other.providerKey &&
+ device == other.device &&
+ expiryDate == other.expiryDate &&
+ properties == other.properties &&
+ tracking == other.tracking &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy {
+ Objects.hash(
+ providerKey,
+ device,
+ expiryDate,
+ properties,
+ tracking,
+ additionalProperties,
+ )
+ }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "Body{providerKey=$providerKey, device=$device, expiryDate=$expiryDate, properties=$properties, tracking=$tracking, additionalProperties=$additionalProperties}"
+ }
+
+ class ProviderKey @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 FIREBASE_FCM = of("firebase-fcm")
+
+ @JvmField val APN = of("apn")
+
+ @JvmField val EXPO = of("expo")
+
+ @JvmField val ONESIGNAL = of("onesignal")
+
+ @JvmStatic fun of(value: String) = ProviderKey(JsonField.of(value))
+ }
+
+ /** An enum containing [ProviderKey]'s known values. */
+ enum class Known {
+ FIREBASE_FCM,
+ APN,
+ EXPO,
+ ONESIGNAL,
+ }
+
+ /**
+ * An enum containing [ProviderKey]'s known values, as well as an [_UNKNOWN] member.
+ *
+ * An instance of [ProviderKey] 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 {
+ FIREBASE_FCM,
+ APN,
+ EXPO,
+ ONESIGNAL,
+ /**
+ * An enum member indicating that [ProviderKey] 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) {
+ FIREBASE_FCM -> Value.FIREBASE_FCM
+ APN -> Value.APN
+ EXPO -> Value.EXPO
+ ONESIGNAL -> Value.ONESIGNAL
+ 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 CourierInvalidDataException if this class instance's value is a not a known
+ * member.
+ */
+ fun known(): Known =
+ when (this) {
+ FIREBASE_FCM -> Known.FIREBASE_FCM
+ APN -> Known.APN
+ EXPO -> Known.EXPO
+ ONESIGNAL -> Known.ONESIGNAL
+ else -> throw CourierInvalidDataException("Unknown ProviderKey: $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 CourierInvalidDataException if this class instance's value does not have the
+ * expected primitive type.
+ */
+ fun asString(): String =
+ _value().asString().orElseThrow { CourierInvalidDataException("Value is not a String") }
+
+ private var validated: Boolean = false
+
+ fun validate(): ProviderKey = apply {
+ if (validated) {
+ return@apply
+ }
+
+ known()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: CourierInvalidDataException) {
+ 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 ProviderKey && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+ }
+
+ /** Information about the device the token came from. */
+ class Device
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val adId: JsonField,
+ private val appId: JsonField,
+ private val deviceId: JsonField,
+ private val manufacturer: JsonField,
+ private val model: JsonField,
+ private val platform: JsonField,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("ad_id") @ExcludeMissing adId: JsonField = JsonMissing.of(),
+ @JsonProperty("app_id") @ExcludeMissing appId: JsonField = JsonMissing.of(),
+ @JsonProperty("device_id")
+ @ExcludeMissing
+ deviceId: JsonField = JsonMissing.of(),
+ @JsonProperty("manufacturer")
+ @ExcludeMissing
+ manufacturer: JsonField = JsonMissing.of(),
+ @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(),
+ @JsonProperty("platform") @ExcludeMissing platform: JsonField = JsonMissing.of(),
+ ) : this(adId, appId, deviceId, manufacturer, model, platform, mutableMapOf())
+
+ /**
+ * Id of the advertising identifier
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun adId(): Optional = adId.getOptional("ad_id")
+
+ /**
+ * Id of the application the token is used for
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun appId(): Optional = appId.getOptional("app_id")
+
+ /**
+ * Id of the device the token is associated with
+ *
+ * @throws CourierInvalidDataException 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 device manufacturer
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun manufacturer(): Optional = manufacturer.getOptional("manufacturer")
+
+ /**
+ * The device model
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun model(): Optional = model.getOptional("model")
+
+ /**
+ * The device platform i.e. android, ios, web
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun platform(): Optional = platform.getOptional("platform")
+
+ /**
+ * Returns the raw JSON value of [adId].
+ *
+ * Unlike [adId], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("ad_id") @ExcludeMissing fun _adId(): JsonField = adId
+
+ /**
+ * Returns the raw JSON value of [appId].
+ *
+ * Unlike [appId], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("app_id") @ExcludeMissing fun _appId(): JsonField = appId
+
+ /**
+ * 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 [manufacturer].
+ *
+ * Unlike [manufacturer], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("manufacturer")
+ @ExcludeMissing
+ fun _manufacturer(): JsonField = manufacturer
+
+ /**
+ * Returns the raw JSON value of [model].
+ *
+ * Unlike [model], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model
+
+ /**
+ * Returns the raw JSON value of [platform].
+ *
+ * Unlike [platform], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("platform") @ExcludeMissing fun _platform(): JsonField = platform
+
+ @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 [Device]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Device]. */
+ class Builder internal constructor() {
+
+ private var adId: JsonField = JsonMissing.of()
+ private var appId: JsonField = JsonMissing.of()
+ private var deviceId: JsonField = JsonMissing.of()
+ private var manufacturer: JsonField = JsonMissing.of()
+ private var model: JsonField = JsonMissing.of()
+ private var platform: JsonField = JsonMissing.of()
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(device: Device) = apply {
+ adId = device.adId
+ appId = device.appId
+ deviceId = device.deviceId
+ manufacturer = device.manufacturer
+ model = device.model
+ platform = device.platform
+ additionalProperties = device.additionalProperties.toMutableMap()
+ }
+
+ /** Id of the advertising identifier */
+ fun adId(adId: String?) = adId(JsonField.ofNullable(adId))
+
+ /** Alias for calling [Builder.adId] with `adId.orElse(null)`. */
+ fun adId(adId: Optional) = adId(adId.getOrNull())
+
+ /**
+ * Sets [Builder.adId] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.adId] with a well-typed [String] value instead. This
+ * method is primarily for setting the field to an undocumented or not yet supported
+ * value.
+ */
+ fun adId(adId: JsonField) = apply { this.adId = adId }
+
+ /** Id of the application the token is used for */
+ fun appId(appId: String?) = appId(JsonField.ofNullable(appId))
+
+ /** Alias for calling [Builder.appId] with `appId.orElse(null)`. */
+ fun appId(appId: Optional) = appId(appId.getOrNull())
+
+ /**
+ * Sets [Builder.appId] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.appId] with a well-typed [String] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun appId(appId: JsonField) = apply { this.appId = appId }
+
+ /** Id of the device the token is associated with */
+ fun deviceId(deviceId: String?) = deviceId(JsonField.ofNullable(deviceId))
+
+ /** Alias for calling [Builder.deviceId] with `deviceId.orElse(null)`. */
+ fun deviceId(deviceId: Optional) = deviceId(deviceId.getOrNull())
+
+ /**
+ * 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 device manufacturer */
+ fun manufacturer(manufacturer: String?) =
+ manufacturer(JsonField.ofNullable(manufacturer))
+
+ /** Alias for calling [Builder.manufacturer] with `manufacturer.orElse(null)`. */
+ fun manufacturer(manufacturer: Optional) =
+ manufacturer(manufacturer.getOrNull())
+
+ /**
+ * Sets [Builder.manufacturer] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.manufacturer] with a well-typed [String] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun manufacturer(manufacturer: JsonField) = apply {
+ this.manufacturer = manufacturer
+ }
+
+ /** The device model */
+ fun model(model: String?) = model(JsonField.ofNullable(model))
+
+ /** Alias for calling [Builder.model] with `model.orElse(null)`. */
+ fun model(model: Optional) = model(model.getOrNull())
+
+ /**
+ * Sets [Builder.model] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.model] with a well-typed [String] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun model(model: JsonField) = apply { this.model = model }
+
+ /** The device platform i.e. android, ios, web */
+ fun platform(platform: String?) = platform(JsonField.ofNullable(platform))
+
+ /** Alias for calling [Builder.platform] with `platform.orElse(null)`. */
+ fun platform(platform: Optional) = platform(platform.getOrNull())
+
+ /**
+ * Sets [Builder.platform] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.platform] with a well-typed [String] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun platform(platform: JsonField) = apply { this.platform = platform }
+
+ 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 [Device].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Device =
+ Device(
+ adId,
+ appId,
+ deviceId,
+ manufacturer,
+ model,
+ platform,
+ additionalProperties.toMutableMap(),
+ )
+ }
+
+ private var validated: Boolean = false
+
+ fun validate(): Device = apply {
+ if (validated) {
+ return@apply
+ }
+
+ adId()
+ appId()
+ deviceId()
+ manufacturer()
+ model()
+ platform()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: CourierInvalidDataException) {
+ 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 (adId.asKnown().isPresent) 1 else 0) +
+ (if (appId.asKnown().isPresent) 1 else 0) +
+ (if (deviceId.asKnown().isPresent) 1 else 0) +
+ (if (manufacturer.asKnown().isPresent) 1 else 0) +
+ (if (model.asKnown().isPresent) 1 else 0) +
+ (if (platform.asKnown().isPresent) 1 else 0)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Device &&
+ adId == other.adId &&
+ appId == other.appId &&
+ deviceId == other.deviceId &&
+ manufacturer == other.manufacturer &&
+ model == other.model &&
+ platform == other.platform &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy {
+ Objects.hash(adId, appId, deviceId, manufacturer, model, platform, additionalProperties)
+ }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "Device{adId=$adId, appId=$appId, deviceId=$deviceId, manufacturer=$manufacturer, model=$model, platform=$platform, additionalProperties=$additionalProperties}"
+ }
+
+ /**
+ * ISO 8601 formatted date the token expires. Defaults to 2 months. Set to false to disable
+ * expiration.
+ */
+ @JsonDeserialize(using = ExpiryDate.Deserializer::class)
+ @JsonSerialize(using = ExpiryDate.Serializer::class)
+ class ExpiryDate
+ private constructor(
+ private val string: String? = null,
+ private val bool: Boolean? = null,
+ private val _json: JsonValue? = null,
+ ) {
+
+ fun string(): Optional = Optional.ofNullable(string)
+
+ fun bool(): Optional = Optional.ofNullable(bool)
+
+ fun isString(): Boolean = string != null
+
+ fun isBool(): Boolean = bool != null
+
+ fun asString(): String = string.getOrThrow("string")
+
+ fun asBool(): Boolean = bool.getOrThrow("bool")
+
+ fun _json(): Optional = Optional.ofNullable(_json)
+
+ fun accept(visitor: Visitor): T =
+ when {
+ string != null -> visitor.visitString(string)
+ bool != null -> visitor.visitBool(bool)
+ else -> visitor.unknown(_json)
+ }
+
+ private var validated: Boolean = false
+
+ fun validate(): ExpiryDate = apply {
+ if (validated) {
+ return@apply
+ }
+
+ accept(
+ object : Visitor {
+ override fun visitString(string: String) {}
+
+ override fun visitBool(bool: Boolean) {}
+ }
+ )
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: CourierInvalidDataException) {
+ 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 =
+ accept(
+ object : Visitor {
+ override fun visitString(string: String) = 1
+
+ override fun visitBool(bool: Boolean) = 1
+
+ override fun unknown(json: JsonValue?) = 0
+ }
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is ExpiryDate && string == other.string && bool == other.bool
+ }
+
+ override fun hashCode(): Int = Objects.hash(string, bool)
+
+ override fun toString(): String =
+ when {
+ string != null -> "ExpiryDate{string=$string}"
+ bool != null -> "ExpiryDate{bool=$bool}"
+ _json != null -> "ExpiryDate{_unknown=$_json}"
+ else -> throw IllegalStateException("Invalid ExpiryDate")
+ }
+
+ companion object {
+
+ @JvmStatic fun ofString(string: String) = ExpiryDate(string = string)
+
+ @JvmStatic fun ofBool(bool: Boolean) = ExpiryDate(bool = bool)
+ }
+
+ /**
+ * An interface that defines how to map each variant of [ExpiryDate] to a value of type [T].
+ */
+ interface Visitor {
+
+ fun visitString(string: String): T
+
+ fun visitBool(bool: Boolean): T
+
+ /**
+ * Maps an unknown variant of [ExpiryDate] to a value of type [T].
+ *
+ * An instance of [ExpiryDate] can contain an unknown variant if it was deserialized
+ * from data that doesn't match any known variant. For example, if the SDK is on an
+ * older version than the API, then the API may respond with new variants that the SDK
+ * is unaware of.
+ *
+ * @throws CourierInvalidDataException in the default implementation.
+ */
+ fun unknown(json: JsonValue?): T {
+ throw CourierInvalidDataException("Unknown ExpiryDate: $json")
+ }
+ }
+
+ internal class Deserializer : BaseDeserializer(ExpiryDate::class) {
+
+ override fun ObjectCodec.deserialize(node: JsonNode): ExpiryDate {
+ val json = JsonValue.fromJsonNode(node)
+
+ val bestMatches =
+ sequenceOf(
+ tryDeserialize(node, jacksonTypeRef())?.let {
+ ExpiryDate(string = it, _json = json)
+ },
+ tryDeserialize(node, jacksonTypeRef())?.let {
+ ExpiryDate(bool = it, _json = json)
+ },
+ )
+ .filterNotNull()
+ .allMaxBy { it.validity() }
+ .toList()
+ return when (bestMatches.size) {
+ // This can happen if what we're deserializing is completely incompatible with
+ // all the possible variants (e.g. deserializing from integer).
+ 0 -> ExpiryDate(_json = json)
+ 1 -> bestMatches.single()
+ // If there's more than one match with the highest validity, then use the first
+ // completely valid match, or simply the first match if none are completely
+ // valid.
+ else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first()
+ }
+ }
+ }
+
+ internal class Serializer : BaseSerializer(ExpiryDate::class) {
+
+ override fun serialize(
+ value: ExpiryDate,
+ generator: JsonGenerator,
+ provider: SerializerProvider,
+ ) {
+ when {
+ value.string != null -> generator.writeObject(value.string)
+ value.bool != null -> generator.writeObject(value.bool)
+ value._json != null -> generator.writeObject(value._json)
+ else -> throw IllegalStateException("Invalid ExpiryDate")
+ }
+ }
+ }
+ }
+
+ /** Tracking information about the device the token came from. */
+ class Tracking
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val ip: JsonField,
+ private val lat: JsonField,
+ private val long_: JsonField,
+ private val osVersion: JsonField,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("ip") @ExcludeMissing ip: JsonField = JsonMissing.of(),
+ @JsonProperty("lat") @ExcludeMissing lat: JsonField = JsonMissing.of(),
+ @JsonProperty("long") @ExcludeMissing long_: JsonField = JsonMissing.of(),
+ @JsonProperty("os_version")
+ @ExcludeMissing
+ osVersion: JsonField = JsonMissing.of(),
+ ) : this(ip, lat, long_, osVersion, mutableMapOf())
+
+ /**
+ * The IP address of the device
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun ip(): Optional = ip.getOptional("ip")
+
+ /**
+ * The latitude of the device
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun lat(): Optional = lat.getOptional("lat")
+
+ /**
+ * The longitude of the device
+ *
+ * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun long_(): Optional = long_.getOptional("long")
+
+ /**
+ * The operating system version
+ *
+ * @throws CourierInvalidDataException 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")
+
+ /**
+ * 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 [lat].
+ *
+ * Unlike [lat], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("lat") @ExcludeMissing fun _lat(): JsonField = lat
+
+ /**
+ * Returns the raw JSON value of [long_].
+ *
+ * Unlike [long_], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("long") @ExcludeMissing fun _long_(): JsonField = long_
+
+ /**
+ * 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
+
+ @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 [Tracking]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Tracking]. */
+ class Builder internal constructor() {
+
+ private var ip: JsonField = JsonMissing.of()
+ private var lat: JsonField = JsonMissing.of()
+ private var long_: JsonField = JsonMissing.of()
+ private var osVersion: JsonField = JsonMissing.of()
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(tracking: Tracking) = apply {
+ ip = tracking.ip
+ lat = tracking.lat
+ long_ = tracking.long_
+ osVersion = tracking.osVersion
+ additionalProperties = tracking.additionalProperties.toMutableMap()
+ }
+
+ /** The IP address of the device */
+ fun ip(ip: String?) = ip(JsonField.ofNullable(ip))
+
+ /** Alias for calling [Builder.ip] with `ip.orElse(null)`. */
+ fun ip(ip: Optional) = ip(ip.getOrNull())
+
+ /**
+ * 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 }
+
+ /** The latitude of the device */
+ fun lat(lat: String?) = lat(JsonField.ofNullable(lat))
+
+ /** Alias for calling [Builder.lat] with `lat.orElse(null)`. */
+ fun lat(lat: Optional) = lat(lat.getOrNull())
+
+ /**
+ * Sets [Builder.lat] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.lat] with a well-typed [String] value instead. This
+ * method is primarily for setting the field to an undocumented or not yet supported
+ * value.
+ */
+ fun lat(lat: JsonField) = apply { this.lat = lat }
+
+ /** The longitude of the device */
+ fun long_(long_: String?) = long_(JsonField.ofNullable(long_))
+
+ /** Alias for calling [Builder.long_] with `long_.orElse(null)`. */
+ fun long_(long_: Optional) = long_(long_.getOrNull())
+
+ /**
+ * Sets [Builder.long_] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.long_] with a well-typed [String] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun long_(long_: JsonField) = apply { this.long_ = long_ }
+
+ /** The operating system version */
+ fun osVersion(osVersion: String?) = osVersion(JsonField.ofNullable(osVersion))
+
+ /** Alias for calling [Builder.osVersion] with `osVersion.orElse(null)`. */
+ fun osVersion(osVersion: Optional) = osVersion(osVersion.getOrNull())
+
+ /**
+ * 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 }
+
+ 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 [Tracking].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Tracking =
+ Tracking(ip, lat, long_, osVersion, additionalProperties.toMutableMap())
+ }
+
+ private var validated: Boolean = false
+
+ fun validate(): Tracking = apply {
+ if (validated) {
+ return@apply
+ }
+
+ ip()
+ lat()
+ long_()
+ osVersion()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: CourierInvalidDataException) {
+ 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 (ip.asKnown().isPresent) 1 else 0) +
+ (if (lat.asKnown().isPresent) 1 else 0) +
+ (if (long_.asKnown().isPresent) 1 else 0) +
+ (if (osVersion.asKnown().isPresent) 1 else 0)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Tracking &&
+ ip == other.ip &&
+ lat == other.lat &&
+ long_ == other.long_ &&
+ osVersion == other.osVersion &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy {
+ Objects.hash(ip, lat, long_, osVersion, additionalProperties)
+ }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "Tracking{ip=$ip, lat=$lat, long_=$long_, osVersion=$osVersion, additionalProperties=$additionalProperties}"
+ }
+
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
@@ -219,15 +1665,15 @@ private constructor(
return other is TokenAddSingleParams &&
userId == other.userId &&
- pathToken == other.pathToken &&
- userToken == other.userToken &&
+ token == other.token &&
+ body == other.body &&
additionalHeaders == other.additionalHeaders &&
additionalQueryParams == other.additionalQueryParams
}
override fun hashCode(): Int =
- Objects.hash(userId, pathToken, userToken, additionalHeaders, additionalQueryParams)
+ Objects.hash(userId, token, body, additionalHeaders, additionalQueryParams)
override fun toString() =
- "TokenAddSingleParams{userId=$userId, pathToken=$pathToken, userToken=$userToken, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}"
+ "TokenAddSingleParams{userId=$userId, token=$token, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}"
}
diff --git a/courier-java-core/src/main/kotlin/com/courier/services/async/users/TokenServiceAsync.kt b/courier-java-core/src/main/kotlin/com/courier/services/async/users/TokenServiceAsync.kt
index a0d96bbf..9be42244 100644
--- a/courier-java-core/src/main/kotlin/com/courier/services/async/users/TokenServiceAsync.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/services/async/users/TokenServiceAsync.kt
@@ -162,16 +162,15 @@ interface TokenServiceAsync {
addMultiple(userId, TokenAddMultipleParams.none(), requestOptions)
/** Adds a single token to a user and overwrites a matching existing token. */
- fun addSingle(pathToken: String, params: TokenAddSingleParams): CompletableFuture =
- addSingle(pathToken, params, RequestOptions.none())
+ fun addSingle(token: String, params: TokenAddSingleParams): CompletableFuture =
+ addSingle(token, params, RequestOptions.none())
/** @see addSingle */
fun addSingle(
- pathToken: String,
+ token: String,
params: TokenAddSingleParams,
requestOptions: RequestOptions = RequestOptions.none(),
- ): CompletableFuture =
- addSingle(params.toBuilder().pathToken(pathToken).build(), requestOptions)
+ ): CompletableFuture = addSingle(params.toBuilder().token(token).build(), requestOptions)
/** @see addSingle */
fun addSingle(params: TokenAddSingleParams): CompletableFuture =
@@ -357,17 +356,17 @@ interface TokenServiceAsync {
* the same as [TokenServiceAsync.addSingle].
*/
fun addSingle(
- pathToken: String,
+ token: String,
params: TokenAddSingleParams,
- ): CompletableFuture = addSingle(pathToken, params, RequestOptions.none())
+ ): CompletableFuture = addSingle(token, params, RequestOptions.none())
/** @see addSingle */
fun addSingle(
- pathToken: String,
+ token: String,
params: TokenAddSingleParams,
requestOptions: RequestOptions = RequestOptions.none(),
): CompletableFuture =
- addSingle(params.toBuilder().pathToken(pathToken).build(), requestOptions)
+ addSingle(params.toBuilder().token(token).build(), requestOptions)
/** @see addSingle */
fun addSingle(params: TokenAddSingleParams): CompletableFuture =
diff --git a/courier-java-core/src/main/kotlin/com/courier/services/async/users/TokenServiceAsyncImpl.kt b/courier-java-core/src/main/kotlin/com/courier/services/async/users/TokenServiceAsyncImpl.kt
index e908d603..493e458a 100644
--- a/courier-java-core/src/main/kotlin/com/courier/services/async/users/TokenServiceAsyncImpl.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/services/async/users/TokenServiceAsyncImpl.kt
@@ -251,7 +251,7 @@ class TokenServiceAsyncImpl internal constructor(private val clientOptions: Clie
): CompletableFuture {
// We check here instead of in the params builder because this can be specified
// positionally or in the params class.
- checkRequired("pathToken", params.pathToken().getOrNull())
+ checkRequired("token", params.token().getOrNull())
val request =
HttpRequest.builder()
.method(HttpMethod.PUT)
diff --git a/courier-java-core/src/main/kotlin/com/courier/services/blocking/users/TokenService.kt b/courier-java-core/src/main/kotlin/com/courier/services/blocking/users/TokenService.kt
index 31a5e6ba..22130973 100644
--- a/courier-java-core/src/main/kotlin/com/courier/services/blocking/users/TokenService.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/services/blocking/users/TokenService.kt
@@ -143,15 +143,15 @@ interface TokenService {
addMultiple(userId, TokenAddMultipleParams.none(), requestOptions)
/** Adds a single token to a user and overwrites a matching existing token. */
- fun addSingle(pathToken: String, params: TokenAddSingleParams) =
- addSingle(pathToken, params, RequestOptions.none())
+ fun addSingle(token: String, params: TokenAddSingleParams) =
+ addSingle(token, params, RequestOptions.none())
/** @see addSingle */
fun addSingle(
- pathToken: String,
+ token: String,
params: TokenAddSingleParams,
requestOptions: RequestOptions = RequestOptions.none(),
- ) = addSingle(params.toBuilder().pathToken(pathToken).build(), requestOptions)
+ ) = addSingle(params.toBuilder().token(token).build(), requestOptions)
/** @see addSingle */
fun addSingle(params: TokenAddSingleParams) = addSingle(params, RequestOptions.none())
@@ -345,16 +345,16 @@ interface TokenService {
* the same as [TokenService.addSingle].
*/
@MustBeClosed
- fun addSingle(pathToken: String, params: TokenAddSingleParams): HttpResponse =
- addSingle(pathToken, params, RequestOptions.none())
+ fun addSingle(token: String, params: TokenAddSingleParams): HttpResponse =
+ addSingle(token, params, RequestOptions.none())
/** @see addSingle */
@MustBeClosed
fun addSingle(
- pathToken: String,
+ token: String,
params: TokenAddSingleParams,
requestOptions: RequestOptions = RequestOptions.none(),
- ): HttpResponse = addSingle(params.toBuilder().pathToken(pathToken).build(), requestOptions)
+ ): HttpResponse = addSingle(params.toBuilder().token(token).build(), requestOptions)
/** @see addSingle */
@MustBeClosed
diff --git a/courier-java-core/src/main/kotlin/com/courier/services/blocking/users/TokenServiceImpl.kt b/courier-java-core/src/main/kotlin/com/courier/services/blocking/users/TokenServiceImpl.kt
index 3c0cfc94..3ba3d017 100644
--- a/courier-java-core/src/main/kotlin/com/courier/services/blocking/users/TokenServiceImpl.kt
+++ b/courier-java-core/src/main/kotlin/com/courier/services/blocking/users/TokenServiceImpl.kt
@@ -224,7 +224,7 @@ class TokenServiceImpl internal constructor(private val clientOptions: ClientOpt
): HttpResponse {
// We check here instead of in the params builder because this can be specified
// positionally or in the params class.
- checkRequired("pathToken", params.pathToken().getOrNull())
+ checkRequired("token", params.token().getOrNull())
val request =
HttpRequest.builder()
.method(HttpMethod.PUT)
diff --git a/courier-java-core/src/test/kotlin/com/courier/TestServerExtension.kt b/courier-java-core/src/test/kotlin/com/courier/TestServerExtension.kt
deleted file mode 100644
index b9615827..00000000
--- a/courier-java-core/src/test/kotlin/com/courier/TestServerExtension.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.courier
-
-import java.lang.RuntimeException
-import java.net.URL
-import org.junit.jupiter.api.extension.BeforeAllCallback
-import org.junit.jupiter.api.extension.ConditionEvaluationResult
-import org.junit.jupiter.api.extension.ExecutionCondition
-import org.junit.jupiter.api.extension.ExtensionContext
-
-class TestServerExtension : BeforeAllCallback, ExecutionCondition {
-
- override fun beforeAll(context: ExtensionContext?) {
- try {
- URL(BASE_URL).openConnection().connect()
- } catch (e: Exception) {
- throw RuntimeException(
- """
- The test suite will not run without a mock Prism 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
- """
- .trimIndent(),
- e,
- )
- }
- }
-
- override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult {
- return if (System.getenv(SKIP_TESTS_ENV).toBoolean()) {
- ConditionEvaluationResult.disabled(
- "Environment variable $SKIP_TESTS_ENV is set to true"
- )
- } else {
- ConditionEvaluationResult.enabled(
- "Environment variable $SKIP_TESTS_ENV is not set to true"
- )
- }
- }
-
- companion object {
-
- val BASE_URL = System.getenv("TEST_API_BASE_URL") ?: "http://localhost:4010"
-
- const val SKIP_TESTS_ENV: String = "SKIP_MOCK_TESTS"
- }
-}
diff --git a/courier-java-core/src/test/kotlin/com/courier/core/http/HttpRequestBodiesTest.kt b/courier-java-core/src/test/kotlin/com/courier/core/http/HttpRequestBodiesTest.kt
new file mode 100644
index 00000000..2c91aef2
--- /dev/null
+++ b/courier-java-core/src/test/kotlin/com/courier/core/http/HttpRequestBodiesTest.kt
@@ -0,0 +1,729 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.courier.core.http
+
+import com.courier.core.MultipartField
+import com.courier.core.jsonMapper
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+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