Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,15 @@
plugins {
id(ThunderbirdPlugins.Library.kmp)
}
android {
namespace = "net.thunderbird.core.featureflag"
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.androidx.annotation)
}
}
}

View file

@ -0,0 +1,6 @@
package net.thunderbird.core.featureflag
data class FeatureFlag(
val key: FeatureFlagKey,
val enabled: Boolean = false,
)

View file

@ -0,0 +1,5 @@
package net.thunderbird.core.featureflag
fun interface FeatureFlagFactory {
fun createFeatureCatalog(): List<FeatureFlag>
}

View file

@ -0,0 +1,12 @@
package net.thunderbird.core.featureflag
@JvmInline
value class FeatureFlagKey(val key: String) {
companion object Keys {
val DisplayInAppNotifications = "display_in_app_notifications".toFeatureFlagKey()
val UseNotificationSenderForSystemNotifications =
"use_notification_sender_for_system_notifications".toFeatureFlagKey()
}
}
fun String.toFeatureFlagKey(): FeatureFlagKey = FeatureFlagKey(this)

View file

@ -0,0 +1,5 @@
package net.thunderbird.core.featureflag
fun interface FeatureFlagProvider {
fun provide(key: FeatureFlagKey): FeatureFlagResult
}

View file

@ -0,0 +1,47 @@
package net.thunderbird.core.featureflag
sealed interface FeatureFlagResult {
data object Enabled : FeatureFlagResult
data object Disabled : FeatureFlagResult
data object Unavailable : FeatureFlagResult
fun <T> whenEnabledOrNot(
onEnabled: () -> T,
onDisabledOrUnavailable: () -> T,
): T = when (this) {
is Enabled -> onEnabled()
is Disabled, Unavailable -> onDisabledOrUnavailable()
}
fun onEnabled(action: () -> Unit): FeatureFlagResult {
if (this is Enabled) {
action()
}
return this
}
fun onDisabled(action: () -> Unit): FeatureFlagResult {
if (this is Disabled) {
action()
}
return this
}
fun onUnavailable(action: () -> Unit): FeatureFlagResult {
if (this is Unavailable) {
action()
}
return this
}
fun onDisabledOrUnavailable(action: () -> Unit): FeatureFlagResult {
if (this is Disabled || this is Unavailable) {
action()
}
return this
}
}

View file

@ -0,0 +1,17 @@
package net.thunderbird.core.featureflag
class InMemoryFeatureFlagProvider(
featureFlagFactory: FeatureFlagFactory,
) : FeatureFlagProvider {
private val features: Map<FeatureFlagKey, FeatureFlag> =
featureFlagFactory.createFeatureCatalog().associateBy { it.key }
override fun provide(key: FeatureFlagKey): FeatureFlagResult {
return when (features[key]?.enabled) {
null -> FeatureFlagResult.Unavailable
true -> FeatureFlagResult.Enabled
false -> FeatureFlagResult.Disabled
}
}
}

View file

@ -0,0 +1,29 @@
@file:JvmName("FeatureFlagProviderCompat")
package net.thunderbird.core.featureflag.compat
import androidx.annotation.Discouraged
import net.thunderbird.core.featureflag.FeatureFlagKey
import net.thunderbird.core.featureflag.FeatureFlagProvider
import net.thunderbird.core.featureflag.FeatureFlagResult
import net.thunderbird.core.featureflag.toFeatureFlagKey
/**
* Provides a feature flag result based on a string key, primarily for Java compatibility.
*
* This function acts as a bridge for Java code to access the Kotlin-idiomatic `provide`
* function that expects a [FeatureFlagKey], as value classes are not compatible with Java
* code.
*
* **Note:** This function is discouraged for use in Kotlin code. Prefer using the
* [FeatureFlagProvider.provide(key: FeatureFlagKey)][FeatureFlagProvider.provide]
* function directly in Kotlin.
*
* @receiver The [FeatureFlagProvider] instance to query.
* @param key The string representation of the feature flag key.
* @return The [FeatureFlagResult] corresponding to the given key.
*/
@Discouraged(message = "This function should be only used within Java files.")
fun FeatureFlagProvider.provide(key: String): FeatureFlagResult {
return provide(key.toFeatureFlagKey())
}

View file

@ -0,0 +1,142 @@
package net.thunderbird.core.featureflag
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
class FeatureFlagResultTest {
@Test
fun `should only call onEnabled when enabled`() {
val testSubject = FeatureFlagResult.Enabled
var resultEnabled = ""
var resultDisabled = ""
var resultUnavailable = ""
testSubject.onEnabled {
resultEnabled = "enabled"
}.onDisabled {
resultDisabled = "disabled"
}.onUnavailable {
resultUnavailable = "unavailable"
}
assertThat(resultEnabled).isEqualTo("enabled")
assertThat(resultDisabled).isEqualTo("")
assertThat(resultUnavailable).isEqualTo("")
}
@Test
fun `should only call onDisabled when disabled`() {
val testSubject = FeatureFlagResult.Disabled
var resultEnabled = ""
var resultDisabled = ""
var resultUnavailable = ""
testSubject.onEnabled {
resultEnabled = "enabled"
}.onDisabled {
resultDisabled = "disabled"
}.onUnavailable {
resultUnavailable = "unavailable"
}
assertThat(resultEnabled).isEqualTo("")
assertThat(resultDisabled).isEqualTo("disabled")
assertThat(resultUnavailable).isEqualTo("")
}
@Test
fun `should only call onUnavailable when unavailable`() {
val testSubject = FeatureFlagResult.Unavailable
var resultEnabled = ""
var resultDisabled = ""
var resultUnavailable = ""
testSubject.onEnabled {
resultEnabled = "enabled"
}.onDisabled {
resultDisabled = "disabled"
}.onUnavailable {
resultUnavailable = "unavailable"
}
assertThat(resultEnabled).isEqualTo("")
assertThat(resultDisabled).isEqualTo("")
assertThat(resultUnavailable).isEqualTo("unavailable")
}
@Test
fun `should call onDisabledOrUnavailable when disabled`() {
val testSubject = FeatureFlagResult.Disabled
var resultEnabled = ""
var resultDisabled = ""
var resultUnavailable = ""
var resultDisabledOrUnavailable = ""
testSubject.onEnabled {
resultEnabled = "enabled"
}.onDisabled {
resultDisabled = "disabled"
}.onUnavailable {
resultUnavailable = "unavailable"
}.onDisabledOrUnavailable {
resultDisabledOrUnavailable = "disabled or unavailable"
}
assertThat(resultEnabled).isEqualTo("")
assertThat(resultDisabled).isEqualTo("disabled")
assertThat(resultUnavailable).isEqualTo("")
assertThat(resultDisabledOrUnavailable).isEqualTo("disabled or unavailable")
}
@Test
fun `should call onDisabledOrUnavailable when unavailable`() {
val testSubject = FeatureFlagResult.Unavailable
var resultEnabled = ""
var resultDisabled = ""
var resultUnavailable = ""
var resultDisabledOrUnavailable = ""
testSubject.onEnabled {
resultEnabled = "enabled"
}.onDisabled {
resultDisabled = "disabled"
}.onUnavailable {
resultUnavailable = "unavailable"
}.onDisabledOrUnavailable {
resultDisabledOrUnavailable = "disabled or unavailable"
}
assertThat(resultEnabled).isEqualTo("")
assertThat(resultDisabled).isEqualTo("")
assertThat(resultUnavailable).isEqualTo("unavailable")
assertThat(resultDisabledOrUnavailable).isEqualTo("disabled or unavailable")
}
@Test
fun `whenEnabledOrNot should return correct value based on state`() {
val enabledResult = FeatureFlagResult.Enabled.whenEnabledOrNot(
onEnabled = { "Feature is ON" },
onDisabledOrUnavailable = { "Feature is OFF" },
)
assertThat(enabledResult).isEqualTo("Feature is ON")
val disabledResult = FeatureFlagResult.Disabled.whenEnabledOrNot(
onEnabled = { "Feature is ON" },
onDisabledOrUnavailable = { "Feature is OFF" },
)
assertThat(disabledResult).isEqualTo("Feature is OFF")
val unavailableResult = FeatureFlagResult.Unavailable.whenEnabledOrNot(
onEnabled = { "Feature is ON" },
onDisabledOrUnavailable = { "Feature is OFF" },
)
assertThat(unavailableResult).isEqualTo("Feature is OFF")
}
}

View file

@ -0,0 +1,57 @@
package net.thunderbird.core.featureflag
import assertk.assertThat
import assertk.assertions.isInstanceOf
import org.junit.Test
class InMemoryFeatureFlagProviderTest {
@Test
fun `should return FeatureFlagResult#Enabled when feature is enabled`() {
val feature1Key = FeatureFlagKey("feature1")
val featureFlagProvider = InMemoryFeatureFlagProvider(
featureFlagFactory = {
listOf(
FeatureFlag(key = feature1Key, enabled = true),
)
},
)
val result = featureFlagProvider.provide(feature1Key)
assertThat(result).isInstanceOf<FeatureFlagResult.Enabled>()
}
@Test
fun `should return FeatureFlagResult#Disabled when feature is disabled`() {
val feature1Key = FeatureFlagKey("feature1")
val featureFlagProvider = InMemoryFeatureFlagProvider(
featureFlagFactory = {
listOf(
FeatureFlag(key = feature1Key, enabled = false),
)
},
)
val result = featureFlagProvider.provide(feature1Key)
assertThat(result).isInstanceOf<FeatureFlagResult.Disabled>()
}
@Test
fun `should return FeatureFlagResult#Unavailable when feature is not found`() {
val feature1Key = FeatureFlagKey("feature1")
val feature2Key = FeatureFlagKey("feature2")
val featureFlagProvider = InMemoryFeatureFlagProvider(
featureFlagFactory = {
listOf(
FeatureFlag(key = feature1Key, enabled = false),
)
},
)
val result = featureFlagProvider.provide(feature2Key)
assertThat(result).isInstanceOf<FeatureFlagResult.Unavailable>()
}
}