Skip to content

Commit 583a0e7

Browse files
authored
Merge 2117741 into a0f67be
2 parents a0f67be + 2117741 commit 583a0e7

File tree

7 files changed

+611
-0
lines changed

7 files changed

+611
-0
lines changed

firebase-dataconnect/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22
* [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher.
33
* [changed] Removed superfluous and noisy debug logging of operation variables.
4+
* [changed] Internal code changes in preparation for user-defined enum support.
45

56

67
# 16.0.3
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2025 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+
/**
20+
* Stores the value of an `enum` or a string if the string does not correspond to one of the enum's
21+
* values.
22+
*/
23+
// TODO: Change the visibility of `EnumValue` to `public` once it gets approval
24+
// by Firebase API Council.
25+
internal sealed interface EnumValue<out T : Enum<out T>> {
26+
27+
/**
28+
* The string value of the enum, either the [Enum.name] in the case of [Known] or the string whose
29+
* corresponding enum value was _not_ known, as in the case of [Unknown].
30+
*/
31+
val stringValue: String
32+
33+
/**
34+
* Represents an unknown enum value.
35+
*
36+
* This could happen, for example, if an enum gained a new value but this code was compiled for
37+
* the older version that lacked the new enum value. Instead of failing, the unknown enum value
38+
* will be gracefully mapped to [Unknown].
39+
*/
40+
class Unknown(override val stringValue: String) : EnumValue<Nothing> {
41+
42+
/**
43+
* Compares this object with another object for equality.
44+
*
45+
* @param other The object to compare to this for equality.
46+
* @return true if, and only if, the other object is an instance of [Unknown] whose
47+
* [stringValue] compares equal to this object's [stringValue] using the `==` operator.
48+
*/
49+
override fun equals(other: Any?): Boolean = other is Unknown && stringValue == other.stringValue
50+
51+
/**
52+
* Calculates and returns the hash code for this object.
53+
*
54+
* The hash code is _not_ guaranteed to be stable across application restarts.
55+
*
56+
* @return the hash code for this object, that incorporates the values of this object's public
57+
* properties.
58+
*/
59+
override fun hashCode(): Int = stringValue.hashCode()
60+
61+
/**
62+
* Returns a string representation of this object, useful for debugging.
63+
*
64+
* The string representation is _not_ guaranteed to be stable and may change without notice at
65+
* any time. Therefore, the only recommended usage of the returned string is debugging and/or
66+
* logging. Namely, parsing the returned string or storing the returned string in non-volatile
67+
* storage should generally be avoided in order to be robust in case that the string
68+
* representation changes.
69+
*/
70+
override fun toString(): String = "Unknown($stringValue)"
71+
72+
/** Creates and returns a new [Unknown] instance with the given property values. */
73+
fun copy(stringValue: String = this.stringValue): Unknown = Unknown(stringValue)
74+
}
75+
76+
/**
77+
* Represents a known enum value.
78+
*
79+
* @param value The enum value.
80+
*/
81+
class Known<T : Enum<T>>(val value: T) : EnumValue<T> {
82+
83+
override val stringValue: String
84+
get() = value.name
85+
86+
/**
87+
* Compares this object with another object for equality.
88+
*
89+
* @param other The object to compare to this for equality.
90+
* @return true if, and only if, the other object is an instance of [Known] whose [value]
91+
* compares equal to this object's [value] using the `==` operator.
92+
*/
93+
override fun equals(other: Any?): Boolean = other is Known<*> && value == other.value
94+
95+
/**
96+
* Calculates and returns the hash code for this object.
97+
*
98+
* The hash code is _not_ guaranteed to be stable across application restarts.
99+
*
100+
* @return the hash code for this object, that incorporates the values of this object's public
101+
* properties.
102+
*/
103+
override fun hashCode(): Int = value.hashCode()
104+
105+
/**
106+
* Returns a string representation of this object, useful for debugging.
107+
*
108+
* The string representation is _not_ guaranteed to be stable and may change without notice at
109+
* any time. Therefore, the only recommended usage of the returned string is debugging and/or
110+
* logging. Namely, parsing the returned string or storing the returned string in non-volatile
111+
* storage should generally be avoided in order to be robust in case that the string
112+
* representation changes.
113+
*/
114+
override fun toString(): String = "Known(${value.name})"
115+
116+
/** Creates and returns a new [Known] instance with the given property values. */
117+
fun copy(value: T = this.value): Known<T> = Known(value)
118+
}
119+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2025 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.EnumValue
20+
import com.google.firebase.dataconnect.EnumValue.Known
21+
import com.google.firebase.dataconnect.EnumValue.Unknown
22+
import kotlinx.serialization.KSerializer
23+
import kotlinx.serialization.descriptors.PrimitiveKind
24+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
25+
import kotlinx.serialization.descriptors.SerialDescriptor
26+
import kotlinx.serialization.encoding.Decoder
27+
import kotlinx.serialization.encoding.Encoder
28+
29+
/**
30+
* A [KSerializer] implementation for [EnumValue].
31+
*
32+
* @param values The values of the enum to deserialize; for example, for an enum named `Foo` this
33+
* value should be `Foo.entries` or `Foo.values()`.
34+
*/
35+
// TODO: Change the visibility of `EnumValueSerializer` to `public` once it gets approval
36+
// by Firebase API Council.
37+
internal open class EnumValueSerializer<T : Enum<T>>(values: Iterable<T>) :
38+
KSerializer<EnumValue<T>> {
39+
40+
override val descriptor: SerialDescriptor =
41+
PrimitiveSerialDescriptor("com.google.firebase.dataconnect.EnumValue", PrimitiveKind.STRING)
42+
43+
private val enumValueByStringValue: Map<String, T> = buildMap {
44+
for (value in values) {
45+
val oldValue = put(value.name, value)
46+
require(oldValue === null) { "duplicate value.name in values: ${value.name}" }
47+
}
48+
}
49+
50+
/**
51+
* Deserializes an [EnumValue] from the given decoder.
52+
*
53+
* If the decoded string is equal to the [Enum.name] of one of the values given to the constructor
54+
* then [Known] is returned with that value; otherwise, [Unknown] is returned.
55+
*/
56+
override fun deserialize(decoder: Decoder): EnumValue<T> {
57+
val stringValue = decoder.decodeString()
58+
val enumValue = enumValueByStringValue.get(stringValue) ?: return Unknown(stringValue)
59+
return Known(enumValue)
60+
}
61+
62+
/** Serializes the given [EnumValue] to the given encoder. */
63+
override fun serialize(encoder: Encoder, value: EnumValue<T>) {
64+
encoder.encodeString(value.stringValue)
65+
}
66+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright 2025 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+
@file:OptIn(ExperimentalKotest::class)
18+
19+
package com.google.firebase.dataconnect
20+
21+
import com.google.firebase.dataconnect.testutil.property.arbitrary.distinctPair
22+
import io.kotest.assertions.withClue
23+
import io.kotest.common.ExperimentalKotest
24+
import io.kotest.matchers.shouldBe
25+
import io.kotest.matchers.shouldNotBe
26+
import io.kotest.matchers.types.shouldBeSameInstanceAs
27+
import io.kotest.matchers.types.shouldNotBeSameInstanceAs
28+
import io.kotest.property.Arb
29+
import io.kotest.property.PropTestConfig
30+
import io.kotest.property.arbitrary.enum
31+
import io.kotest.property.arbitrary.of
32+
import io.kotest.property.assume
33+
import io.kotest.property.checkAll
34+
import kotlinx.coroutines.test.runTest
35+
import org.junit.Test
36+
37+
@Suppress("ReplaceCallWithBinaryOperator")
38+
class EnumValueKnownUnitTest {
39+
40+
@Test
41+
fun `constructor() should set properties to corresponding arguments`() = runTest {
42+
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
43+
val enumValue = EnumValue.Known(enum)
44+
enumValue.value shouldBeSameInstanceAs enum
45+
}
46+
}
47+
48+
@Test
49+
fun `equals() should return true when invoked with itself`() = runTest {
50+
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
51+
val enumValue = EnumValue.Known(enum)
52+
enumValue.equals(enumValue) shouldBe true
53+
}
54+
}
55+
56+
@Test
57+
fun `equals() should return true when invoked with a distinct, but equal, instance`() = runTest {
58+
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
59+
val enumValue1 = EnumValue.Known(enum)
60+
val enumValue2 = EnumValue.Known(enum)
61+
enumValue1.equals(enumValue2) shouldBe true
62+
}
63+
}
64+
65+
@Test
66+
fun `equals() should return false when invoked with null`() = runTest {
67+
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
68+
val enumValue = EnumValue.Known(enum)
69+
enumValue.equals(null) shouldBe false
70+
}
71+
}
72+
73+
@Test
74+
fun `equals() should return false when invoked with a different type`() = runTest {
75+
val others = Arb.of("foo", 42, java.time.LocalDate.now())
76+
checkAll(propTestConfig, Arb.enum<Food>(), others) { enum, other ->
77+
val enumValue = EnumValue.Known(enum)
78+
enumValue.equals(other) shouldBe false
79+
}
80+
}
81+
82+
@Test
83+
fun `equals() should return false when the enum differs`() = runTest {
84+
checkAll(propTestConfig, Arb.enum<Food>().distinctPair()) { (enum1, enum2) ->
85+
val enumValue1 = EnumValue.Known(enum1)
86+
val enumValue2 = EnumValue.Known(enum2)
87+
enumValue1.equals(enumValue2) shouldBe false
88+
}
89+
}
90+
91+
@Test
92+
fun `hashCode() should return the same value when invoked repeatedly`() = runTest {
93+
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
94+
val enumValue = EnumValue.Known(enum)
95+
val hashCode = enumValue.hashCode()
96+
repeat(5) { withClue("iteration=$it") { enumValue.hashCode() shouldBe hashCode } }
97+
}
98+
}
99+
100+
@Test
101+
fun `hashCode() should return the same value when invoked on equal, but distinct, objects`() =
102+
runTest {
103+
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
104+
val enumValue1 = EnumValue.Known(enum)
105+
val enumValue2 = EnumValue.Known(enum)
106+
enumValue1.hashCode() shouldBe enumValue2.hashCode()
107+
}
108+
}
109+
110+
@Test
111+
fun `hashCode() should return different values for different enum values`() = runTest {
112+
checkAll(propTestConfig, Arb.enum<Food>().distinctPair()) { (enum1, enum2) ->
113+
assume(enum1.hashCode() != enum2.hashCode())
114+
val enumValue1 = EnumValue.Known(enum1)
115+
val enumValue2 = EnumValue.Known(enum2)
116+
enumValue1.hashCode() shouldNotBe enumValue2.hashCode()
117+
}
118+
}
119+
120+
@Test
121+
fun `toString() should return a string conforming to what is expected`() = runTest {
122+
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
123+
val enumValue = EnumValue.Known(enum)
124+
enumValue.toString() shouldBe "Known(${enum.name})"
125+
}
126+
}
127+
128+
@Test
129+
fun `copy() with no arguments should return an equal, but distinct, instance`() = runTest {
130+
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
131+
val enumValue = EnumValue.Known(enum)
132+
val enumValueCopy = enumValue.copy()
133+
enumValue shouldBe enumValueCopy
134+
enumValue shouldNotBeSameInstanceAs enumValueCopy
135+
}
136+
}
137+
138+
@Test
139+
fun `copy() with all arguments should return a new instance with the given arguments`() =
140+
runTest {
141+
checkAll(propTestConfig, Arb.enum<Food>().distinctPair()) { (enum1, enum2) ->
142+
val enumValue1 = EnumValue.Known(enum1)
143+
val enumValue2 = enumValue1.copy(enum2)
144+
enumValue2 shouldBe EnumValue.Known(enum2)
145+
}
146+
}
147+
148+
@Suppress("unused")
149+
private enum class Food {
150+
Burrito,
151+
Cake,
152+
Pizza,
153+
Shawarma,
154+
Sushi,
155+
}
156+
157+
private companion object {
158+
val propTestConfig = PropTestConfig(iterations = 50)
159+
}
160+
}

0 commit comments

Comments
 (0)