diff --git a/courier-java-core/src/main/kotlin/com/courier/models/providers/ProviderPatchParams.kt b/courier-java-core/src/main/kotlin/com/courier/models/providers/ProviderPatchParams.kt index 01b3395b..8085f79b 100644 --- a/courier-java-core/src/main/kotlin/com/courier/models/providers/ProviderPatchParams.kt +++ b/courier-java-core/src/main/kotlin/com/courier/models/providers/ProviderPatchParams.kt @@ -2,11 +2,14 @@ package com.courier.models.providers +import com.courier.core.Enum import com.courier.core.ExcludeMissing import com.courier.core.JsonField import com.courier.core.JsonMissing import com.courier.core.JsonValue import com.courier.core.Params +import com.courier.core.checkKnown +import com.courier.core.checkRequired import com.courier.core.http.Headers import com.courier.core.http.QueryParams import com.courier.core.toImmutable @@ -21,8 +24,9 @@ import java.util.Optional import kotlin.jvm.optionals.getOrNull /** - * Updates part of a provider's configuration and keeps every field you omit. Settings merge into - * the stored settings key by key, and a setting sent as null is removed. + * Applies JSON Patch (RFC 6902) operations to a provider configuration and keeps every field the + * operations don't touch. Paths address the provider as it is returned: /title, /alias and + * /settings/. The operations apply in order, and if one fails, nothing is saved. */ class ProviderPatchParams private constructor( @@ -35,69 +39,23 @@ private constructor( fun id(): Optional = Optional.ofNullable(id) /** - * Updated alias. Set to null to clear it. + * Operations to apply, in order. Paths address the provider as it is returned: `/title`, + * `/alias`, `/provider`, and `/settings/` with the snake_case setting keys, including + * nested paths such as `/settings/rate_limit/rate`. If any operation fails, for example a + * `test`, nothing is saved and the request returns a 400. The result can't change `provider`, + * remove `title`, or add other top-level fields. Removing `/alias` clears it. * - * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). + * @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 alias(): Optional = body.alias() + fun patch(): List = body.patch() /** - * The provider key identifying the type. Optional. When sent, it must match the stored - * provider, otherwise the request returns a 400. + * Returns the raw JSON value of [patch]. * - * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). + * Unlike [patch], this method doesn't throw if the JSON field has an unexpected type. */ - fun provider(): Optional = body.provider() - - /** - * Provider-specific settings (snake_case keys) to merge into the stored settings, using JSON - * Merge Patch (RFC 7396). Each key you send replaces the stored value, a key set to null is - * removed, nested objects merge recursively, and arrays replace the stored value. Keys you omit - * keep their stored values. Set `rate_limit` to null to remove this configuration's send rate - * limit. - * - * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). - */ - fun settings(): Optional = body.settings() - - /** - * Updated display title. - * - * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). - */ - fun title(): Optional = body.title() - - /** - * Returns the raw JSON value of [alias]. - * - * Unlike [alias], this method doesn't throw if the JSON field has an unexpected type. - */ - fun _alias(): JsonField = body._alias() - - /** - * Returns the raw JSON value of [provider]. - * - * Unlike [provider], this method doesn't throw if the JSON field has an unexpected type. - */ - fun _provider(): JsonField = body._provider() - - /** - * Returns the raw JSON value of [settings]. - * - * Unlike [settings], this method doesn't throw if the JSON field has an unexpected type. - */ - fun _settings(): JsonField = body._settings() - - /** - * Returns the raw JSON value of [title]. - * - * Unlike [title], this method doesn't throw if the JSON field has an unexpected type. - */ - fun _title(): JsonField = body._title() + fun _patch(): JsonField> = body._patch() fun _additionalBodyProperties(): Map = body._additionalProperties() @@ -111,9 +69,14 @@ private constructor( companion object { - @JvmStatic fun none(): ProviderPatchParams = builder().build() - - /** Returns a mutable builder for constructing an instance of [ProviderPatchParams]. */ + /** + * Returns a mutable builder for constructing an instance of [ProviderPatchParams]. + * + * The following fields are required: + * ```java + * .patch() + * ``` + */ @JvmStatic fun builder() = Builder() } @@ -143,69 +106,34 @@ private constructor( * * This is generally only useful if you are already constructing the body separately. * Otherwise, it's more convenient to use the top-level setters instead: - * - [alias] - * - [provider] - * - [settings] - * - [title] + * - [patch] */ fun body(body: Body) = apply { this.body = body.toBuilder() } - /** Updated alias. Set to null to clear it. */ - fun alias(alias: String?) = apply { body.alias(alias) } - - /** Alias for calling [Builder.alias] with `alias.orElse(null)`. */ - fun alias(alias: Optional) = alias(alias.getOrNull()) - - /** - * Sets [Builder.alias] to an arbitrary JSON value. - * - * You should usually call [Builder.alias] with a well-typed [String] value instead. This - * method is primarily for setting the field to an undocumented or not yet supported value. - */ - fun alias(alias: JsonField) = apply { body.alias(alias) } - - /** - * The provider key identifying the type. Optional. When sent, it must match the stored - * provider, otherwise the request returns a 400. - */ - fun provider(provider: String) = apply { body.provider(provider) } - /** - * Sets [Builder.provider] to an arbitrary JSON value. - * - * You should usually call [Builder.provider] with a well-typed [String] value instead. This - * method is primarily for setting the field to an undocumented or not yet supported value. + * Operations to apply, in order. Paths address the provider as it is returned: `/title`, + * `/alias`, `/provider`, and `/settings/` with the snake_case setting keys, including + * nested paths such as `/settings/rate_limit/rate`. If any operation fails, for example a + * `test`, nothing is saved and the request returns a 400. The result can't change + * `provider`, remove `title`, or add other top-level fields. Removing `/alias` clears it. */ - fun provider(provider: JsonField) = apply { body.provider(provider) } + fun patch(patch: List) = apply { body.patch(patch) } /** - * Provider-specific settings (snake_case keys) to merge into the stored settings, using - * JSON Merge Patch (RFC 7396). Each key you send replaces the stored value, a key set to - * null is removed, nested objects merge recursively, and arrays replace the stored value. - * Keys you omit keep their stored values. Set `rate_limit` to null to remove this - * configuration's send rate limit. - */ - fun settings(settings: Settings) = apply { body.settings(settings) } - - /** - * Sets [Builder.settings] to an arbitrary JSON value. + * Sets [Builder.patch] to an arbitrary JSON value. * - * You should usually call [Builder.settings] with a well-typed [Settings] value instead. + * You should usually call [Builder.patch] with a well-typed `List` value instead. * This method is primarily for setting the field to an undocumented or not yet supported * value. */ - fun settings(settings: JsonField) = apply { body.settings(settings) } - - /** Updated display title. */ - fun title(title: String) = apply { body.title(title) } + fun patch(patch: JsonField>) = apply { body.patch(patch) } /** - * Sets [Builder.title] to an arbitrary JSON value. + * Adds a single [Patch] to [Builder.patch]. * - * You should usually call [Builder.title] with a well-typed [String] value instead. This - * method is primarily for setting the field to an undocumented or not yet supported value. + * @throws IllegalStateException if the field was previously set to a non-list. */ - fun title(title: JsonField) = apply { body.title(title) } + fun addPatch(patch: Patch) = apply { body.addPatch(patch) } fun additionalBodyProperties(additionalBodyProperties: Map) = apply { body.additionalProperties(additionalBodyProperties) @@ -328,6 +256,13 @@ private constructor( * Returns an immutable instance of [ProviderPatchParams]. * * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .patch() + * ``` + * + * @throws IllegalStateException if any required field is unset. */ fun build(): ProviderPatchParams = ProviderPatchParams( @@ -351,97 +286,39 @@ private constructor( override fun _queryParams(): QueryParams = additionalQueryParams /** - * Request body for a partial update of a provider configuration. Every field is optional, and a - * field you omit keeps its stored value. The body must include at least one of `title`, - * `alias`, or `settings`, otherwise the request returns a 400. The provider type of an existing - * configuration cannot be changed. + * Request body for a partial update of a provider configuration: a list of JSON Patch + * (RFC 6902) operations, the same body as the profile and preference PATCH endpoints. */ class Body @JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( - private val alias: JsonField, - private val provider: JsonField, - private val settings: JsonField, - private val title: JsonField, + private val patch: JsonField>, private val additionalProperties: MutableMap, ) { @JsonCreator private constructor( - @JsonProperty("alias") @ExcludeMissing alias: JsonField = JsonMissing.of(), - @JsonProperty("provider") - @ExcludeMissing - provider: JsonField = JsonMissing.of(), - @JsonProperty("settings") - @ExcludeMissing - settings: JsonField = JsonMissing.of(), - @JsonProperty("title") @ExcludeMissing title: JsonField = JsonMissing.of(), - ) : this(alias, provider, settings, title, mutableMapOf()) - - /** - * Updated alias. Set to null to clear it. - * - * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). - */ - fun alias(): Optional = alias.getOptional("alias") + @JsonProperty("patch") @ExcludeMissing patch: JsonField> = JsonMissing.of() + ) : this(patch, mutableMapOf()) /** - * The provider key identifying the type. Optional. When sent, it must match the stored - * provider, otherwise the request returns a 400. + * Operations to apply, in order. Paths address the provider as it is returned: `/title`, + * `/alias`, `/provider`, and `/settings/` with the snake_case setting keys, including + * nested paths such as `/settings/rate_limit/rate`. If any operation fails, for example a + * `test`, nothing is saved and the request returns a 400. The result can't change + * `provider`, remove `title`, or add other top-level fields. Removing `/alias` clears it. * - * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). + * @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 provider(): Optional = provider.getOptional("provider") + fun patch(): List = patch.getRequired("patch") /** - * Provider-specific settings (snake_case keys) to merge into the stored settings, using - * JSON Merge Patch (RFC 7396). Each key you send replaces the stored value, a key set to - * null is removed, nested objects merge recursively, and arrays replace the stored value. - * Keys you omit keep their stored values. Set `rate_limit` to null to remove this - * configuration's send rate limit. + * Returns the raw JSON value of [patch]. * - * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). + * Unlike [patch], this method doesn't throw if the JSON field has an unexpected type. */ - fun settings(): Optional = settings.getOptional("settings") - - /** - * Updated display title. - * - * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the - * server responded with an unexpected value). - */ - fun title(): Optional = title.getOptional("title") - - /** - * Returns the raw JSON value of [alias]. - * - * Unlike [alias], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("alias") @ExcludeMissing fun _alias(): JsonField = alias - - /** - * Returns the raw JSON value of [provider]. - * - * Unlike [provider], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("provider") @ExcludeMissing fun _provider(): JsonField = provider - - /** - * Returns the raw JSON value of [settings]. - * - * Unlike [settings], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("settings") @ExcludeMissing fun _settings(): JsonField = settings - - /** - * Returns the raw JSON value of [title]. - * - * Unlike [title], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("title") @ExcludeMissing fun _title(): JsonField = title + @JsonProperty("patch") @ExcludeMissing fun _patch(): JsonField> = patch @JsonAnySetter private fun putAdditionalProperty(key: String, value: JsonValue) { @@ -457,87 +334,61 @@ private constructor( companion object { - /** Returns a mutable builder for constructing an instance of [Body]. */ + /** + * Returns a mutable builder for constructing an instance of [Body]. + * + * The following fields are required: + * ```java + * .patch() + * ``` + */ @JvmStatic fun builder() = Builder() } /** A builder for [Body]. */ class Builder internal constructor() { - private var alias: JsonField = JsonMissing.of() - private var provider: JsonField = JsonMissing.of() - private var settings: JsonField = JsonMissing.of() - private var title: JsonField = JsonMissing.of() + private var patch: JsonField>? = null private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic internal fun from(body: Body) = apply { - alias = body.alias - provider = body.provider - settings = body.settings - title = body.title + patch = body.patch.map { it.toMutableList() } additionalProperties = body.additionalProperties.toMutableMap() } - /** Updated alias. Set to null to clear it. */ - fun alias(alias: String?) = alias(JsonField.ofNullable(alias)) - - /** Alias for calling [Builder.alias] with `alias.orElse(null)`. */ - fun alias(alias: Optional) = alias(alias.getOrNull()) - - /** - * Sets [Builder.alias] to an arbitrary JSON value. - * - * You should usually call [Builder.alias] with a well-typed [String] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun alias(alias: JsonField) = apply { this.alias = alias } - - /** - * The provider key identifying the type. Optional. When sent, it must match the stored - * provider, otherwise the request returns a 400. - */ - fun provider(provider: String) = provider(JsonField.of(provider)) - - /** - * Sets [Builder.provider] to an arbitrary JSON value. - * - * You should usually call [Builder.provider] with a well-typed [String] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun provider(provider: JsonField) = apply { this.provider = provider } - /** - * Provider-specific settings (snake_case keys) to merge into the stored settings, using - * JSON Merge Patch (RFC 7396). Each key you send replaces the stored value, a key set - * to null is removed, nested objects merge recursively, and arrays replace the stored - * value. Keys you omit keep their stored values. Set `rate_limit` to null to remove - * this configuration's send rate limit. + * Operations to apply, in order. Paths address the provider as it is returned: + * `/title`, `/alias`, `/provider`, and `/settings/` with the snake_case setting + * keys, including nested paths such as `/settings/rate_limit/rate`. If any operation + * fails, for example a `test`, nothing is saved and the request returns a 400. The + * result can't change `provider`, remove `title`, or add other top-level fields. + * Removing `/alias` clears it. */ - fun settings(settings: Settings) = settings(JsonField.of(settings)) + fun patch(patch: List) = patch(JsonField.of(patch)) /** - * Sets [Builder.settings] to an arbitrary JSON value. + * Sets [Builder.patch] to an arbitrary JSON value. * - * You should usually call [Builder.settings] with a well-typed [Settings] value + * You should usually call [Builder.patch] with a well-typed `List` value * instead. This method is primarily for setting the field to an undocumented or not yet * supported value. */ - fun settings(settings: JsonField) = apply { this.settings = settings } - - /** Updated display title. */ - fun title(title: String) = title(JsonField.of(title)) + fun patch(patch: JsonField>) = apply { + this.patch = patch.map { it.toMutableList() } + } /** - * Sets [Builder.title] to an arbitrary JSON value. + * Adds a single [Patch] to [Builder.patch]. * - * You should usually call [Builder.title] with a well-typed [String] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. + * @throws IllegalStateException if the field was previously set to a non-list. */ - fun title(title: JsonField) = apply { this.title = title } + fun addPatch(patch: Patch) = apply { + this.patch = + (this.patch ?: JsonField.of(mutableListOf())).also { + checkKnown("patch", it).add(patch) + } + } fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() @@ -562,9 +413,19 @@ private constructor( * Returns an immutable instance of [Body]. * * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .patch() + * ``` + * + * @throws IllegalStateException if any required field is unset. */ fun build(): Body = - Body(alias, provider, settings, title, additionalProperties.toMutableMap()) + Body( + checkRequired("patch", patch).map { it.toImmutable() }, + additionalProperties.toMutableMap(), + ) } private var validated: Boolean = false @@ -583,10 +444,7 @@ private constructor( return@apply } - alias() - provider() - settings().ifPresent { it.validate() } - title() + patch().forEach { it.validate() } validated = true } @@ -606,10 +464,7 @@ private constructor( */ @JvmSynthetic internal fun validity(): Int = - (if (alias.asKnown().isPresent) 1 else 0) + - (if (provider.asKnown().isPresent) 1 else 0) + - (settings.asKnown().getOrNull()?.validity() ?: 0) + - (if (title.asKnown().isPresent) 1 else 0) + (patch.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) override fun equals(other: Any?): Boolean { if (this === other) { @@ -617,59 +472,174 @@ private constructor( } return other is Body && - alias == other.alias && - provider == other.provider && - settings == other.settings && - title == other.title && + patch == other.patch && additionalProperties == other.additionalProperties } - private val hashCode: Int by lazy { - Objects.hash(alias, provider, settings, title, additionalProperties) - } + private val hashCode: Int by lazy { Objects.hash(patch, additionalProperties) } override fun hashCode(): Int = hashCode - override fun toString() = - "Body{alias=$alias, provider=$provider, settings=$settings, title=$title, additionalProperties=$additionalProperties}" + override fun toString() = "Body{patch=$patch, additionalProperties=$additionalProperties}" } - /** - * Provider-specific settings (snake_case keys) to merge into the stored settings, using JSON - * Merge Patch (RFC 7396). Each key you send replaces the stored value, a key set to null is - * removed, nested objects merge recursively, and arrays replace the stored value. Keys you omit - * keep their stored values. Set `rate_limit` to null to remove this configuration's send rate - * limit. - */ - class Settings - @JsonCreator + /** One JSON Patch (RFC 6902) operation. */ + class Patch + @JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( - @com.fasterxml.jackson.annotation.JsonValue - private val additionalProperties: Map + private val op: JsonField, + private val path: JsonField, + private val from: JsonField, + private val value: JsonValue, + private val additionalProperties: MutableMap, ) { + @JsonCreator + private constructor( + @JsonProperty("op") @ExcludeMissing op: JsonField = JsonMissing.of(), + @JsonProperty("path") @ExcludeMissing path: JsonField = JsonMissing.of(), + @JsonProperty("from") @ExcludeMissing from: JsonField = JsonMissing.of(), + @JsonProperty("value") @ExcludeMissing value: JsonValue = JsonMissing.of(), + ) : this(op, path, from, value, mutableMapOf()) + + /** + * The operation to perform. + * + * @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 op(): Op = op.getRequired("op") + + /** + * JSON Pointer to the field to operate on, such as `/settings/api_key`. + * + * @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 path(): String = path.getRequired("path") + + /** + * JSON Pointer to the source field for `move` and `copy`. + * + * @throws CourierInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun from(): Optional = from.getOptional("from") + + /** + * The value for `add`, `replace` and `test`. Any JSON value. + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = patch.value().convert(MyClass.class); + * ``` + */ + @JsonProperty("value") @ExcludeMissing fun _value(): JsonValue = value + + /** + * Returns the raw JSON value of [op]. + * + * Unlike [op], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("op") @ExcludeMissing fun _op(): JsonField = op + + /** + * Returns the raw JSON value of [path]. + * + * Unlike [path], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("path") @ExcludeMissing fun _path(): JsonField = path + + /** + * Returns the raw JSON value of [from]. + * + * Unlike [from], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("from") @ExcludeMissing fun _from(): JsonField = from + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + @JsonAnyGetter @ExcludeMissing - fun _additionalProperties(): Map = additionalProperties + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) fun toBuilder() = Builder().from(this) companion object { - /** Returns a mutable builder for constructing an instance of [Settings]. */ + /** + * Returns a mutable builder for constructing an instance of [Patch]. + * + * The following fields are required: + * ```java + * .op() + * .path() + * ``` + */ @JvmStatic fun builder() = Builder() } - /** A builder for [Settings]. */ + /** A builder for [Patch]. */ class Builder internal constructor() { + private var op: JsonField? = null + private var path: JsonField? = null + private var from: JsonField = JsonMissing.of() + private var value: JsonValue = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic - internal fun from(settings: Settings) = apply { - additionalProperties = settings.additionalProperties.toMutableMap() + internal fun from(patch: Patch) = apply { + op = patch.op + path = patch.path + from = patch.from + value = patch.value + additionalProperties = patch.additionalProperties.toMutableMap() } + /** The operation to perform. */ + fun op(op: Op) = op(JsonField.of(op)) + + /** + * Sets [Builder.op] to an arbitrary JSON value. + * + * You should usually call [Builder.op] with a well-typed [Op] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun op(op: JsonField) = apply { this.op = op } + + /** JSON Pointer to the field to operate on, such as `/settings/api_key`. */ + fun path(path: String) = path(JsonField.of(path)) + + /** + * Sets [Builder.path] to an arbitrary JSON value. + * + * You should usually call [Builder.path] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun path(path: JsonField) = apply { this.path = path } + + /** JSON Pointer to the source field for `move` and `copy`. */ + fun from(from: String) = from(JsonField.of(from)) + + /** + * Sets [Builder.from] to an arbitrary JSON value. + * + * You should usually call [Builder.from] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun from(from: JsonField) = apply { this.from = from } + + /** The value for `add`, `replace` and `test`. Any JSON value. */ + fun value(value: JsonValue) = apply { this.value = value } + fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() putAllAdditionalProperties(additionalProperties) @@ -690,11 +660,26 @@ private constructor( } /** - * Returns an immutable instance of [Settings]. + * Returns an immutable instance of [Patch]. * * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .op() + * .path() + * ``` + * + * @throws IllegalStateException if any required field is unset. */ - fun build(): Settings = Settings(additionalProperties.toImmutable()) + fun build(): Patch = + Patch( + checkRequired("op", op), + checkRequired("path", path), + from, + value, + additionalProperties.toMutableMap(), + ) } private var validated: Boolean = false @@ -708,11 +693,14 @@ private constructor( * @throws CourierInvalidDataException if any value type in this object doesn't match its * expected type. */ - fun validate(): Settings = apply { + fun validate(): Patch = apply { if (validated) { return@apply } + op().validate() + path() + from() validated = true } @@ -732,21 +720,193 @@ private constructor( */ @JvmSynthetic internal fun validity(): Int = - additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + (op.asKnown().getOrNull()?.validity() ?: 0) + + (if (path.asKnown().isPresent) 1 else 0) + + (if (from.asKnown().isPresent) 1 else 0) + + /** The operation to perform. */ + class Op @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is + * on an older version than the API, then the API may respond with new members that the + * SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val ADD = of("add") + + @JvmField val REMOVE = of("remove") + + @JvmField val REPLACE = of("replace") + + @JvmField val MOVE = of("move") + + @JvmField val COPY = of("copy") + + @JvmField val TEST = of("test") + + @JvmStatic fun of(value: String) = Op(JsonField.of(value)) + } + + /** An enum containing [Op]'s known values. */ + enum class Known { + ADD, + REMOVE, + REPLACE, + MOVE, + COPY, + TEST, + } + + /** + * An enum containing [Op]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Op] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + ADD, + REMOVE, + REPLACE, + MOVE, + COPY, + TEST, + /** An enum member indicating that [Op] was instantiated with an unknown value. */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you + * want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + ADD -> Value.ADD + REMOVE -> Value.REMOVE + REPLACE -> Value.REPLACE + MOVE -> Value.MOVE + COPY -> Value.COPY + TEST -> Value.TEST + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws CourierInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + ADD -> Known.ADD + REMOVE -> Known.REMOVE + REPLACE -> Known.REPLACE + MOVE -> Known.MOVE + COPY -> Known.COPY + TEST -> Known.TEST + else -> throw CourierInvalidDataException("Unknown Op: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws CourierInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + CourierInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * 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(): Op = apply { + if (validated) { + return@apply + } + + known() + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: CourierInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Op && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } override fun equals(other: Any?): Boolean { if (this === other) { return true } - return other is Settings && additionalProperties == other.additionalProperties + return other is Patch && + op == other.op && + path == other.path && + from == other.from && + value == other.value && + additionalProperties == other.additionalProperties } - private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + private val hashCode: Int by lazy { + Objects.hash(op, path, from, value, additionalProperties) + } override fun hashCode(): Int = hashCode - override fun toString() = "Settings{additionalProperties=$additionalProperties}" + override fun toString() = + "Patch{op=$op, path=$path, from=$from, value=$value, additionalProperties=$additionalProperties}" } override fun equals(other: Any?): Boolean { diff --git a/courier-java-core/src/main/kotlin/com/courier/services/async/ProviderServiceAsync.kt b/courier-java-core/src/main/kotlin/com/courier/services/async/ProviderServiceAsync.kt index f924ff8f..3f3e9f16 100644 --- a/courier-java-core/src/main/kotlin/com/courier/services/async/ProviderServiceAsync.kt +++ b/courier-java-core/src/main/kotlin/com/courier/services/async/ProviderServiceAsync.kt @@ -168,23 +168,23 @@ interface ProviderServiceAsync { delete(id, ProviderDeleteParams.none(), requestOptions) /** - * Updates part of a provider's configuration and keeps every field you omit. Settings merge - * into the stored settings key by key, and a setting sent as null is removed. + * Applies JSON Patch (RFC 6902) operations to a provider configuration and keeps every field + * the operations don't touch. Paths address the provider as it is returned: /title, /alias and + * /settings/. The operations apply in order, and if one fails, nothing is saved. */ - fun patch(id: String): CompletableFuture = patch(id, ProviderPatchParams.none()) + fun patch(id: String, params: ProviderPatchParams): CompletableFuture = + patch(id, params, RequestOptions.none()) /** @see patch */ fun patch( id: String, - params: ProviderPatchParams = ProviderPatchParams.none(), + params: ProviderPatchParams, requestOptions: RequestOptions = RequestOptions.none(), ): CompletableFuture = patch(params.toBuilder().id(id).build(), requestOptions) /** @see patch */ - fun patch( - id: String, - params: ProviderPatchParams = ProviderPatchParams.none(), - ): CompletableFuture = patch(id, params, RequestOptions.none()) + fun patch(params: ProviderPatchParams): CompletableFuture = + patch(params, RequestOptions.none()) /** @see patch */ fun patch( @@ -192,14 +192,6 @@ interface ProviderServiceAsync { requestOptions: RequestOptions = RequestOptions.none(), ): CompletableFuture - /** @see patch */ - fun patch(params: ProviderPatchParams): CompletableFuture = - patch(params, RequestOptions.none()) - - /** @see patch */ - fun patch(id: String, requestOptions: RequestOptions): CompletableFuture = - patch(id, ProviderPatchParams.none(), requestOptions) - /** * A view of [ProviderServiceAsync] that provides access to raw HTTP responses for each method. */ @@ -363,28 +355,18 @@ interface ProviderServiceAsync { * Returns a raw HTTP response for `patch /providers/{id}`, but is otherwise the same as * [ProviderServiceAsync.patch]. */ - fun patch(id: String): CompletableFuture> = - patch(id, ProviderPatchParams.none()) - - /** @see patch */ fun patch( id: String, - params: ProviderPatchParams = ProviderPatchParams.none(), - requestOptions: RequestOptions = RequestOptions.none(), - ): CompletableFuture> = - patch(params.toBuilder().id(id).build(), requestOptions) - - /** @see patch */ - fun patch( - id: String, - params: ProviderPatchParams = ProviderPatchParams.none(), + params: ProviderPatchParams, ): CompletableFuture> = patch(id, params, RequestOptions.none()) /** @see patch */ fun patch( + id: String, params: ProviderPatchParams, requestOptions: RequestOptions = RequestOptions.none(), - ): CompletableFuture> + ): CompletableFuture> = + patch(params.toBuilder().id(id).build(), requestOptions) /** @see patch */ fun patch(params: ProviderPatchParams): CompletableFuture> = @@ -392,9 +374,8 @@ interface ProviderServiceAsync { /** @see patch */ fun patch( - id: String, - requestOptions: RequestOptions, - ): CompletableFuture> = - patch(id, ProviderPatchParams.none(), requestOptions) + params: ProviderPatchParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> } } diff --git a/courier-java-core/src/main/kotlin/com/courier/services/blocking/ProviderService.kt b/courier-java-core/src/main/kotlin/com/courier/services/blocking/ProviderService.kt index b80a4713..ee8fb862 100644 --- a/courier-java-core/src/main/kotlin/com/courier/services/blocking/ProviderService.kt +++ b/courier-java-core/src/main/kotlin/com/courier/services/blocking/ProviderService.kt @@ -157,21 +157,22 @@ interface ProviderService { delete(id, ProviderDeleteParams.none(), requestOptions) /** - * Updates part of a provider's configuration and keeps every field you omit. Settings merge - * into the stored settings key by key, and a setting sent as null is removed. + * Applies JSON Patch (RFC 6902) operations to a provider configuration and keeps every field + * the operations don't touch. Paths address the provider as it is returned: /title, /alias and + * /settings/. The operations apply in order, and if one fails, nothing is saved. */ - fun patch(id: String): Provider = patch(id, ProviderPatchParams.none()) + fun patch(id: String, params: ProviderPatchParams): Provider = + patch(id, params, RequestOptions.none()) /** @see patch */ fun patch( id: String, - params: ProviderPatchParams = ProviderPatchParams.none(), + params: ProviderPatchParams, requestOptions: RequestOptions = RequestOptions.none(), ): Provider = patch(params.toBuilder().id(id).build(), requestOptions) /** @see patch */ - fun patch(id: String, params: ProviderPatchParams = ProviderPatchParams.none()): Provider = - patch(id, params, RequestOptions.none()) + fun patch(params: ProviderPatchParams): Provider = patch(params, RequestOptions.none()) /** @see patch */ fun patch( @@ -179,13 +180,6 @@ interface ProviderService { requestOptions: RequestOptions = RequestOptions.none(), ): Provider - /** @see patch */ - fun patch(params: ProviderPatchParams): Provider = patch(params, RequestOptions.none()) - - /** @see patch */ - fun patch(id: String, requestOptions: RequestOptions): Provider = - patch(id, ProviderPatchParams.none(), requestOptions) - /** A view of [ProviderService] that provides access to raw HTTP responses for each method. */ interface WithRawResponse { @@ -353,22 +347,21 @@ interface ProviderService { * [ProviderService.patch]. */ @MustBeClosed - fun patch(id: String): HttpResponseFor = patch(id, ProviderPatchParams.none()) + fun patch(id: String, params: ProviderPatchParams): HttpResponseFor = + patch(id, params, RequestOptions.none()) /** @see patch */ @MustBeClosed fun patch( id: String, - params: ProviderPatchParams = ProviderPatchParams.none(), + params: ProviderPatchParams, requestOptions: RequestOptions = RequestOptions.none(), ): HttpResponseFor = patch(params.toBuilder().id(id).build(), requestOptions) /** @see patch */ @MustBeClosed - fun patch( - id: String, - params: ProviderPatchParams = ProviderPatchParams.none(), - ): HttpResponseFor = patch(id, params, RequestOptions.none()) + fun patch(params: ProviderPatchParams): HttpResponseFor = + patch(params, RequestOptions.none()) /** @see patch */ @MustBeClosed @@ -376,15 +369,5 @@ interface ProviderService { params: ProviderPatchParams, requestOptions: RequestOptions = RequestOptions.none(), ): HttpResponseFor - - /** @see patch */ - @MustBeClosed - fun patch(params: ProviderPatchParams): HttpResponseFor = - patch(params, RequestOptions.none()) - - /** @see patch */ - @MustBeClosed - fun patch(id: String, requestOptions: RequestOptions): HttpResponseFor = - patch(id, ProviderPatchParams.none(), requestOptions) } } diff --git a/courier-java-core/src/test/kotlin/com/courier/models/providers/ProviderPatchParamsTest.kt b/courier-java-core/src/test/kotlin/com/courier/models/providers/ProviderPatchParamsTest.kt index 2b0db39c..da9af95c 100644 --- a/courier-java-core/src/test/kotlin/com/courier/models/providers/ProviderPatchParamsTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/models/providers/ProviderPatchParamsTest.kt @@ -12,20 +12,43 @@ internal class ProviderPatchParamsTest { fun create() { ProviderPatchParams.builder() .id("id") - .alias("alias") - .provider("provider") - .settings( - ProviderPatchParams.Settings.builder() - .putAdditionalProperty("api_key", JsonValue.from("bar")) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/title") + .from("from") + .value(JsonValue.from("Production SendGrid")) + .build() + ) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/settings/api_key") + .from("from") + .value(JsonValue.from("SG.xxxxxxxx")) .build() ) - .title("Production SendGrid") .build() } @Test fun pathParams() { - val params = ProviderPatchParams.builder().id("id").build() + val params = + ProviderPatchParams.builder() + .id("id") + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/title") + .build() + ) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/settings/api_key") + .build() + ) + .build() assertThat(params._pathParam(0)).isEqualTo("id") // out-of-bound path param @@ -37,33 +60,74 @@ internal class ProviderPatchParamsTest { val params = ProviderPatchParams.builder() .id("id") - .alias("alias") - .provider("provider") - .settings( - ProviderPatchParams.Settings.builder() - .putAdditionalProperty("api_key", JsonValue.from("bar")) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/title") + .from("from") + .value(JsonValue.from("Production SendGrid")) + .build() + ) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/settings/api_key") + .from("from") + .value(JsonValue.from("SG.xxxxxxxx")) .build() ) - .title("Production SendGrid") .build() val body = params._body() - assertThat(body.alias()).contains("alias") - assertThat(body.provider()).contains("provider") - assertThat(body.settings()) - .contains( - ProviderPatchParams.Settings.builder() - .putAdditionalProperty("api_key", JsonValue.from("bar")) - .build() + assertThat(body.patch()) + .containsExactly( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/title") + .from("from") + .value(JsonValue.from("Production SendGrid")) + .build(), + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/settings/api_key") + .from("from") + .value(JsonValue.from("SG.xxxxxxxx")) + .build(), ) - assertThat(body.title()).contains("Production SendGrid") } @Test fun bodyWithoutOptionalFields() { - val params = ProviderPatchParams.builder().id("id").build() + val params = + ProviderPatchParams.builder() + .id("id") + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/title") + .build() + ) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/settings/api_key") + .build() + ) + .build() val body = params._body() + + assertThat(body.patch()) + .containsExactly( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/title") + .build(), + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/settings/api_key") + .build(), + ) } } diff --git a/courier-java-core/src/test/kotlin/com/courier/services/async/ProviderServiceAsyncTest.kt b/courier-java-core/src/test/kotlin/com/courier/services/async/ProviderServiceAsyncTest.kt index 82402a4c..3963b2d3 100644 --- a/courier-java-core/src/test/kotlin/com/courier/services/async/ProviderServiceAsyncTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/services/async/ProviderServiceAsyncTest.kt @@ -110,14 +110,22 @@ internal class ProviderServiceAsyncTest { providerServiceAsync.patch( ProviderPatchParams.builder() .id("id") - .alias("alias") - .provider("provider") - .settings( - ProviderPatchParams.Settings.builder() - .putAdditionalProperty("api_key", JsonValue.from("bar")) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/title") + .from("from") + .value(JsonValue.from("Production SendGrid")) + .build() + ) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/settings/api_key") + .from("from") + .value(JsonValue.from("SG.xxxxxxxx")) .build() ) - .title("Production SendGrid") .build() ) diff --git a/courier-java-core/src/test/kotlin/com/courier/services/blocking/ProviderServiceTest.kt b/courier-java-core/src/test/kotlin/com/courier/services/blocking/ProviderServiceTest.kt index a1c0f365..60c3b29b 100644 --- a/courier-java-core/src/test/kotlin/com/courier/services/blocking/ProviderServiceTest.kt +++ b/courier-java-core/src/test/kotlin/com/courier/services/blocking/ProviderServiceTest.kt @@ -103,14 +103,22 @@ internal class ProviderServiceTest { providerService.patch( ProviderPatchParams.builder() .id("id") - .alias("alias") - .provider("provider") - .settings( - ProviderPatchParams.Settings.builder() - .putAdditionalProperty("api_key", JsonValue.from("bar")) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/title") + .from("from") + .value(JsonValue.from("Production SendGrid")) + .build() + ) + .addPatch( + ProviderPatchParams.Patch.builder() + .op(ProviderPatchParams.Patch.Op.REPLACE) + .path("/settings/api_key") + .from("from") + .value(JsonValue.from("SG.xxxxxxxx")) .build() ) - .title("Production SendGrid") .build() )