From 41c9837633b219c112b05446107960558415d3d5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:50:52 +0000 Subject: [PATCH 1/2] feat(openapi): Journeys cancel-by-token endpoint + send-node experiments (C-18986) --- .stats.yml | 8 +- .../models/journeys/CancelJourneyRequest.kt | 585 ++++++++++++++++ .../models/journeys/CancelJourneyResponse.kt | 632 ++++++++++++++++++ .../models/journeys/JourneyCancelParams.kt | 229 +++++++ .../models/journeys/JourneyExperiment.kt | 332 +++++++++ .../journeys/JourneyExperimentVariant.kt | 297 ++++++++ .../courier/models/journeys/JourneyNode.kt | 24 +- .../models/journeys/JourneySendNode.kt | 153 +++-- .../services/async/JourneyServiceAsync.kt | 116 ++++ .../services/async/JourneyServiceAsyncImpl.kt | 40 ++ .../services/blocking/JourneyService.kt | 116 ++++ .../services/blocking/JourneyServiceImpl.kt | 37 + .../journeys/CancelJourneyRequestTest.kt | 89 +++ .../journeys/CancelJourneyResponseTest.kt | 94 +++ .../journeys/JourneyCancelParamsTest.kt | 43 ++ .../models/journeys/JourneyExperimentTest.kt | 91 +++ .../journeys/JourneyExperimentVariantTest.kt | 47 ++ .../models/journeys/JourneyNodeTest.kt | 50 +- .../models/journeys/JourneySendNodeTest.kt | 76 ++- .../services/async/JourneyServiceAsyncTest.kt | 18 + .../services/blocking/JourneyServiceTest.kt | 17 + 21 files changed, 3015 insertions(+), 79 deletions(-) create mode 100644 courier-java-core/src/main/kotlin/com/courier/models/journeys/CancelJourneyRequest.kt create mode 100644 courier-java-core/src/main/kotlin/com/courier/models/journeys/CancelJourneyResponse.kt create mode 100644 courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyCancelParams.kt create mode 100644 courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyExperiment.kt create mode 100644 courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyExperimentVariant.kt create mode 100644 courier-java-core/src/test/kotlin/com/courier/models/journeys/CancelJourneyRequestTest.kt create mode 100644 courier-java-core/src/test/kotlin/com/courier/models/journeys/CancelJourneyResponseTest.kt create mode 100644 courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyCancelParamsTest.kt create mode 100644 courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyExperimentTest.kt create mode 100644 courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyExperimentVariantTest.kt diff --git a/.stats.yml b/.stats.yml index 5fa69941..4fd0815c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 134 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-97bb4b698571b6dbe884e93397d14aff0ec7e7279de272a15fa0defb3b2515d5.yml -openapi_spec_hash: c33bf8733151f4f5693788b6e57a8344 -config_hash: 74aad10d1512ec69541ece3ca51cf7fa +configured_endpoints: 135 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-e593915c67b3e0e375b6f9cf57d9931f86bc3ee4fd6759ba0e8098d5421fa04e.yml +openapi_spec_hash: 66cc0ce5dda5bda9f416ea3e53c3f5a0 +config_hash: 66d7703eac15d2affc326ac25b55bcd6 diff --git a/courier-java-core/src/main/kotlin/com/courier/models/journeys/CancelJourneyRequest.kt b/courier-java-core/src/main/kotlin/com/courier/models/journeys/CancelJourneyRequest.kt new file mode 100644 index 00000000..4dd03e5c --- /dev/null +++ b/courier-java-core/src/main/kotlin/com/courier/models/journeys/CancelJourneyRequest.kt @@ -0,0 +1,585 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.BaseDeserializer +import com.courier.core.BaseSerializer +import com.courier.core.ExcludeMissing +import com.courier.core.JsonField +import com.courier.core.JsonMissing +import com.courier.core.JsonValue +import com.courier.core.allMaxBy +import com.courier.core.checkRequired +import com.courier.core.getOrThrow +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 + +/** + * Request body for `POST /journeys/cancel`. Provide EXACTLY ONE of `cancelation_token` (cancels + * every run associated with the token) or `run_id` (cancels a single tenant-scoped run). + */ +@JsonDeserialize(using = CancelJourneyRequest.Deserializer::class) +@JsonSerialize(using = CancelJourneyRequest.Serializer::class) +class CancelJourneyRequest +private constructor( + private val byCancelationToken: ByCancelationToken? = null, + private val byRunId: ByRunId? = null, + private val _json: JsonValue? = null, +) { + + fun byCancelationToken(): Optional = Optional.ofNullable(byCancelationToken) + + fun byRunId(): Optional = Optional.ofNullable(byRunId) + + fun isByCancelationToken(): Boolean = byCancelationToken != null + + fun isByRunId(): Boolean = byRunId != null + + fun asByCancelationToken(): ByCancelationToken = + byCancelationToken.getOrThrow("byCancelationToken") + + fun asByRunId(): ByRunId = byRunId.getOrThrow("byRunId") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, unless + * [visitor] overrides [Visitor.unknown]. To handle variants not known to this version of the + * SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.courier.core.JsonValue; + * import java.util.Optional; + * + * Optional result = cancelJourneyRequest.accept(new CancelJourneyRequest.Visitor>() { + * @Override + * public Optional visitByCancelationToken(ByCancelationToken byCancelationToken) { + * return Optional.of(byCancelationToken.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws CourierInvalidDataException if [Visitor.unknown] is not overridden in [visitor] and + * the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + byCancelationToken != null -> visitor.visitByCancelationToken(byCancelationToken) + byRunId != null -> visitor.visitByRunId(byRunId) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws CourierInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): CancelJourneyRequest = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitByCancelationToken(byCancelationToken: ByCancelationToken) { + byCancelationToken.validate() + } + + override fun visitByRunId(byRunId: ByRunId) { + byRunId.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 = + accept( + object : Visitor { + override fun visitByCancelationToken(byCancelationToken: ByCancelationToken) = + byCancelationToken.validity() + + override fun visitByRunId(byRunId: ByRunId) = byRunId.validity() + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is CancelJourneyRequest && + byCancelationToken == other.byCancelationToken && + byRunId == other.byRunId + } + + override fun hashCode(): Int = Objects.hash(byCancelationToken, byRunId) + + override fun toString(): String = + when { + byCancelationToken != null -> + "CancelJourneyRequest{byCancelationToken=$byCancelationToken}" + byRunId != null -> "CancelJourneyRequest{byRunId=$byRunId}" + _json != null -> "CancelJourneyRequest{_unknown=$_json}" + else -> throw IllegalStateException("Invalid CancelJourneyRequest") + } + + companion object { + + @JvmStatic + fun ofByCancelationToken(byCancelationToken: ByCancelationToken) = + CancelJourneyRequest(byCancelationToken = byCancelationToken) + + @JvmStatic fun ofByRunId(byRunId: ByRunId) = CancelJourneyRequest(byRunId = byRunId) + } + + /** + * An interface that defines how to map each variant of [CancelJourneyRequest] to a value of + * type [T]. + */ + interface Visitor { + + fun visitByCancelationToken(byCancelationToken: ByCancelationToken): T + + fun visitByRunId(byRunId: ByRunId): T + + /** + * Maps an unknown variant of [CancelJourneyRequest] to a value of type [T]. + * + * An instance of [CancelJourneyRequest] 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 CancelJourneyRequest: $json") + } + } + + internal class Deserializer : + BaseDeserializer(CancelJourneyRequest::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): CancelJourneyRequest { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + CancelJourneyRequest(byCancelationToken = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + CancelJourneyRequest(byRunId = 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 boolean). + 0 -> CancelJourneyRequest(_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(CancelJourneyRequest::class) { + + override fun serialize( + value: CancelJourneyRequest, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.byCancelationToken != null -> generator.writeObject(value.byCancelationToken) + value.byRunId != null -> generator.writeObject(value.byRunId) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid CancelJourneyRequest") + } + } + } + + class ByCancelationToken + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val cancelationToken: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("cancelation_token") + @ExcludeMissing + cancelationToken: JsonField = JsonMissing.of() + ) : this(cancelationToken, 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 cancelationToken(): String = cancelationToken.getRequired("cancelation_token") + + /** + * Returns the raw JSON value of [cancelationToken]. + * + * Unlike [cancelationToken], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("cancelation_token") + @ExcludeMissing + fun _cancelationToken(): JsonField = cancelationToken + + @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 [ByCancelationToken]. + * + * The following fields are required: + * ```java + * .cancelationToken() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [ByCancelationToken]. */ + class Builder internal constructor() { + + private var cancelationToken: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(byCancelationToken: ByCancelationToken) = apply { + cancelationToken = byCancelationToken.cancelationToken + additionalProperties = byCancelationToken.additionalProperties.toMutableMap() + } + + fun cancelationToken(cancelationToken: String) = + cancelationToken(JsonField.of(cancelationToken)) + + /** + * Sets [Builder.cancelationToken] to an arbitrary JSON value. + * + * You should usually call [Builder.cancelationToken] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun cancelationToken(cancelationToken: JsonField) = apply { + this.cancelationToken = cancelationToken + } + + 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 [ByCancelationToken]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .cancelationToken() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): ByCancelationToken = + ByCancelationToken( + checkRequired("cancelationToken", cancelationToken), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws CourierInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): ByCancelationToken = apply { + if (validated) { + return@apply + } + + cancelationToken() + 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 (cancelationToken.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is ByCancelationToken && + cancelationToken == other.cancelationToken && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(cancelationToken, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "ByCancelationToken{cancelationToken=$cancelationToken, additionalProperties=$additionalProperties}" + } + + class ByRunId + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val runId: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("run_id") @ExcludeMissing runId: JsonField = JsonMissing.of() + ) : this(runId, 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 runId(): String = runId.getRequired("run_id") + + /** + * Returns the raw JSON value of [runId]. + * + * Unlike [runId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("run_id") @ExcludeMissing fun _runId(): JsonField = runId + + @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 [ByRunId]. + * + * The following fields are required: + * ```java + * .runId() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [ByRunId]. */ + class Builder internal constructor() { + + private var runId: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(byRunId: ByRunId) = apply { + runId = byRunId.runId + additionalProperties = byRunId.additionalProperties.toMutableMap() + } + + fun runId(runId: String) = runId(JsonField.of(runId)) + + /** + * Sets [Builder.runId] to an arbitrary JSON value. + * + * You should usually call [Builder.runId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun runId(runId: JsonField) = apply { this.runId = runId } + + 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 [ByRunId]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .runId() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): ByRunId = + ByRunId(checkRequired("runId", runId), additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws CourierInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): ByRunId = apply { + if (validated) { + return@apply + } + + runId() + 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 (runId.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is ByRunId && + runId == other.runId && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(runId, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "ByRunId{runId=$runId, additionalProperties=$additionalProperties}" + } +} diff --git a/courier-java-core/src/main/kotlin/com/courier/models/journeys/CancelJourneyResponse.kt b/courier-java-core/src/main/kotlin/com/courier/models/journeys/CancelJourneyResponse.kt new file mode 100644 index 00000000..5044a7f9 --- /dev/null +++ b/courier-java-core/src/main/kotlin/com/courier/models/journeys/CancelJourneyResponse.kt @@ -0,0 +1,632 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.BaseDeserializer +import com.courier.core.BaseSerializer +import com.courier.core.ExcludeMissing +import com.courier.core.JsonField +import com.courier.core.JsonMissing +import com.courier.core.JsonValue +import com.courier.core.allMaxBy +import com.courier.core.checkRequired +import com.courier.core.getOrThrow +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 + +/** + * `202 Accepted` body for `POST /journeys/cancel`, echoing the submitted identifier. The token + * branch returns `{ cancelation_token }`; the run_id branch returns `{ run_id, status }`. + */ +@JsonDeserialize(using = CancelJourneyResponse.Deserializer::class) +@JsonSerialize(using = CancelJourneyResponse.Serializer::class) +class CancelJourneyResponse +private constructor( + private val tokenBranch: TokenBranch? = null, + private val runIdBranch: RunIdBranch? = null, + private val _json: JsonValue? = null, +) { + + fun tokenBranch(): Optional = Optional.ofNullable(tokenBranch) + + fun runIdBranch(): Optional = Optional.ofNullable(runIdBranch) + + fun isTokenBranch(): Boolean = tokenBranch != null + + fun isRunIdBranch(): Boolean = runIdBranch != null + + fun asTokenBranch(): TokenBranch = tokenBranch.getOrThrow("tokenBranch") + + fun asRunIdBranch(): RunIdBranch = runIdBranch.getOrThrow("runIdBranch") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, unless + * [visitor] overrides [Visitor.unknown]. To handle variants not known to this version of the + * SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.courier.core.JsonValue; + * import java.util.Optional; + * + * Optional result = cancelJourneyResponse.accept(new CancelJourneyResponse.Visitor>() { + * @Override + * public Optional visitTokenBranch(TokenBranch tokenBranch) { + * return Optional.of(tokenBranch.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws CourierInvalidDataException if [Visitor.unknown] is not overridden in [visitor] and + * the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + tokenBranch != null -> visitor.visitTokenBranch(tokenBranch) + runIdBranch != null -> visitor.visitRunIdBranch(runIdBranch) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws CourierInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): CancelJourneyResponse = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitTokenBranch(tokenBranch: TokenBranch) { + tokenBranch.validate() + } + + override fun visitRunIdBranch(runIdBranch: RunIdBranch) { + runIdBranch.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 = + accept( + object : Visitor { + override fun visitTokenBranch(tokenBranch: TokenBranch) = tokenBranch.validity() + + override fun visitRunIdBranch(runIdBranch: RunIdBranch) = runIdBranch.validity() + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is CancelJourneyResponse && + tokenBranch == other.tokenBranch && + runIdBranch == other.runIdBranch + } + + override fun hashCode(): Int = Objects.hash(tokenBranch, runIdBranch) + + override fun toString(): String = + when { + tokenBranch != null -> "CancelJourneyResponse{tokenBranch=$tokenBranch}" + runIdBranch != null -> "CancelJourneyResponse{runIdBranch=$runIdBranch}" + _json != null -> "CancelJourneyResponse{_unknown=$_json}" + else -> throw IllegalStateException("Invalid CancelJourneyResponse") + } + + companion object { + + @JvmStatic + fun ofTokenBranch(tokenBranch: TokenBranch) = + CancelJourneyResponse(tokenBranch = tokenBranch) + + @JvmStatic + fun ofRunIdBranch(runIdBranch: RunIdBranch) = + CancelJourneyResponse(runIdBranch = runIdBranch) + } + + /** + * An interface that defines how to map each variant of [CancelJourneyResponse] to a value of + * type [T]. + */ + interface Visitor { + + fun visitTokenBranch(tokenBranch: TokenBranch): T + + fun visitRunIdBranch(runIdBranch: RunIdBranch): T + + /** + * Maps an unknown variant of [CancelJourneyResponse] to a value of type [T]. + * + * An instance of [CancelJourneyResponse] 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 CancelJourneyResponse: $json") + } + } + + internal class Deserializer : + BaseDeserializer(CancelJourneyResponse::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): CancelJourneyResponse { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + CancelJourneyResponse(tokenBranch = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + CancelJourneyResponse(runIdBranch = 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 boolean). + 0 -> CancelJourneyResponse(_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(CancelJourneyResponse::class) { + + override fun serialize( + value: CancelJourneyResponse, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.tokenBranch != null -> generator.writeObject(value.tokenBranch) + value.runIdBranch != null -> generator.writeObject(value.runIdBranch) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid CancelJourneyResponse") + } + } + } + + class TokenBranch + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val cancelationToken: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("cancelation_token") + @ExcludeMissing + cancelationToken: JsonField = JsonMissing.of() + ) : this(cancelationToken, 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 cancelationToken(): String = cancelationToken.getRequired("cancelation_token") + + /** + * Returns the raw JSON value of [cancelationToken]. + * + * Unlike [cancelationToken], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("cancelation_token") + @ExcludeMissing + fun _cancelationToken(): JsonField = cancelationToken + + @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 [TokenBranch]. + * + * The following fields are required: + * ```java + * .cancelationToken() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [TokenBranch]. */ + class Builder internal constructor() { + + private var cancelationToken: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(tokenBranch: TokenBranch) = apply { + cancelationToken = tokenBranch.cancelationToken + additionalProperties = tokenBranch.additionalProperties.toMutableMap() + } + + fun cancelationToken(cancelationToken: String) = + cancelationToken(JsonField.of(cancelationToken)) + + /** + * Sets [Builder.cancelationToken] to an arbitrary JSON value. + * + * You should usually call [Builder.cancelationToken] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun cancelationToken(cancelationToken: JsonField) = apply { + this.cancelationToken = cancelationToken + } + + 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 [TokenBranch]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .cancelationToken() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): TokenBranch = + TokenBranch( + checkRequired("cancelationToken", cancelationToken), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws CourierInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): TokenBranch = apply { + if (validated) { + return@apply + } + + cancelationToken() + 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 (cancelationToken.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is TokenBranch && + cancelationToken == other.cancelationToken && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(cancelationToken, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "TokenBranch{cancelationToken=$cancelationToken, additionalProperties=$additionalProperties}" + } + + class RunIdBranch + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val runId: JsonField, + private val status: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("run_id") @ExcludeMissing runId: JsonField = JsonMissing.of(), + @JsonProperty("status") @ExcludeMissing status: JsonField = JsonMissing.of(), + ) : this(runId, status, 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 runId(): String = runId.getRequired("run_id") + + /** + * The run's resulting status. `CANCELED` when the run was active and we canceled it; + * `PROCESSED` or `ERROR` when the run had already finished and was left untouched; + * `CANCELED` for an already-canceled run. + * + * @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 status(): String = status.getRequired("status") + + /** + * Returns the raw JSON value of [runId]. + * + * Unlike [runId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("run_id") @ExcludeMissing fun _runId(): JsonField = runId + + /** + * Returns the raw JSON value of [status]. + * + * Unlike [status], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("status") @ExcludeMissing fun _status(): JsonField = status + + @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 [RunIdBranch]. + * + * The following fields are required: + * ```java + * .runId() + * .status() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [RunIdBranch]. */ + class Builder internal constructor() { + + private var runId: JsonField? = null + private var status: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(runIdBranch: RunIdBranch) = apply { + runId = runIdBranch.runId + status = runIdBranch.status + additionalProperties = runIdBranch.additionalProperties.toMutableMap() + } + + fun runId(runId: String) = runId(JsonField.of(runId)) + + /** + * Sets [Builder.runId] to an arbitrary JSON value. + * + * You should usually call [Builder.runId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun runId(runId: JsonField) = apply { this.runId = runId } + + /** + * The run's resulting status. `CANCELED` when the run was active and we canceled it; + * `PROCESSED` or `ERROR` when the run had already finished and was left untouched; + * `CANCELED` for an already-canceled run. + */ + fun status(status: String) = status(JsonField.of(status)) + + /** + * Sets [Builder.status] to an arbitrary JSON value. + * + * You should usually call [Builder.status] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun status(status: JsonField) = apply { this.status = status } + + 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 [RunIdBranch]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .runId() + * .status() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): RunIdBranch = + RunIdBranch( + checkRequired("runId", runId), + checkRequired("status", status), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws CourierInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): RunIdBranch = apply { + if (validated) { + return@apply + } + + runId() + status() + 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 (runId.asKnown().isPresent) 1 else 0) + (if (status.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is RunIdBranch && + runId == other.runId && + status == other.status && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(runId, status, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "RunIdBranch{runId=$runId, status=$status, additionalProperties=$additionalProperties}" + } +} diff --git a/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyCancelParams.kt b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyCancelParams.kt new file mode 100644 index 00000000..c4e29f97 --- /dev/null +++ b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyCancelParams.kt @@ -0,0 +1,229 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.Params +import com.courier.core.checkRequired +import com.courier.core.http.Headers +import com.courier.core.http.QueryParams +import java.util.Objects + +/** + * Cancel journey runs. The request body must contain EXACTLY ONE of `cancelation_token` (cancels + * every run associated with the token) or `run_id` (cancels a single tenant-scoped run). Supplying + * both or neither is a `400`. A `run_id` that does not exist for the caller's tenant returns `404`. + * Cancelation is idempotent and non-clobbering: a run that has already finished + * (`PROCESSED`/`ERROR`) or was already `CANCELED` is left untouched and its current status is + * echoed back. + */ +class JourneyCancelParams +private constructor( + private val cancelJourneyRequest: CancelJourneyRequest, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** + * Request body for `POST /journeys/cancel`. Provide EXACTLY ONE of `cancelation_token` (cancels + * every run associated with the token) or `run_id` (cancels a single tenant-scoped run). + */ + fun cancelJourneyRequest(): CancelJourneyRequest = cancelJourneyRequest + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [JourneyCancelParams]. + * + * The following fields are required: + * ```java + * .cancelJourneyRequest() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [JourneyCancelParams]. */ + class Builder internal constructor() { + + private var cancelJourneyRequest: CancelJourneyRequest? = null + private var additionalHeaders: Headers.Builder = Headers.builder() + private var additionalQueryParams: QueryParams.Builder = QueryParams.builder() + + @JvmSynthetic + internal fun from(journeyCancelParams: JourneyCancelParams) = apply { + cancelJourneyRequest = journeyCancelParams.cancelJourneyRequest + additionalHeaders = journeyCancelParams.additionalHeaders.toBuilder() + additionalQueryParams = journeyCancelParams.additionalQueryParams.toBuilder() + } + + /** + * Request body for `POST /journeys/cancel`. Provide EXACTLY ONE of `cancelation_token` + * (cancels every run associated with the token) or `run_id` (cancels a single tenant-scoped + * run). + */ + fun cancelJourneyRequest(cancelJourneyRequest: CancelJourneyRequest) = apply { + this.cancelJourneyRequest = cancelJourneyRequest + } + + /** + * Alias for calling [cancelJourneyRequest] with + * `CancelJourneyRequest.ofByCancelationToken(byCancelationToken)`. + */ + fun cancelJourneyRequest(byCancelationToken: CancelJourneyRequest.ByCancelationToken) = + cancelJourneyRequest(CancelJourneyRequest.ofByCancelationToken(byCancelationToken)) + + /** + * Alias for calling [cancelJourneyRequest] with `CancelJourneyRequest.ofByRunId(byRunId)`. + */ + fun cancelJourneyRequest(byRunId: CancelJourneyRequest.ByRunId) = + cancelJourneyRequest(CancelJourneyRequest.ofByRunId(byRunId)) + + fun additionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [JourneyCancelParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .cancelJourneyRequest() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): JourneyCancelParams = + JourneyCancelParams( + checkRequired("cancelJourneyRequest", cancelJourneyRequest), + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _body(): CancelJourneyRequest = cancelJourneyRequest + + override fun _headers(): Headers = additionalHeaders + + override fun _queryParams(): QueryParams = additionalQueryParams + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is JourneyCancelParams && + cancelJourneyRequest == other.cancelJourneyRequest && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(cancelJourneyRequest, additionalHeaders, additionalQueryParams) + + override fun toString() = + "JourneyCancelParams{cancelJourneyRequest=$cancelJourneyRequest, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyExperiment.kt b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyExperiment.kt new file mode 100644 index 00000000..94d33949 --- /dev/null +++ b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyExperiment.kt @@ -0,0 +1,332 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.ExcludeMissing +import com.courier.core.JsonField +import com.courier.core.JsonMissing +import com.courier.core.JsonValue +import com.courier.core.checkKnown +import com.courier.core.checkRequired +import com.courier.core.toImmutable +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 java.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** + * A/B experiment config for a send node. The recipient is deterministically bucketed by + * `bucketingKey` and routed to one of the `variants` in proportion to its `weight`. Present on a + * send node INSTEAD OF `message.template`. + */ +class JourneyExperiment +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val bucketingKey: JsonField, + private val variants: JsonField>, + private val id: JsonField, + private val name: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("bucketingKey") + @ExcludeMissing + bucketingKey: JsonField = JsonMissing.of(), + @JsonProperty("variants") + @ExcludeMissing + variants: JsonField> = JsonMissing.of(), + @JsonProperty("id") @ExcludeMissing id: JsonField = JsonMissing.of(), + @JsonProperty("name") @ExcludeMissing name: JsonField = JsonMissing.of(), + ) : this(bucketingKey, variants, id, name, mutableMapOf()) + + /** + * The value used to deterministically assign a recipient to a variant. Must be non-empty with + * no leading or trailing whitespace. + * + * @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 bucketingKey(): String = bucketingKey.getRequired("bucketingKey") + + /** + * Between 2 and 10 weighted template variants. + * + * @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 variants(): List = variants.getRequired("variants") + + /** + * Server-authoritative experiment id (prefixed `exp_`). Omit to have the server mint one; when + * supplied it must be a valid `exp_` id. + * + * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun id(): Optional = id.getOptional("id") + + /** + * Optional, cosmetic display name for the experiment. + * + * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun name(): Optional = name.getOptional("name") + + /** + * Returns the raw JSON value of [bucketingKey]. + * + * Unlike [bucketingKey], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("bucketingKey") + @ExcludeMissing + fun _bucketingKey(): JsonField = bucketingKey + + /** + * Returns the raw JSON value of [variants]. + * + * Unlike [variants], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("variants") + @ExcludeMissing + fun _variants(): JsonField> = variants + + /** + * Returns the raw JSON value of [id]. + * + * Unlike [id], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("id") @ExcludeMissing fun _id(): JsonField = id + + /** + * Returns the raw JSON value of [name]. + * + * Unlike [name], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("name") @ExcludeMissing fun _name(): JsonField = name + + @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 [JourneyExperiment]. + * + * The following fields are required: + * ```java + * .bucketingKey() + * .variants() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [JourneyExperiment]. */ + class Builder internal constructor() { + + private var bucketingKey: JsonField? = null + private var variants: JsonField>? = null + private var id: JsonField = JsonMissing.of() + private var name: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(journeyExperiment: JourneyExperiment) = apply { + bucketingKey = journeyExperiment.bucketingKey + variants = journeyExperiment.variants.map { it.toMutableList() } + id = journeyExperiment.id + name = journeyExperiment.name + additionalProperties = journeyExperiment.additionalProperties.toMutableMap() + } + + /** + * The value used to deterministically assign a recipient to a variant. Must be non-empty + * with no leading or trailing whitespace. + */ + fun bucketingKey(bucketingKey: String) = bucketingKey(JsonField.of(bucketingKey)) + + /** + * Sets [Builder.bucketingKey] to an arbitrary JSON value. + * + * You should usually call [Builder.bucketingKey] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun bucketingKey(bucketingKey: JsonField) = apply { + this.bucketingKey = bucketingKey + } + + /** Between 2 and 10 weighted template variants. */ + fun variants(variants: List) = variants(JsonField.of(variants)) + + /** + * Sets [Builder.variants] to an arbitrary JSON value. + * + * You should usually call [Builder.variants] with a well-typed + * `List` value instead. This method is primarily for setting the + * field to an undocumented or not yet supported value. + */ + fun variants(variants: JsonField>) = apply { + this.variants = variants.map { it.toMutableList() } + } + + /** + * Adds a single [JourneyExperimentVariant] to [variants]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addVariant(variant: JourneyExperimentVariant) = apply { + variants = + (variants ?: JsonField.of(mutableListOf())).also { + checkKnown("variants", it).add(variant) + } + } + + /** + * Server-authoritative experiment id (prefixed `exp_`). Omit to have the server mint one; + * when supplied it must be a valid `exp_` id. + */ + fun id(id: String) = id(JsonField.of(id)) + + /** + * Sets [Builder.id] to an arbitrary JSON value. + * + * You should usually call [Builder.id] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun id(id: JsonField) = apply { this.id = id } + + /** Optional, cosmetic display name for the experiment. */ + fun name(name: String) = name(JsonField.of(name)) + + /** + * Sets [Builder.name] to an arbitrary JSON value. + * + * You should usually call [Builder.name] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun name(name: JsonField) = apply { this.name = name } + + 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 [JourneyExperiment]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .bucketingKey() + * .variants() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): JourneyExperiment = + JourneyExperiment( + checkRequired("bucketingKey", bucketingKey), + checkRequired("variants", variants).map { it.toImmutable() }, + id, + name, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws CourierInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): JourneyExperiment = apply { + if (validated) { + return@apply + } + + bucketingKey() + variants().forEach { it.validate() } + id() + name() + 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 (bucketingKey.asKnown().isPresent) 1 else 0) + + (variants.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + (if (id.asKnown().isPresent) 1 else 0) + + (if (name.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is JourneyExperiment && + bucketingKey == other.bucketingKey && + variants == other.variants && + id == other.id && + name == other.name && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(bucketingKey, variants, id, name, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "JourneyExperiment{bucketingKey=$bucketingKey, variants=$variants, id=$id, name=$name, additionalProperties=$additionalProperties}" +} diff --git a/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyExperimentVariant.kt b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyExperimentVariant.kt new file mode 100644 index 00000000..2290dcbe --- /dev/null +++ b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyExperimentVariant.kt @@ -0,0 +1,297 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.ExcludeMissing +import com.courier.core.JsonField +import com.courier.core.JsonMissing +import com.courier.core.JsonValue +import com.courier.core.checkRequired +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 java.util.Collections +import java.util.Objects +import java.util.Optional + +/** + * A single weighted arm of an experiment. Variant ids must be unique within the experiment and the + * sum of all variant weights must be greater than 0. Weights are relative (no sum-to-100 + * requirement) — routing normalizes them proportionally. + */ +class JourneyExperimentVariant +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val id: JsonField, + private val templateId: JsonField, + private val weight: JsonField, + private val name: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("id") @ExcludeMissing id: JsonField = JsonMissing.of(), + @JsonProperty("templateId") + @ExcludeMissing + templateId: JsonField = JsonMissing.of(), + @JsonProperty("weight") @ExcludeMissing weight: JsonField = JsonMissing.of(), + @JsonProperty("name") @ExcludeMissing name: JsonField = JsonMissing.of(), + ) : this(id, templateId, weight, name, 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 id(): String = id.getRequired("id") + + /** + * The notification template sent for this variant. + * + * @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 templateId(): String = templateId.getRequired("templateId") + + /** + * Relative routing weight. Must be non-negative. + * + * @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 weight(): Double = weight.getRequired("weight") + + /** + * Optional, cosmetic display name for the variant. + * + * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun name(): Optional = name.getOptional("name") + + /** + * Returns the raw JSON value of [id]. + * + * Unlike [id], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("id") @ExcludeMissing fun _id(): JsonField = id + + /** + * Returns the raw JSON value of [templateId]. + * + * Unlike [templateId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("templateId") @ExcludeMissing fun _templateId(): JsonField = templateId + + /** + * Returns the raw JSON value of [weight]. + * + * Unlike [weight], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("weight") @ExcludeMissing fun _weight(): JsonField = weight + + /** + * Returns the raw JSON value of [name]. + * + * Unlike [name], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("name") @ExcludeMissing fun _name(): JsonField = name + + @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 [JourneyExperimentVariant]. + * + * The following fields are required: + * ```java + * .id() + * .templateId() + * .weight() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [JourneyExperimentVariant]. */ + class Builder internal constructor() { + + private var id: JsonField? = null + private var templateId: JsonField? = null + private var weight: JsonField? = null + private var name: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(journeyExperimentVariant: JourneyExperimentVariant) = apply { + id = journeyExperimentVariant.id + templateId = journeyExperimentVariant.templateId + weight = journeyExperimentVariant.weight + name = journeyExperimentVariant.name + additionalProperties = journeyExperimentVariant.additionalProperties.toMutableMap() + } + + fun id(id: String) = id(JsonField.of(id)) + + /** + * Sets [Builder.id] to an arbitrary JSON value. + * + * You should usually call [Builder.id] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun id(id: JsonField) = apply { this.id = id } + + /** The notification template sent for this variant. */ + fun templateId(templateId: String) = templateId(JsonField.of(templateId)) + + /** + * Sets [Builder.templateId] to an arbitrary JSON value. + * + * You should usually call [Builder.templateId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun templateId(templateId: JsonField) = apply { this.templateId = templateId } + + /** Relative routing weight. Must be non-negative. */ + fun weight(weight: Double) = weight(JsonField.of(weight)) + + /** + * Sets [Builder.weight] to an arbitrary JSON value. + * + * You should usually call [Builder.weight] with a well-typed [Double] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun weight(weight: JsonField) = apply { this.weight = weight } + + /** Optional, cosmetic display name for the variant. */ + fun name(name: String) = name(JsonField.of(name)) + + /** + * Sets [Builder.name] to an arbitrary JSON value. + * + * You should usually call [Builder.name] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun name(name: JsonField) = apply { this.name = name } + + 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 [JourneyExperimentVariant]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .id() + * .templateId() + * .weight() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): JourneyExperimentVariant = + JourneyExperimentVariant( + checkRequired("id", id), + checkRequired("templateId", templateId), + checkRequired("weight", weight), + name, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws CourierInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): JourneyExperimentVariant = apply { + if (validated) { + return@apply + } + + id() + templateId() + weight() + name() + 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 (id.asKnown().isPresent) 1 else 0) + + (if (templateId.asKnown().isPresent) 1 else 0) + + (if (weight.asKnown().isPresent) 1 else 0) + + (if (name.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is JourneyExperimentVariant && + id == other.id && + templateId == other.templateId && + weight == other.weight && + name == other.name && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(id, templateId, weight, name, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "JourneyExperimentVariant{id=$id, templateId=$templateId, weight=$weight, name=$name, additionalProperties=$additionalProperties}" +} diff --git a/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyNode.kt b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyNode.kt index c7c163fc..2004bf10 100644 --- a/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyNode.kt +++ b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneyNode.kt @@ -68,8 +68,10 @@ private constructor( fun segmentTrigger(): Optional = Optional.ofNullable(segmentTrigger) /** - * Send a notification template to the recipient. Optionally override the recipient address, - * delay the send, or attach `data`. + * Send to the recipient. A send node sources its content from EXACTLY ONE of `message.template` + * (a single notification template) or `experiment` (an A/B split across weighted template + * variants) — supplying both, or neither, is rejected. Optionally override the recipient + * address, delay the send, or attach `data`. */ fun send(): Optional = Optional.ofNullable(send) @@ -172,8 +174,10 @@ private constructor( fun asSegmentTrigger(): JourneySegmentTriggerNode = segmentTrigger.getOrThrow("segmentTrigger") /** - * Send a notification template to the recipient. Optionally override the recipient address, - * delay the send, or attach `data`. + * Send to the recipient. A send node sources its content from EXACTLY ONE of `message.template` + * (a single notification template) or `experiment` (an A/B split across weighted template + * variants) — supplying both, or neither, is rejected. Optionally override the recipient + * address, delay the send, or attach `data`. */ fun asSend(): JourneySendNode = send.getOrThrow("send") @@ -498,8 +502,10 @@ private constructor( JourneyNode(segmentTrigger = segmentTrigger) /** - * Send a notification template to the recipient. Optionally override the recipient address, - * delay the send, or attach `data`. + * Send to the recipient. A send node sources its content from EXACTLY ONE of + * `message.template` (a single notification template) or `experiment` (an A/B split across + * weighted template variants) — supplying both, or neither, is rejected. Optionally + * override the recipient address, delay the send, or attach `data`. */ @JvmStatic fun ofSend(send: JourneySendNode) = JourneyNode(send = send) @@ -591,8 +597,10 @@ private constructor( fun visitSegmentTrigger(segmentTrigger: JourneySegmentTriggerNode): T /** - * Send a notification template to the recipient. Optionally override the recipient address, - * delay the send, or attach `data`. + * Send to the recipient. A send node sources its content from EXACTLY ONE of + * `message.template` (a single notification template) or `experiment` (an A/B split across + * weighted template variants) — supplying both, or neither, is rejected. Optionally + * override the recipient address, delay the send, or attach `data`. */ fun visitSend(send: JourneySendNode): T diff --git a/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneySendNode.kt b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneySendNode.kt index ee3b108e..d2e249ce 100644 --- a/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneySendNode.kt +++ b/courier-java-core/src/main/kotlin/com/courier/models/journeys/JourneySendNode.kt @@ -20,8 +20,10 @@ import java.util.Optional import kotlin.jvm.optionals.getOrNull /** - * Send a notification template to the recipient. Optionally override the recipient address, delay - * the send, or attach `data`. + * Send to the recipient. A send node sources its content from EXACTLY ONE of `message.template` (a + * single notification template) or `experiment` (an A/B split across weighted template variants) — + * supplying both, or neither, is rejected. Optionally override the recipient address, delay the + * send, or attach `data`. */ class JourneySendNode @JsonCreator(mode = JsonCreator.Mode.DISABLED) @@ -30,6 +32,7 @@ private constructor( private val type: JsonField, private val id: JsonField, private val conditions: JsonField, + private val experiment: JsonField, private val additionalProperties: MutableMap, ) { @@ -41,7 +44,10 @@ private constructor( @JsonProperty("conditions") @ExcludeMissing conditions: JsonField = JsonMissing.of(), - ) : this(message, type, id, conditions, mutableMapOf()) + @JsonProperty("experiment") + @ExcludeMissing + experiment: JsonField = JsonMissing.of(), + ) : this(message, type, id, conditions, experiment, mutableMapOf()) /** * @throws CourierInvalidDataException if the JSON field has an unexpected type or is @@ -70,6 +76,16 @@ private constructor( */ fun conditions(): Optional = conditions.getOptional("conditions") + /** + * A/B experiment config for a send node. The recipient is deterministically bucketed by + * `bucketingKey` and routed to one of the `variants` in proportion to its `weight`. Present on + * a send node INSTEAD OF `message.template`. + * + * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun experiment(): Optional = experiment.getOptional("experiment") + /** * Returns the raw JSON value of [message]. * @@ -100,6 +116,15 @@ private constructor( @ExcludeMissing fun _conditions(): JsonField = conditions + /** + * Returns the raw JSON value of [experiment]. + * + * Unlike [experiment], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("experiment") + @ExcludeMissing + fun _experiment(): JsonField = experiment + @JsonAnySetter private fun putAdditionalProperty(key: String, value: JsonValue) { additionalProperties.put(key, value) @@ -133,6 +158,7 @@ private constructor( private var type: JsonField? = null private var id: JsonField = JsonMissing.of() private var conditions: JsonField = JsonMissing.of() + private var experiment: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic @@ -141,6 +167,7 @@ private constructor( type = journeySendNode.type id = journeySendNode.id conditions = journeySendNode.conditions + experiment = journeySendNode.experiment additionalProperties = journeySendNode.additionalProperties.toMutableMap() } @@ -213,6 +240,24 @@ private constructor( fun conditions(conditionNestedGroup: JourneyConditionNestedGroup) = conditions(JourneyConditionsField.ofConditionNestedGroup(conditionNestedGroup)) + /** + * A/B experiment config for a send node. The recipient is deterministically bucketed by + * `bucketingKey` and routed to one of the `variants` in proportion to its `weight`. Present + * on a send node INSTEAD OF `message.template`. + */ + fun experiment(experiment: JourneyExperiment) = experiment(JsonField.of(experiment)) + + /** + * Sets [Builder.experiment] to an arbitrary JSON value. + * + * You should usually call [Builder.experiment] with a well-typed [JourneyExperiment] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun experiment(experiment: JsonField) = apply { + this.experiment = experiment + } + fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() putAllAdditionalProperties(additionalProperties) @@ -251,6 +296,7 @@ private constructor( checkRequired("type", type), id, conditions, + experiment, additionalProperties.toMutableMap(), ) } @@ -274,6 +320,7 @@ private constructor( type().validate() id() conditions().ifPresent { it.validate() } + experiment().ifPresent { it.validate() } validated = true } @@ -295,33 +342,28 @@ private constructor( (message.asKnown().getOrNull()?.validity() ?: 0) + (type.asKnown().getOrNull()?.validity() ?: 0) + (if (id.asKnown().isPresent) 1 else 0) + - (conditions.asKnown().getOrNull()?.validity() ?: 0) + (conditions.asKnown().getOrNull()?.validity() ?: 0) + + (experiment.asKnown().getOrNull()?.validity() ?: 0) class Message @JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( - private val template: JsonField, private val data: JsonField, private val delay: JsonField, + private val template: JsonField, private val to: JsonField, private val additionalProperties: MutableMap, ) { @JsonCreator private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("delay") @ExcludeMissing delay: JsonField = JsonMissing.of(), @JsonProperty("template") @ExcludeMissing template: JsonField = JsonMissing.of(), - @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), - @JsonProperty("delay") @ExcludeMissing delay: JsonField = JsonMissing.of(), @JsonProperty("to") @ExcludeMissing to: JsonField = JsonMissing.of(), - ) : this(template, data, delay, to, 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 template(): String = template.getRequired("template") + ) : this(data, delay, template, to, mutableMapOf()) /** * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the @@ -339,14 +381,13 @@ private constructor( * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the * server responded with an unexpected value). */ - fun to(): Optional = to.getOptional("to") + fun template(): Optional = template.getOptional("template") /** - * Returns the raw JSON value of [template]. - * - * Unlike [template], this method doesn't throw if the JSON field has an unexpected type. + * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). */ - @JsonProperty("template") @ExcludeMissing fun _template(): JsonField = template + fun to(): Optional = to.getOptional("to") /** * Returns the raw JSON value of [data]. @@ -362,6 +403,13 @@ private constructor( */ @JsonProperty("delay") @ExcludeMissing fun _delay(): JsonField = delay + /** + * Returns the raw JSON value of [template]. + * + * Unlike [template], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("template") @ExcludeMissing fun _template(): JsonField = template + /** * Returns the raw JSON value of [to]. * @@ -383,46 +431,28 @@ private constructor( companion object { - /** - * Returns a mutable builder for constructing an instance of [Message]. - * - * The following fields are required: - * ```java - * .template() - * ``` - */ + /** Returns a mutable builder for constructing an instance of [Message]. */ @JvmStatic fun builder() = Builder() } /** A builder for [Message]. */ class Builder internal constructor() { - private var template: JsonField? = null private var data: JsonField = JsonMissing.of() private var delay: JsonField = JsonMissing.of() + private var template: JsonField = JsonMissing.of() private var to: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic internal fun from(message: Message) = apply { - template = message.template data = message.data delay = message.delay + template = message.template to = message.to additionalProperties = message.additionalProperties.toMutableMap() } - fun template(template: String) = template(JsonField.of(template)) - - /** - * Sets [Builder.template] to an arbitrary JSON value. - * - * You should usually call [Builder.template] with a well-typed [String] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun template(template: JsonField) = apply { this.template = template } - fun data(data: Data) = data(JsonField.of(data)) /** @@ -445,6 +475,17 @@ private constructor( */ fun delay(delay: JsonField) = apply { this.delay = delay } + fun template(template: String) = template(JsonField.of(template)) + + /** + * Sets [Builder.template] to an arbitrary JSON value. + * + * You should usually call [Builder.template] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun template(template: JsonField) = apply { this.template = template } + fun to(to: To) = to(JsonField.of(to)) /** @@ -479,22 +520,9 @@ private constructor( * Returns an immutable instance of [Message]. * * Further updates to this [Builder] will not mutate the returned instance. - * - * The following fields are required: - * ```java - * .template() - * ``` - * - * @throws IllegalStateException if any required field is unset. */ fun build(): Message = - Message( - checkRequired("template", template), - data, - delay, - to, - additionalProperties.toMutableMap(), - ) + Message(data, delay, template, to, additionalProperties.toMutableMap()) } private var validated: Boolean = false @@ -513,9 +541,9 @@ private constructor( return@apply } - template() data().ifPresent { it.validate() } delay().ifPresent { it.validate() } + template() to().ifPresent { it.validate() } validated = true } @@ -536,9 +564,9 @@ private constructor( */ @JvmSynthetic internal fun validity(): Int = - (if (template.asKnown().isPresent) 1 else 0) + - (data.asKnown().getOrNull()?.validity() ?: 0) + + (data.asKnown().getOrNull()?.validity() ?: 0) + (delay.asKnown().getOrNull()?.validity() ?: 0) + + (if (template.asKnown().isPresent) 1 else 0) + (to.asKnown().getOrNull()?.validity() ?: 0) class Data @@ -1121,21 +1149,21 @@ private constructor( } return other is Message && - template == other.template && data == other.data && delay == other.delay && + template == other.template && to == other.to && additionalProperties == other.additionalProperties } private val hashCode: Int by lazy { - Objects.hash(template, data, delay, to, additionalProperties) + Objects.hash(data, delay, template, to, additionalProperties) } override fun hashCode(): Int = hashCode override fun toString() = - "Message{template=$template, data=$data, delay=$delay, to=$to, additionalProperties=$additionalProperties}" + "Message{data=$data, delay=$delay, template=$template, to=$to, additionalProperties=$additionalProperties}" } class Type @JsonCreator private constructor(private val value: JsonField) : Enum { @@ -1276,15 +1304,16 @@ private constructor( type == other.type && id == other.id && conditions == other.conditions && + experiment == other.experiment && additionalProperties == other.additionalProperties } private val hashCode: Int by lazy { - Objects.hash(message, type, id, conditions, additionalProperties) + Objects.hash(message, type, id, conditions, experiment, additionalProperties) } override fun hashCode(): Int = hashCode override fun toString() = - "JourneySendNode{message=$message, type=$type, id=$id, conditions=$conditions, additionalProperties=$additionalProperties}" + "JourneySendNode{message=$message, type=$type, id=$id, conditions=$conditions, experiment=$experiment, additionalProperties=$additionalProperties}" } diff --git a/courier-java-core/src/main/kotlin/com/courier/services/async/JourneyServiceAsync.kt b/courier-java-core/src/main/kotlin/com/courier/services/async/JourneyServiceAsync.kt index c7e0e767..61f2079f 100644 --- a/courier-java-core/src/main/kotlin/com/courier/services/async/JourneyServiceAsync.kt +++ b/courier-java-core/src/main/kotlin/com/courier/services/async/JourneyServiceAsync.kt @@ -6,8 +6,11 @@ import com.courier.core.ClientOptions import com.courier.core.RequestOptions import com.courier.core.http.HttpResponse import com.courier.core.http.HttpResponseFor +import com.courier.models.journeys.CancelJourneyRequest +import com.courier.models.journeys.CancelJourneyResponse import com.courier.models.journeys.CreateJourneyRequest import com.courier.models.journeys.JourneyArchiveParams +import com.courier.models.journeys.JourneyCancelParams import com.courier.models.journeys.JourneyCreateParams import com.courier.models.journeys.JourneyInvokeParams import com.courier.models.journeys.JourneyListParams @@ -160,6 +163,62 @@ interface JourneyServiceAsync { fun archive(templateId: String, requestOptions: RequestOptions): CompletableFuture = archive(templateId, JourneyArchiveParams.none(), requestOptions) + /** + * Cancel journey runs. The request body must contain EXACTLY ONE of `cancelation_token` + * (cancels every run associated with the token) or `run_id` (cancels a single tenant-scoped + * run). Supplying both or neither is a `400`. A `run_id` that does not exist for the caller's + * tenant returns `404`. Cancelation is idempotent and non-clobbering: a run that has already + * finished (`PROCESSED`/`ERROR`) or was already `CANCELED` is left untouched and its current + * status is echoed back. + */ + fun cancel(params: JourneyCancelParams): CompletableFuture = + cancel(params, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + params: JourneyCancelParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** @see cancel */ + fun cancel( + cancelJourneyRequest: CancelJourneyRequest, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + cancel( + JourneyCancelParams.builder().cancelJourneyRequest(cancelJourneyRequest).build(), + requestOptions, + ) + + /** @see cancel */ + fun cancel( + cancelJourneyRequest: CancelJourneyRequest + ): CompletableFuture = + cancel(cancelJourneyRequest, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + byCancelationToken: CancelJourneyRequest.ByCancelationToken, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + cancel(CancelJourneyRequest.ofByCancelationToken(byCancelationToken), requestOptions) + + /** @see cancel */ + fun cancel( + byCancelationToken: CancelJourneyRequest.ByCancelationToken + ): CompletableFuture = cancel(byCancelationToken, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + byRunId: CancelJourneyRequest.ByRunId, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + cancel(CancelJourneyRequest.ofByRunId(byRunId), requestOptions) + + /** @see cancel */ + fun cancel(byRunId: CancelJourneyRequest.ByRunId): CompletableFuture = + cancel(byRunId, RequestOptions.none()) + /** * Invoke a journey by id or alias to start a new run. The response includes a `runId` * identifying the run. @@ -442,6 +501,63 @@ interface JourneyServiceAsync { ): CompletableFuture = archive(templateId, JourneyArchiveParams.none(), requestOptions) + /** + * Returns a raw HTTP response for `post /journeys/cancel`, but is otherwise the same as + * [JourneyServiceAsync.cancel]. + */ + fun cancel( + params: JourneyCancelParams + ): CompletableFuture> = + cancel(params, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + params: JourneyCancelParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** @see cancel */ + fun cancel( + cancelJourneyRequest: CancelJourneyRequest, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + cancel( + JourneyCancelParams.builder().cancelJourneyRequest(cancelJourneyRequest).build(), + requestOptions, + ) + + /** @see cancel */ + fun cancel( + cancelJourneyRequest: CancelJourneyRequest + ): CompletableFuture> = + cancel(cancelJourneyRequest, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + byCancelationToken: CancelJourneyRequest.ByCancelationToken, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + cancel(CancelJourneyRequest.ofByCancelationToken(byCancelationToken), requestOptions) + + /** @see cancel */ + fun cancel( + byCancelationToken: CancelJourneyRequest.ByCancelationToken + ): CompletableFuture> = + cancel(byCancelationToken, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + byRunId: CancelJourneyRequest.ByRunId, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + cancel(CancelJourneyRequest.ofByRunId(byRunId), requestOptions) + + /** @see cancel */ + fun cancel( + byRunId: CancelJourneyRequest.ByRunId + ): CompletableFuture> = + cancel(byRunId, RequestOptions.none()) + /** * Returns a raw HTTP response for `post /journeys/{templateId}/invoke`, but is otherwise * the same as [JourneyServiceAsync.invoke]. diff --git a/courier-java-core/src/main/kotlin/com/courier/services/async/JourneyServiceAsyncImpl.kt b/courier-java-core/src/main/kotlin/com/courier/services/async/JourneyServiceAsyncImpl.kt index 85afc2ec..2873bfc5 100644 --- a/courier-java-core/src/main/kotlin/com/courier/services/async/JourneyServiceAsyncImpl.kt +++ b/courier-java-core/src/main/kotlin/com/courier/services/async/JourneyServiceAsyncImpl.kt @@ -17,7 +17,9 @@ import com.courier.core.http.HttpResponseFor import com.courier.core.http.json import com.courier.core.http.parseable import com.courier.core.prepareAsync +import com.courier.models.journeys.CancelJourneyResponse import com.courier.models.journeys.JourneyArchiveParams +import com.courier.models.journeys.JourneyCancelParams import com.courier.models.journeys.JourneyCreateParams import com.courier.models.journeys.JourneyInvokeParams import com.courier.models.journeys.JourneyListParams @@ -79,6 +81,13 @@ class JourneyServiceAsyncImpl internal constructor(private val clientOptions: Cl // delete /journeys/{templateId} withRawResponse().archive(params, requestOptions).thenAccept {} + override fun cancel( + params: JourneyCancelParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // post /journeys/cancel + withRawResponse().cancel(params, requestOptions).thenApply { it.parse() } + override fun invoke( params: JourneyInvokeParams, requestOptions: RequestOptions, @@ -247,6 +256,37 @@ class JourneyServiceAsyncImpl internal constructor(private val clientOptions: Cl } } + private val cancelHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun cancel( + params: JourneyCancelParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("journeys", "cancel") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { cancelHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + private val invokeHandler: Handler = jsonHandler(clientOptions.jsonMapper) diff --git a/courier-java-core/src/main/kotlin/com/courier/services/blocking/JourneyService.kt b/courier-java-core/src/main/kotlin/com/courier/services/blocking/JourneyService.kt index 9d7484d6..db2d3311 100644 --- a/courier-java-core/src/main/kotlin/com/courier/services/blocking/JourneyService.kt +++ b/courier-java-core/src/main/kotlin/com/courier/services/blocking/JourneyService.kt @@ -6,8 +6,11 @@ import com.courier.core.ClientOptions import com.courier.core.RequestOptions import com.courier.core.http.HttpResponse import com.courier.core.http.HttpResponseFor +import com.courier.models.journeys.CancelJourneyRequest +import com.courier.models.journeys.CancelJourneyResponse import com.courier.models.journeys.CreateJourneyRequest import com.courier.models.journeys.JourneyArchiveParams +import com.courier.models.journeys.JourneyCancelParams import com.courier.models.journeys.JourneyCreateParams import com.courier.models.journeys.JourneyInvokeParams import com.courier.models.journeys.JourneyListParams @@ -149,6 +152,58 @@ interface JourneyService { fun archive(templateId: String, requestOptions: RequestOptions) = archive(templateId, JourneyArchiveParams.none(), requestOptions) + /** + * Cancel journey runs. The request body must contain EXACTLY ONE of `cancelation_token` + * (cancels every run associated with the token) or `run_id` (cancels a single tenant-scoped + * run). Supplying both or neither is a `400`. A `run_id` that does not exist for the caller's + * tenant returns `404`. Cancelation is idempotent and non-clobbering: a run that has already + * finished (`PROCESSED`/`ERROR`) or was already `CANCELED` is left untouched and its current + * status is echoed back. + */ + fun cancel(params: JourneyCancelParams): CancelJourneyResponse = + cancel(params, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + params: JourneyCancelParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CancelJourneyResponse + + /** @see cancel */ + fun cancel( + cancelJourneyRequest: CancelJourneyRequest, + requestOptions: RequestOptions = RequestOptions.none(), + ): CancelJourneyResponse = + cancel( + JourneyCancelParams.builder().cancelJourneyRequest(cancelJourneyRequest).build(), + requestOptions, + ) + + /** @see cancel */ + fun cancel(cancelJourneyRequest: CancelJourneyRequest): CancelJourneyResponse = + cancel(cancelJourneyRequest, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + byCancelationToken: CancelJourneyRequest.ByCancelationToken, + requestOptions: RequestOptions = RequestOptions.none(), + ): CancelJourneyResponse = + cancel(CancelJourneyRequest.ofByCancelationToken(byCancelationToken), requestOptions) + + /** @see cancel */ + fun cancel(byCancelationToken: CancelJourneyRequest.ByCancelationToken): CancelJourneyResponse = + cancel(byCancelationToken, RequestOptions.none()) + + /** @see cancel */ + fun cancel( + byRunId: CancelJourneyRequest.ByRunId, + requestOptions: RequestOptions = RequestOptions.none(), + ): CancelJourneyResponse = cancel(CancelJourneyRequest.ofByRunId(byRunId), requestOptions) + + /** @see cancel */ + fun cancel(byRunId: CancelJourneyRequest.ByRunId): CancelJourneyResponse = + cancel(byRunId, RequestOptions.none()) + /** * Invoke a journey by id or alias to start a new run. The response includes a `runId` * identifying the run. @@ -421,6 +476,67 @@ interface JourneyService { fun archive(templateId: String, requestOptions: RequestOptions): HttpResponse = archive(templateId, JourneyArchiveParams.none(), requestOptions) + /** + * Returns a raw HTTP response for `post /journeys/cancel`, but is otherwise the same as + * [JourneyService.cancel]. + */ + @MustBeClosed + fun cancel(params: JourneyCancelParams): HttpResponseFor = + cancel(params, RequestOptions.none()) + + /** @see cancel */ + @MustBeClosed + fun cancel( + params: JourneyCancelParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** @see cancel */ + @MustBeClosed + fun cancel( + cancelJourneyRequest: CancelJourneyRequest, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + cancel( + JourneyCancelParams.builder().cancelJourneyRequest(cancelJourneyRequest).build(), + requestOptions, + ) + + /** @see cancel */ + @MustBeClosed + fun cancel( + cancelJourneyRequest: CancelJourneyRequest + ): HttpResponseFor = + cancel(cancelJourneyRequest, RequestOptions.none()) + + /** @see cancel */ + @MustBeClosed + fun cancel( + byCancelationToken: CancelJourneyRequest.ByCancelationToken, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + cancel(CancelJourneyRequest.ofByCancelationToken(byCancelationToken), requestOptions) + + /** @see cancel */ + @MustBeClosed + fun cancel( + byCancelationToken: CancelJourneyRequest.ByCancelationToken + ): HttpResponseFor = + cancel(byCancelationToken, RequestOptions.none()) + + /** @see cancel */ + @MustBeClosed + fun cancel( + byRunId: CancelJourneyRequest.ByRunId, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + cancel(CancelJourneyRequest.ofByRunId(byRunId), requestOptions) + + /** @see cancel */ + @MustBeClosed + fun cancel(byRunId: CancelJourneyRequest.ByRunId): HttpResponseFor = + cancel(byRunId, RequestOptions.none()) + /** * Returns a raw HTTP response for `post /journeys/{templateId}/invoke`, but is otherwise * the same as [JourneyService.invoke]. diff --git a/courier-java-core/src/main/kotlin/com/courier/services/blocking/JourneyServiceImpl.kt b/courier-java-core/src/main/kotlin/com/courier/services/blocking/JourneyServiceImpl.kt index 32d25f82..52471954 100644 --- a/courier-java-core/src/main/kotlin/com/courier/services/blocking/JourneyServiceImpl.kt +++ b/courier-java-core/src/main/kotlin/com/courier/services/blocking/JourneyServiceImpl.kt @@ -17,7 +17,9 @@ import com.courier.core.http.HttpResponseFor import com.courier.core.http.json import com.courier.core.http.parseable import com.courier.core.prepare +import com.courier.models.journeys.CancelJourneyResponse import com.courier.models.journeys.JourneyArchiveParams +import com.courier.models.journeys.JourneyCancelParams import com.courier.models.journeys.JourneyCreateParams import com.courier.models.journeys.JourneyInvokeParams import com.courier.models.journeys.JourneyListParams @@ -76,6 +78,13 @@ class JourneyServiceImpl internal constructor(private val clientOptions: ClientO withRawResponse().archive(params, requestOptions) } + override fun cancel( + params: JourneyCancelParams, + requestOptions: RequestOptions, + ): CancelJourneyResponse = + // post /journeys/cancel + withRawResponse().cancel(params, requestOptions).parse() + override fun invoke( params: JourneyInvokeParams, requestOptions: RequestOptions, @@ -232,6 +241,34 @@ class JourneyServiceImpl internal constructor(private val clientOptions: ClientO } } + private val cancelHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun cancel( + params: JourneyCancelParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("journeys", "cancel") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { cancelHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + private val invokeHandler: Handler = jsonHandler(clientOptions.jsonMapper) diff --git a/courier-java-core/src/test/kotlin/com/courier/models/journeys/CancelJourneyRequestTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/journeys/CancelJourneyRequestTest.kt new file mode 100644 index 00000000..c0035ef2 --- /dev/null +++ b/courier-java-core/src/test/kotlin/com/courier/models/journeys/CancelJourneyRequestTest.kt @@ -0,0 +1,89 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.JsonValue +import com.courier.core.jsonMapper +import com.courier.errors.CourierInvalidDataException +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +internal class CancelJourneyRequestTest { + + @Test + fun ofByCancelationToken() { + val byCancelationToken = + CancelJourneyRequest.ByCancelationToken.builder().cancelationToken("x").build() + + val cancelJourneyRequest = CancelJourneyRequest.ofByCancelationToken(byCancelationToken) + + assertThat(cancelJourneyRequest.byCancelationToken()).contains(byCancelationToken) + assertThat(cancelJourneyRequest.byRunId()).isEmpty + } + + @Test + fun ofByCancelationTokenRoundtrip() { + val jsonMapper = jsonMapper() + val cancelJourneyRequest = + CancelJourneyRequest.ofByCancelationToken( + CancelJourneyRequest.ByCancelationToken.builder().cancelationToken("x").build() + ) + + val roundtrippedCancelJourneyRequest = + jsonMapper.readValue( + jsonMapper.writeValueAsString(cancelJourneyRequest), + jacksonTypeRef(), + ) + + assertThat(roundtrippedCancelJourneyRequest).isEqualTo(cancelJourneyRequest) + } + + @Test + fun ofByRunId() { + val byRunId = CancelJourneyRequest.ByRunId.builder().runId("x").build() + + val cancelJourneyRequest = CancelJourneyRequest.ofByRunId(byRunId) + + assertThat(cancelJourneyRequest.byCancelationToken()).isEmpty + assertThat(cancelJourneyRequest.byRunId()).contains(byRunId) + } + + @Test + fun ofByRunIdRoundtrip() { + val jsonMapper = jsonMapper() + val cancelJourneyRequest = + CancelJourneyRequest.ofByRunId( + CancelJourneyRequest.ByRunId.builder().runId("x").build() + ) + + val roundtrippedCancelJourneyRequest = + jsonMapper.readValue( + jsonMapper.writeValueAsString(cancelJourneyRequest), + jacksonTypeRef(), + ) + + assertThat(roundtrippedCancelJourneyRequest).isEqualTo(cancelJourneyRequest) + } + + enum class IncompatibleJsonShapeTestCase(val value: JsonValue) { + BOOLEAN(JsonValue.from(false)), + STRING(JsonValue.from("invalid")), + INTEGER(JsonValue.from(-1)), + FLOAT(JsonValue.from(3.14)), + ARRAY(JsonValue.from(listOf("invalid", "array"))), + } + + @ParameterizedTest + @EnumSource + fun incompatibleJsonShapeDeserializesToUnknown(testCase: IncompatibleJsonShapeTestCase) { + val cancelJourneyRequest = + jsonMapper().convertValue(testCase.value, jacksonTypeRef()) + + val e = assertThrows { cancelJourneyRequest.validate() } + assertThat(e).hasMessageStartingWith("Unknown ") + } +} diff --git a/courier-java-core/src/test/kotlin/com/courier/models/journeys/CancelJourneyResponseTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/journeys/CancelJourneyResponseTest.kt new file mode 100644 index 00000000..6dca08a0 --- /dev/null +++ b/courier-java-core/src/test/kotlin/com/courier/models/journeys/CancelJourneyResponseTest.kt @@ -0,0 +1,94 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.JsonValue +import com.courier.core.jsonMapper +import com.courier.errors.CourierInvalidDataException +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +internal class CancelJourneyResponseTest { + + @Test + fun ofTokenBranch() { + val tokenBranch = + CancelJourneyResponse.TokenBranch.builder() + .cancelationToken("cancelation_token") + .build() + + val cancelJourneyResponse = CancelJourneyResponse.ofTokenBranch(tokenBranch) + + assertThat(cancelJourneyResponse.tokenBranch()).contains(tokenBranch) + assertThat(cancelJourneyResponse.runIdBranch()).isEmpty + } + + @Test + fun ofTokenBranchRoundtrip() { + val jsonMapper = jsonMapper() + val cancelJourneyResponse = + CancelJourneyResponse.ofTokenBranch( + CancelJourneyResponse.TokenBranch.builder() + .cancelationToken("cancelation_token") + .build() + ) + + val roundtrippedCancelJourneyResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(cancelJourneyResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedCancelJourneyResponse).isEqualTo(cancelJourneyResponse) + } + + @Test + fun ofRunIdBranch() { + val runIdBranch = + CancelJourneyResponse.RunIdBranch.builder().runId("run_id").status("status").build() + + val cancelJourneyResponse = CancelJourneyResponse.ofRunIdBranch(runIdBranch) + + assertThat(cancelJourneyResponse.tokenBranch()).isEmpty + assertThat(cancelJourneyResponse.runIdBranch()).contains(runIdBranch) + } + + @Test + fun ofRunIdBranchRoundtrip() { + val jsonMapper = jsonMapper() + val cancelJourneyResponse = + CancelJourneyResponse.ofRunIdBranch( + CancelJourneyResponse.RunIdBranch.builder().runId("run_id").status("status").build() + ) + + val roundtrippedCancelJourneyResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(cancelJourneyResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedCancelJourneyResponse).isEqualTo(cancelJourneyResponse) + } + + enum class IncompatibleJsonShapeTestCase(val value: JsonValue) { + BOOLEAN(JsonValue.from(false)), + STRING(JsonValue.from("invalid")), + INTEGER(JsonValue.from(-1)), + FLOAT(JsonValue.from(3.14)), + ARRAY(JsonValue.from(listOf("invalid", "array"))), + } + + @ParameterizedTest + @EnumSource + fun incompatibleJsonShapeDeserializesToUnknown(testCase: IncompatibleJsonShapeTestCase) { + val cancelJourneyResponse = + jsonMapper().convertValue(testCase.value, jacksonTypeRef()) + + val e = assertThrows { cancelJourneyResponse.validate() } + assertThat(e).hasMessageStartingWith("Unknown ") + } +} diff --git a/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyCancelParamsTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyCancelParamsTest.kt new file mode 100644 index 00000000..edb7b0c0 --- /dev/null +++ b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyCancelParamsTest.kt @@ -0,0 +1,43 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class JourneyCancelParamsTest { + + @Test + fun create() { + JourneyCancelParams.builder() + .cancelJourneyRequest( + CancelJourneyRequest.ByCancelationToken.builder() + .cancelationToken("order-1234") + .build() + ) + .build() + } + + @Test + fun body() { + val params = + JourneyCancelParams.builder() + .cancelJourneyRequest( + CancelJourneyRequest.ByCancelationToken.builder() + .cancelationToken("order-1234") + .build() + ) + .build() + + val body = params._body() + + assertThat(body) + .isEqualTo( + CancelJourneyRequest.ofByCancelationToken( + CancelJourneyRequest.ByCancelationToken.builder() + .cancelationToken("order-1234") + .build() + ) + ) + } +} diff --git a/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyExperimentTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyExperimentTest.kt new file mode 100644 index 00000000..3c64b6ce --- /dev/null +++ b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyExperimentTest.kt @@ -0,0 +1,91 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class JourneyExperimentTest { + + @Test + fun create() { + val journeyExperiment = + JourneyExperiment.builder() + .bucketingKey("x") + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .id("x") + .name("name") + .build() + + assertThat(journeyExperiment.bucketingKey()).isEqualTo("x") + assertThat(journeyExperiment.variants()) + .containsExactly( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build(), + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build(), + ) + assertThat(journeyExperiment.id()).contains("x") + assertThat(journeyExperiment.name()).contains("name") + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val journeyExperiment = + JourneyExperiment.builder() + .bucketingKey("x") + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .id("x") + .name("name") + .build() + + val roundtrippedJourneyExperiment = + jsonMapper.readValue( + jsonMapper.writeValueAsString(journeyExperiment), + jacksonTypeRef(), + ) + + assertThat(roundtrippedJourneyExperiment).isEqualTo(journeyExperiment) + } +} diff --git a/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyExperimentVariantTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyExperimentVariantTest.kt new file mode 100644 index 00000000..bf745490 --- /dev/null +++ b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyExperimentVariantTest.kt @@ -0,0 +1,47 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.courier.models.journeys + +import com.courier.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class JourneyExperimentVariantTest { + + @Test + fun create() { + val journeyExperimentVariant = + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + + assertThat(journeyExperimentVariant.id()).isEqualTo("x") + assertThat(journeyExperimentVariant.templateId()).isEqualTo("x") + assertThat(journeyExperimentVariant.weight()).isEqualTo(0.0) + assertThat(journeyExperimentVariant.name()).contains("name") + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val journeyExperimentVariant = + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + + val roundtrippedJourneyExperimentVariant = + jsonMapper.readValue( + jsonMapper.writeValueAsString(journeyExperimentVariant), + jacksonTypeRef(), + ) + + assertThat(roundtrippedJourneyExperimentVariant).isEqualTo(journeyExperimentVariant) + } +} diff --git a/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyNodeTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyNodeTest.kt index 92ab4947..853b261a 100644 --- a/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyNodeTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneyNodeTest.kt @@ -134,7 +134,6 @@ internal class JourneyNodeTest { JourneySendNode.builder() .message( JourneySendNode.Message.builder() - .template("x") .data( JourneySendNode.Message.Data.builder() .putAdditionalProperty("foo", JsonValue.from("bar")) @@ -143,6 +142,7 @@ internal class JourneyNodeTest { .delay( JourneySendNode.Message.Delay.builder().until("x").timezone("x").build() ) + .template("x") .to( JourneySendNode.Message.To.builder() .emailOverride("x") @@ -155,6 +155,29 @@ internal class JourneyNodeTest { .type(JourneySendNode.Type.SEND) .id("x") .conditionsOfConditionAtom(listOf("string", "string")) + .experiment( + JourneyExperiment.builder() + .bucketingKey("x") + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .id("x") + .name("name") + .build() + ) .build() val journeyNode = JourneyNode.ofSend(send) @@ -183,7 +206,6 @@ internal class JourneyNodeTest { JourneySendNode.builder() .message( JourneySendNode.Message.builder() - .template("x") .data( JourneySendNode.Message.Data.builder() .putAdditionalProperty("foo", JsonValue.from("bar")) @@ -195,6 +217,7 @@ internal class JourneyNodeTest { .timezone("x") .build() ) + .template("x") .to( JourneySendNode.Message.To.builder() .emailOverride("x") @@ -207,6 +230,29 @@ internal class JourneyNodeTest { .type(JourneySendNode.Type.SEND) .id("x") .conditionsOfConditionAtom(listOf("string", "string")) + .experiment( + JourneyExperiment.builder() + .bucketingKey("x") + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .id("x") + .name("name") + .build() + ) .build() ) diff --git a/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneySendNodeTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneySendNodeTest.kt index f8b6e72e..95dc2708 100644 --- a/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneySendNodeTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/models/journeys/JourneySendNodeTest.kt @@ -16,7 +16,6 @@ internal class JourneySendNodeTest { JourneySendNode.builder() .message( JourneySendNode.Message.builder() - .template("x") .data( JourneySendNode.Message.Data.builder() .putAdditionalProperty("foo", JsonValue.from("bar")) @@ -25,6 +24,7 @@ internal class JourneySendNodeTest { .delay( JourneySendNode.Message.Delay.builder().until("x").timezone("x").build() ) + .template("x") .to( JourneySendNode.Message.To.builder() .emailOverride("x") @@ -37,18 +37,41 @@ internal class JourneySendNodeTest { .type(JourneySendNode.Type.SEND) .id("x") .conditionsOfConditionAtom(listOf("string", "string")) + .experiment( + JourneyExperiment.builder() + .bucketingKey("x") + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .id("x") + .name("name") + .build() + ) .build() assertThat(journeySendNode.message()) .isEqualTo( JourneySendNode.Message.builder() - .template("x") .data( JourneySendNode.Message.Data.builder() .putAdditionalProperty("foo", JsonValue.from("bar")) .build() ) .delay(JourneySendNode.Message.Delay.builder().until("x").timezone("x").build()) + .template("x") .to( JourneySendNode.Message.To.builder() .emailOverride("x") @@ -62,6 +85,30 @@ internal class JourneySendNodeTest { assertThat(journeySendNode.id()).contains("x") assertThat(journeySendNode.conditions()) .contains(JourneyConditionsField.ofConditionAtom(listOf("string", "string"))) + assertThat(journeySendNode.experiment()) + .contains( + JourneyExperiment.builder() + .bucketingKey("x") + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .id("x") + .name("name") + .build() + ) } @Test @@ -71,7 +118,6 @@ internal class JourneySendNodeTest { JourneySendNode.builder() .message( JourneySendNode.Message.builder() - .template("x") .data( JourneySendNode.Message.Data.builder() .putAdditionalProperty("foo", JsonValue.from("bar")) @@ -80,6 +126,7 @@ internal class JourneySendNodeTest { .delay( JourneySendNode.Message.Delay.builder().until("x").timezone("x").build() ) + .template("x") .to( JourneySendNode.Message.To.builder() .emailOverride("x") @@ -92,6 +139,29 @@ internal class JourneySendNodeTest { .type(JourneySendNode.Type.SEND) .id("x") .conditionsOfConditionAtom(listOf("string", "string")) + .experiment( + JourneyExperiment.builder() + .bucketingKey("x") + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .addVariant( + JourneyExperimentVariant.builder() + .id("x") + .templateId("x") + .weight(0.0) + .name("name") + .build() + ) + .id("x") + .name("name") + .build() + ) .build() val roundtrippedJourneySendNode = diff --git a/courier-java-core/src/test/kotlin/com/courier/services/async/JourneyServiceAsyncTest.kt b/courier-java-core/src/test/kotlin/com/courier/services/async/JourneyServiceAsyncTest.kt index 00da17a7..34878453 100644 --- a/courier-java-core/src/test/kotlin/com/courier/services/async/JourneyServiceAsyncTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/services/async/JourneyServiceAsyncTest.kt @@ -4,6 +4,7 @@ package com.courier.services.async import com.courier.client.okhttp.CourierOkHttpClientAsync import com.courier.core.JsonValue +import com.courier.models.journeys.CancelJourneyRequest import com.courier.models.journeys.CreateJourneyRequest import com.courier.models.journeys.JourneyApiInvokeTriggerNode import com.courier.models.journeys.JourneyInvokeParams @@ -108,6 +109,23 @@ internal class JourneyServiceAsyncTest { val response = future.get() } + @Disabled("Mock server tests are disabled") + @Test + fun cancel() { + val client = CourierOkHttpClientAsync.builder().apiKey("My API Key").build() + val journeyServiceAsync = client.journeys() + + val cancelJourneyResponseFuture = + journeyServiceAsync.cancel( + CancelJourneyRequest.ByCancelationToken.builder() + .cancelationToken("order-1234") + .build() + ) + + val cancelJourneyResponse = cancelJourneyResponseFuture.get() + cancelJourneyResponse.validate() + } + @Disabled("Mock server tests are disabled") @Test fun invoke() { diff --git a/courier-java-core/src/test/kotlin/com/courier/services/blocking/JourneyServiceTest.kt b/courier-java-core/src/test/kotlin/com/courier/services/blocking/JourneyServiceTest.kt index 5e946adf..9e8d9996 100644 --- a/courier-java-core/src/test/kotlin/com/courier/services/blocking/JourneyServiceTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/services/blocking/JourneyServiceTest.kt @@ -4,6 +4,7 @@ package com.courier.services.blocking import com.courier.client.okhttp.CourierOkHttpClient import com.courier.core.JsonValue +import com.courier.models.journeys.CancelJourneyRequest import com.courier.models.journeys.CreateJourneyRequest import com.courier.models.journeys.JourneyApiInvokeTriggerNode import com.courier.models.journeys.JourneyInvokeParams @@ -103,6 +104,22 @@ internal class JourneyServiceTest { journeyService.archive("x") } + @Disabled("Mock server tests are disabled") + @Test + fun cancel() { + val client = CourierOkHttpClient.builder().apiKey("My API Key").build() + val journeyService = client.journeys() + + val cancelJourneyResponse = + journeyService.cancel( + CancelJourneyRequest.ByCancelationToken.builder() + .cancelationToken("order-1234") + .build() + ) + + cancelJourneyResponse.validate() + } + @Disabled("Mock server tests are disabled") @Test fun invoke() { From e34549359eb5c595b0cfcfc0e3b30162c1231b73 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:51:16 +0000 Subject: [PATCH 2/2] release: 4.19.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ build.gradle.kts | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5866fdcf..647ed9ca 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.18.2" + ".": "4.19.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 024ae8d8..8c5bf813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 4.19.0 (2026-06-29) + +Full Changelog: [v4.18.2...v4.19.0](https://github.com/trycourier/courier-java/compare/v4.18.2...v4.19.0) + +### Features + +* **openapi:** Journeys cancel-by-token endpoint + send-node experiments (C-18986) ([41c9837](https://github.com/trycourier/courier-java/commit/41c9837633b219c112b05446107960558415d3d5)) + ## 4.18.2 (2026-06-25) Full Changelog: [v4.18.1...v4.18.2](https://github.com/trycourier/courier-java/compare/v4.18.1...v4.18.2) diff --git a/build.gradle.kts b/build.gradle.kts index e43b879b..71ca8824 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "com.courier" - version = "4.18.2" // x-release-please-version + version = "4.19.0" // x-release-please-version } subprojects {