diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index 08d7767569d..537784308a5 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased * [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. * [changed] Removed superfluous and noisy debug logging of operation variables. +* [changed] Internal code changes in preparation for user-defined enum support. # 16.0.3 diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/EnumValue.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/EnumValue.kt new file mode 100644 index 00000000000..9fe4fe18bc1 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/EnumValue.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect + +/** + * Stores the value of an `enum` or a string if the string does not correspond to one of the enum's + * values. + */ +// TODO: Change the visibility of `EnumValue` to `public` once it gets approval +// by Firebase API Council. +internal sealed interface EnumValue> { + + /** + * The string value of the enum, either the [Enum.name] in the case of [Known] or the string whose + * corresponding enum value was _not_ known, as in the case of [Unknown]. + */ + val stringValue: String + + /** + * Represents an unknown enum value. + * + * This could happen, for example, if an enum gained a new value but this code was compiled for + * the older version that lacked the new enum value. Instead of failing, the unknown enum value + * will be gracefully mapped to [Unknown]. + */ + class Unknown(override val stringValue: String) : EnumValue { + + /** + * Compares this object with another object for equality. + * + * @param other The object to compare to this for equality. + * @return true if, and only if, the other object is an instance of [Unknown] whose + * [stringValue] compares equal to this object's [stringValue] using the `==` operator. + */ + override fun equals(other: Any?): Boolean = other is Unknown && stringValue == other.stringValue + + /** + * Calculates and returns the hash code for this object. + * + * The hash code is _not_ guaranteed to be stable across application restarts. + * + * @return the hash code for this object, that incorporates the values of this object's public + * properties. + */ + override fun hashCode(): Int = stringValue.hashCode() + + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at + * any time. Therefore, the only recommended usage of the returned string is debugging and/or + * logging. Namely, parsing the returned string or storing the returned string in non-volatile + * storage should generally be avoided in order to be robust in case that the string + * representation changes. + */ + override fun toString(): String = "Unknown($stringValue)" + + /** Creates and returns a new [Unknown] instance with the given property values. */ + fun copy(stringValue: String = this.stringValue): Unknown = Unknown(stringValue) + } + + /** + * Represents a known enum value. + * + * @param value The enum value. + */ + class Known>(val value: T) : EnumValue { + + override val stringValue: String + get() = value.name + + /** + * Compares this object with another object for equality. + * + * @param other The object to compare to this for equality. + * @return true if, and only if, the other object is an instance of [Known] whose [value] + * compares equal to this object's [value] using the `==` operator. + */ + override fun equals(other: Any?): Boolean = other is Known<*> && value == other.value + + /** + * Calculates and returns the hash code for this object. + * + * The hash code is _not_ guaranteed to be stable across application restarts. + * + * @return the hash code for this object, that incorporates the values of this object's public + * properties. + */ + override fun hashCode(): Int = value.hashCode() + + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at + * any time. Therefore, the only recommended usage of the returned string is debugging and/or + * logging. Namely, parsing the returned string or storing the returned string in non-volatile + * storage should generally be avoided in order to be robust in case that the string + * representation changes. + */ + override fun toString(): String = "Known(${value.name})" + + /** Creates and returns a new [Known] instance with the given property values. */ + fun copy(value: T = this.value): Known = Known(value) + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/EnumValueSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/EnumValueSerializer.kt new file mode 100644 index 00000000000..2e51fe003d2 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/EnumValueSerializer.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect.serializers + +import com.google.firebase.dataconnect.EnumValue +import com.google.firebase.dataconnect.EnumValue.Known +import com.google.firebase.dataconnect.EnumValue.Unknown +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * A [KSerializer] implementation for [EnumValue]. + * + * @param values The values of the enum to deserialize; for example, for an enum named `Foo` this + * value should be `Foo.entries` or `Foo.values()`. + */ +// TODO: Change the visibility of `EnumValueSerializer` to `public` once it gets approval +// by Firebase API Council. +internal open class EnumValueSerializer>(values: Iterable) : + KSerializer> { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("com.google.firebase.dataconnect.EnumValue", PrimitiveKind.STRING) + + private val enumValueByStringValue: Map = buildMap { + for (value in values) { + val oldValue = put(value.name, value) + require(oldValue === null) { "duplicate value.name in values: ${value.name}" } + } + } + + /** + * Deserializes an [EnumValue] from the given decoder. + * + * If the decoded string is equal to the [Enum.name] of one of the values given to the constructor + * then [Known] is returned with that value; otherwise, [Unknown] is returned. + */ + override fun deserialize(decoder: Decoder): EnumValue { + val stringValue = decoder.decodeString() + val enumValue = enumValueByStringValue.get(stringValue) ?: return Unknown(stringValue) + return Known(enumValue) + } + + /** Serializes the given [EnumValue] to the given encoder. */ + override fun serialize(encoder: Encoder, value: EnumValue) { + encoder.encodeString(value.stringValue) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/EnumValueKnownUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/EnumValueKnownUnitTest.kt new file mode 100644 index 00000000000..7e42000a172 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/EnumValueKnownUnitTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalKotest::class) + +package com.google.firebase.dataconnect + +import com.google.firebase.dataconnect.testutil.property.arbitrary.distinctPair +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.of +import io.kotest.property.assume +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@Suppress("ReplaceCallWithBinaryOperator") +class EnumValueKnownUnitTest { + + @Test + fun `constructor() should set properties to corresponding arguments`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.value shouldBeSameInstanceAs enum + } + } + + @Test + fun `equals() should return true when invoked with itself`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.equals(enumValue) shouldBe true + } + } + + @Test + fun `equals() should return true when invoked with a distinct, but equal, instance`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue1 = EnumValue.Known(enum) + val enumValue2 = EnumValue.Known(enum) + enumValue1.equals(enumValue2) shouldBe true + } + } + + @Test + fun `equals() should return false when invoked with null`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false when invoked with a different type`() = runTest { + val others = Arb.of("foo", 42, java.time.LocalDate.now()) + checkAll(propTestConfig, Arb.enum(), others) { enum, other -> + val enumValue = EnumValue.Known(enum) + enumValue.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when the enum differs`() = runTest { + checkAll(propTestConfig, Arb.enum().distinctPair()) { (enum1, enum2) -> + val enumValue1 = EnumValue.Known(enum1) + val enumValue2 = EnumValue.Known(enum2) + enumValue1.equals(enumValue2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value when invoked repeatedly`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + val hashCode = enumValue.hashCode() + repeat(5) { withClue("iteration=$it") { enumValue.hashCode() shouldBe hashCode } } + } + } + + @Test + fun `hashCode() should return the same value when invoked on equal, but distinct, objects`() = + runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue1 = EnumValue.Known(enum) + val enumValue2 = EnumValue.Known(enum) + enumValue1.hashCode() shouldBe enumValue2.hashCode() + } + } + + @Test + fun `hashCode() should return different values for different enum values`() = runTest { + checkAll(propTestConfig, Arb.enum().distinctPair()) { (enum1, enum2) -> + assume(enum1.hashCode() != enum2.hashCode()) + val enumValue1 = EnumValue.Known(enum1) + val enumValue2 = EnumValue.Known(enum2) + enumValue1.hashCode() shouldNotBe enumValue2.hashCode() + } + } + + @Test + fun `toString() should return a string conforming to what is expected`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + enumValue.toString() shouldBe "Known(${enum.name})" + } + } + + @Test + fun `copy() with no arguments should return an equal, but distinct, instance`() = runTest { + checkAll(propTestConfig, Arb.enum()) { enum -> + val enumValue = EnumValue.Known(enum) + val enumValueCopy = enumValue.copy() + enumValue shouldBe enumValueCopy + enumValue shouldNotBeSameInstanceAs enumValueCopy + } + } + + @Test + fun `copy() with all arguments should return a new instance with the given arguments`() = + runTest { + checkAll(propTestConfig, Arb.enum().distinctPair()) { (enum1, enum2) -> + val enumValue1 = EnumValue.Known(enum1) + val enumValue2 = enumValue1.copy(enum2) + enumValue2 shouldBe EnumValue.Known(enum2) + } + } + + @Suppress("unused") + private enum class Food { + Burrito, + Cake, + Pizza, + Shawarma, + Sushi, + } + + private companion object { + val propTestConfig = PropTestConfig(iterations = 50) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/EnumValueUnknownUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/EnumValueUnknownUnitTest.kt new file mode 100644 index 00000000000..dd0659186af --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/EnumValueUnknownUnitTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalKotest::class) + +package com.google.firebase.dataconnect + +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import com.google.firebase.dataconnect.testutil.property.arbitrary.distinctPair +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.of +import io.kotest.property.assume +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@Suppress("ReplaceCallWithBinaryOperator") +class EnumValueUnknownUnitTest { + + @Test + fun `constructor() should set properties to corresponding arguments`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.stringValue shouldBeSameInstanceAs stringValue + } + } + + @Test + fun `equals() should return true when invoked with itself`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.equals(enumValue) shouldBe true + } + } + + @Test + fun `equals() should return true when invoked with a distinct, but equal, instance`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue1 = EnumValue.Unknown(stringValue) + val enumValue2 = EnumValue.Unknown(stringValue) + enumValue1.equals(enumValue2) shouldBe true + } + } + + @Test + fun `equals() should return false when invoked with null`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false when invoked with a different type`() = runTest { + val others = Arb.of("foo", 42, java.time.LocalDate.now()) + checkAll(propTestConfig, Arb.dataConnect.string(), others) { stringValue, other -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when the stringValue differs`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string().distinctPair()) { (stringValue1, stringValue2) + -> + val enumValue1 = EnumValue.Unknown(stringValue1) + val enumValue2 = EnumValue.Unknown(stringValue2) + enumValue1.equals(enumValue2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value when invoked repeatedly`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + val hashCode = enumValue.hashCode() + repeat(5) { withClue("iteration=$it") { enumValue.hashCode() shouldBe hashCode } } + } + } + + @Test + fun `hashCode() should return the same value when invoked on equal, but distinct, objects`() = + runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue1 = EnumValue.Unknown(stringValue) + val enumValue2 = EnumValue.Unknown(stringValue) + enumValue1.hashCode() shouldBe enumValue2.hashCode() + } + } + + @Test + fun `hashCode() should return different values for different stringValue values`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string().distinctPair()) { (stringValue1, stringValue2) + -> + assume(stringValue1.hashCode() != stringValue2.hashCode()) + val enumValue1 = EnumValue.Unknown(stringValue1) + val enumValue2 = EnumValue.Unknown(stringValue2) + enumValue1.hashCode() shouldNotBe enumValue2.hashCode() + } + } + + @Test + fun `toString() should return a string conforming to what is expected`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + enumValue.toString() shouldBe "Unknown($stringValue)" + } + } + + @Test + fun `copy() with no arguments should return an equal, but distinct, instance`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { stringValue -> + val enumValue = EnumValue.Unknown(stringValue) + val enumValueCopy = enumValue.copy() + enumValue shouldBe enumValueCopy + enumValue shouldNotBeSameInstanceAs enumValueCopy + } + } + + @Test + fun `copy() with all arguments should return a new instance with the given arguments`() = + runTest { + checkAll(propTestConfig, Arb.dataConnect.string().distinctPair()) { + (stringValue1, stringValue2) -> + val enumValue1 = EnumValue.Unknown(stringValue1) + val enumValue2 = enumValue1.copy(stringValue2) + enumValue2 shouldBe EnumValue.Unknown(stringValue2) + } + } + + private companion object { + val propTestConfig = PropTestConfig(iterations = 50) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/EnumValueSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/EnumValueSerializerUnitTest.kt new file mode 100644 index 00000000000..6fde249c209 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/serializers/EnumValueSerializerUnitTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalKotest::class) + +package com.google.firebase.dataconnect.serializers + +import com.google.firebase.dataconnect.EnumValue +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.map +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive +import org.junit.Test + +class EnumValueSerializerUnitTest { + + @Test + fun `serialize() should produce the expected serialized string for _known_ values`() = runTest { + checkAll(propTestConfig, Arb.knownEnumValue()) { knownEnumValue: EnumValue.Known -> + val encodedValue = Json.encodeToJsonElement(Dog.serializer, knownEnumValue) + encodedValue.jsonPrimitive.content shouldBe knownEnumValue.value.name + } + } + + @Test + fun `serialize() should produce the expected serialized string for _unknown_ values`() = runTest { + checkAll(propTestConfig, Arb.unknownEnumValue()) { unknownEnumValue: EnumValue.Unknown -> + val encodedValue = Json.encodeToJsonElement(Dog.serializer, unknownEnumValue) + encodedValue.jsonPrimitive.content shouldBe unknownEnumValue.stringValue + } + } + + @Test + fun `deserialize() should produce the expected EnumValue object for _known_ values`() = runTest { + checkAll(propTestConfig, Arb.knownEnumValue()) { knownEnumValue: EnumValue.Known -> + val encodedValue = JsonPrimitive(knownEnumValue.value.name) + val decodedEnumValue = Json.decodeFromJsonElement(Dog.serializer, encodedValue) + decodedEnumValue shouldBe knownEnumValue + } + } + + @Test + fun `deserialize() should produce the expected EnumValue object for _unknown_ values`() = + runTest { + checkAll(propTestConfig, Arb.unknownEnumValue()) { unknownEnumValue: EnumValue.Unknown -> + val encodedValue = JsonPrimitive(unknownEnumValue.stringValue) + val decodedEnumValue = Json.decodeFromJsonElement(Dog.serializer, encodedValue) + decodedEnumValue shouldBe unknownEnumValue + } + } + + @Suppress("unused") + private enum class Dog { + Boxer, + Bulldog, + Dachshund, + Labrador, + Poodle; + + companion object { + val serializer: KSerializer> = EnumValueSerializer(Dog.entries) + } + } + + private companion object { + val propTestConfig = + PropTestConfig( + iterations = 500, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2) + ) + + fun Arb.Companion.unknownEnumValue( + stringValue: Arb = Arb.dataConnect.string() + ): Arb = stringValue.map { EnumValue.Unknown(it) } + + fun Arb.Companion.knownEnumValue( + enumValue: Arb = Arb.enum() + ): Arb> = enumValue.map { EnumValue.Known(it) } + } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/misc.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/misc.kt index c49271abade..61703b784f5 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/misc.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/misc.kt @@ -18,8 +18,17 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary import io.kotest.property.Arb import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.bind +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.flatMap +import io.kotest.property.arbitrary.map import kotlin.random.nextInt +/** Returns a new [Arb] that produces two _unequal_ values of this [Arb]. */ +fun Arb.distinctPair(): Arb> = flatMap { value1 -> + this@distinctPair.filter { it != value1 }.map { Pair(value1, it) } +} + fun Arb.withPrefix(prefix: String): Arb = arbitrary { "$prefix${bind()}" } fun Arb.Companion.positiveIntWithUniformNumDigitsProbability(range: IntRange): Arb {