From 3f49379ba6cba6ec72582831a0f446a2fe94921c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 20 Sep 2024 18:18:18 +0000 Subject: [PATCH 1/3] AnyValue and AnyValueSerializer added --- .../google/firebase/dataconnect/AnyValue.kt | 252 ++++++++ .../dataconnect/FirebaseDataConnect.kt | 4 + .../serializers/AnyValueSerializer.kt | 45 ++ .../dataconnect/util/ProtoStructDecoder.kt | 208 ++++++- .../dataconnect/util/ProtoStructEncoder.kt | 159 ++++- .../firebase/dataconnect/util/ProtoUtil.kt | 176 +++--- .../dataconnect/AnyValueSerializerUnitTest.kt | 48 ++ .../firebase/dataconnect/AnyValueUnitTest.kt | 560 ++++++++++++++++++ .../testutil/DataConnectAnySerializer.kt | 79 +++ .../firebase/dataconnect/testutil/Arbs.kt | 4 + 10 files changed, 1424 insertions(+), 111 deletions(-) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/AnyValue.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/AnyValueSerializer.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/AnyValueSerializerUnitTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/AnyValueUnitTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/DataConnectAnySerializer.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/AnyValue.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/AnyValue.kt new file mode 100644 index 00000000000..b7fc4124c64 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/AnyValue.kt @@ -0,0 +1,252 @@ +/* + * Copyright 2024 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 + +import com.google.firebase.dataconnect.serializers.AnyValueSerializer +import com.google.firebase.dataconnect.util.decodeFromValue +import com.google.firebase.dataconnect.util.encodeToValue +import com.google.firebase.dataconnect.util.toAny +import com.google.firebase.dataconnect.util.toCompactString +import com.google.firebase.dataconnect.util.toValueProto +import com.google.protobuf.Struct +import com.google.protobuf.Value +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.serializer + +/** + * Represents a variable or field of the Data Connect custom scalar type `Any`. + * + * ### Valid Values for `AnyValue` + * + * `AnyValue` can encapsulate [String], [Boolean], [Double], a [List] of one of these types, or a + * [Map] whose values are one of these types. The values can be arbitrarily nested (e.g. a list that + * contains a map that contains other maps, and so on. The lists and maps can contain heterogeneous + * values; for example, a single [List] can contain a [String] value, some [Boolean] values, and + * some [List] values. The values of a [List] or a [Map] may be `null`. The only exception is that a + * variable or field declared as `[Any]` in GraphQL may _not_ have `null` values in the top-level + * list; however, nested lists or maps _may_ contain null values. + * + * ### Storing `Int` in an `AnyValue` + * + * To store an [Int] value, simply convert it to a [Double] and store the [Double] value. + * + * ### Storing `Long` in an `AnyValue` + * + * To store a [Long] value, converting it to a [Double] can be lossy if the value is sufficiently + * large (or small) to not be exactly represented by [Double]. The _largest_ [Long] value that can + * be stored in a [Double] with its exact value is `2^53 – 1` (`9007199254740991`). The _smallest_ + * [Long] value that can be stored in a [Double] with its exact value is `-(2^53 – 1)` + * (`-9007199254740991`). This limitation is exactly the same in JavaScript, which does not have a + * native "int" or "long" type, but rather stores all numeric values in a 64-bit floating point + * value. See + * [MAX_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER]) + * and + * [MIN_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER]) + * for more details. + * + * ### Integration with `kotlinx.serialization` + * + * To serialize a value of this type when using Data Connect, use [AnyValueSerializer]. + * + * ### Example + * + * For example, suppose this schema and operation is defined in the GraphQL source: + * + * ``` + * type Foo @table { value: Any } + * mutation FooInsert($value: Any) { + * key: foo_insert(data: { value: $value }) + * } + * ``` + * + * then a serializable "Variables" type could be defined as follows: + * + * ``` + * @Serializable + * data class FooInsertVariables( + * @Serializable(with=AnyValueSerializer::class) val value: AnyValue? + * ) + * ``` + */ +@Serializable(with = AnyValueSerializer::class) +public class AnyValue internal constructor(internal val protoValue: Value) { + + init { + require(protoValue.kindCase != Value.KindCase.NULL_VALUE) { + "NULL_VALUE is not allowed; just use null" + } + } + + internal constructor(struct: Struct) : this(struct.toValueProto()) + + /** + * Creates an instance that encapsulates the given [Map]. + * + * An exception is thrown if any of the values of the map, or its sub-values, are invalid for + * being stored in [AnyValue]; see the [AnyValue] class documentation for a detailed description + * of value values. + * + * This class makes a _copy_ of the given map; therefore, any modifications to the map after this + * object is created will have no effect on this [AnyValue] object. + */ + public constructor(value: Map) : this(value.toValueProto()) + + /** + * Creates an instance that encapsulates the given [List]. + * + * An exception is thrown if any of the values of the list, or its sub-values, are invalid for + * being stored in [AnyValue]; see the [AnyValue] class documentation for a detailed description + * of value values. + * + * This class makes a _copy_ of the given list; therefore, any modifications to the list after + * this object is created will have no effect on this [AnyValue] object. + */ + public constructor(value: List) : this(value.toValueProto()) + + /** Creates an instance that encapsulates the given [String]. */ + public constructor(value: String) : this(value.toValueProto()) + + /** Creates an instance that encapsulates the given [Boolean]. */ + public constructor(value: Boolean) : this(value.toValueProto()) + + /** Creates an instance that encapsulates the given [Double]. */ + public constructor(value: Double) : this(value.toValueProto()) + + /** + * The native Kotlin type of the value encapsulated in this object. + * + * Although this type is `Any` it will be one of `String, `Boolean`, `Double`, `List` or + * `Map`. See the [AnyValue] class documentation for a detailed description of the + * types of values that are supported. + */ + public val value: Any + // NOTE: The not-null assertion operator (!!) below will never throw because the `init` block + // of this class asserts that `protoValue` is not NULL_VALUE. + get() = protoValue.toAny()!! + + /** + * Decodes the encapsulated value using the given deserializer. + * + * @param deserializer The deserializer for the decoder to use. + * + * @return the object of type `T` created by decoding the encapsulated value using the given + * deserializer. + */ + public fun decode(deserializer: DeserializationStrategy): T = + decodeFromValue(deserializer, protoValue) + + /** + * Decodes the encapsulated value using the _default_ serializer for the return type, as computed + * by [serializer]. + * + * @return the object of type `T` created by decoding the encapsulated value using the _default_ + * serializer for the return type, as computed by [serializer]. + */ + public inline fun decode(): T = decode(serializer()) + + /** + * 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 [AnyValue] whose encapsulated + * value compares equal using the `==` operator to the given object. + */ + override fun equals(other: Any?): Boolean = other is AnyValue && other.value == 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, calculated from the encapsulated value. + */ + 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. + * + * @return a string representation of this object's encapsulated value. + */ + override fun toString(): String = protoValue.toCompactString(keySortSelector = { it }) + + public companion object { + + /** + * Encodes the given value using the given serializer to an [AnyValue] object, and returns it. + * + * @param value the value to serialize. + * @param serializer the serializer for the encoder to use. + * + * @return a new `AnyValue` object whose encapsulated value is the encoding of the given value + * when decoded with the given serializer. + */ + public fun encode(value: T, serializer: SerializationStrategy): AnyValue = + AnyValue(encodeToValue(serializer, value)) + + /** + * Encodes the given value using the given _default_ serializer for the given object, as + * computed by [serializer]. + * + * @param value the value to serialize. + * @return a new `AnyValue` object whose encapsulated value is the encoding of the given value + * when decoded with the _default_ serializer for the given object, as computed by [serializer]. + */ + public inline fun encode(value: T): AnyValue = encode(value, serializer()) + + /** + * Creates and returns an `AnyValue` object created using the `AnyValue` constructor that + * corresponds to the runtime type of the given value, or returns `null` if the given value is + * `null`. + * + * @throws IllegalArgumentException if the given value is not supported by `AnyValue`; see the + * `AnyValue` constructor for details. + */ + @JvmName("fromNullableAny") + public fun fromAny(value: Any?): AnyValue? = if (value === null) null else fromAny(value) + + /** + * Creates and returns an `AnyValue` object created using the `AnyValue` constructor that + * corresponds to the runtime type of the given value. + * + * @throws IllegalArgumentException if the given value is not supported by `AnyValue`; see the + * `AnyValue` constructor for details. + */ + public fun fromAny(value: Any): AnyValue { + @Suppress("UNCHECKED_CAST") + return when (value) { + is String -> AnyValue(value) + is Boolean -> AnyValue(value) + is Double -> AnyValue(value) + is List<*> -> AnyValue(value) + is Map<*, *> -> AnyValue(value as Map) + else -> + throw IllegalArgumentException( + "unsupported type: ${value::class.qualifiedName}" + + " (supported types: null, String, Boolean, Double, List, Map)" + ) + } + } + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index b15cc4b58ea..34c1f4434cb 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -101,6 +101,10 @@ import kotlinx.serialization.SerializationStrategy * or later; otherwise, a compilation error like `Class 'FooConnector' is not abstract and does not * implement abstract member public abstract fun equals(other: Any?): Boolean defined in * com.google.firebase.dataconnect.generated.GeneratedConnector` will occur. + * - [#NNNN](https://github.com/firebase/firebase-android-sdk/pull/NNNN]) Added [AnyValue] class to + * support the custom `Any` GraphQL scalar type. Support for `Any` scalars in the Android SDK code + * generation was added in the dataconnect toolkit v1.3.8 (released Sept 20, 2024), which will be + * included in the next release of firebase-tools (the release following v13.18.0). * * #### 16.0.0-alpha05 (June 24, 2024) * - [#6003](https://github.com/firebase/firebase-android-sdk/pull/6003]) Fixed [close] to diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/AnyValueSerializer.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/AnyValueSerializer.kt new file mode 100644 index 00000000000..28c5ffb3953 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/serializers/AnyValueSerializer.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.AnyValue +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * An implementation of [KSerializer] for serializing and deserializing [AnyValue] objects. + * + * Note that this is _not_ a generic serializer, but is only useful in the Data Connect SDK. + */ +public object AnyValueSerializer : KSerializer { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("com.google.firebase.dataconnect.AnyValue") {} + + override fun serialize(encoder: Encoder, value: AnyValue): Unit = unsupported() + + override fun deserialize(decoder: Decoder): AnyValue = unsupported() + + private fun unsupported(): Nothing = + throw UnsupportedOperationException( + "The AnyValueSerializer class cannot actually be used;" + + " it is merely a sentinel that gets special treatment during Data Connect serialization" + ) +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructDecoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructDecoder.kt index fc6915bf742..040ffed6278 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructDecoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructDecoder.kt @@ -18,6 +18,8 @@ package com.google.firebase.dataconnect.util +import com.google.firebase.dataconnect.AnyValue +import com.google.firebase.dataconnect.serializers.AnyValueSerializer import com.google.protobuf.ListValue import com.google.protobuf.NullValue import com.google.protobuf.Struct @@ -32,6 +34,7 @@ import kotlinx.serialization.descriptors.elementNames import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer internal inline fun decodeFromStruct(struct: Struct): T = @@ -39,19 +42,14 @@ internal inline fun decodeFromStruct(struct: Struct): T = internal fun decodeFromStruct(deserializer: DeserializationStrategy, struct: Struct): T { val protoValue = Value.newBuilder().setStructValue(struct).build() - return ProtoValueDecoder(protoValue, path = null).decodeSerializableValue(deserializer) + return decodeFromValue(deserializer, protoValue) } -private fun Value.toAny(): Any? = - when (kindCase) { - KindCase.BOOL_VALUE -> boolValue - KindCase.NUMBER_VALUE -> numberValue - KindCase.STRING_VALUE -> stringValue - KindCase.LIST_VALUE -> listValue.valuesList - KindCase.STRUCT_VALUE -> structValue.fieldsMap - KindCase.NULL_VALUE -> null - else -> "ERROR: unsupported kindCase: $kindCase" - } +internal inline fun decodeFromValue(value: Value): T = + decodeFromValue(serializer(), value) + +internal fun decodeFromValue(deserializer: DeserializationStrategy, value: Value): T = + ProtoValueDecoder(value, path = null).decodeSerializableValue(deserializer) private fun Value.decode(path: String?, expectedKindCase: KindCase, block: (Value) -> T): T = if (kindCase != expectedKindCase) { @@ -102,8 +100,7 @@ private fun Value.decodeLong(path: String?): Long = private fun Value.decodeShort(path: String?): Short = decode(path, KindCase.NUMBER_VALUE) { it.numberValue.toInt().toShort() } -private class ProtoValueDecoder(private val valueProto: Value, private val path: String?) : - Decoder { +internal class ProtoValueDecoder(val valueProto: Value, private val path: String?) : Decoder { override val serializersModule = EmptySerializersModule() @@ -111,6 +108,7 @@ private class ProtoValueDecoder(private val valueProto: Value, private val path: when (val kind = descriptor.kind) { is StructureKind.CLASS -> ProtoStructValueDecoder(valueProto.decodeStruct(path), path) is StructureKind.LIST -> ProtoListValueDecoder(valueProto.decodeList(path), path) + is StructureKind.MAP -> ProtoMapValueDecoder(valueProto.decodeStruct(path), path) is StructureKind.OBJECT -> ProtoObjectValueDecoder else -> throw IllegalArgumentException("unsupported SerialKind: ${kind::class.qualifiedName}") } @@ -236,7 +234,16 @@ private class ProtoStructValueDecoder(private val struct: Struct, private val pa ?: if (elementKind is StructureKind.OBJECT) Value.getDefaultInstance() else throw SerializationException("element \"$elementPath\" missing; expected $elementKind") - return deserializer.deserialize(ProtoValueDecoder(valueProto, elementPath)) + return when (deserializer) { + is AnyValueSerializer -> { + @Suppress("UNCHECKED_CAST") + AnyValue(valueProto) as T + } + else -> { + val protoValueDecoder = ProtoValueDecoder(valueProto, elementPath) + deserializer.deserialize(protoValueDecoder) + } + } } @ExperimentalSerializationApi @@ -314,11 +321,16 @@ private class ProtoListValueDecoder(private val list: ListValue, private val pat deserializer: DeserializationStrategy, previousValue: T? ): T = - if (previousValue !== null) previousValue - else + if (previousValue !== null) { + previousValue + } else if (deserializer is AnyValueSerializer) { + @Suppress("UNCHECKED_CAST") + AnyValue(list.valuesList[index]) as T + } else { deserializer.deserialize( ProtoValueDecoder(list.valuesList[index], elementPathForIndex(index)) ) + } @ExperimentalSerializationApi override fun decodeNullableSerializableElement( @@ -339,6 +351,128 @@ private class ProtoListValueDecoder(private val list: ListValue, private val pat private fun elementPathForIndex(index: Int) = if (path === null) "[$index]" else "${path}[$index]" } +private class ProtoMapValueDecoder(private val struct: Struct, private val path: String?) : + CompositeDecoder { + override val serializersModule = EmptySerializersModule() + + override fun decodeSequentially() = true + + override fun decodeCollectionSize(descriptor: SerialDescriptor) = struct.fieldsCount + + override fun endStructure(descriptor: SerialDescriptor) {} + + private val structEntries: List> = struct.fieldsMap.entries.toList() + private val elementIndexes: IntIterator = (0 until structEntries.size * 2).iterator() + + private fun structEntryByElementIndex(index: Int): Map.Entry = + structEntries[index / 2] + + override fun decodeElementIndex(descriptor: SerialDescriptor) = + if (elementIndexes.hasNext()) elementIndexes.next() else CompositeDecoder.DECODE_DONE + + override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeBoolean) + + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeByte) + + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeChar) + + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeDouble) + + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeFloat) + + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index) { ProtoValueDecoder(this, it) } + + override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeInt) + + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeLong) + + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeShort) + + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = + if (index % 2 == 0) { + structEntryByElementIndex(index).key + } else { + decodeValueElement(index, Value::decodeString) + } + + private inline fun decodeValueElement(index: Int, block: Value.(String?) -> T): T { + require(index % 2 != 0) { "invalid value index: $index" } + val value = structEntryByElementIndex(index).value + val elementPath = elementPathForIndex(index) + return block(value, elementPath) + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T = + if (previousValue !== null) { + previousValue + } else { + decodeSerializableElement(index, deserializer) + } + + override fun decodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T? { + if (previousValue !== null) { + return previousValue + } + + if (index % 2 != 0) { + val structEntry = structEntryByElementIndex(index) + if (structEntry.value.hasNullValue()) { + return null + } + } + + return decodeSerializableElement(index, deserializer) + } + + private fun decodeSerializableElement( + index: Int, + deserializer: DeserializationStrategy + ): T { + val structEntry = structEntryByElementIndex(index) + val elementPath = elementPathForIndex(index) + + val elementDecoder = + if (index % 2 == 0) { + MapKeyDecoder(structEntry.key, elementPath, serializersModule) + } else { + ProtoValueDecoder(structEntry.value, elementPath) + } + + return deserializer.deserialize(elementDecoder) + } + + private fun elementPathForIndex(index: Int): String { + val structEntry = structEntryByElementIndex(index) + val key = structEntry.key + return if (index % 2 == 0) { + if (path === null) "[$key]" else "${path}[$key]" + } else { + if (path === null) "[$key].value" else "${path}[$key].value" + } + } + + override fun toString() = "ProtoMapValueDecoder{path=$path, size=${struct.fieldsCount}" +} + private object ProtoObjectValueDecoder : CompositeDecoder { override val serializersModule = EmptySerializersModule() @@ -387,3 +521,45 @@ private object ProtoObjectValueDecoder : CompositeDecoder { override fun endStructure(descriptor: SerialDescriptor) {} } + +private class MapKeyDecoder( + val key: String, + val path: String, + override val serializersModule: SerializersModule +) : Decoder { + + override fun decodeString() = key + + override fun beginStructure(descriptor: SerialDescriptor) = notSupported() + + override fun decodeBoolean() = notSupported() + + override fun decodeByte() = notSupported() + + override fun decodeChar() = notSupported() + + override fun decodeDouble() = notSupported() + + override fun decodeEnum(enumDescriptor: SerialDescriptor) = notSupported() + + override fun decodeFloat() = notSupported() + + override fun decodeInline(descriptor: SerialDescriptor) = notSupported() + + override fun decodeInt() = notSupported() + + override fun decodeLong() = notSupported() + + override fun decodeNotNullMark() = notSupported() + + override fun decodeNull() = notSupported() + + override fun decodeShort() = notSupported() + + private fun notSupported(): Nothing = + throw UnsupportedOperationException( + "The only valid method call on MapKeyDecoder is decodeString()" + ) + + override fun toString() = "MapKeyDecoder{path=$path}" +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructEncoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructEncoder.kt index 9fa0bc94d81..5c0ff3081ec 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructEncoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructEncoder.kt @@ -18,6 +18,8 @@ package com.google.firebase.dataconnect.util +import com.google.firebase.dataconnect.AnyValue +import com.google.firebase.dataconnect.serializers.AnyValueSerializer import com.google.protobuf.ListValue import com.google.protobuf.Struct import com.google.protobuf.Value @@ -36,31 +38,41 @@ internal inline fun encodeToStruct(value: T): Struct = encodeToStruct(serializer(), value) internal fun encodeToStruct(serializer: SerializationStrategy, value: T): Struct { + val valueProto = encodeToValue(serializer, value) + if (valueProto.kindCase == KindCase.KIND_NOT_SET) { + return Struct.getDefaultInstance() + } + require(valueProto.hasStructValue()) { + "encoding produced ${valueProto.kindCase}, " + + "but expected ${KindCase.STRUCT_VALUE} or ${KindCase.KIND_NOT_SET}" + } + return valueProto.structValue +} + +internal inline fun encodeToValue(value: T): Value = encodeToValue(serializer(), value) + +internal fun encodeToValue(serializer: SerializationStrategy, value: T): Value { val values = mutableListOf() ProtoValueEncoder(path = null, onValue = values::add).encodeSerializableValue(serializer, value) if (values.isEmpty()) { - return Struct.getDefaultInstance() + return Value.getDefaultInstance() } require(values.size == 1) { "encoding produced ${values.size} Value objects, but expected either 0 or 1" } - val valueProto = values.single() - require(valueProto.hasStructValue()) { - "encoding produced ${valueProto.kindCase}, but expected ${KindCase.STRUCT_VALUE}" - } - return valueProto.structValue + return values.single() } -private class ProtoValueEncoder(private val path: String?, private val onValue: (Value) -> Unit) : +internal class ProtoValueEncoder(private val path: String?, val onValue: (Value) -> Unit) : Encoder { override val serializersModule = EmptySerializersModule() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder = when (val kind = descriptor.kind) { - is StructureKind.MAP, is StructureKind.CLASS -> ProtoStructValueEncoder(path, onValue) is StructureKind.LIST -> ProtoListValueEncoder(path, onValue) + is StructureKind.MAP -> ProtoMapValueEncoder(path, onValue) is StructureKind.OBJECT -> ProtoObjectValueEncoder else -> throw IllegalArgumentException("unsupported SerialKind: ${kind::class.qualifiedName}") } @@ -116,6 +128,16 @@ private class ProtoValueEncoder(private val path: String?, private val onValue: override fun encodeString(value: String) { onValue(value.toValueProto()) } + + override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { + when (serializer) { + is AnyValueSerializer -> { + val anyValue = value as AnyValue + onValue(anyValue.protoValue) + } + else -> super.encodeSerializableValue(serializer, value) + } + } } private abstract class ProtoCompositeValueEncoder( @@ -266,6 +288,127 @@ private class ProtoStructValueEncoder(path: String?, onValue: (Value) -> Unit) : } } +private class ProtoMapValueEncoder( + private val path: String?, + private val onValue: (Value) -> Unit +) : CompositeEncoder { + + override val serializersModule = EmptySerializersModule() + + private val keyByIndex = mutableMapOf() + private val valueByIndex = mutableMapOf() + + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { + require(index % 2 != 0) { "invalid index: $index (must be odd)" } + valueByIndex[index] = value.toValueProto() + } + + override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { + require(index % 2 != 0) { "invalid index: $index (must be odd)" } + valueByIndex[index] = value.toValueProto() + } + + override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { + require(index % 2 != 0) { "invalid index: $index (must be odd)" } + valueByIndex[index] = value.toValueProto() + } + + override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { + require(index % 2 != 0) { "invalid index: $index (must be odd)" } + valueByIndex[index] = value.toValueProto() + } + + override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { + require(index % 2 != 0) { "invalid index: $index (must be odd)" } + valueByIndex[index] = value.toValueProto() + } + + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { + throw UnsupportedOperationException("inline is not implemented yet") + } + + override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { + require(index % 2 != 0) { "invalid index: $index (must be odd)" } + valueByIndex[index] = value.toValueProto() + } + + override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { + require(index % 2 != 0) { "invalid index: $index (must be odd)" } + valueByIndex[index] = value.toValueProto() + } + + @ExperimentalSerializationApi + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) { + if (index % 2 == 0) { + require(value is String) { + "even indexes must be a String, but got index=$index value=$value" + } + keyByIndex[index] = value + return + } + + val protoValue = + if (value === null) { + null + } else { + val subPath = keyByIndex[index - 1] ?: "$index" + var encodedValue: Value? = null + val encoder = ProtoValueEncoder(subPath) { encodedValue = it } + encoder.encodeNullableSerializableValue(serializer, value) + requireNotNull(encodedValue) { "ProtoValueEncoder should have produced a value" } + encodedValue + } + valueByIndex[index] = protoValue ?: nullProtoValue + } + + override fun encodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) { + if (index % 2 == 0) { + require(value is String) { + "even indexes must be a String, but got index=$index value=$value" + } + keyByIndex[index] = value + } else { + val subPath = keyByIndex[index - 1] ?: "$index" + val encoder = ProtoValueEncoder(subPath) { valueByIndex[index] = it } + encoder.encodeSerializableValue(serializer, value) + } + } + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { + require(index % 2 != 0) { "invalid index: $index (must be odd)" } + valueByIndex[index] = value.toValueProto() + } + + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { + if (index % 2 != 0) { + valueByIndex[index] = value.toValueProto() + } else { + keyByIndex[index] = value + } + } + + override fun endStructure(descriptor: SerialDescriptor) { + var i = 0 + val structBuilder = Struct.newBuilder() + while (keyByIndex.containsKey(i)) { + val key = keyByIndex[i++] + val value = valueByIndex[i++] + structBuilder.putFields(key, value) + } + onValue(structBuilder.build().toValueProto()) + } +} + private object ProtoObjectValueEncoder : CompositeEncoder { override val serializersModule = EmptySerializersModule() diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt index 071cc0597be..23b4d734a91 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt @@ -331,104 +331,106 @@ internal fun EmulatorIssue.toStructProto(): Struct = buildStructProto { put("message", message) } -internal fun Struct.toMap(): Map { - val mutualRecursion = - object { - val listForListValue: DeepRecursiveFunction> = DeepRecursiveFunction { - buildList { - it.valuesList.forEach { value -> - add( - when (val kind = value.kindCase) { - KindCase.NULL_VALUE -> null - KindCase.BOOL_VALUE -> value.boolValue - KindCase.NUMBER_VALUE -> value.numberValue - KindCase.STRING_VALUE -> value.stringValue - KindCase.LIST_VALUE -> callRecursive(value.listValue) - KindCase.STRUCT_VALUE -> mapForStruct.callRecursive(value.structValue) - else -> throw IllegalArgumentException("unsupported kind: $kind") - } - ) +internal fun ListValue.toListOfAny(): List = valueToAnyMutualRecursion.anyFromListValue(this) + +internal fun Struct.toMap(): Map = valueToAnyMutualRecursion.anyValueFromStruct(this) + +internal fun Value.toAny(): Any? = valueToAnyMutualRecursion.anyValueFromValue(this) + +internal fun List.toValueProto(): Value { + val key = "y8czq9rh75" + return mapOf(key to this).toStructProto().getFieldsOrThrow(key) +} + +internal fun Map.toValueProto(): Value = + Value.newBuilder().setStructValue(toStructProto()).build() + +internal fun Map.toStructProto(): Struct = + mapToStructProtoMutualRecursion.structForMap(this) + +private val mapToStructProtoMutualRecursion = + object { + val listValueForList: DeepRecursiveFunction, ListValue> = DeepRecursiveFunction { + val listValueProtoBuilder = ListValue.newBuilder() + it.forEach { value -> + listValueProtoBuilder.addValues( + when (value) { + null -> nullProtoValue + is Boolean -> value.toValueProto() + is Double -> value.toValueProto() + is String -> value.toValueProto() + is List<*> -> callRecursive(value).toValueProto() + is Map<*, *> -> structForMap.callRecursive(value).toValueProto() + else -> + throw IllegalArgumentException( + "unsupported type: ${value::class.qualifiedName}; " + + "supported types are: Boolean, Double, String, List, and Map" + ) } - } + ) } + listValueProtoBuilder.build() + } - val mapForStruct: DeepRecursiveFunction> = DeepRecursiveFunction { - buildMap { - it.fieldsMap.entries.forEach { (key, value) -> - put( - key, - when (val kind = value.kindCase) { - KindCase.NULL_VALUE -> null - KindCase.BOOL_VALUE -> value.boolValue - KindCase.NUMBER_VALUE -> value.numberValue - KindCase.STRING_VALUE -> value.stringValue - KindCase.LIST_VALUE -> listForListValue.callRecursive(value.listValue) - KindCase.STRUCT_VALUE -> callRecursive(value.structValue) - else -> throw IllegalArgumentException("unsupported kind: $kind") - } + val structForMap: DeepRecursiveFunction, Struct> = DeepRecursiveFunction { + val structProtoBuilder = Struct.newBuilder() + it.entries.forEach { (untypedKey, value) -> + val key = + (untypedKey as? String) + ?: throw IllegalArgumentException( + "map keys must be string, but got: " + + if (untypedKey === null) "null" else untypedKey::class.qualifiedName ) + structProtoBuilder.putFields( + key, + when (value) { + null -> nullProtoValue + is Double -> value.toValueProto() + is Boolean -> value.toValueProto() + is String -> value.toValueProto() + is List<*> -> listValueForList.callRecursive(value).toValueProto() + is Map<*, *> -> callRecursive(value).toValueProto() + else -> + throw IllegalArgumentException( + "unsupported type: ${value::class.qualifiedName}; " + + "supported types are: Boolean, Double, String, List, and Map" + ) } - } + ) } + structProtoBuilder.build() } + } - return mutualRecursion.mapForStruct(this) -} - -internal fun Map.toStructProto(): Struct { - val mutualRecursion = - object { - val listValueForList: DeepRecursiveFunction, ListValue> = DeepRecursiveFunction { - val listValueProtoBuilder = ListValue.newBuilder() - it.forEach { value -> - listValueProtoBuilder.addValues( - when (value) { - null -> nullProtoValue - is Boolean -> value.toValueProto() - is Double -> value.toValueProto() - is String -> value.toValueProto() - is List<*> -> callRecursive(value).toValueProto() - is Map<*, *> -> structForMap.callRecursive(value).toValueProto() - else -> - throw IllegalArgumentException( - "unsupported type: ${value::class.qualifiedName}; " + - "supported types are: Boolean, Double, String, List, and Map" - ) - } - ) +private val valueToAnyMutualRecursion = + object { + val anyFromListValue: DeepRecursiveFunction> = + DeepRecursiveFunction { listValue -> + buildList { + for (element in listValue.valuesList) { + add(anyValueFromValue.callRecursive(element)) + } } - listValueProtoBuilder.build() } - val structForMap: DeepRecursiveFunction, Struct> = DeepRecursiveFunction { - val structProtoBuilder = Struct.newBuilder() - it.entries.forEach { (untypedKey, value) -> - val key = - (untypedKey as? String) - ?: throw IllegalArgumentException( - "map keys must be string, but got: " + - if (untypedKey === null) "null" else untypedKey::class.qualifiedName - ) - structProtoBuilder.putFields( - key, - when (value) { - null -> nullProtoValue - is Double -> value.toValueProto() - is Boolean -> value.toValueProto() - is String -> value.toValueProto() - is List<*> -> listValueForList.callRecursive(value).toValueProto() - is Map<*, *> -> callRecursive(value).toValueProto() - else -> - throw IllegalArgumentException( - "unsupported type: ${value::class.qualifiedName}; " + - "supported types are: Boolean, Double, String, List, and Map" - ) - } - ) + val anyValueFromStruct: DeepRecursiveFunction> = + DeepRecursiveFunction { struct -> + buildMap { + for (entry in struct.fieldsMap) { + put(entry.key, anyValueFromValue.callRecursive(entry.value)) + } } - structProtoBuilder.build() } - } - return mutualRecursion.structForMap(this) -} + val anyValueFromValue: DeepRecursiveFunction = DeepRecursiveFunction { value -> + when (value.kindCase) { + KindCase.BOOL_VALUE -> value.boolValue + KindCase.NUMBER_VALUE -> value.numberValue + KindCase.STRING_VALUE -> value.stringValue + KindCase.LIST_VALUE -> anyFromListValue.callRecursive(value.listValue) + KindCase.STRUCT_VALUE -> anyValueFromStruct.callRecursive(value.structValue) + KindCase.NULL_VALUE -> null + else -> "ERROR: unsupported kindCase: ${value.kindCase}" + } + } + } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/AnyValueSerializerUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/AnyValueSerializerUnitTest.kt new file mode 100644 index 00000000000..4914a1db09b --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/AnyValueSerializerUnitTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 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 + +import com.google.firebase.dataconnect.serializers.AnyValueSerializer +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.StructureKind +import org.junit.Test + +@OptIn(ExperimentalSerializationApi::class) +class AnyValueSerializerUnitTest { + + @Test + fun `descriptor should have expected values`() { + assertSoftly { + AnyValueSerializer.descriptor.serialName shouldBe "com.google.firebase.dataconnect.AnyValue" + AnyValueSerializer.descriptor.kind shouldBe StructureKind.CLASS + } + } + + @Test + fun `serialize() should throw UnsupportedOperationException`() { + shouldThrow { AnyValueSerializer.serialize(mockk(), mockk()) } + } + + @Test + fun `deserialize() should throw UnsupportedOperationException`() { + shouldThrow { AnyValueSerializer.deserialize(mockk()) } + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/AnyValueUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/AnyValueUnitTest.kt new file mode 100644 index 00000000000..023c9aa4f13 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/AnyValueUnitTest.kt @@ -0,0 +1,560 @@ +/* + * Copyright 2024 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 + +import com.google.firebase.dataconnect.serializers.AnyValueSerializer +import com.google.firebase.dataconnect.testutil.DataConnectAnySerializer +import com.google.firebase.dataconnect.testutil.EdgeCases +import com.google.firebase.dataconnect.testutil.anyListScalar +import com.google.firebase.dataconnect.testutil.anyMapScalar +import com.google.firebase.dataconnect.testutil.anyNumberScalar +import com.google.firebase.dataconnect.testutil.anyScalar +import com.google.firebase.dataconnect.testutil.anyStringScalar +import com.google.firebase.dataconnect.testutil.filterNotNull +import com.google.firebase.dataconnect.util.encodeToValue +import io.kotest.assertions.assertSoftly +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.filterNot +import io.kotest.property.arbitrary.map +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.serializer +import org.junit.Test + +@Suppress("ReplaceCallWithBinaryOperator") +@OptIn(ExperimentalKotest::class) +class AnyValueUnitTest { + + @Test + fun `default serializer should be AnyValueSerializer`() { + serializer() shouldBeSameInstanceAs AnyValueSerializer + } + + @Test + fun `constructor(String) creates an object with the expected value (edge cases)`() = runTest { + for (value in EdgeCases.strings) { + AnyValue(value).value shouldBe value + } + } + + @Test + fun `constructor(String) creates an object with the expected value (normal cases)`() = runTest { + checkAll(normalCasePropTestConfig, Arb.anyStringScalar()) { AnyValue(it).value shouldBe it } + } + + @Test + fun `constructor(Double) creates an object with the expected value (edge cases)`() = runTest { + for (value in EdgeCases.numbers) { + AnyValue(value).value shouldBe value + } + } + + @Test + fun `constructor(Double) creates an object with the expected value (normal cases)`() = runTest { + checkAll(normalCasePropTestConfig, Arb.anyNumberScalar()) { AnyValue(it).value shouldBe it } + } + + @Test + fun `constructor(Boolean) creates an object with the expected value`() { + assertSoftly { + AnyValue(true).value shouldBe true + AnyValue(false).value shouldBe false + } + } + + @Test + fun `constructor(List) creates an object with the expected value (edge cases)`() { + assertSoftly { + for (value in EdgeCases.lists) { + AnyValue(value).value shouldBe value + } + } + } + + @Test + fun `constructor(List) creates an object with the expected value (normal cases)`() = runTest { + checkAll(normalCasePropTestConfig, Arb.anyListScalar()) { AnyValue(it).value shouldBe it } + } + + @Test + fun `constructor(Map) creates an object with the expected value (edge cases)`() { + assertSoftly { + for (value in EdgeCases.maps) { + AnyValue(value).value shouldBe value + } + } + } + + @Test + fun `constructor(Map) creates an object with the expected value (normal cases)`() = runTest { + checkAll(normalCasePropTestConfig, Arb.anyMapScalar()) { AnyValue(it).value shouldBe it } + } + + @Test + fun `decode() can decode strings (edge cases)`() { + val serializer = serializer() + assertSoftly { + for (value in EdgeCases.strings) { + AnyValue(value).decode(serializer) shouldBe value + } + } + } + + @Test + fun `decode() can decode strings (normal cases)`() = runTest { + val serializer = serializer() + checkAll(normalCasePropTestConfig, Arb.anyStringScalar()) { + AnyValue(it).decode(serializer) shouldBe it + } + } + + @Test + fun `decode() can decode doubles (edge cases)`() { + val serializer = serializer() + assertSoftly { + for (value in EdgeCases.numbers) { + AnyValue(value).decode(serializer) shouldBe value + } + } + } + + @Test + fun `decode() can decode doubles (normal cases)`() = runTest { + val serializer = serializer() + checkAll(normalCasePropTestConfig, Arb.anyNumberScalar()) { + AnyValue(it).decode(serializer) shouldBe it + } + } + + @Test + fun `decode() can decode booleans (edge cases)`() { + val serializer = serializer() + assertSoftly { + AnyValue(true).decode(serializer) shouldBe true + AnyValue(false).decode(serializer) shouldBe false + } + } + + @Test + fun `decode() can decode lists (edge cases)`() { + val serializer = ListSerializer(DataConnectAnySerializer) + assertSoftly { + for (value in EdgeCases.lists) { + AnyValue(value).decode(serializer) shouldContainExactly value + } + } + } + + @Test + fun `decode() can decode lists (normal cases)`() = runTest { + val serializer = ListSerializer(DataConnectAnySerializer) + checkAll(normalCasePropTestConfig, Arb.anyListScalar()) { + AnyValue(it).decode(serializer) shouldContainExactly it + } + } + + @Test + fun `decode() can decode maps (edge cases)`() { + val serializer = MapSerializer(serializer(), DataConnectAnySerializer) + assertSoftly { + for (value in EdgeCases.maps) { + AnyValue(value).decode(serializer) shouldBe value + } + } + } + + @Test + fun `decode() can decode maps (normal cases)`() = runTest { + val serializer = MapSerializer(serializer(), DataConnectAnySerializer) + checkAll(normalCasePropTestConfig, Arb.anyMapScalar()) { + AnyValue(it).decode(serializer) shouldBe it + } + } + + @Test + fun `decode() can decode nested AnyValue`() { + @Serializable data class TestData(val a: Int, val b: AnyValue) + val nestedAnyValueList = listOf("a", 12.34, mapOf("foo" to "bar")) + val anyValue = AnyValue(mapOf("a" to 12.0, "b" to nestedAnyValueList)) + + anyValue.decode(serializer()) shouldBe + TestData(a = 12, b = AnyValue(nestedAnyValueList)) + } + + @Test + fun `decode() can decode @Serializable class with non-nullable scalar properties`() { + @Serializable + data class TestData( + val int: Int, + val string: String, + val boolean: Boolean, + val double: Double, + ) + val testData = TestData(42, "w82qq4jbb6", true, 12.34) + val anyValue = AnyValue(encodeToValue(testData)) + + anyValue.decode(serializer()) shouldBe testData + } + + @Test + fun `decode() can decode @Serializable class with nullable scalar properties with non-null values`() { + @Serializable + data class TestData( + val int: Int?, + val string: String?, + val boolean: Boolean?, + val double: Double?, + ) + val testData = TestData(42, "srg7yzecwq", true, 12.34) + val anyValue = AnyValue(encodeToValue(testData)) + + anyValue.decode(serializer()) shouldBe testData + } + + @Test + fun `decode() can decode @Serializable class with nullable scalar properties with null values`() { + @Serializable + data class TestData( + val int: Int?, + val string: String?, + val boolean: Boolean?, + val double: Double?, + ) + val testData = TestData(null, null, null, null) + val anyValue = AnyValue(encodeToValue(testData)) + + anyValue.decode(serializer()) shouldBe testData + } + + @Test + fun `decode() can decode @Serializable class with nested values`() { + @Serializable data class Foo(val int: Int, val foo: Foo? = null) + @Serializable data class TestData(val list: List, val foo: Foo) + val testData = TestData(listOf(Foo(111), Foo(222, Foo(333))), Foo(444, Foo(555))) + val anyValue = AnyValue(encodeToValue(testData)) + + anyValue.decode(serializer()) shouldBe testData + } + + @Test + fun `decode() uses the default serializer if not explicitly specified`() { + val anyValue = AnyValue("mb6jq8jabp") + anyValue.decode() shouldBe "mb6jq8jabp" + } + + @Test + fun `equals(this) returns true`() { + val anyValue = AnyValue(42.0) + anyValue.equals(anyValue).shouldBeTrue() + } + + @Test + fun `equals(equal, but distinct, instance) returns true`() = runTest { + checkAll(iterations = 1000, Arb.anyScalar().filterNotNull()) { + val anyValue1 = AnyValue.fromAny(it) + val anyValue2 = AnyValue.fromAny(it) + anyValue1.equals(anyValue2).shouldBeTrue() + } + } + + @Test + fun `equals(null) returns false`() { + val anyValue = AnyValue(42.0) + anyValue.equals(null).shouldBeFalse() + } + + @Test + fun `equals(some other type) returns false`() { + val anyValue = AnyValue(42.0) + anyValue.equals("not an AnyValue object").shouldBeFalse() + } + + @Test + fun `equals(unequal instance) returns false`() = runTest { + val values = Arb.anyScalar().filterNotNull() + checkAll(iterations = 1000, values) { value -> + val anyValue1 = AnyValue.fromAny(value) + val anyValue2 = AnyValue.fromAny(values.filterNot { it == value }.bind()) + anyValue1.equals(anyValue2).shouldBeFalse() + } + } + + @Test + fun `hashCode() should return the same value when invoked repeatedly`() = runTest { + val values = Arb.anyScalar().filterNotNull().map(AnyValue::fromAny) + checkAll(iterations = 1000, values) { anyValue -> + val hashCode = anyValue.hashCode() + val hashCodes = List(100) { anyValue.hashCode() }.toSet() + hashCodes.shouldContainExactly(hashCode) + } + } + + @Test + fun `hashCode() should return different value when the encapsulated value has a different hash code`() = + runTest { + val values = Arb.anyScalar().filterNotNull() + checkAll(normalCasePropTestConfig, values) { value1 -> + val value2 = values.bind() + val anyValue1 = AnyValue.fromAny(value1) + val anyValue2 = AnyValue.fromAny(value2) + if (value1.hashCode() == value2.hashCode()) { + anyValue1.hashCode() shouldBe anyValue2.hashCode() + } else { + anyValue1.hashCode() shouldNotBe anyValue2.hashCode() + } + } + } + + @Test + fun `toString() should not throw`() = runTest { + val values = Arb.anyScalar().filterNotNull().map(AnyValue::fromAny) + checkAll(normalCasePropTestConfig, values) { it.toString() } + } + + @Test + fun `encode() can encode strings (edge cases)`() { + val serializer = serializer() + assertSoftly { + for (value in EdgeCases.strings) { + AnyValue.encode(value, serializer) shouldBe AnyValue(value) + } + } + } + + @Test + fun `encode() can encode strings (normal cases)`() = runTest { + val serializer = serializer() + checkAll(normalCasePropTestConfig, Arb.anyStringScalar()) { + AnyValue.encode(it, serializer) shouldBe AnyValue(it) + } + } + + @Test + fun `encode() can encode doubles (edge cases)`() { + val serializer = serializer() + assertSoftly { + for (value in EdgeCases.numbers) { + AnyValue.encode(value, serializer) shouldBe AnyValue(value) + } + } + } + + @Test + fun `encode() can encode doubles (normal cases)`() = runTest { + val serializer = serializer() + checkAll(normalCasePropTestConfig, Arb.anyNumberScalar()) { + AnyValue.encode(it, serializer) shouldBe AnyValue(it) + } + } + + @Test + fun `encode() can encode booleans (edge cases)`() { + val serializer = serializer() + assertSoftly { + AnyValue.encode(true, serializer) shouldBe AnyValue(true) + AnyValue.encode(false, serializer) shouldBe AnyValue(false) + } + } + + @Test + fun `encode() can encode lists (edge cases)`() { + val serializer = ListSerializer(DataConnectAnySerializer) + assertSoftly { + for (value in EdgeCases.lists) { + AnyValue.encode(value, serializer) shouldBe AnyValue(value) + } + } + } + + @Test + fun `encode() can encode lists (normal cases)`() = runTest { + val serializer = ListSerializer(DataConnectAnySerializer) + checkAll(normalCasePropTestConfig, Arb.anyListScalar()) { + AnyValue.encode(it, serializer) shouldBe AnyValue(it) + } + } + + @Test + fun `encode() can encode maps (edge cases)`() { + val serializer = MapSerializer(serializer(), DataConnectAnySerializer) + assertSoftly { + for (value in EdgeCases.maps) { + AnyValue.encode(value, serializer) shouldBe AnyValue(value) + } + } + } + + @Test + fun `encode() can encode maps (normal cases)`() = runTest { + val serializer = MapSerializer(serializer(), DataConnectAnySerializer) + checkAll(normalCasePropTestConfig, Arb.anyMapScalar()) { + AnyValue.encode(it, serializer) shouldBe AnyValue(it) + } + } + + @Test + fun `encode() can encode nested AnyValue`() { + @Serializable data class TestData(val a: Int, val b: AnyValue) + val nestedAnyValueList = listOf("a", 12.34, mapOf("foo" to "bar")) + val testData = TestData(a = 12, b = AnyValue(nestedAnyValueList)) + + val anyValue = AnyValue.encode(testData) + + anyValue.value shouldBe mapOf("a" to 12.0, "b" to nestedAnyValueList) + } + + @Test + fun `encode() can encode @Serializable class with non-nullable scalar properties`() { + @Serializable + data class TestData( + val int: Int, + val string: String, + val boolean: Boolean, + val double: Double, + ) + val testData = TestData(42, "gkg3jsp2jz", true, 12.34) + + val anyValue = AnyValue.encode(testData) + + anyValue.value shouldBe + (mapOf("int" to 42.0, "string" to "gkg3jsp2jz", "boolean" to true, "double" to 12.34)) + } + + @Test + fun `encode() can encode @Serializable class with nullable scalar properties with non-null values`() { + @Serializable + data class TestData( + val int: Int?, + val string: String?, + val boolean: Boolean?, + val double: Double?, + ) + val testData = TestData(42, "mj64xgc2sz", true, 12.34) + + val anyValue = AnyValue.encode(testData) + + anyValue.value shouldBe + (mapOf("int" to 42.0, "string" to "mj64xgc2sz", "boolean" to true, "double" to 12.34)) + } + + @Test + fun `encode() can encode @Serializable class with nullable scalar properties with null values`() { + @Serializable + data class TestData( + val int: Int?, + val string: String?, + val boolean: Boolean?, + val double: Double?, + ) + val testData = TestData(null, null, null, null) + + val anyValue = AnyValue.encode(testData) + + anyValue.value shouldBe + (mapOf("int" to null, "string" to null, "boolean" to null, "double" to null)) + } + + @Test + fun `encode() can encode @Serializable class with nested values`() { + @Serializable data class Foo(val int: Int, val foo: Foo? = null) + @Serializable data class TestData(val list: List, val foo: Foo) + val testData = TestData(listOf(Foo(111), Foo(222, Foo(333))), Foo(444, Foo(555))) + + val anyValue = AnyValue.encode(testData) + + anyValue.value shouldBe + (mapOf( + "list" to + listOf( + mapOf("int" to 111.0, "foo" to null), + mapOf("int" to 222.0, "foo" to mapOf("int" to 333.0, "foo" to null)) + ), + "foo" to mapOf("int" to 444.0, "foo" to mapOf("int" to 555.0, "foo" to null)) + )) + } + + @Test + fun `encode() uses the default serializer if not explicitly specified`() { + AnyValue.encode("we47rcjzm4") shouldBe AnyValue("we47rcjzm4") + } + + @Test + fun `fromNullableAny() edge cases`() { + for (value in EdgeCases.anyScalars) { + val anyValue = AnyValue.fromAny(value) + if (value === null) { + anyValue.shouldBeNull() + } else { + anyValue.shouldNotBeNull() + anyValue.value shouldBe value + } + } + } + + @Test + fun `fromNullableAny() normal cases`() = runTest { + checkAll(normalCasePropTestConfig, Arb.anyScalar()) { value -> + val anyValue = AnyValue.fromAny(value) + if (value === null) { + anyValue.shouldBeNull() + } else { + anyValue.shouldNotBeNull() + anyValue.value shouldBe value + } + } + } + + @Test + fun `fromNonNullableAny() edge cases`() { + for (value in EdgeCases.anyScalars.filterNotNull()) { + val anyValue = AnyValue.fromAny(value) + anyValue.value shouldBe value + } + } + + @Test + fun `fromNonNullableAny() normal cases`() = runTest { + checkAll(normalCasePropTestConfig, Arb.anyScalar().filterNotNull()) { value -> + val anyValue = AnyValue.fromAny(value) + anyValue.shouldNotBeNull() + anyValue.value shouldBe value + } + } + + private companion object { + + val normalCasePropTestConfig = + PropTestConfig( + iterations = 1000, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.0) + ) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/DataConnectAnySerializer.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/DataConnectAnySerializer.kt new file mode 100644 index 00000000000..ae15e816846 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/DataConnectAnySerializer.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2024 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.testutil + +import com.google.firebase.dataconnect.util.ProtoValueDecoder +import com.google.firebase.dataconnect.util.ProtoValueEncoder +import com.google.firebase.dataconnect.util.nullProtoValue +import com.google.firebase.dataconnect.util.toListOfAny +import com.google.firebase.dataconnect.util.toMap +import com.google.firebase.dataconnect.util.toValueProto +import com.google.protobuf.Value +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.buildSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +// Adapted from JsonContentPolymorphicSerializer +// https://github.com/Kotlin/kotlinx.serialization/blob/8c84a5b4dd/formats/json/commonMain/src/kotlinx/serialization/json/JsonContentPolymorphicSerializer.kt#L67 +object DataConnectAnySerializer : KSerializer { + + @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) + override val descriptor = + buildSerialDescriptor("DataConnectAnySerializer", PolymorphicKind.SEALED) + + @Suppress("UNCHECKED_CAST") + override fun serialize(encoder: Encoder, value: Any?) { + require(encoder is ProtoValueEncoder) { + "DataConnectAnySerializer only supports ProtoValueEncoder" + + ", but got ${encoder::class.qualifiedName}" + } + val protoValue = + when (value) { + null -> nullProtoValue + is String -> value.toValueProto() + is Double -> value.toValueProto() + is Boolean -> value.toValueProto() + is List<*> -> value.toValueProto() + is Map<*, *> -> (value as Map).toValueProto() + else -> + throw IllegalArgumentException( + "unsupported type: ${value::class.qualifiedName} (error code: av5kpmwb8h)" + ) + } + encoder.onValue(protoValue) + } + + override fun deserialize(decoder: Decoder): Any? { + require(decoder is ProtoValueDecoder) { + "DataConnectAnySerializer only supports ProtoValueDecoder" + + ", but got ${decoder::class.qualifiedName}" + } + return when (val kindCase = decoder.valueProto.kindCase) { + Value.KindCase.NULL_VALUE -> null + Value.KindCase.STRING_VALUE -> decoder.valueProto.stringValue + Value.KindCase.NUMBER_VALUE -> decoder.valueProto.numberValue + Value.KindCase.BOOL_VALUE -> decoder.valueProto.boolValue + Value.KindCase.LIST_VALUE -> decoder.valueProto.listValue.toListOfAny() + Value.KindCase.STRUCT_VALUE -> decoder.valueProto.structValue.toMap() + else -> + throw IllegalArgumentException("unsupported KindCase: $kindCase (error code: 3bde44vczt)") + } + } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/Arbs.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/Arbs.kt index 73db74d6f5b..a316fb1f6ab 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/Arbs.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/Arbs.kt @@ -84,6 +84,10 @@ fun Arb.Companion.dataConnectSettings( DataConnectSettings(host = host.next(rs), sslEnabled = sslEnabled.next(rs)) } +fun Arb.Companion.anyNumberScalar(): Arb = anyScalar().filterIsInstance() + +fun Arb.Companion.anyStringScalar(): Arb = anyScalar().filterIsInstance() + fun Arb.Companion.anyListScalar(): Arb> = anyScalar().filterIsInstance>() fun Arb.Companion.anyMapScalar(): Arb> = From 7ed742fcebdb318ab81053856c3d6ac02d2ec992 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 20 Sep 2024 18:43:37 +0000 Subject: [PATCH 2/3] updated firebase-dataconnect/api.txt by running ./gradlew firebase-dataconnect:generateApiTxtFile --- firebase-dataconnect/api.txt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt index fa432dab5da..9d4e4ff986d 100644 --- a/firebase-dataconnect/api.txt +++ b/firebase-dataconnect/api.txt @@ -1,6 +1,26 @@ // Signature format: 2.0 package com.google.firebase.dataconnect { + @kotlinx.serialization.Serializable(with=AnyValueSerializer::class) public final class AnyValue { + ctor public AnyValue(@NonNull java.util.Map value); + ctor public AnyValue(@NonNull java.util.List value); + ctor public AnyValue(@NonNull String value); + ctor public AnyValue(boolean value); + ctor public AnyValue(double value); + method public T decode(@NonNull kotlinx.serialization.DeserializationStrategy deserializer); + method public inline T decode(); + method @NonNull public Object getValue(); + property @NonNull public final Object value; + field @NonNull public static final com.google.firebase.dataconnect.AnyValue.Companion Companion; + } + + public static final class AnyValue.Companion { + method @NonNull public com.google.firebase.dataconnect.AnyValue encode(@Nullable T value, @NonNull kotlinx.serialization.SerializationStrategy serializer); + method public inline com.google.firebase.dataconnect.AnyValue encode(@Nullable T value); + method @NonNull public com.google.firebase.dataconnect.AnyValue fromAny(@NonNull Object value); + method @Nullable public com.google.firebase.dataconnect.AnyValue fromNullableAny(@Nullable Object value); + } + public final class ConnectorConfig { ctor public ConnectorConfig(@NonNull String connector, @NonNull String location, @NonNull String serviceId); method @NonNull public com.google.firebase.dataconnect.ConnectorConfig copy(@NonNull String connector = connector, @NonNull String location = location, @NonNull String serviceId = serviceId); @@ -223,6 +243,14 @@ package com.google.firebase.dataconnect.querymgr { package com.google.firebase.dataconnect.serializers { + public final class AnyValueSerializer implements kotlinx.serialization.KSerializer { + method @NonNull public com.google.firebase.dataconnect.AnyValue deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder); + method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor(); + method public void serialize(@NonNull kotlinx.serialization.encoding.Encoder encoder, @NonNull com.google.firebase.dataconnect.AnyValue value); + property @NonNull public kotlinx.serialization.descriptors.SerialDescriptor descriptor; + field @NonNull public static final com.google.firebase.dataconnect.serializers.AnyValueSerializer INSTANCE; + } + public final class DateSerializer implements kotlinx.serialization.KSerializer { method @NonNull public java.util.Date deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder); method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor(); From 7fdd3dfebefe590d068795d69e0147a8ade0d234 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 20 Sep 2024 19:48:32 +0000 Subject: [PATCH 3/3] AnyScalarIntegrationTest.kt added --- .../connectors/connectors.gradle.kts | 2 + .../demo/AnyScalarIntegrationTest.kt | 1032 +++++++++++++++++ 2 files changed, 1034 insertions(+) create mode 100644 firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/AnyScalarIntegrationTest.kt diff --git a/firebase-dataconnect/connectors/connectors.gradle.kts b/firebase-dataconnect/connectors/connectors.gradle.kts index d3c21d7415d..334e70f9ab8 100644 --- a/firebase-dataconnect/connectors/connectors.gradle.kts +++ b/firebase-dataconnect/connectors/connectors.gradle.kts @@ -84,6 +84,8 @@ dependencies { androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.kotest.assertions) + androidTestImplementation(libs.kotest.property) androidTestImplementation(libs.kotlin.coroutines.test) androidTestImplementation(libs.truth) androidTestImplementation(libs.truth.liteproto.extension) diff --git a/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/AnyScalarIntegrationTest.kt b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/AnyScalarIntegrationTest.kt new file mode 100644 index 00000000000..35dfffa9d79 --- /dev/null +++ b/firebase-dataconnect/connectors/src/androidTest/kotlin/com/google/firebase/dataconnect/connectors/demo/AnyScalarIntegrationTest.kt @@ -0,0 +1,1032 @@ +/* + * Copyright 2024 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.connectors.demo + +import com.google.firebase.dataconnect.AnyValue +import com.google.firebase.dataconnect.DataConnectException +import com.google.firebase.dataconnect.OperationRef +import com.google.firebase.dataconnect.connectors.demo.testutil.DemoConnectorIntegrationTestBase +import com.google.firebase.dataconnect.generated.GeneratedMutation +import com.google.firebase.dataconnect.generated.GeneratedQuery +import com.google.firebase.dataconnect.testutil.EdgeCases +import com.google.firebase.dataconnect.testutil.anyListScalar +import com.google.firebase.dataconnect.testutil.anyScalar +import com.google.firebase.dataconnect.testutil.expectedAnyScalarRoundTripValue +import com.google.firebase.dataconnect.testutil.filterNotAnyScalarMatching +import com.google.firebase.dataconnect.testutil.filterNotIncludesAllMatchingAnyScalars +import com.google.firebase.dataconnect.testutil.filterNotNull +import io.kotest.assertions.asClue +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContainIgnoringCase +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.orNull +import io.kotest.property.checkAll +import java.util.UUID +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import org.junit.Test + +class AnyScalarIntegrationTest : DemoConnectorIntegrationTestBase() { + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type AnyScalarNonNullable @table { value: Any!, tag: String, position: Int } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun anyScalarNonNullable_MutationVariableEdgeCases() = + runTest(timeout = 60.seconds) { + assertSoftly { + for (value in EdgeCases.anyScalars.filterNotNull()) { + withClue("value=$value") { verifyAnyScalarNonNullableRoundTrip(value) } + } + } + } + + @Test + fun anyScalarNonNullable_QueryVariableEdgeCases() = + runTest(timeout = 60.seconds) { + assertSoftly { + for (value in EdgeCases.anyScalars.filterNotNull()) { + val otherValues = Arb.anyScalar().filterNotNull().filterNotAnyScalarMatching(value) + withClue("value=$value otherValues=$otherValues") { + verifyAnyScalarNonNullableQueryVariable(value, otherValues.next(), otherValues.next()) + } + } + } + } + + @Test + fun anyScalarNonNullable_MutationVariableNormalCases() = + runTest(timeout = 60.seconds) { + checkAll(normalCasePropTestConfig, Arb.anyScalar().filterNotNull()) { value -> + verifyAnyScalarNonNullableRoundTrip(value) + } + } + + @Test + fun anyScalarNonNullable_QueryVariableNormalCases() = + runTest(timeout = 60.seconds) { + checkAll(normalCasePropTestConfig, Arb.anyScalar().filterNotNull()) { value -> + val otherValues = Arb.anyScalar().filterNotNull().filterNotAnyScalarMatching(value) + verifyAnyScalarNonNullableQueryVariable(value, otherValues.next(), otherValues.next()) + } + } + + @Test + fun anyScalarNonNullable_MutationFailsIfAnyVariableIsMissing() = runTest { + connector.anyScalarNonNullableInsert.verifyFailsWithMissingVariableValue() + } + + @Test + fun anyScalarNonNullable_QueryFailsIfAnyVariableIsMissing() = runTest { + connector.anyScalarNonNullableGetAllByTagAndValue.verifyFailsWithMissingVariableValue() + } + + @Test + fun anyScalarNonNullable_MutationFailsIfAnyVariableIsNull() = runTest { + connector.anyScalarNonNullableInsert.verifyFailsWithNullVariableValue() + } + + @Test + fun anyScalarNonNullable_QueryFailsIfAnyVariableIsNull() = runTest { + connector.anyScalarNonNullableGetAllByTagAndValue.verifyFailsWithNullVariableValue() + } + + private suspend fun verifyAnyScalarNonNullableRoundTrip(value: Any) { + val anyValue = AnyValue.fromAny(value) + val expectedQueryResult = AnyValue.fromAny(expectedAnyScalarRoundTripValue(value)) + val key = connector.anyScalarNonNullableInsert.execute(anyValue) {}.data.key + + val queryResult = connector.anyScalarNonNullableGetByKey.execute(key) + queryResult.data shouldBe + AnyScalarNonNullableGetByKeyQuery.Data( + AnyScalarNonNullableGetByKeyQuery.Data.Item(expectedQueryResult) + ) + } + + private suspend fun verifyAnyScalarNonNullableQueryVariable( + value: Any, + value2: Any, + value3: Any, + ) { + require(value != value2) + require(value != value3) + require(expectedAnyScalarRoundTripValue(value) != expectedAnyScalarRoundTripValue(value2)) + require(expectedAnyScalarRoundTripValue(value) != expectedAnyScalarRoundTripValue(value3)) + + val tag = UUID.randomUUID().toString() + val anyValue = AnyValue.fromAny(value) + val anyValue2 = AnyValue.fromAny(value2) + val anyValue3 = AnyValue.fromAny(value3) + val keys = + connector.anyScalarNonNullableInsert3 + .execute(anyValue, anyValue2, anyValue3) { this.tag = tag } + .data + + val queryResult = + connector.anyScalarNonNullableGetAllByTagAndValue.execute(anyValue) { this.tag = tag } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type AnyScalarNullable @table { value: Any, tag: String, position: Int } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun anyScalarNullable_MutationVariableEdgeCases() = + runTest(timeout = 60.seconds) { + assertSoftly { + for (value in EdgeCases.anyScalars) { + withClue("value=$value") { verifyAnyScalarNullableRoundTrip(value) } + } + } + } + + @Test + fun anyScalarNullable_QueryVariableEdgeCases() = + runTest(timeout = 60.seconds) { + assertSoftly { + for (value in EdgeCases.anyScalars) { + val otherValues = Arb.anyScalar().filterNotAnyScalarMatching(value) + withClue("value=$value otherValues=$otherValues") { + verifyAnyScalarNullableQueryVariable(value, otherValues.next(), otherValues.next()) + } + } + } + } + + @Test + fun anyScalarNullable_MutationVariableNormalCases() = + runTest(timeout = 60.seconds) { + checkAll(normalCasePropTestConfig, Arb.anyScalar()) { value -> + verifyAnyScalarNullableRoundTrip(value) + } + } + + @Test + fun anyScalarNullable_QueryVariableNormalCases() = + runTest(timeout = 60.seconds) { + checkAll(normalCasePropTestConfig, Arb.anyScalar()) { value -> + val otherValues = Arb.anyScalar().filterNotAnyScalarMatching(value) + verifyAnyScalarNullableQueryVariable(value, otherValues.next(), otherValues.next()) + } + } + + @Test + fun anyScalarNullable_MutationSucceedsIfAnyVariableIsMissing() = runTest { + val key = connector.anyScalarNullableInsert.execute {}.data.key + val queryResult = connector.anyScalarNullableGetByKey.execute(key) + queryResult.data.asClue { it.item?.value.shouldBeNull() } + } + + @Test + fun anyScalarNullable_QuerySucceedsIfAnyVariableIsMissing() = runTest { + val values = Arb.anyScalar().map { AnyValue.fromAny(it) } + val tag = UUID.randomUUID().toString() + val keys = + connector.anyScalarNullableInsert3 + .execute { + this.tag = tag + this.value1 = values.next() + this.value2 = values.next() + this.value3 = values.next() + } + .data + val keyIds = listOf(keys.key1, keys.key2, keys.key3).map { it.id } + + val queryResult = connector.anyScalarNullableGetAllByTagAndValue.execute { this.tag = tag } + val queryIds = queryResult.data.items.map { it.id } + queryIds shouldContainExactlyInAnyOrder keyIds + } + + @Test + fun anyScalarNullable_MutationSucceedsIfAnyVariableIsNull() = runTest { + val key = connector.anyScalarNullableInsert.execute { value = null }.data.key + val queryResult = connector.anyScalarNullableGetByKey.execute(key) + queryResult.data.asClue { it.item?.value.shouldBeNull() } + } + + @Test + fun anyScalarNullable_QuerySucceedsIfAnyVariableIsNull() = runTest { + val values = Arb.anyScalar().filter { it !== null }.map { AnyValue.fromAny(it) } + val tag = UUID.randomUUID().toString() + val keys = + connector.anyScalarNullableInsert3 + .execute { + this.tag = tag + this.value1 = null + this.value2 = values.next() + this.value3 = values.next() + } + .data + + val queryResult = + connector.anyScalarNullableGetAllByTagAndValue.execute { + this.tag = tag + this.value = null + } + + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id) + } + + private suspend fun verifyAnyScalarNullableRoundTrip(value: Any?) { + val anyValue = AnyValue.fromAny(value) + val expectedQueryResult = AnyValue.fromAny(expectedAnyScalarRoundTripValue(value)) + val key = connector.anyScalarNullableInsert.execute { this.value = anyValue }.data.key + + val queryResult = connector.anyScalarNullableGetByKey.execute(key) + queryResult.data shouldBe + AnyScalarNullableGetByKeyQuery.Data( + AnyScalarNullableGetByKeyQuery.Data.Item(expectedQueryResult) + ) + } + + private suspend fun verifyAnyScalarNullableQueryVariable( + value: Any?, + value2: Any?, + value3: Any? + ) { + require(value != value2) + require(value != value3) + require(expectedAnyScalarRoundTripValue(value) != expectedAnyScalarRoundTripValue(value2)) + require(expectedAnyScalarRoundTripValue(value) != expectedAnyScalarRoundTripValue(value3)) + + val tag = UUID.randomUUID().toString() + val anyValue = AnyValue.fromAny(value) + val anyValue2 = AnyValue.fromAny(value2) + val anyValue3 = AnyValue.fromAny(value3) + val keys = + connector.anyScalarNullableInsert3 + .execute { + this.tag = tag + this.value1 = anyValue + this.value2 = anyValue2 + this.value3 = anyValue3 + } + .data + + val queryResult = + connector.anyScalarNullableGetAllByTagAndValue.execute { + this.value = anyValue + this.tag = tag + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type AnyScalarNullableListOfNullable @table { value: [Any], tag: String, position: Int } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun anyScalarNullableListOfNullable_MutationVariableEdgeCases() = + runTest(timeout = 60.seconds) { + assertSoftly { + val edgeCases = EdgeCases.lists.map { it.filterNotNull() } + for (value in edgeCases) { + withClue("value=$value") { verifyAnyScalarNullableListOfNullableRoundTrip(value) } + } + } + } + + @Test + fun anyScalarNullableListOfNullable_QueryVariableEdgeCases() = + runTest(timeout = 60.seconds) { + val edgeCases = EdgeCases.lists.map { it.filterNotNull() }.filter { it.isNotEmpty() } + val otherValues = Arb.anyListScalar().map { it.filterNotNull() } + + assertSoftly { + for (value in edgeCases) { + val curOtherValues = + otherValues.filterNotIncludesAllMatchingAnyScalars(value).orNull(nullProbability = 0.1) + withClue("value=$value") { + verifyAnyScalarNullableListOfNullableQueryVariable( + value, + curOtherValues.next(), + curOtherValues.next() + ) + } + } + } + } + + @Test + fun anyScalarNullableListOfNullable_MutationVariableNormalCases() = + runTest(timeout = 60.seconds) { + val values = Arb.anyListScalar().map { it.filterNotNull() } + checkAll(normalCasePropTestConfig, values) { value -> + verifyAnyScalarNullableListOfNullableRoundTrip(value) + } + } + + @Test + fun anyScalarNullableListOfNullable_QueryVariableNormalCases() = + runTest(timeout = 60.seconds) { + val values = Arb.anyListScalar().map { it.filterNotNull() }.filter { it.isNotEmpty() } + val otherValues = Arb.anyListScalar().map { it.filterNotNull() } + + checkAll(normalCasePropTestConfig, values) { value -> + val curOtherValues = + otherValues.filterNotIncludesAllMatchingAnyScalars(value).orNull(nullProbability = 0.1) + verifyAnyScalarNullableListOfNullableQueryVariable( + value, + curOtherValues.next(), + curOtherValues.next() + ) + } + } + + @Test + fun anyScalarNullableListOfNullable_MutationSucceedsIfAnyVariableIsMissing() = runTest { + val key = connector.anyScalarNullableListOfNullableInsert.execute {}.data.key + val queryResult = connector.anyScalarNullableListOfNullableGetByKey.execute(key) + queryResult.data.asClue { it.item?.value.shouldBeNull() } + } + + @Test + fun anyScalarNullableListOfNullable_QuerySucceedsIfAnyVariableIsMissing() = runTest { + val values = Arb.anyListScalar().map { it.filterNotNull() }.map { it.map(AnyValue::fromAny) } + val tag = UUID.randomUUID().toString() + val keys = + connector.anyScalarNullableListOfNullableInsert3 + .execute { + this.tag = tag + this.value1 = null + this.value2 = emptyList() + this.value3 = values.next() + } + .data + val keyIds = listOf(keys.key1, keys.key2, keys.key3).map { it.id } + + val queryResult = + connector.anyScalarNullableListOfNullableGetAllByTagAndValue.execute { this.tag = tag } + val queryIds = queryResult.data.items.map { it.id } + queryIds shouldContainExactlyInAnyOrder keyIds + } + + @Test + fun anyScalarNullableListOfNullable_MutationSucceedsIfAnyVariableIsNull() = runTest { + val key = connector.anyScalarNullableListOfNullableInsert.execute { value = null }.data.key + val queryResult = connector.anyScalarNullableListOfNullableGetByKey.execute(key) + queryResult.data.asClue { it.item?.value.shouldBeNull() } + } + + @Test + fun anyScalarNullableListOfNullable_QuerySucceedsIfAnyVariableIsNull() = runTest { + val values = Arb.anyListScalar().map { it.filterNotNull() }.map { it.map(AnyValue::fromAny) } + val tag = UUID.randomUUID().toString() + connector.anyScalarNullableListOfNullableInsert3.execute { + this.tag = tag + this.value1 = null + this.value2 = emptyList() + this.value3 = values.next() + } + + val queryResult = + connector.anyScalarNullableListOfNullableGetAllByTagAndValue.execute { + this.tag = tag + this.value = null + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldBeEmpty() + } + + @Test + fun anyScalarNullableListOfNullable_QuerySucceedsIfAnyVariableIsEmpty() = runTest { + val values = Arb.anyListScalar().map { it.filterNotNull() }.map { it.map(AnyValue::fromAny) } + val tag = UUID.randomUUID().toString() + val keys = + connector.anyScalarNullableListOfNullableInsert3 + .execute { + this.tag = tag + this.value1 = null + this.value2 = emptyList() + this.value3 = values.next() + } + .data + + val queryResult = + connector.anyScalarNullableListOfNullableGetAllByTagAndValue.execute { + this.tag = tag + this.value = emptyList() + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key2.id, keys.key3.id) + } + + private suspend fun verifyAnyScalarNullableListOfNullableRoundTrip(value: List?) { + val anyValue = value?.map { AnyValue.fromAny(it) } + val expectedQueryResult = value?.map { AnyValue.fromAny(expectedAnyScalarRoundTripValue(it)) } + val key = + connector.anyScalarNullableListOfNullableInsert.execute { this.value = anyValue }.data.key + + val queryResult = connector.anyScalarNullableListOfNullableGetByKey.execute(key) + queryResult.data shouldBe + AnyScalarNullableListOfNullableGetByKeyQuery.Data( + AnyScalarNullableListOfNullableGetByKeyQuery.Data.Item(expectedQueryResult) + ) + } + + private suspend fun verifyAnyScalarNullableListOfNullableQueryVariable( + value: List?, + value2: List?, + value3: List?, + ) { + require(value != value2) + require(value != value3) + // TODO: implement a check to ensure that value is not a subset of value2 and value3. + + val tag = UUID.randomUUID().toString() + val anyValue = value?.map(AnyValue::fromAny) + val anyValue2 = value2?.map(AnyValue::fromAny) + val anyValue3 = value3?.map(AnyValue::fromAny) + val keys = + connector.anyScalarNullableListOfNullableInsert3 + .execute { + this.tag = tag + this.value1 = anyValue + this.value2 = anyValue2 + this.value3 = anyValue3 + } + .data + + val queryResult = + connector.anyScalarNullableListOfNullableGetAllByTagAndValue.execute { + this.value = anyValue + this.tag = tag + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type AnyScalarNullableListOfNonNullable @table { value: [Any!], tag: String, position: Int } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun anyScalarNullableListOfNonNullable_MutationVariableEdgeCases() = + runTest(timeout = 60.seconds) { + assertSoftly { + val edgeCases = EdgeCases.lists.map { it.filterNotNull() } + for (value in edgeCases) { + withClue("value=$value") { verifyAnyScalarNullableListOfNonNullableRoundTrip(value) } + } + } + } + + @Test + fun anyScalarNullableListOfNonNullable_QueryVariableEdgeCases() = + runTest(timeout = 60.seconds) { + val edgeCases = EdgeCases.lists.map { it.filterNotNull() }.filter { it.isNotEmpty() } + val otherValues = Arb.anyListScalar().map { it.filterNotNull() } + + assertSoftly { + for (value in edgeCases) { + val curOtherValues = + otherValues.filterNotIncludesAllMatchingAnyScalars(value).orNull(nullProbability = 0.1) + withClue("value=$value") { + verifyAnyScalarNullableListOfNonNullableQueryVariable( + value, + curOtherValues.next(), + curOtherValues.next() + ) + } + } + } + } + + @Test + fun anyScalarNullableListOfNonNullable_MutationVariableNormalCases() = + runTest(timeout = 60.seconds) { + val values = Arb.anyListScalar().map { it.filterNotNull() } + checkAll(normalCasePropTestConfig, values) { value -> + verifyAnyScalarNullableListOfNonNullableRoundTrip(value) + } + } + + @Test + fun anyScalarNullableListOfNonNullable_QueryVariableNormalCases() = + runTest(timeout = 60.seconds) { + val values = Arb.anyListScalar().map { it.filterNotNull() }.filter { it.isNotEmpty() } + val otherValues = Arb.anyListScalar().map { it.filterNotNull() } + + checkAll(normalCasePropTestConfig, values) { value -> + val curOtherValues = + otherValues.filterNotIncludesAllMatchingAnyScalars(value).orNull(nullProbability = 0.1) + verifyAnyScalarNullableListOfNonNullableQueryVariable( + value, + curOtherValues.next(), + curOtherValues.next() + ) + } + } + + @Test + fun anyScalarNullableListOfNonNullable_MutationSucceedsIfAnyVariableIsMissing() = runTest { + val key = connector.anyScalarNullableListOfNonNullableInsert.execute {}.data.key + val queryResult = connector.anyScalarNullableListOfNonNullableGetByKey.execute(key) + queryResult.data.asClue { it.item?.value.shouldBeNull() } + } + + @Test + fun anyScalarNullableListOfNonNullable_QuerySucceedsIfAnyVariableIsMissing() = runTest { + val values = Arb.anyListScalar().map { it.filterNotNull() }.map { it.map(AnyValue::fromAny) } + val tag = UUID.randomUUID().toString() + val keys = + connector.anyScalarNullableListOfNonNullableInsert3 + .execute { + this.tag = tag + this.value1 = null + this.value2 = emptyList() + this.value3 = values.next() + } + .data + val keyIds = listOf(keys.key1, keys.key2, keys.key3).map { it.id } + + val queryResult = + connector.anyScalarNullableListOfNonNullableGetAllByTagAndValue.execute { this.tag = tag } + val queryIds = queryResult.data.items.map { it.id } + queryIds shouldContainExactlyInAnyOrder keyIds + } + + @Test + fun anyScalarNullableListOfNonNullable_MutationSucceedsIfAnyVariableIsNull() = runTest { + val key = connector.anyScalarNullableListOfNonNullableInsert.execute { value = null }.data.key + val queryResult = connector.anyScalarNullableListOfNonNullableGetByKey.execute(key) + queryResult.data.asClue { it.item?.value.shouldBeNull() } + } + + @Test + fun anyScalarNullableListOfNonNullable_QuerySucceedsIfAnyVariableIsNull() = runTest { + val values = Arb.anyListScalar().map { it.filterNotNull() }.map { it.map(AnyValue::fromAny) } + val tag = UUID.randomUUID().toString() + connector.anyScalarNullableListOfNonNullableInsert3.execute { + this.tag = tag + this.value1 = null + this.value2 = emptyList() + this.value3 = values.next() + } + + val queryResult = + connector.anyScalarNullableListOfNonNullableGetAllByTagAndValue.execute { + this.tag = tag + this.value = null + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldBeEmpty() + } + + @Test + fun anyScalarNullableListOfNonNullable_QuerySucceedsIfAnyVariableIsEmpty() = runTest { + val values = Arb.anyListScalar().map { it.filterNotNull() }.map { it.map(AnyValue::fromAny) } + val tag = UUID.randomUUID().toString() + val keys = + connector.anyScalarNullableListOfNonNullableInsert3 + .execute { + this.tag = tag + this.value1 = null + this.value2 = emptyList() + this.value3 = values.next() + } + .data + + val queryResult = + connector.anyScalarNullableListOfNonNullableGetAllByTagAndValue.execute { + this.tag = tag + this.value = emptyList() + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key2.id, keys.key3.id) + } + + private suspend fun verifyAnyScalarNullableListOfNonNullableRoundTrip(value: List?) { + val anyValue = value?.map { AnyValue.fromAny(it) } + val expectedQueryResult = value?.map { AnyValue.fromAny(expectedAnyScalarRoundTripValue(it)) } + val key = + connector.anyScalarNullableListOfNonNullableInsert.execute { this.value = anyValue }.data.key + + val queryResult = connector.anyScalarNullableListOfNonNullableGetByKey.execute(key) + queryResult.data shouldBe + AnyScalarNullableListOfNonNullableGetByKeyQuery.Data( + AnyScalarNullableListOfNonNullableGetByKeyQuery.Data.Item(expectedQueryResult) + ) + } + + private suspend fun verifyAnyScalarNullableListOfNonNullableQueryVariable( + value: List?, + value2: List?, + value3: List?, + ) { + require(value != value2) + require(value != value3) + // TODO: implement a check to ensure that value is not a subset of value2 and value3. + + val tag = UUID.randomUUID().toString() + val anyValue = value?.map(AnyValue::fromAny) + val anyValue2 = value2?.map(AnyValue::fromAny) + val anyValue3 = value3?.map(AnyValue::fromAny) + val keys = + connector.anyScalarNullableListOfNonNullableInsert3 + .execute { + this.tag = tag + this.value1 = anyValue + this.value2 = anyValue2 + this.value3 = anyValue3 + } + .data + + val queryResult = + connector.anyScalarNullableListOfNonNullableGetAllByTagAndValue.execute { + this.value = anyValue + this.tag = tag + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type AnyScalarNonNullableListOfNullable @table { value: [Any]!, tag: String, position: Int } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun anyScalarNonNullableListOfNullable_MutationVariableEdgeCases() = + runTest(timeout = 60.seconds) { + assertSoftly { + val edgeCases = EdgeCases.lists.map { it.filterNotNull() } + for (value in edgeCases) { + withClue("value=$value") { verifyAnyScalarNonNullableListOfNullableRoundTrip(value) } + } + } + } + + @Test + fun anyScalarNonNullableListOfNullable_QueryVariableEdgeCases() = + runTest(timeout = 60.seconds) { + val edgeCases = EdgeCases.lists.map { it.filterNotNull() }.filter { it.isNotEmpty() } + val otherValues = Arb.anyListScalar().map { it.filterNotNull() } + + assertSoftly { + for (value in edgeCases) { + val curOtherValues = otherValues.filterNotIncludesAllMatchingAnyScalars(value) + withClue("value=$value") { + verifyAnyScalarNonNullableListOfNullableQueryVariable( + value, + curOtherValues.next(), + curOtherValues.next() + ) + } + } + } + } + + @Test + fun anyScalarNonNullableListOfNullable_MutationVariableNormalCases() = + runTest(timeout = 60.seconds) { + val values = Arb.anyListScalar().map { it.filterNotNull() } + checkAll(normalCasePropTestConfig, values) { value -> + verifyAnyScalarNonNullableListOfNullableRoundTrip(value) + } + } + + @Test + fun anyScalarNonNullableListOfNullable_QueryVariableNormalCases() = + runTest(timeout = 60.seconds) { + val values = Arb.anyListScalar().map { it.filterNotNull() }.filter { it.isNotEmpty() } + val otherValues = Arb.anyListScalar().map { it.filterNotNull() } + + checkAll(normalCasePropTestConfig, values) { value -> + val curOtherValues = otherValues.filterNotIncludesAllMatchingAnyScalars(value) + verifyAnyScalarNonNullableListOfNullableQueryVariable( + value, + curOtherValues.next(), + curOtherValues.next() + ) + } + } + + @Test + fun anyScalarNonNullableListOfNullable_MutationFailsIfAnyVariableIsMissing() = runTest { + connector.anyScalarNonNullableListOfNullableInsert.verifyFailsWithMissingVariableValue() + } + + @Test + fun anyScalarNonNullableListOfNullable_QueryFailsIfAnyVariableIsMissing() = runTest { + connector.anyScalarNonNullableListOfNullableGetAllByTagAndValue + .verifyFailsWithMissingVariableValue() + } + + @Test + fun anyScalarNonNullableListOfNullable_MutationFailsIfAnyVariableIsNull() = runTest { + connector.anyScalarNonNullableListOfNullableInsert.verifyFailsWithNullVariableValue() + } + + @Test + fun anyScalarNonNullableListOfNullable_QueryFailsIfAnyVariableIsNull() = runTest { + connector.anyScalarNonNullableListOfNullableGetAllByTagAndValue + .verifyFailsWithNullVariableValue() + } + + @Test + fun anyScalarNonNullableListOfNullable_QuerySucceedsIfAnyVariableIsEmpty() = runTest { + val values = Arb.anyListScalar().map { it.filterNotNull() }.map { it.map(AnyValue::fromAny) } + val tag = UUID.randomUUID().toString() + val keys = + connector.anyScalarNonNullableListOfNullableInsert3 + .execute(value1 = emptyList(), value2 = values.next(), value3 = values.next()) { + this.tag = tag + } + .data + + val queryResult = + connector.anyScalarNonNullableListOfNullableGetAllByTagAndValue.execute(value = emptyList()) { + this.tag = tag + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id, keys.key2.id, keys.key3.id) + } + + private suspend fun verifyAnyScalarNonNullableListOfNullableRoundTrip(value: List) { + val anyValue = value.map { AnyValue.fromAny(it) } + val expectedQueryResult = value.map { AnyValue.fromAny(expectedAnyScalarRoundTripValue(it)) } + val key = connector.anyScalarNonNullableListOfNullableInsert.execute(anyValue) {}.data.key + + val queryResult = connector.anyScalarNonNullableListOfNullableGetByKey.execute(key) + queryResult.data shouldBe + AnyScalarNonNullableListOfNullableGetByKeyQuery.Data( + AnyScalarNonNullableListOfNullableGetByKeyQuery.Data.Item(expectedQueryResult) + ) + } + + private suspend fun verifyAnyScalarNonNullableListOfNullableQueryVariable( + value: List, + value2: List, + value3: List, + ) { + require(value != value2) + require(value != value3) + // TODO: implement a check to ensure that value is not a subset of value2 and value3. + + val tag = UUID.randomUUID().toString() + val anyValue = value.map(AnyValue::fromAny) + val anyValue2 = value2.map(AnyValue::fromAny) + val anyValue3 = value3.map(AnyValue::fromAny) + val keys = + connector.anyScalarNonNullableListOfNullableInsert3 + .execute(value1 = anyValue, value2 = anyValue2, value3 = anyValue3) { this.tag = tag } + .data + + val queryResult = + connector.anyScalarNonNullableListOfNullableGetAllByTagAndValue.execute(anyValue) { + this.tag = tag + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Tests for inserting into and querying this table: + // type AnyScalarNonNullableListOfNonNullable @table { + // value: [Any!]!, tag: String, position: Int } + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + fun anyScalarNonNullableListOfNonNullable_MutationVariableEdgeCases() = + runTest(timeout = 60.seconds) { + assertSoftly { + val edgeCases = EdgeCases.lists.map { it.filterNotNull() } + for (value in edgeCases) { + withClue("value=$value") { verifyAnyScalarNonNullableListOfNonNullableRoundTrip(value) } + } + } + } + + @Test + fun anyScalarNonNullableListOfNonNullable_QueryVariableEdgeCases() = + runTest(timeout = 60.seconds) { + val edgeCases = EdgeCases.lists.map { it.filterNotNull() }.filter { it.isNotEmpty() } + val otherValues = Arb.anyListScalar().map { it.filterNotNull() } + + assertSoftly { + for (value in edgeCases) { + val curOtherValues = otherValues.filterNotIncludesAllMatchingAnyScalars(value) + withClue("value=$value") { + verifyAnyScalarNonNullableListOfNonNullableQueryVariable( + value, + curOtherValues.next(), + curOtherValues.next() + ) + } + } + } + } + + @Test + fun anyScalarNonNullableListOfNonNullable_MutationVariableNormalCases() = + runTest(timeout = 60.seconds) { + val values = Arb.anyListScalar().map { it.filterNotNull() } + checkAll(normalCasePropTestConfig, values) { value -> + verifyAnyScalarNonNullableListOfNonNullableRoundTrip(value) + } + } + + @Test + fun anyScalarNonNullableListOfNonNullable_QueryVariableNormalCases() = + runTest(timeout = 60.seconds) { + val values = Arb.anyListScalar().map { it.filterNotNull() }.filter { it.isNotEmpty() } + val otherValues = Arb.anyListScalar().map { it.filterNotNull() } + + checkAll(normalCasePropTestConfig, values) { value -> + val curOtherValues = otherValues.filterNotIncludesAllMatchingAnyScalars(value) + verifyAnyScalarNonNullableListOfNonNullableQueryVariable( + value, + curOtherValues.next(), + curOtherValues.next() + ) + } + } + + @Test + fun anyScalarNonNullableListOfNonNullable_MutationFailsIfAnyVariableIsMissing() = runTest { + connector.anyScalarNonNullableListOfNonNullableInsert.verifyFailsWithMissingVariableValue() + } + + @Test + fun anyScalarNonNullableListOfNonNullable_QueryFailsIfAnyVariableIsMissing() = runTest { + connector.anyScalarNonNullableListOfNonNullableGetAllByTagAndValue + .verifyFailsWithMissingVariableValue() + } + + @Test + fun anyScalarNonNullableListOfNonNullable_MutationFailsIfAnyVariableIsNull() = runTest { + connector.anyScalarNonNullableListOfNonNullableInsert.verifyFailsWithNullVariableValue() + } + + @Test + fun anyScalarNonNullableListOfNonNullable_QueryFailsIfAnyVariableIsNull() = runTest { + connector.anyScalarNonNullableListOfNonNullableGetAllByTagAndValue + .verifyFailsWithNullVariableValue() + } + + @Test + fun anyScalarNonNullableListOfNonNullable_QuerySucceedsIfAnyVariableIsEmpty() = runTest { + val values = Arb.anyListScalar().map { it.filterNotNull() }.map { it.map(AnyValue::fromAny) } + val tag = UUID.randomUUID().toString() + val keys = + connector.anyScalarNonNullableListOfNonNullableInsert3 + .execute(value1 = emptyList(), value2 = values.next(), value3 = values.next()) { + this.tag = tag + } + .data + + val queryResult = + connector.anyScalarNonNullableListOfNonNullableGetAllByTagAndValue.execute( + value = emptyList() + ) { + this.tag = tag + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id, keys.key2.id, keys.key3.id) + } + + private suspend fun verifyAnyScalarNonNullableListOfNonNullableRoundTrip(value: List) { + val anyValue = value.map { AnyValue.fromAny(it) } + val expectedQueryResult = value.map { AnyValue.fromAny(expectedAnyScalarRoundTripValue(it)) } + val key = connector.anyScalarNonNullableListOfNonNullableInsert.execute(anyValue) {}.data.key + + val queryResult = connector.anyScalarNonNullableListOfNonNullableGetByKey.execute(key) + queryResult.data shouldBe + AnyScalarNonNullableListOfNonNullableGetByKeyQuery.Data( + AnyScalarNonNullableListOfNonNullableGetByKeyQuery.Data.Item(expectedQueryResult) + ) + } + + private suspend fun verifyAnyScalarNonNullableListOfNonNullableQueryVariable( + value: List, + value2: List, + value3: List, + ) { + require(value != value2) + require(value != value3) + // TODO: implement a check to ensure that value is not a subset of value2 and value3. + + val tag = UUID.randomUUID().toString() + val anyValue = value.map(AnyValue::fromAny) + val anyValue2 = value2.map(AnyValue::fromAny) + val anyValue3 = value3.map(AnyValue::fromAny) + val keys = + connector.anyScalarNonNullableListOfNonNullableInsert3 + .execute(value1 = anyValue, value2 = anyValue2, value3 = anyValue3) { this.tag = tag } + .data + + val queryResult = + connector.anyScalarNonNullableListOfNonNullableGetAllByTagAndValue.execute(anyValue) { + this.tag = tag + } + val queryIds = queryResult.data.items.map { it.id } + queryIds.shouldContainExactlyInAnyOrder(keys.key1.id) + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // End of tests; everything below is helper functions and classes. + ////////////////////////////////////////////////////////////////////////////////////////////////// + + @Serializable private data class VariablesWithNullValue(val value: String?) + + private companion object { + + @OptIn(ExperimentalKotest::class) + val normalCasePropTestConfig = + PropTestConfig(iterations = 5, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.0)) + + suspend fun GeneratedMutation<*, *, *>.verifyFailsWithMissingVariableValue() { + val mutationRef = + connector.dataConnect.mutation( + operationName = operationName, + variables = Unit, + dataDeserializer = dataDeserializer, + variablesSerializer = serializer(), + ) + mutationRef.verifyExecuteFailsDueToMissingVariable() + } + + suspend fun GeneratedQuery<*, *, *>.verifyFailsWithMissingVariableValue() { + val queryRef = + connector.dataConnect.query( + operationName = operationName, + variables = Unit, + dataDeserializer = dataDeserializer, + variablesSerializer = serializer(), + ) + queryRef.verifyExecuteFailsDueToMissingVariable() + } + + suspend fun OperationRef<*, *>.verifyExecuteFailsDueToMissingVariable() { + val exception = shouldThrow { execute() } + exception.message shouldContainIgnoringCase "\$value is missing" + } + + suspend fun GeneratedMutation<*, *, *>.verifyFailsWithNullVariableValue() { + val mutationRef = + connector.dataConnect.mutation( + operationName = operationName, + variables = VariablesWithNullValue(null), + dataDeserializer = dataDeserializer, + variablesSerializer = serializer(), + ) + + mutationRef.verifyExecuteFailsDueToNullVariable() + } + + suspend fun GeneratedQuery<*, *, *>.verifyFailsWithNullVariableValue() { + val queryRef = + connector.dataConnect.query( + operationName = operationName, + variables = VariablesWithNullValue(null), + dataDeserializer = dataDeserializer, + variablesSerializer = serializer(), + ) + + queryRef.verifyExecuteFailsDueToNullVariable() + } + + suspend fun OperationRef<*, *>.verifyExecuteFailsDueToNullVariable() { + val exception = shouldThrow { execute() } + exception.message shouldContainIgnoringCase "\$value is null" + } + } +}