From e2ba87ce367065deccedf04cdb019d646828a745 Mon Sep 17 00:00:00 2001 From: "a.tabolin" Date: Mon, 22 Jul 2024 14:30:24 +0300 Subject: [PATCH] [Move] - move lib to GitHub --- .github/workflows/main.yml | 2 +- .gitignore | 4 + .idea/dictionaries/project.xml | 7 + README.md | 322 +++++++++++++++++- gradle/libs.versions.toml | 6 +- konfeature/build.gradle.kts | 18 +- .../redmadrobot/konfeature/FeatureValue.kt | 10 + .../com/redmadrobot/konfeature/Konfeature.kt | 11 + .../com/redmadrobot/konfeature/Logger.kt | 18 + .../konfeature/builder/KonfeatureBuilder.kt | 92 +++++ .../konfeature/builder/KonfeatureImpl.kt | 97 ++++++ .../konfeature/delegate/FeatureGroup.kt | 93 +++++ .../konfeature/delegate/FeatureGroupSpec.kt | 7 + .../konfeature/delegate/FeatureValueSpec.kt | 12 + .../exception/KonfeatureException.kt | 21 ++ .../konfeature/source/FeatureSource.kt | 8 + .../konfeature/source/FeatureValueSource.kt | 17 + .../konfeature/source/Interceptor.kt | 8 + .../source/SourceSelectionStrategy.kt | 13 + .../redmadrobot/konfeature/KonfeatureTest.kt | 252 ++++++++++++++ .../builder/KonfeatureBuilderTest.kt | 64 ++++ .../konfeature/helper/KonfeatureTestHelper.kt | 23 ++ .../konfeature/helper/TestFeatureGroup.kt | 33 ++ konfeature/src/main/kotlin/Konfeature.kt | 3 - sample/build.gradle.kts | 12 + .../com/redmadrobot/konfeature/sample/App.kt | 47 +++ .../FeatureToggleDebugPanelInterceptor.kt | 19 ++ .../konfeature/sample/SampleFeatureGroup.kt | 59 ++++ .../konfeature/sample/SampleSources.kt | 32 ++ settings.gradle.kts | 3 +- 30 files changed, 1296 insertions(+), 17 deletions(-) create mode 100644 .idea/dictionaries/project.xml create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureValue.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Logger.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroup.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroupSpec.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureValueSpec.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureSource.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureValueSource.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/Interceptor.kt create mode 100644 konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/SourceSelectionStrategy.kt create mode 100644 konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt create mode 100644 konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilderTest.kt create mode 100644 konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/KonfeatureTestHelper.kt create mode 100644 konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureGroup.kt delete mode 100644 konfeature/src/main/kotlin/Konfeature.kt create mode 100644 sample/build.gradle.kts create mode 100644 sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt create mode 100644 sample/src/main/kotlin/com/redmadrobot/konfeature/sample/FeatureToggleDebugPanelInterceptor.kt create mode 100644 sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureGroup.kt create mode 100644 sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleSources.kt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 06deafa..bad5479 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 - name: Run Check - run: ./gradlew check detektAll detektReleaseAll + run: ./gradlew check detektAll publish: name: Publish diff --git a/.gitignore b/.gitignore index fd68970..990e6cb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ # Keep VCS config !.idea/vcs.xml +# Keep project dictionary +!.idea/dictionaries +!.idea/dictionaries/project.xml + ## Gradle # Ignore Gradle project-specific cache directory .gradle diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 0000000..6f47080 --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + konfeature + + + diff --git a/README.md b/README.md index e422b3a..54a5d9f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,15 @@ [![License](https://img.shields.io/github/license/RedMadRobot/Konfeature?style=flat-square)][license] -Konfeature and no more spoilers. +Working with remote configuration has become a standard part of the development process for almost any application. Depending on the complexity of the application, several requirements for such functionality may arise, including: +- convenient syntax for declaring configuration elements +- the ability to separate configuration into different files for different features +- the ability to make the configuration local-only during active feature development +- support for multiple data sources for RemoteConfig +- the ability to view a list of all configurations and modify their values for debugging purposes +- logging the value and its source when accessing the configuration, as well as logging non-critical errors + +We have made every effort to meet all these requirements in the development of Konfeature. --- @@ -10,6 +18,13 @@ Konfeature and no more spoilers. - [Installation](#installation) - [Usage](#usage) + - [FeatureGroup](#featuregroup) + - [FeatureSource](#featuresource) + - [SourceSelectionStrategy](#sourceselectionstrategy) + - [Interceptor](#interceptor) + - [Logger](#logger) + - [Spec](#spec) + - [Ordering](#ordering) - [Contributing](#contributing) @@ -21,7 +36,6 @@ Add the dependency: ```groovy repositories { mavenCentral() - google() } dependencies { @@ -31,14 +45,308 @@ dependencies { ## Usage +### FeatureGroup + +Defines a set of configuration elements, where each element is defined using a delegate. +There are two types of delegates: +- `by toggle(...)` - used for elements of type `Boolean` +- `by value(...)` - used for elements of any other type + +```kotlin +class ProfileFeatureGroup : FeatureGroup( + name = "profile_feature_config", + description = "Config of features for profile usage" +) { + val isProfileFeatureEnabled: Boolean by toggle( + key = "profile_feature", + description = "show profile entry point for user", + defaultValue = false, + ) + + val profileFeatureTitle: String by value( + key = "profile_feature_title", + description = "title of profile entry point button", + defaultValue = "Feature number nine", + sourceSelectionStrategy = SourceSelectionStrategy.Any + ) + + val profileButtonAppearDuration: Long by value( + key = "profile_button_appear_duration", + description = "duration of profile button appearing in ms", + defaultValue = 200, + sourceSelectionStrategy = SourceSelectionStrategy.Any + ) +} +``` + +The configuration requires specifying: +- `name` - the name of the configuration +- `description` - a detailed description of the configuration + +Each configuration element requires specifying: +- `key` - used to retrieve the value of the element from a `Source` +- `description` - a detailed description of the element +- `defaultValue` - used if the value cannot be found in a `Source` +- `sourceSelectionStrategy` - the strategy for selecting a `Source` using [SourceSelectionStrategy](#sourceselectionstrategy) + +After that, you need to register the configuration in `Konfeature`: + +```kotlin +val profileFeatureGroup: FeatureGroup = ProfileFeatureGroup() + +val konfeatureInstance = konfeature { + register(profileFeatureGroup) +} +``` + +>Similarly, you can add multiple configurations, for example, for each module, when organizing multi-modularity by features. + +### FeatureSource + +An abstraction over the value source for configuration elements. + +```kotlin +public interface FeatureSource { + + public val name: String + + public fun get(key: String): Any? +} +``` +- `name` - source name +- `get(key: String)` - logic for getting values by `key` + +Example implementation based on `FirebaseRemoteConfig`: + +```kotlin +class FirebaseFeatureSource( + private val remoteConfig: FirebaseRemoteConfig +) : FeatureSource { + + override val name: String = "FirebaseRemoteConfig" + + override fun get(key: String): Any? { + return remoteConfig + .getValue(key) + .takeIf { source == FirebaseRemoteConfig.VALUE_SOURCE_REMOTE } + ?.let { value: FirebaseRemoteConfigValue -> + value.getOrNull { asBoolean() } + ?: value.getOrNull { asString() } + ?: value.getOrNull { asLong() } + ?: value.getOrNull { asDouble() } + } + } + + private fun FirebaseRemoteConfigValue.getOrNull( + getter: FirebaseRemoteConfigValue.() -> Any? + ): Any? { + return try { + getter() + } catch (error: IllegalArgumentException) { + null + } + } +} +``` +After that, you need to add the `Source` in `Konfeature`: + +```kotlin +val profileFeatureGroup: FeatureGroup = ProfileFeatureGroup() +val source: FeatureSource = FirebaseFeatureSource(remoteConfig) + +val konfeatureInstance = konfeature { + addSource(source) + register(profileFeatureGroup) +} +``` + +>Similarly, you can add multiple sources, for example, Huawei AppGallery, RuStore, or your own backend. + +### SourceSelectionStrategy + +You can configure the retrieval of an element's value from the source more flexibly by using the `sourceSelectionStrategy` parameter: + +```kotlin +val profileFeatureTitle: String by value( + key = "profile_feature_title", + description = "title of profile entry point button", + defaultValue = "Feature number nine", + sourceSelectionStrategy = SourceSelectionStrategy.Any +) +``` + +Where `sourceSelectionStrategy` filters the available data sources. + +```kotlin +public fun interface SourceSelectionStrategy { + + public fun select(names: Set): Set + + public companion object { + public val None: SourceSelectionStrategy = SourceSelectionStrategy { emptySet() } + public val Any: SourceSelectionStrategy = SourceSelectionStrategy { it } + + public fun anyOf(vararg sources: String): SourceSelectionStrategy = SourceSelectionStrategy { sources.toSet() } + } +} +``` + +The `select(...)` method receives a list of available `Source` names and returns a list of sources from which the configuration element can retrieve a value. + +For most scenarios, predefined implementations will be sufficient: +- `SourceSelectionStrategy.None` - prohibits taking values from any source, i.e., the value specified in `defaultValue` will always be used +- `SourceSelectionStrategy.Any` - allows taking values from any source +- `SourceSelectionStrategy.anyOf("Source 1", ... ,"Source N")` - allows taking values from the specified list of sources + +>**!!!By default, `SourceSelectionStrategy.None` is used!!!** + +### Interceptor + +Allows intercepting and overriding the value of the element. + +```kotlin +public interface Interceptor { + + public val name: String + + public fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? +} +``` + +- `name` - the name of the interceptor +- `intercept(valueSource: FeatureValueSource, key: String, value: Any): Any?` - called when accessing the element with `key` and `value` from `valueSource(Source(), Interceptor(), Default)`, and returns its new value or `null` if it doesn't change + +Example of implementation based on `DebugPanelInterceptor`: + +```kotlin +class DebugPanelInterceptor : Interceptor { + + private val values = mutableMapOf() + + override val name: String = "DebugPanelInterceptor" + + override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? { + return values[key] + } + + fun setFeatureValue(key: String, value: Any) { + values[key] = value + } + + fun removeFeatureValue(key: String) { + values.remove(key) + } +} +``` + +After that, you need to add the `Interceptor` in `Konfeature`: + +```kotlin +val profileFeatureGroup: FeatureGroup = ProfileFeatureGroup() +val source: FeatureSource = FirebaseFeatureSource(remoteConfig) +val debugPanelInterceptor: Interceptor = DebugPanelInterceptor() + +val konfeatureInstance = konfeature { + addSource(source) + register(profileFeatureGroup) + addInterceptor(debugPanelInterceptor) +} +``` + +>Similarly, you can add multiple interceptors. + +### Logger + +```kotlin +public interface Logger { + + public fun log(severity: Severity, message: String) + + public enum class Severity { + WARNING, INFO + } +} +``` + +The following events are logged: + +- key, value, and its source when requested +>Get value 'true' by key 'profile_feature' from 'Source(name=FirebaseRemoteConfig)' +- `Source` or `Interceptor` returns an unexpected type for `key` +>Unexpected value type for 'profile_button_appear_duration': expected type is 'kotlin.Long', but value from 'Source(name=FirebaseRemoteConfig)' is 'true' with type 'kotlin.Boolean' + +Example of implementation based on `Timber`: + +```kotlin +class TimberLogger: Logger { + + override fun log(severity: Severity, message: String) { + if (severity == INFO) { + Timber.tag(TAG).i(message) + } else if (severity == WARNING) { + Timber.tag(TAG).w(message) + } + } + + companion object { + private const val TAG = "Konfeature" + } +} +``` + +After that, you need to add the `Logger` in `Konfeature`: + +```kotlin +val profileFeatureGroup: FeatureGroup = ProfileFeatureGroup() +val source: FeatureSource = FirebaseFeatureSource(remoteConfig) +val debugPanelInterceptor: Interceptor = DebugPanelInterceptor() +val logger: Logger = TimberLogger() + +val konfeatureInstance = konfeature { + addSource(source) + register(profileFeatureGroup) + addInterceptor(debugPanelInterceptor) + setLogger(logger) +} +``` + +### Spec + +Konfeature contains information about all registered `FeatureGroups` in the form of `spec`: + +```kotlin +public interface Konfeature { + + public val spec: List + + public fun getValue(spec: FeatureValueSpec): FeatureValue +} +``` + +This allows you to obtain information about added configurations as well as the current value of each element: + +```kotlin +val konfeatureInstance = konfeature {...} + +val featureGroupSpec = konfeatureInstance.spec[0] +val featureSpec = groupSpec.values[0] +val featureValue = konfeatureInstance.getValue(featureSpec) +``` +> This can be useful for use in the DebugPanel + +## Ordering +The value of the configuration element is determined in the following order: + +- `defaultValue` and `Default` source are assigned. +- Using `sourceSelectionStrategy`, a list of `Sources` from which a value can be requested is determined. +- Search the list of `Sources` in the order they were added to `Konfeature`, **stopping at the first occurrence** of the element by `key`. + Upon successful search, the value from `Source` is assigned with `Source(name=SourceName)` source. +- Search the list of `Interceptors` in the order they were added to `Konfeature`. + If `Interceptor` returns a value other than `null`, this value is assigned with `Interceptor(name=InterceptorName)` source. + ## Contributing Merge requests are welcome. For major changes, please open an issue first to discuss what you would like to change. -## Checklist after repository creation (remove after checked) - -- Update developers in [publishing plugin](buildSrc/src/main/kotlin/convention.publishing.gradle.kts) -- remove `Cleanup` step from [main.yml](.github/workflows/main.yml) - [license]: ../LICENSE diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e337b1f..40805ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,9 +6,11 @@ android-gradle-plugin = "8.4.0" detekt = "1.23.6" gradle-android-cacheFix = "3.0.1" gradle-infrastructure = "0.18.1" -kotlin = "1.9.23" +kotlin = "2.0.0" versionsPlugin = "0.51.0" publish-plugin = "0.28.0" +poko = "0.16.0" +kotest = "5.9.1" [libraries] android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" } @@ -18,7 +20,9 @@ infrastructure-android = { module = "com.redmadrobot.build:infrastructure-androi infrastructure-publish = { module = "com.redmadrobot.build:infrastructure-publish", version.ref = "gradle-infrastructure" } publish-gradlePlugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish-plugin" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } [plugins] infrastructure-detekt = { id = "com.redmadrobot.detekt", version.ref = "gradle-infrastructure" } versions = { id = "com.github.ben-manes.versions", version.ref = "versionsPlugin" } +poko = { id = "dev.drewhamilton.poko", version.ref = "poko" } diff --git a/konfeature/build.gradle.kts b/konfeature/build.gradle.kts index a53ac47..5569b96 100644 --- a/konfeature/build.gradle.kts +++ b/konfeature/build.gradle.kts @@ -1,11 +1,21 @@ plugins { - id("com.redmadrobot.kotlin-library") + kotlin("multiplatform") + alias(libs.plugins.poko) convention.publishing convention.detekt } -description = "konfeature" +kotlin { + explicitApi() + jvm() -dependencies { - api(kotlin("stdlib")) + sourceSets { + commonMain.dependencies { + api(kotlin("stdlib")) + } + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotest.assertions.core) + } + } } diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureValue.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureValue.kt new file mode 100644 index 0000000..e94517b --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureValue.kt @@ -0,0 +1,10 @@ +package com.redmadrobot.konfeature + +import com.redmadrobot.konfeature.source.FeatureValueSource +import dev.drewhamilton.poko.Poko + +@Poko +public class FeatureValue( + public val source: FeatureValueSource, + public val value: T, +) diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt new file mode 100644 index 0000000..140b167 --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt @@ -0,0 +1,11 @@ +package com.redmadrobot.konfeature + +import com.redmadrobot.konfeature.delegate.FeatureGroupSpec +import com.redmadrobot.konfeature.delegate.FeatureValueSpec + +public interface Konfeature { + + public val spec: List + + public fun getValue(spec: FeatureValueSpec): FeatureValue +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Logger.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Logger.kt new file mode 100644 index 0000000..6337eea --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Logger.kt @@ -0,0 +1,18 @@ +package com.redmadrobot.konfeature + +public interface Logger { + + public fun log(severity: Severity, message: String) + + public enum class Severity { + WARNING, INFO + } +} + +internal fun Logger.logWarn(message: String) { + log(Logger.Severity.WARNING, message) +} + +internal fun Logger.logInfo(message: String) { + log(Logger.Severity.INFO, message) +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt new file mode 100644 index 0000000..e477f3d --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt @@ -0,0 +1,92 @@ +package com.redmadrobot.konfeature.builder + +import com.redmadrobot.konfeature.Konfeature +import com.redmadrobot.konfeature.Logger +import com.redmadrobot.konfeature.delegate.FeatureGroup +import com.redmadrobot.konfeature.delegate.FeatureGroupSpec +import com.redmadrobot.konfeature.exception.GroupNameAlreadyExistException +import com.redmadrobot.konfeature.exception.KeyDuplicationException +import com.redmadrobot.konfeature.exception.NoFeatureGroupException +import com.redmadrobot.konfeature.exception.SourceNameAlreadyExistException +import com.redmadrobot.konfeature.logWarn +import com.redmadrobot.konfeature.source.FeatureSource +import com.redmadrobot.konfeature.source.Interceptor + +public class KonfeatureBuilder { + private val sources = mutableListOf() + private var interceptors = mutableListOf() + private var featureGroups = mutableListOf() + private var logger: Logger? = null + + public fun addInterceptor(interceptor: Interceptor): KonfeatureBuilder { + interceptors.add(interceptor) + return this + } + + public fun addSource(source: FeatureSource): KonfeatureBuilder { + if (sources.any { it.name == source.name }) { + throw SourceNameAlreadyExistException(source.name) + } + + sources.add(source) + return this + } + + public fun register(featureGroup: FeatureGroup): KonfeatureBuilder { + if (featureGroups.any { it.name == featureGroup.name }) { + throw GroupNameAlreadyExistException(featureGroup.name) + } + featureGroups.add(featureGroup) + return this + } + + public fun setLogger(logger: Logger): KonfeatureBuilder { + this.logger = logger + return this + } + + public fun build(): Konfeature { + if (featureGroups.isEmpty()) { + throw NoFeatureGroupException() + } + + featureGroups.forEach(::validateGroupSpec) + + return KonfeatureImpl( + sources = sources, + interceptors = interceptors, + logger = logger, + spec = featureGroups + ).also { toggleEase -> + featureGroups.forEach { values -> + values.bind(toggleEase) + } + } + } + + private fun validateGroupSpec(group: FeatureGroupSpec) { + val counter = mutableMapOf().withDefault { 0 } + var hasDuplicates = false + group.values.forEach { valueSpec -> + val value = counter.getValue(valueSpec.key) + if (value > 0) { + hasDuplicates = true + } + counter[valueSpec.key] = value + 1 + } + + if (hasDuplicates) { + val values = counter.asSequence() + .filter { it.value > 1 } + .map { it.key } + .toList() + throw KeyDuplicationException(values, group.name) + } else if (counter.isEmpty()) { + logger?.logWarn("Group '${group.name}' is empty") + } + } +} + +public fun konfeature(build: KonfeatureBuilder.() -> Unit): Konfeature { + return KonfeatureBuilder().apply(build).build() +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt new file mode 100644 index 0000000..47a6e6b --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt @@ -0,0 +1,97 @@ +@file:Suppress("NoWildcardImports", "WildcardImport") + +package com.redmadrobot.konfeature.builder + +import com.redmadrobot.konfeature.* +import com.redmadrobot.konfeature.delegate.FeatureGroupSpec +import com.redmadrobot.konfeature.delegate.FeatureValueSpec +import com.redmadrobot.konfeature.source.FeatureSource +import com.redmadrobot.konfeature.source.FeatureValueSource +import com.redmadrobot.konfeature.source.Interceptor +import kotlin.reflect.KClass + +internal class KonfeatureImpl( + private val sources: List, + private val interceptors: List, + private val logger: Logger?, + override val spec: List, +) : Konfeature { + + private val sourcesNames = sources.map { it.name }.toSet() + + @Suppress("LoopWithTooManyJumpStatements") + override fun getValue(spec: FeatureValueSpec): FeatureValue { + val selectedSourcesNames = spec.sourceSelectionStrategy.select(sourcesNames) + + val expectedClass = spec.defaultValue::class + var value: T = spec.defaultValue + var valueSource: FeatureValueSource = FeatureValueSource.Default + + for (source in sources) { + if (source.name !in selectedSourcesNames) continue + val actualSourceValue = source.get(spec.key) + val sourceValue = expectedClass.tryCastOrNull(actualSourceValue) + + if (actualSourceValue != null && sourceValue == null) { + logger?.logUnexpectedValueType( + key = spec.key, + source = FeatureValueSource.Source(source.name), + value = actualSourceValue, + actualClass = actualSourceValue::class.qualifiedName, + expectedClass = expectedClass.qualifiedName, + ) + } + + if (sourceValue != null) { + value = sourceValue + valueSource = FeatureValueSource.Source(source.name) + break + } + } + + for (interceptor in interceptors) { + val actualInterceptorValue = interceptor.intercept(valueSource, spec.key, value) + val interceptorValue = expectedClass.tryCastOrNull(actualInterceptorValue) + + if (actualInterceptorValue != null && interceptorValue == null) { + logger?.logUnexpectedValueType( + key = spec.key, + source = FeatureValueSource.Interceptor(interceptor.name), + value = actualInterceptorValue, + actualClass = actualInterceptorValue::class.qualifiedName, + expectedClass = expectedClass.qualifiedName, + ) + } + + if (interceptorValue != null) { + value = interceptorValue + valueSource = FeatureValueSource.Interceptor(interceptor.name) + } + } + + logger?.logInfo("Get value '$value' by key '${spec.key}' from '$valueSource'") + + return FeatureValue(valueSource, value) + } + + private fun Logger.logUnexpectedValueType( + key: String, + source: FeatureValueSource, + value: Any, + actualClass: String?, + expectedClass: String?, + ) { + logWarn( + "Unexpected value type for '$key': " + + "expected type is '$expectedClass', but " + + "value from '$source' " + + "is '$value' " + + "with type '$actualClass'", + ) + } + + @Suppress("UNCHECKED_CAST") + private fun KClass.tryCastOrNull(value: Any?): T? { + return if (isInstance(value)) value as T else null + } +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroup.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroup.kt new file mode 100644 index 0000000..e3241d7 --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroup.kt @@ -0,0 +1,93 @@ +package com.redmadrobot.konfeature.delegate + +import com.redmadrobot.konfeature.Konfeature +import com.redmadrobot.konfeature.source.SourceSelectionStrategy +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +public abstract class FeatureGroup( + override val name: String, + override val description: String +) : FeatureGroupSpec { + private var konfeature: Konfeature? = null + private val _values = mutableListOf>() + + override val values: List> + get() = _values.toList() + + internal fun bind(konfeature: Konfeature) { + this.konfeature = konfeature + } + + @JvmName("ValueBoolean") + @Deprecated( + message = "Use toggle instead", + replaceWith = ReplaceWith("toggle(key, description, defaultValue, sourceSelectionStrategy)"), + level = DeprecationLevel.ERROR, + ) + @Suppress("UNUSED_PARAMETER", "FINAL_UPPER_BOUND") + public fun value( + key: String, + description: String, + defaultValue: T, + sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None + ): ReadOnlyProperty { + error("Use toggle instead of boolean value") + } + + public fun value( + key: String, + description: String, + defaultValue: T, + sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None + ): ReadOnlyProperty { + return createValue( + key = key, + description = description, + defaultValue = defaultValue, + sourceSelectionStrategy = sourceSelectionStrategy + ) + } + + public fun toggle( + key: String, + description: String, + defaultValue: Boolean, + sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None, + ): ReadOnlyProperty { + return createValue( + key = key, + description = description, + defaultValue = defaultValue, + sourceSelectionStrategy = sourceSelectionStrategy + ) + } + + private fun createValue( + key: String, + description: String, + defaultValue: T, + sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None + ): ReadOnlyProperty { + val spec = FeatureValueSpec( + key = key, + description = description, + defaultValue = defaultValue, + sourceSelectionStrategy = sourceSelectionStrategy + ) + _values.add(spec) + return Value(spec) + } + + private class Value( + private val spec: FeatureValueSpec, + ) : ReadOnlyProperty { + override fun getValue(thisRef: FeatureGroup?, property: KProperty<*>): T { + return checkBinding(thisRef?.konfeature).getValue(spec).value + } + + private fun checkBinding(konFeature: Konfeature?): Konfeature { + return checkNotNull(konFeature) { "FeatureGroup is not bound to Konfeature" } + } + } +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroupSpec.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroupSpec.kt new file mode 100644 index 0000000..705ffa2 --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroupSpec.kt @@ -0,0 +1,7 @@ +package com.redmadrobot.konfeature.delegate + +public interface FeatureGroupSpec { + public val name: String + public val description: String + public val values: List> +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureValueSpec.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureValueSpec.kt new file mode 100644 index 0000000..feb0f1a --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureValueSpec.kt @@ -0,0 +1,12 @@ +package com.redmadrobot.konfeature.delegate + +import com.redmadrobot.konfeature.source.SourceSelectionStrategy +import dev.drewhamilton.poko.Poko + +@Poko +public class FeatureValueSpec( + public val key: String, + public val description: String, + public val defaultValue: T, + public val sourceSelectionStrategy: SourceSelectionStrategy +) diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt new file mode 100644 index 0000000..10fdf6e --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt @@ -0,0 +1,21 @@ +package com.redmadrobot.konfeature.exception + +public sealed class KonfeatureException(messageProvider: () -> String) : Exception(messageProvider.invoke()) + +public class GroupNameAlreadyExistException( + name: String +) : KonfeatureException({ "feature group with name '$name' already registered" }) + +public class KeyDuplicationException( + values: List, + group: String +) : KonfeatureException({ + val duplicatedValues = values.joinToString(separator = ", ", transform = { "'$it'" }) + "values with keys <$duplicatedValues> are duplicated in group '$group'" +}) + +public class NoFeatureGroupException : KonfeatureException({ "No feature group added" }) + +public class SourceNameAlreadyExistException( + name: String +) : KonfeatureException({ "source with name '$name' already registered" }) diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureSource.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureSource.kt new file mode 100644 index 0000000..2d7ed51 --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureSource.kt @@ -0,0 +1,8 @@ +package com.redmadrobot.konfeature.source + +public interface FeatureSource { + + public val name: String + + public fun get(key: String): Any? +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureValueSource.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureValueSource.kt new file mode 100644 index 0000000..6148383 --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureValueSource.kt @@ -0,0 +1,17 @@ +package com.redmadrobot.konfeature.source + +import dev.drewhamilton.poko.Poko + +public sealed class FeatureValueSource { + + @Poko + public class Source(public val name: String) : FeatureValueSource() + + @Poko + public class Interceptor(public val name: String) : FeatureValueSource() + + @Suppress("ConvertObjectToDataObject") + public object Default : FeatureValueSource() { + override fun toString(): String = "Default" + } +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/Interceptor.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/Interceptor.kt new file mode 100644 index 0000000..f05170d --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/Interceptor.kt @@ -0,0 +1,8 @@ +package com.redmadrobot.konfeature.source + +public interface Interceptor { + + public val name: String + + public fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? +} diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/SourceSelectionStrategy.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/SourceSelectionStrategy.kt new file mode 100644 index 0000000..db127c3 --- /dev/null +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/SourceSelectionStrategy.kt @@ -0,0 +1,13 @@ +package com.redmadrobot.konfeature.source + +public fun interface SourceSelectionStrategy { + + public fun select(names: Set): Set + + public companion object { + public val None: SourceSelectionStrategy = SourceSelectionStrategy { emptySet() } + public val Any: SourceSelectionStrategy = SourceSelectionStrategy { it } + + public fun anyOf(vararg sources: String): SourceSelectionStrategy = SourceSelectionStrategy { sources.toSet() } + } +} diff --git a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt new file mode 100644 index 0000000..ddc16fb --- /dev/null +++ b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt @@ -0,0 +1,252 @@ +package com.redmadrobot.konfeature + +import com.redmadrobot.konfeature.builder.konfeature +import com.redmadrobot.konfeature.helper.TestFeatureGroup +import com.redmadrobot.konfeature.helper.createTestSource +import com.redmadrobot.konfeature.source.FeatureValueSource +import com.redmadrobot.konfeature.source.Interceptor +import com.redmadrobot.konfeature.source.SourceSelectionStrategy +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class KonfeatureTest { + + @Test + fun `when correct group added - then code should pass`() { + val featureGroup = TestFeatureGroup() + + konfeature { + register(featureGroup) + } + } + + @Test + fun `when correct group added - should be correct spec`() { + // GIVEN + val sourceNames = listOf("Test Source 1", "Test Source 2", "Test Source 3") + val selectedSource = sourceNames[2] + + val featureGroup = TestFeatureGroup( + cSourceSelectionStrategy = SourceSelectionStrategy.anyOf(selectedSource), + ) + + // WHEN + val toggleEase = konfeature { + register(featureGroup) + } + + // THEN + toggleEase.spec.size shouldBe 1 + + val group = toggleEase.spec.first() + + group.name shouldBe featureGroup.name + group.description shouldBe featureGroup.description + group.values.size shouldBe 3 + + assertSoftly(group) { + values[0].apply { + key shouldBe "a" + description shouldBe "feature a desc" + defaultValue shouldBe true + sourceSelectionStrategy.select(sourceNames.toSet()).size shouldBe sourceNames.size + } + + values[1].apply { + key shouldBe "b" + description shouldBe "feature b desc" + defaultValue shouldBe true + sourceSelectionStrategy.select(sourceNames.toSet()).size shouldBe 0 + } + + values[2].apply { + key shouldBe "c" + description shouldBe "feature c desc" + defaultValue shouldBe "feature c" + sourceSelectionStrategy.select(sourceNames.toSet()).first() shouldBe selectedSource + } + } + } + + @Test + fun `when source have value - group should return it`() { + // GIVEN + val source = createTestSource( + name = "Test source", + values = mapOf("a" to false), + ) + val featureGroup = TestFeatureGroup() + + konfeature { + addSource(source) + register(featureGroup) + } + + // WHEN + val a = featureGroup.a + + // THEN + a shouldBe false + } + + @Test + fun `when source don't have value - group should return default value`() { + // GIVEN + val source = createTestSource( + name = "Test source", + values = mapOf("b" to false), + ) + val featureGroup = TestFeatureGroup() + + konfeature { + addSource(source) + register(featureGroup) + } + + // WHEN + val a = featureGroup.a + + // THEN + a shouldBe true + } + + @Test + fun `when source have value with unexpected type - group should return default value`() { + // GIVEN + val source = createTestSource( + name = "Test source", + values = mapOf("a" to 5), + ) + val featureGroup = TestFeatureGroup() + + konfeature { + addSource(source) + register(featureGroup) + } + + // WHEN + val a = featureGroup.a + + // THEN + a shouldBe true + } + + @Test + fun `when both sources contain same key - group should return value of first added source`() { + // GIVEN + val source1 = createTestSource( + name = "Test source 1", + values = mapOf("a" to false), + ) + val source2 = createTestSource( + name = "Test source 2", + values = mapOf("a" to true), + ) + + val featureGroup = TestFeatureGroup() + + // WHEN + konfeature { + addSource(source1) + addSource(source2) + register(featureGroup) + } + + val a = featureGroup.a + + // THEN + a shouldBe false + } + + @Test + fun `when source specified by SourceSelectionStrategy - group should return value from it`() { + // GIVEN + val source1 = createTestSource( + name = "Test source 1", + values = mapOf("c" to "test_source_1_c"), + ) + val source2 = createTestSource( + name = "Test source 2", + values = mapOf("c" to "test_source_2_c"), + ) + + val featureGroup = TestFeatureGroup( + cSourceSelectionStrategy = SourceSelectionStrategy.anyOf(source2.name), + ) + + // WHEN + konfeature { + addSource(source1) + addSource(source2) + register(featureGroup) + } + + val c = featureGroup.c + + // THEN + c shouldBe "test_source_2_c" + } + + @Test + fun `when value changed by interceptor - group should return it`() { + // GIVEN + val source = createTestSource( + name = "Test source", + values = mapOf( + "a" to false, + "b" to true, + "c" to "test_source_1_c", + ), + ) + + val interceptedValue = "intercepted_value_c" + + val interceptor = object : Interceptor { + override val name: String = "test interceptor" + + override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? { + return if (key == "c") interceptedValue else null + } + } + + val featureGroup = TestFeatureGroup() + + // WHEN + konfeature { + addSource(source) + addInterceptor(interceptor) + register(featureGroup) + } + + // THEN + assertSoftly(featureGroup) { + a shouldBe false + b shouldBe true + c shouldBe interceptedValue + } + } + + @Test + fun `when value changed by interceptor but has unexpected type - group should return default`() { + // GIVEN + val interceptor = object : Interceptor { + override val name: String = "test interceptor" + + override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? { + return if (key == "c") 100 else null + } + } + + val featureGroup = TestFeatureGroup() + + // WHEN + konfeature { + addInterceptor(interceptor) + register(featureGroup) + } + + // THEN + featureGroup.c shouldBe "feature c" + } +} diff --git a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilderTest.kt b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilderTest.kt new file mode 100644 index 0000000..4bdf1e0 --- /dev/null +++ b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilderTest.kt @@ -0,0 +1,64 @@ +package com.redmadrobot.konfeature.builder + +import com.redmadrobot.konfeature.exception.GroupNameAlreadyExistException +import com.redmadrobot.konfeature.exception.KeyDuplicationException +import com.redmadrobot.konfeature.exception.NoFeatureGroupException +import com.redmadrobot.konfeature.exception.SourceNameAlreadyExistException +import com.redmadrobot.konfeature.helper.TestFeatureGroup +import com.redmadrobot.konfeature.helper.createEmptyFeatureGroup +import com.redmadrobot.konfeature.helper.createTestSource +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class KonfeatureBuilderTest { + + @Test + fun `when no any feature group registered - should throw exception`() { + shouldThrow { + KonfeatureBuilder().build() + } + } + + @Test + fun `when group with duplicated keys added - should throw exception`() { + val featureGroup = TestFeatureGroup(withDuplicates = true) + + val exception = shouldThrow { + KonfeatureBuilder().register(featureGroup).build() + } + + exception.message shouldBe "values with keys <'a'> are duplicated in group '${featureGroup.name}'" + } + + @Test + fun `when source with same name added twice - should throw exception`() { + val featureGroupName = "Test Feature Group" + + val sourceName = "Test Source" + + val exception = shouldThrow { + konfeature { + addSource(createTestSource(sourceName)) + addSource(createTestSource(sourceName)) + register(createEmptyFeatureGroup(featureGroupName)) + } + } + + exception.message shouldBe "source with name '$sourceName' already registered" + } + + @Test + fun `when feature group with same name registered twice - should throw exception`() { + val featureGroupName = "Test Feature Group" + + val exception = shouldThrow { + konfeature { + register(createEmptyFeatureGroup(featureGroupName)) + register(createEmptyFeatureGroup(featureGroupName)) + } + } + + exception.message shouldBe "feature group with name '$featureGroupName' already registered" + } +} diff --git a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/KonfeatureTestHelper.kt b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/KonfeatureTestHelper.kt new file mode 100644 index 0000000..750471a --- /dev/null +++ b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/KonfeatureTestHelper.kt @@ -0,0 +1,23 @@ +package com.redmadrobot.konfeature.helper + +import com.redmadrobot.konfeature.delegate.FeatureGroup +import com.redmadrobot.konfeature.source.FeatureSource + +fun createTestSource( + name: String, + values: Map = emptyMap(), +): FeatureSource { + return object : FeatureSource { + + override val name: String = name + + override fun get(key: String): Any? = values[key] + } +} + +fun createEmptyFeatureGroup( + name: String, + description: String = "test description for $name", +): FeatureGroup { + return object : FeatureGroup(name = name, description = description) {} +} diff --git a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureGroup.kt b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureGroup.kt new file mode 100644 index 0000000..3bc6766 --- /dev/null +++ b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureGroup.kt @@ -0,0 +1,33 @@ +package com.redmadrobot.konfeature.helper + +import com.redmadrobot.konfeature.delegate.FeatureGroup +import com.redmadrobot.konfeature.source.SourceSelectionStrategy + +class TestFeatureGroup( + withDuplicates: Boolean = false, + cSourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.Any, +) : FeatureGroup( + name = "TestFeatureGroup", + description = "TestFeatureGroup description", +) { + val a by toggle( + key = "a", + description = "feature a desc", + defaultValue = true, + sourceSelectionStrategy = SourceSelectionStrategy.Any, + ) + + val b by toggle( + key = if (withDuplicates) "a" else "b", + description = "feature b desc", + defaultValue = true, + sourceSelectionStrategy = SourceSelectionStrategy.None, + ) + + val c: String by value( + key = if (withDuplicates) "a" else "c", + description = "feature c desc", + defaultValue = "feature c", + sourceSelectionStrategy = cSourceSelectionStrategy, + ) +} diff --git a/konfeature/src/main/kotlin/Konfeature.kt b/konfeature/src/main/kotlin/Konfeature.kt deleted file mode 100644 index da970b3..0000000 --- a/konfeature/src/main/kotlin/Konfeature.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.redmadrobot.konfeature - -public interface Konfeature diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts new file mode 100644 index 0000000..f7b36d3 --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + kotlin("jvm") + application + convention.detekt +} +dependencies { + implementation(projects.konfeature) +} + +application { + mainClass = "com.redmadrobot.konfeature.sample.AppKt" +} diff --git a/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt new file mode 100644 index 0000000..a29b156 --- /dev/null +++ b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt @@ -0,0 +1,47 @@ +package com.redmadrobot.konfeature.sample + +import com.redmadrobot.konfeature.Logger +import com.redmadrobot.konfeature.builder.konfeature + +fun main() { + val featureGroup = SampleFeatureGroup() + + val debugPanelInterceptor = FeatureToggleDebugPanelInterceptor() + + val logger = object : Logger { + override fun log(severity: Logger.Severity, message: String) { + println("${severity.name}: $message") + } + } + + val konfeature = konfeature { + addSource(RemoteFeatureSource()) + addSource(FirebaseFeatureSource()) + register(featureGroup) + addInterceptor(debugPanelInterceptor) + setLogger(logger) + } + + konfeature.spec.forEach { + println("Spec: --name: '${it.name}', description: '${it.description}'") + it.values.forEach(::println) + } + + println() + val spec = konfeature.spec.first().values.first() + println("getFeatureToggleValue('${spec.key}') -> ${konfeature.getValue(spec)}") + + println() + println("feature1: " + featureGroup.isFeature1Enabled) + println("feature2: " + featureGroup.isFeature2Enabled) + println("feature3: " + featureGroup.isFeature3Enabled) + println("velocity: " + featureGroup.velocity) + println("puhFeature: " + featureGroup.puhFeature) + + debugPanelInterceptor.setFeatureValue("feature2", false) + println() + println("debugPanelInterceptor.setFeatureValue(\"feature2\", false)") + println("feature1: " + featureGroup.isFeature1Enabled) + println("feature2: " + featureGroup.isFeature2Enabled) + println("feature3: " + featureGroup.isFeature3Enabled) +} diff --git a/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/FeatureToggleDebugPanelInterceptor.kt b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/FeatureToggleDebugPanelInterceptor.kt new file mode 100644 index 0000000..6b3ca75 --- /dev/null +++ b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/FeatureToggleDebugPanelInterceptor.kt @@ -0,0 +1,19 @@ +package com.redmadrobot.konfeature.sample + +import com.redmadrobot.konfeature.source.FeatureValueSource +import com.redmadrobot.konfeature.source.Interceptor + +class FeatureToggleDebugPanelInterceptor : Interceptor { + + private val values = mutableMapOf() + + override val name: String = "DebugPanelInterceptor" + + override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? { + return values[key] + } + + fun setFeatureValue(key: String, value: Any) { + values[key] = value + } +} diff --git a/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureGroup.kt b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureGroup.kt new file mode 100644 index 0000000..621ddf8 --- /dev/null +++ b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureGroup.kt @@ -0,0 +1,59 @@ +package com.redmadrobot.konfeature.sample + +import com.redmadrobot.konfeature.delegate.FeatureGroup +import com.redmadrobot.konfeature.source.SourceSelectionStrategy + +class SampleFeatureGroup : FeatureGroup( + name = "Sample", + description = "simple sample set" +) { + + val isFeature1Enabled: Boolean by toggle( + key = "feature1", + description = "feature1 desc", + defaultValue = false, + ) + + val isFeature2Enabled: Boolean by toggle( + key = "feature2", + description = "feature2 desc", + defaultValue = true, + sourceSelectionStrategy = SourceSelectionStrategy.Any + ) + + val isFeature3Enabled: Boolean by toggle( + key = "feature3", + description = "feature3 desc", + defaultValue = false, + sourceSelectionStrategy = SourceSelectionStrategy.Any + ) + + val velocity: Long by value( + key = "velocity_value", + description = "velocity value", + defaultValue = 90, + sourceSelectionStrategy = SourceSelectionStrategy.Any + ) + + val isGroupFeatureEnable: Boolean + get() = isFeature1Enabled && isFeature3Enabled + + val feature4: String by value( + key = "feature4", + description = "feature4 desc", + defaultValue = "true", + sourceSelectionStrategy = SourceSelectionStrategy.Any + ) + + enum class PUH { A, B, C } + + private val _puhFeature by value( + key = "puhFeature", + description = "puhFeature desc", + defaultValue = PUH.B.name, + sourceSelectionStrategy = SourceSelectionStrategy.Any + ) + + val puhFeature: PUH + get() = _puhFeature.let { PUH.valueOf(it) } +} diff --git a/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleSources.kt b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleSources.kt new file mode 100644 index 0000000..2a5995a --- /dev/null +++ b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleSources.kt @@ -0,0 +1,32 @@ +package com.redmadrobot.konfeature.sample + +import com.redmadrobot.konfeature.source.FeatureSource + +class RemoteFeatureSource : FeatureSource { + + private val store = mutableMapOf().apply { + put("feature1", false) + put("feature3", true) + put("velocity_value", true) + } + + override val name: String = "RemoteFeatureToggleSource" + + override fun get(key: String): Any? { + return store[key] + } +} + +class FirebaseFeatureSource : FeatureSource { + + private val store = mutableMapOf().apply { + put("feature2", true) + put("puhFeature", "C") + } + + override val name: String = "FirebaseFeatureToggleSource" + + override fun get(key: String): Any? { + return store[key] + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 25976a8..d1e5c47 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,5 +23,6 @@ dependencyResolutionManagement { rootProject.name = "konfeature-root" include( - "konfeature", + ":sample", + ":konfeature", )