Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions firebase-dataconnect/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased
* [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher.
* [changed] Removed superfluous and noisy debug logging of operation variables.
* [changed] Internal code changes in preparation for user-defined enum support.


# 16.0.3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.dataconnect

/**
* Stores the value of an `enum` or a string if the string does not correspond to one of the enum's
* values.
*/
// TODO: Change the visilibity of `EnumValue` to `public` once it gets approval
// by Firebase API Council.
internal sealed interface EnumValue<out T : Enum<out T>> {

/**
* The string value of the enum, either the [Enum.name] in the case of [Known] or the string whose
* corresponding enum value was _not_ known, as in the case of [Unknown].
*/
val stringValue: String

/**
* Represents an unknown enum value.
*
* This could happen, for example, if an enum gained a new value but this code was compiled for
* the older version that lacked the new enum value. Instead of failing, the unknown enum value
* will be gracefully mapped to [Unknown].
*/
class Unknown(override val stringValue: String) : EnumValue<Nothing> {

/**
* Compares this object with another object for equality.
*
* @param other The object to compare to this for equality.
* @return true if, and only if, the other object is an instance of [Unknown] whose
* [stringValue] compares equal to this object's [stringValue] using the `==` operator.
*/
override fun equals(other: Any?): Boolean = other is Unknown && stringValue == other.stringValue

/**
* Calculates and returns the hash code for this object.
*
* The hash code is _not_ guaranteed to be stable across application restarts.
*
* @return the hash code for this object, that incorporates the values of this object's public
* properties.
*/
override fun hashCode(): Int = stringValue.hashCode()

/**
* Returns a string representation of this object, useful for debugging.
*
* The string representation is _not_ guaranteed to be stable and may change without notice at
* any time. Therefore, the only recommended usage of the returned string is debugging and/or
* logging. Namely, parsing the returned string or storing the returned string in non-volatile
* storage should generally be avoided in order to be robust in case that the string
* representation changes.
*/
override fun toString(): String = "Unknown($stringValue)"

/** Creates and returns a new [Unknown] instance with the given property values. */
fun copy(stringValue: String = this.stringValue): Unknown = Unknown(stringValue)
}

/**
* Represents a known enum value.
*
* @param value The enum value.
*/
class Known<T : Enum<T>>(val value: T) : EnumValue<T> {

override val stringValue: String
get() = value.name

/**
* Compares this object with another object for equality.
*
* @param other The object to compare to this for equality.
* @return true if, and only if, the other object is an instance of [Known] whose [value]
* compares equal to this object's [value] using the `==` operator.
*/
override fun equals(other: Any?): Boolean = other is Known<*> && value == other.value

/**
* Calculates and returns the hash code for this object.
*
* The hash code is _not_ guaranteed to be stable across application restarts.
*
* @return the hash code for this object, that incorporates the values of this object's public
* properties.
*/
override fun hashCode(): Int = value.hashCode()

/**
* Returns a string representation of this object, useful for debugging.
*
* The string representation is _not_ guaranteed to be stable and may change without notice at
* any time. Therefore, the only recommended usage of the returned string is debugging and/or
* logging. Namely, parsing the returned string or storing the returned string in non-volatile
* storage should generally be avoided in order to be robust in case that the string
* representation changes.
*/
override fun toString(): String = "Known(${value.name})"

/** Creates and returns a new [Known] instance with the given property values. */
fun copy(value: T = this.value): Known<T> = Known(value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.dataconnect.serializers

import com.google.firebase.dataconnect.EnumValue
import com.google.firebase.dataconnect.EnumValue.Known
import com.google.firebase.dataconnect.EnumValue.Unknown
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* A [KSerializer] implementation for [EnumValue].
*
* @param values The values of the enum to deserialize; for example, for an enum named `Foo` this
* value should be `Foo.entries` or `Foo.values()`.
*/
// TODO: Change the visilibity of `EnumValueSerializer` to `public` once it gets approval
// by Firebase API Council.
internal open class EnumValueSerializer<T : Enum<T>>(values: Iterable<T>) :
KSerializer<EnumValue<T>> {

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("com.google.firebase.dataconnect.EnumValue", PrimitiveKind.STRING)

private val enumValueByStringValue: Map<String, T> = buildMap {
for (value in values) {
val oldValue = put(value.name, value)
require(oldValue === null) { "duplicate value.name in values: ${value.name}" }
}
}

/**
* Deserializes an [EnumValue] from the given decoder.
*
* If the decoded string is equal to the [Enum.name] of one of the values given to the constructor
* then [Known] is returned with that value; otherwise, [Unknown] is returned.
*/
override fun deserialize(decoder: Decoder): EnumValue<T> {
val stringValue = decoder.decodeString()
val enumValue = enumValueByStringValue.get(stringValue) ?: return Unknown(stringValue)
return Known(enumValue)
}

/** Serializes the given [EnumValue] to the given encoder. */
override fun serialize(encoder: Encoder, value: EnumValue<T>) {
encoder.encodeString(value.stringValue)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@file:OptIn(ExperimentalKotest::class)

package com.google.firebase.dataconnect

import com.google.firebase.dataconnect.testutil.property.arbitrary.distinctPair
import io.kotest.assertions.withClue
import io.kotest.common.ExperimentalKotest
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.types.shouldBeSameInstanceAs
import io.kotest.matchers.types.shouldNotBeSameInstanceAs
import io.kotest.property.Arb
import io.kotest.property.PropTestConfig
import io.kotest.property.arbitrary.enum
import io.kotest.property.arbitrary.of
import io.kotest.property.assume
import io.kotest.property.checkAll
import kotlinx.coroutines.test.runTest
import org.junit.Test

@Suppress("ReplaceCallWithBinaryOperator")
class EnumValueKnownUnitTest {

@Test
fun `constructor() should set properties to corresponding arguments`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
val enumValue = EnumValue.Known(enum)
enumValue.value shouldBeSameInstanceAs enum
}
}

@Test
fun `equals() should return true when invoked with itself`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
val enumValue = EnumValue.Known(enum)
enumValue.equals(enumValue) shouldBe true
}
}

@Test
fun `equals() should return true when invoked with a distinct, but equal, instance`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
val enumValue1 = EnumValue.Known(enum)
val enumValue2 = EnumValue.Known(enum)
enumValue1.equals(enumValue2) shouldBe true
}
}

@Test
fun `equals() should return false when invoked with null`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
val enumValue = EnumValue.Known(enum)
enumValue.equals(null) shouldBe false
}
}

@Test
fun `equals() should return false when invoked with a different type`() = runTest {
val others = Arb.of("foo", 42, java.time.LocalDate.now())
checkAll(propTestConfig, Arb.enum<Food>(), others) { enum, other ->
val enumValue = EnumValue.Known(enum)
enumValue.equals(other) shouldBe false
}
}

@Test
fun `equals() should return false when the enum differs`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>().distinctPair()) { (enum1, enum2) ->
val enumValue1 = EnumValue.Known(enum1)
val enumValue2 = EnumValue.Known(enum2)
enumValue1.equals(enumValue2) shouldBe false
}
}

@Test
fun `hashCode() should return the same value when invoked repeatedly`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
val enumValue = EnumValue.Known(enum)
val hashCode = enumValue.hashCode()
repeat(5) { withClue("iteration=$it") { enumValue.hashCode() shouldBe hashCode } }
}
}

@Test
fun `hashCode() should return the same value when invoked on equal, but distinct, objects`() =
runTest {
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
val enumValue1 = EnumValue.Known(enum)
val enumValue2 = EnumValue.Known(enum)
enumValue1.hashCode() shouldBe enumValue2.hashCode()
}
}

@Test
fun `hashCode() should return different values for different enum values`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>().distinctPair()) { (enum1, enum2) ->
assume(enum1.hashCode() != enum2.hashCode())
val enumValue1 = EnumValue.Known(enum1)
val enumValue2 = EnumValue.Known(enum2)
enumValue1.hashCode() shouldNotBe enumValue2.hashCode()
}
}

@Test
fun `toString() should return a string conforming to what is expected`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
val enumValue = EnumValue.Known(enum)
enumValue.toString() shouldBe "Known(${enum.name})"
}
}

@Test
fun `copy() with no arguments should return an equal, but distinct, instance`() = runTest {
checkAll(propTestConfig, Arb.enum<Food>()) { enum ->
val enumValue = EnumValue.Known(enum)
val enumValueCopy = enumValue.copy()
enumValue shouldBe enumValueCopy
enumValue shouldNotBeSameInstanceAs enumValueCopy
}
}

@Test
fun `copy() with all arguments should return a new instance with the given arguments`() =
runTest {
checkAll(propTestConfig, Arb.enum<Food>().distinctPair()) { (enum1, enum2) ->
val enumValue1 = EnumValue.Known(enum1)
val enumValue2 = enumValue1.copy(enum2)
enumValue2 shouldBe EnumValue.Known(enum2)
}
}

@Suppress("unused")
private enum class Food {
Burrito,
Cake,
Pizza,
Shawarma,
Sushi,
}

private companion object {
val propTestConfig = PropTestConfig(iterations = 50)
}
}
Loading
Loading