Skip to content

Commit 146dbec

Browse files
authored
AnyValue and AnyValueSerializer added (#6285)
1 parent dd5a27b commit 146dbec

File tree

11 files changed

+1452
-111
lines changed

11 files changed

+1452
-111
lines changed

firebase-dataconnect/api.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
// Signature format: 2.0
22
package com.google.firebase.dataconnect {
33

4+
@kotlinx.serialization.Serializable(with=AnyValueSerializer::class) public final class AnyValue {
5+
ctor public AnyValue(@NonNull java.util.Map<java.lang.String,?> value);
6+
ctor public AnyValue(@NonNull java.util.List<?> value);
7+
ctor public AnyValue(@NonNull String value);
8+
ctor public AnyValue(boolean value);
9+
ctor public AnyValue(double value);
10+
method public <T> T decode(@NonNull kotlinx.serialization.DeserializationStrategy<? extends T> deserializer);
11+
method public inline <reified T> T decode();
12+
method @NonNull public Object getValue();
13+
property @NonNull public final Object value;
14+
field @NonNull public static final com.google.firebase.dataconnect.AnyValue.Companion Companion;
15+
}
16+
17+
public static final class AnyValue.Companion {
18+
method @NonNull public <T> com.google.firebase.dataconnect.AnyValue encode(@Nullable T value, @NonNull kotlinx.serialization.SerializationStrategy<? super T> serializer);
19+
method public inline <reified T> com.google.firebase.dataconnect.AnyValue encode(@Nullable T value);
20+
method @NonNull public com.google.firebase.dataconnect.AnyValue fromAny(@NonNull Object value);
21+
method @Nullable public com.google.firebase.dataconnect.AnyValue fromNullableAny(@Nullable Object value);
22+
}
23+
424
public final class ConnectorConfig {
525
ctor public ConnectorConfig(@NonNull String connector, @NonNull String location, @NonNull String serviceId);
626
method @NonNull public com.google.firebase.dataconnect.ConnectorConfig copy(@NonNull String connector = connector, @NonNull String location = location, @NonNull String serviceId = serviceId);
@@ -213,6 +233,14 @@ package com.google.firebase.dataconnect.generated {
213233

214234
package com.google.firebase.dataconnect.serializers {
215235

236+
public final class AnyValueSerializer implements kotlinx.serialization.KSerializer<com.google.firebase.dataconnect.AnyValue> {
237+
method @NonNull public com.google.firebase.dataconnect.AnyValue deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder);
238+
method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
239+
method public void serialize(@NonNull kotlinx.serialization.encoding.Encoder encoder, @NonNull com.google.firebase.dataconnect.AnyValue value);
240+
property @NonNull public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
241+
field @NonNull public static final com.google.firebase.dataconnect.serializers.AnyValueSerializer INSTANCE;
242+
}
243+
216244
public final class DateSerializer implements kotlinx.serialization.KSerializer<java.util.Date> {
217245
method @NonNull public java.util.Date deserialize(@NonNull kotlinx.serialization.encoding.Decoder decoder);
218246
method @NonNull public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.dataconnect
18+
19+
import com.google.firebase.dataconnect.serializers.AnyValueSerializer
20+
import com.google.firebase.dataconnect.util.decodeFromValue
21+
import com.google.firebase.dataconnect.util.encodeToValue
22+
import com.google.firebase.dataconnect.util.toAny
23+
import com.google.firebase.dataconnect.util.toCompactString
24+
import com.google.firebase.dataconnect.util.toValueProto
25+
import com.google.protobuf.Struct
26+
import com.google.protobuf.Value
27+
import kotlinx.serialization.DeserializationStrategy
28+
import kotlinx.serialization.Serializable
29+
import kotlinx.serialization.SerializationStrategy
30+
import kotlinx.serialization.serializer
31+
32+
/**
33+
* Represents a variable or field of the Data Connect custom scalar type `Any`.
34+
*
35+
* ### Valid Values for `AnyValue`
36+
*
37+
* `AnyValue` can encapsulate [String], [Boolean], [Double], a [List] of one of these types, or a
38+
* [Map] whose values are one of these types. The values can be arbitrarily nested (e.g. a list that
39+
* contains a map that contains other maps, and so on. The lists and maps can contain heterogeneous
40+
* values; for example, a single [List] can contain a [String] value, some [Boolean] values, and
41+
* some [List] values. The values of a [List] or a [Map] may be `null`. The only exception is that a
42+
* variable or field declared as `[Any]` in GraphQL may _not_ have `null` values in the top-level
43+
* list; however, nested lists or maps _may_ contain null values.
44+
*
45+
* ### Storing `Int` in an `AnyValue`
46+
*
47+
* To store an [Int] value, simply convert it to a [Double] and store the [Double] value.
48+
*
49+
* ### Storing `Long` in an `AnyValue`
50+
*
51+
* To store a [Long] value, converting it to a [Double] can be lossy if the value is sufficiently
52+
* large (or small) to not be exactly represented by [Double]. The _largest_ [Long] value that can
53+
* be stored in a [Double] with its exact value is `2^53 – 1` (`9007199254740991`). The _smallest_
54+
* [Long] value that can be stored in a [Double] with its exact value is `-(2^53 – 1)`
55+
* (`-9007199254740991`). This limitation is exactly the same in JavaScript, which does not have a
56+
* native "int" or "long" type, but rather stores all numeric values in a 64-bit floating point
57+
* value. See
58+
* [MAX_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER])
59+
* and
60+
* [MIN_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER])
61+
* for more details.
62+
*
63+
* ### Integration with `kotlinx.serialization`
64+
*
65+
* To serialize a value of this type when using Data Connect, use [AnyValueSerializer].
66+
*
67+
* ### Example
68+
*
69+
* For example, suppose this schema and operation is defined in the GraphQL source:
70+
*
71+
* ```
72+
* type Foo @table { value: Any }
73+
* mutation FooInsert($value: Any) {
74+
* key: foo_insert(data: { value: $value })
75+
* }
76+
* ```
77+
*
78+
* then a serializable "Variables" type could be defined as follows:
79+
*
80+
* ```
81+
* @Serializable
82+
* data class FooInsertVariables(
83+
* @Serializable(with=AnyValueSerializer::class) val value: AnyValue?
84+
* )
85+
* ```
86+
*/
87+
@Serializable(with = AnyValueSerializer::class)
88+
public class AnyValue internal constructor(internal val protoValue: Value) {
89+
90+
init {
91+
require(protoValue.kindCase != Value.KindCase.NULL_VALUE) {
92+
"NULL_VALUE is not allowed; just use null"
93+
}
94+
}
95+
96+
internal constructor(struct: Struct) : this(struct.toValueProto())
97+
98+
/**
99+
* Creates an instance that encapsulates the given [Map].
100+
*
101+
* An exception is thrown if any of the values of the map, or its sub-values, are invalid for
102+
* being stored in [AnyValue]; see the [AnyValue] class documentation for a detailed description
103+
* of value values.
104+
*
105+
* This class makes a _copy_ of the given map; therefore, any modifications to the map after this
106+
* object is created will have no effect on this [AnyValue] object.
107+
*/
108+
public constructor(value: Map<String, Any?>) : this(value.toValueProto())
109+
110+
/**
111+
* Creates an instance that encapsulates the given [List].
112+
*
113+
* An exception is thrown if any of the values of the list, or its sub-values, are invalid for
114+
* being stored in [AnyValue]; see the [AnyValue] class documentation for a detailed description
115+
* of value values.
116+
*
117+
* This class makes a _copy_ of the given list; therefore, any modifications to the list after
118+
* this object is created will have no effect on this [AnyValue] object.
119+
*/
120+
public constructor(value: List<Any?>) : this(value.toValueProto())
121+
122+
/** Creates an instance that encapsulates the given [String]. */
123+
public constructor(value: String) : this(value.toValueProto())
124+
125+
/** Creates an instance that encapsulates the given [Boolean]. */
126+
public constructor(value: Boolean) : this(value.toValueProto())
127+
128+
/** Creates an instance that encapsulates the given [Double]. */
129+
public constructor(value: Double) : this(value.toValueProto())
130+
131+
/**
132+
* The native Kotlin type of the value encapsulated in this object.
133+
*
134+
* Although this type is `Any` it will be one of `String, `Boolean`, `Double`, `List<Any?>` or
135+
* `Map<String, Any?>`. See the [AnyValue] class documentation for a detailed description of the
136+
* types of values that are supported.
137+
*/
138+
public val value: Any
139+
// NOTE: The not-null assertion operator (!!) below will never throw because the `init` block
140+
// of this class asserts that `protoValue` is not NULL_VALUE.
141+
get() = protoValue.toAny()!!
142+
143+
/**
144+
* Decodes the encapsulated value using the given deserializer.
145+
*
146+
* @param deserializer The deserializer for the decoder to use.
147+
*
148+
* @return the object of type `T` created by decoding the encapsulated value using the given
149+
* deserializer.
150+
*/
151+
public fun <T> decode(deserializer: DeserializationStrategy<T>): T =
152+
decodeFromValue(deserializer, protoValue)
153+
154+
/**
155+
* Decodes the encapsulated value using the _default_ serializer for the return type, as computed
156+
* by [serializer].
157+
*
158+
* @return the object of type `T` created by decoding the encapsulated value using the _default_
159+
* serializer for the return type, as computed by [serializer].
160+
*/
161+
public inline fun <reified T> decode(): T = decode(serializer<T>())
162+
163+
/**
164+
* Compares this object with another object for equality.
165+
*
166+
* @param other The object to compare to this for equality.
167+
* @return true if, and only if, the other object is an instance of [AnyValue] whose encapsulated
168+
* value compares equal using the `==` operator to the given object.
169+
*/
170+
override fun equals(other: Any?): Boolean = other is AnyValue && other.value == value
171+
172+
/**
173+
* Calculates and returns the hash code for this object.
174+
*
175+
* The hash code is _not_ guaranteed to be stable across application restarts.
176+
*
177+
* @return the hash code for this object, calculated from the encapsulated value.
178+
*/
179+
override fun hashCode(): Int = value.hashCode()
180+
181+
/**
182+
* Returns a string representation of this object, useful for debugging.
183+
*
184+
* The string representation is _not_ guaranteed to be stable and may change without notice at any
185+
* time. Therefore, the only recommended usage of the returned string is debugging and/or logging.
186+
* Namely, parsing the returned string or storing the returned string in non-volatile storage
187+
* should generally be avoided in order to be robust in case that the string representation
188+
* changes.
189+
*
190+
* @return a string representation of this object's encapsulated value.
191+
*/
192+
override fun toString(): String = protoValue.toCompactString(keySortSelector = { it })
193+
194+
public companion object {
195+
196+
/**
197+
* Encodes the given value using the given serializer to an [AnyValue] object, and returns it.
198+
*
199+
* @param value the value to serialize.
200+
* @param serializer the serializer for the encoder to use.
201+
*
202+
* @return a new `AnyValue` object whose encapsulated value is the encoding of the given value
203+
* when decoded with the given serializer.
204+
*/
205+
public fun <T> encode(value: T, serializer: SerializationStrategy<T>): AnyValue =
206+
AnyValue(encodeToValue(serializer, value))
207+
208+
/**
209+
* Encodes the given value using the given _default_ serializer for the given object, as
210+
* computed by [serializer].
211+
*
212+
* @param value the value to serialize.
213+
* @return a new `AnyValue` object whose encapsulated value is the encoding of the given value
214+
* when decoded with the _default_ serializer for the given object, as computed by [serializer].
215+
*/
216+
public inline fun <reified T> encode(value: T): AnyValue = encode(value, serializer<T>())
217+
218+
/**
219+
* Creates and returns an `AnyValue` object created using the `AnyValue` constructor that
220+
* corresponds to the runtime type of the given value, or returns `null` if the given value is
221+
* `null`.
222+
*
223+
* @throws IllegalArgumentException if the given value is not supported by `AnyValue`; see the
224+
* `AnyValue` constructor for details.
225+
*/
226+
@JvmName("fromNullableAny")
227+
public fun fromAny(value: Any?): AnyValue? = if (value === null) null else fromAny(value)
228+
229+
/**
230+
* Creates and returns an `AnyValue` object created using the `AnyValue` constructor that
231+
* corresponds to the runtime type of the given value.
232+
*
233+
* @throws IllegalArgumentException if the given value is not supported by `AnyValue`; see the
234+
* `AnyValue` constructor for details.
235+
*/
236+
public fun fromAny(value: Any): AnyValue {
237+
@Suppress("UNCHECKED_CAST")
238+
return when (value) {
239+
is String -> AnyValue(value)
240+
is Boolean -> AnyValue(value)
241+
is Double -> AnyValue(value)
242+
is List<*> -> AnyValue(value)
243+
is Map<*, *> -> AnyValue(value as Map<String, Any?>)
244+
else ->
245+
throw IllegalArgumentException(
246+
"unsupported type: ${value::class.qualifiedName}" +
247+
" (supported types: null, String, Boolean, Double, List<Any?>, Map<String, Any?>)"
248+
)
249+
}
250+
}
251+
}
252+
}

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ import kotlinx.serialization.SerializationStrategy
101101
* or later; otherwise, a compilation error like `Class 'FooConnector' is not abstract and does not
102102
* implement abstract member public abstract fun equals(other: Any?): Boolean defined in
103103
* com.google.firebase.dataconnect.generated.GeneratedConnector` will occur.
104+
* - [#NNNN](https://github.com/firebase/firebase-android-sdk/pull/NNNN]) Added [AnyValue] class to
105+
* support the custom `Any` GraphQL scalar type. Support for `Any` scalars in the Android SDK code
106+
* generation was added in the dataconnect toolkit v1.3.8 (released Sept 20, 2024), which will be
107+
* included in the next release of firebase-tools (the release following v13.18.0).
104108
*
105109
* #### 16.0.0-alpha05 (June 24, 2024)
106110
* - [#6003](https://github.com/firebase/firebase-android-sdk/pull/6003]) Fixed [close] to
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.dataconnect.serializers
18+
19+
import com.google.firebase.dataconnect.AnyValue
20+
import kotlinx.serialization.KSerializer
21+
import kotlinx.serialization.descriptors.SerialDescriptor
22+
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
23+
import kotlinx.serialization.encoding.Decoder
24+
import kotlinx.serialization.encoding.Encoder
25+
26+
/**
27+
* An implementation of [KSerializer] for serializing and deserializing [AnyValue] objects.
28+
*
29+
* Note that this is _not_ a generic serializer, but is only useful in the Data Connect SDK.
30+
*/
31+
public object AnyValueSerializer : KSerializer<AnyValue> {
32+
33+
override val descriptor: SerialDescriptor =
34+
buildClassSerialDescriptor("com.google.firebase.dataconnect.AnyValue") {}
35+
36+
override fun serialize(encoder: Encoder, value: AnyValue): Unit = unsupported()
37+
38+
override fun deserialize(decoder: Decoder): AnyValue = unsupported()
39+
40+
private fun unsupported(): Nothing =
41+
throw UnsupportedOperationException(
42+
"The AnyValueSerializer class cannot actually be used;" +
43+
" it is merely a sentinel that gets special treatment during Data Connect serialization"
44+
)
45+
}

0 commit comments

Comments
 (0)