diff --git a/core/featureflags/build.gradle.kts b/core/featureflags/build.gradle.kts new file mode 100644 index 000000000..cdbc1e9a2 --- /dev/null +++ b/core/featureflags/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id(ThunderbirdPlugins.Library.jvm) + alias(libs.plugins.android.lint) +} + +dependencies { + testImplementation(projects.core.testing) +} diff --git a/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlag.kt b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlag.kt new file mode 100644 index 000000000..dc29aa50a --- /dev/null +++ b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlag.kt @@ -0,0 +1,6 @@ +package app.k9mail.core.featureflag + +data class FeatureFlag( + val key: FeatureFlagKey, + val enabled: Boolean = false, +) diff --git a/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagFactory.kt b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagFactory.kt new file mode 100644 index 000000000..619d85fe0 --- /dev/null +++ b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagFactory.kt @@ -0,0 +1,5 @@ +package app.k9mail.core.featureflag + +fun interface FeatureFlagFactory { + fun createFeatureCatalog(): List +} diff --git a/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagKey.kt b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagKey.kt new file mode 100644 index 000000000..d7901d229 --- /dev/null +++ b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagKey.kt @@ -0,0 +1,4 @@ +package app.k9mail.core.featureflag + +@JvmInline +value class FeatureFlagKey(val key: String) diff --git a/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagProvider.kt b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagProvider.kt new file mode 100644 index 000000000..d6ea69fb3 --- /dev/null +++ b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagProvider.kt @@ -0,0 +1,5 @@ +package app.k9mail.core.featureflag + +fun interface FeatureFlagProvider { + fun provide(key: FeatureFlagKey): FeatureFlagResult +} diff --git a/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagResult.kt b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagResult.kt new file mode 100644 index 000000000..bafa73e4d --- /dev/null +++ b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/FeatureFlagResult.kt @@ -0,0 +1,31 @@ +package app.k9mail.core.featureflag + +sealed interface FeatureFlagResult { + object Enabled : FeatureFlagResult + object Disabled : FeatureFlagResult + object Unavailable : FeatureFlagResult + + 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 + } +} diff --git a/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/InMemoryFeatureFlagProvider.kt b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/InMemoryFeatureFlagProvider.kt new file mode 100644 index 000000000..ddbe770ea --- /dev/null +++ b/core/featureflags/src/main/kotlin/app/k9mail/core/featureflag/InMemoryFeatureFlagProvider.kt @@ -0,0 +1,17 @@ +package app.k9mail.core.featureflag + +class InMemoryFeatureFlagProvider( + featureFlagFactory: FeatureFlagFactory, +) : FeatureFlagProvider { + + private val features: Map = + 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 + } + } +} diff --git a/core/featureflags/src/test/kotlin/app/k9mail/core/featureflags/FeatureFlagResultTest.kt b/core/featureflags/src/test/kotlin/app/k9mail/core/featureflags/FeatureFlagResultTest.kt new file mode 100644 index 000000000..835dbd5d6 --- /dev/null +++ b/core/featureflags/src/test/kotlin/app/k9mail/core/featureflags/FeatureFlagResultTest.kt @@ -0,0 +1,60 @@ +package app.k9mail.core.featureflags + +import app.k9mail.core.featureflag.FeatureFlagResult +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 result = "" + + testSubject.onEnabled { + result = "enabled" + }.onDisabled { + result = "disabled" + }.onUnavailable { + result = "unavailable" + } + + assertThat(result).isEqualTo("enabled") + } + + @Test + fun `should only call onDisabled when disabled`() { + val testSubject = FeatureFlagResult.Disabled + + var result = "" + + testSubject.onEnabled { + result = "enabled" + }.onDisabled { + result = "disabled" + }.onUnavailable { + result = "unavailable" + } + + assertThat(result).isEqualTo("disabled") + } + + @Test + fun `should only call onUnavailable when unavailable`() { + val testSubject = FeatureFlagResult.Unavailable + + var result = "" + + testSubject.onEnabled { + result = "enabled" + }.onDisabled { + result = "disabled" + }.onUnavailable { + result = "unavailable" + } + + assertThat(result).isEqualTo("unavailable") + } +} diff --git a/core/featureflags/src/test/kotlin/app/k9mail/core/featureflags/InMemoryFeatureFlagProviderTest.kt b/core/featureflags/src/test/kotlin/app/k9mail/core/featureflags/InMemoryFeatureFlagProviderTest.kt new file mode 100644 index 000000000..4142e7701 --- /dev/null +++ b/core/featureflags/src/test/kotlin/app/k9mail/core/featureflags/InMemoryFeatureFlagProviderTest.kt @@ -0,0 +1,61 @@ +package app.k9mail.core.featureflags + +import app.k9mail.core.featureflag.FeatureFlag +import app.k9mail.core.featureflag.FeatureFlagKey +import app.k9mail.core.featureflag.FeatureFlagResult +import app.k9mail.core.featureflag.InMemoryFeatureFlagProvider +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() + } + + @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() + } + + @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() + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index db0bd1ad4..6f6e3d919 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,6 +52,7 @@ include( include( ":core:common", + ":core:featureflags", ":core:testing", ":core:android:common", ":core:android:testing",