From e2ba87ce367065deccedf04cdb019d646828a745 Mon Sep 17 00:00:00 2001 From: "a.tabolin" Date: Mon, 22 Jul 2024 14:30:24 +0300 Subject: [PATCH 1/3] [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", ) From 59dca94b2f76b31f3b2bb9c2b8917a45b4895997 Mon Sep 17 00:00:00 2001 From: "a.tabolin" Date: Wed, 24 Jul 2024 12:08:43 +0300 Subject: [PATCH 2/3] rename FeatureGroup to FeatureConfig, minor refactoring, --- .../FeatureGroup.kt => FeatureConfig.kt} | 21 +++-- ...atureGroupSpec.kt => FeatureConfigSpec.kt} | 4 +- .../{delegate => }/FeatureValueSpec.kt | 2 +- .../com/redmadrobot/konfeature/Konfeature.kt | 5 +- .../konfeature/builder/KonfeatureBuilder.kt | 36 ++++----- .../konfeature/builder/KonfeatureImpl.kt | 6 +- .../exception/KonfeatureException.kt | 10 +-- .../redmadrobot/konfeature/KonfeatureTest.kt | 80 +++++++++---------- .../builder/KonfeatureBuilderTest.kt | 36 ++++----- .../konfeature/helper/KonfeatureTestHelper.kt | 8 +- ...stFeatureGroup.kt => TestFeatureConfig.kt} | 10 +-- .../com/redmadrobot/konfeature/sample/App.kt | 22 ++--- ...FeatureGroup.kt => SampleFeatureConfig.kt} | 4 +- 13 files changed, 119 insertions(+), 125 deletions(-) rename konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/{delegate/FeatureGroup.kt => FeatureConfig.kt} (80%) rename konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/{delegate/FeatureGroupSpec.kt => FeatureConfigSpec.kt} (58%) rename konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/{delegate => }/FeatureValueSpec.kt (84%) rename konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/{TestFeatureGroup.kt => TestFeatureConfig.kt} (82%) rename sample/src/main/kotlin/com/redmadrobot/konfeature/sample/{SampleFeatureGroup.kt => SampleFeatureConfig.kt} (90%) diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroup.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureConfig.kt similarity index 80% rename from konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroup.kt rename to konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureConfig.kt index e3241d7..197a83e 100644 --- a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroup.kt +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureConfig.kt @@ -1,14 +1,13 @@ -package com.redmadrobot.konfeature.delegate +package com.redmadrobot.konfeature -import com.redmadrobot.konfeature.Konfeature import com.redmadrobot.konfeature.source.SourceSelectionStrategy import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty -public abstract class FeatureGroup( +public abstract class FeatureConfig( override val name: String, override val description: String -) : FeatureGroupSpec { +) : FeatureConfigSpec { private var konfeature: Konfeature? = null private val _values = mutableListOf>() @@ -31,7 +30,7 @@ public abstract class FeatureGroup( description: String, defaultValue: T, sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None - ): ReadOnlyProperty { + ): ReadOnlyProperty { error("Use toggle instead of boolean value") } @@ -40,7 +39,7 @@ public abstract class FeatureGroup( description: String, defaultValue: T, sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None - ): ReadOnlyProperty { + ): ReadOnlyProperty { return createValue( key = key, description = description, @@ -54,7 +53,7 @@ public abstract class FeatureGroup( description: String, defaultValue: Boolean, sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None, - ): ReadOnlyProperty { + ): ReadOnlyProperty { return createValue( key = key, description = description, @@ -68,7 +67,7 @@ public abstract class FeatureGroup( description: String, defaultValue: T, sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None - ): ReadOnlyProperty { + ): ReadOnlyProperty { val spec = FeatureValueSpec( key = key, description = description, @@ -81,13 +80,13 @@ public abstract class FeatureGroup( private class Value( private val spec: FeatureValueSpec, - ) : ReadOnlyProperty { - override fun getValue(thisRef: FeatureGroup?, property: KProperty<*>): T { + ) : ReadOnlyProperty { + override fun getValue(thisRef: FeatureConfig?, 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" } + return checkNotNull(konFeature) { "FeatureConfig 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/FeatureConfigSpec.kt similarity index 58% rename from konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroupSpec.kt rename to konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureConfigSpec.kt index 705ffa2..f03557c 100644 --- a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureGroupSpec.kt +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureConfigSpec.kt @@ -1,6 +1,6 @@ -package com.redmadrobot.konfeature.delegate +package com.redmadrobot.konfeature -public interface FeatureGroupSpec { +public interface FeatureConfigSpec { 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/FeatureValueSpec.kt similarity index 84% rename from konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureValueSpec.kt rename to konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureValueSpec.kt index feb0f1a..08aaea5 100644 --- a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/delegate/FeatureValueSpec.kt +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureValueSpec.kt @@ -1,4 +1,4 @@ -package com.redmadrobot.konfeature.delegate +package com.redmadrobot.konfeature import com.redmadrobot.konfeature.source.SourceSelectionStrategy import dev.drewhamilton.poko.Poko diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt index 140b167..b8b0deb 100644 --- a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt @@ -1,11 +1,8 @@ package com.redmadrobot.konfeature -import com.redmadrobot.konfeature.delegate.FeatureGroupSpec -import com.redmadrobot.konfeature.delegate.FeatureValueSpec - public interface Konfeature { - public val spec: List + public val spec: List public fun getValue(spec: FeatureValueSpec): FeatureValue } diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt index e477f3d..10c4dd1 100644 --- a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt @@ -2,11 +2,11 @@ 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.FeatureConfig +import com.redmadrobot.konfeature.FeatureConfigSpec +import com.redmadrobot.konfeature.exception.ConfigNameAlreadyExistException import com.redmadrobot.konfeature.exception.KeyDuplicationException -import com.redmadrobot.konfeature.exception.NoFeatureGroupException +import com.redmadrobot.konfeature.exception.NoFeatureConfigException import com.redmadrobot.konfeature.exception.SourceNameAlreadyExistException import com.redmadrobot.konfeature.logWarn import com.redmadrobot.konfeature.source.FeatureSource @@ -15,7 +15,7 @@ import com.redmadrobot.konfeature.source.Interceptor public class KonfeatureBuilder { private val sources = mutableListOf() private var interceptors = mutableListOf() - private var featureGroups = mutableListOf() + private var spec = mutableListOf() private var logger: Logger? = null public fun addInterceptor(interceptor: Interceptor): KonfeatureBuilder { @@ -32,11 +32,11 @@ public class KonfeatureBuilder { return this } - public fun register(featureGroup: FeatureGroup): KonfeatureBuilder { - if (featureGroups.any { it.name == featureGroup.name }) { - throw GroupNameAlreadyExistException(featureGroup.name) + public fun register(featureConfig: FeatureConfig): KonfeatureBuilder { + if (spec.any { it.name == featureConfig.name }) { + throw ConfigNameAlreadyExistException(featureConfig.name) } - featureGroups.add(featureGroup) + spec.add(featureConfig) return this } @@ -46,28 +46,26 @@ public class KonfeatureBuilder { } public fun build(): Konfeature { - if (featureGroups.isEmpty()) { - throw NoFeatureGroupException() - } + if (spec.isEmpty()) throw NoFeatureConfigException() - featureGroups.forEach(::validateGroupSpec) + spec.forEach(::validateConfigSpec) return KonfeatureImpl( sources = sources, interceptors = interceptors, logger = logger, - spec = featureGroups + spec = spec ).also { toggleEase -> - featureGroups.forEach { values -> + spec.forEach { values -> values.bind(toggleEase) } } } - private fun validateGroupSpec(group: FeatureGroupSpec) { + private fun validateConfigSpec(config: FeatureConfigSpec) { val counter = mutableMapOf().withDefault { 0 } var hasDuplicates = false - group.values.forEach { valueSpec -> + config.values.forEach { valueSpec -> val value = counter.getValue(valueSpec.key) if (value > 0) { hasDuplicates = true @@ -80,9 +78,9 @@ public class KonfeatureBuilder { .filter { it.value > 1 } .map { it.key } .toList() - throw KeyDuplicationException(values, group.name) + throw KeyDuplicationException(values, config.name) } else if (counter.isEmpty()) { - logger?.logWarn("Group '${group.name}' is empty") + logger?.logWarn("Config '${config.name}' is empty") } } } diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt index 47a6e6b..c46bcca 100644 --- a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt @@ -3,8 +3,8 @@ 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.FeatureConfigSpec +import com.redmadrobot.konfeature.FeatureValueSpec import com.redmadrobot.konfeature.source.FeatureSource import com.redmadrobot.konfeature.source.FeatureValueSource import com.redmadrobot.konfeature.source.Interceptor @@ -14,7 +14,7 @@ internal class KonfeatureImpl( private val sources: List, private val interceptors: List, private val logger: Logger?, - override val spec: List, + override val spec: List, ) : Konfeature { private val sourcesNames = sources.map { it.name }.toSet() diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt index 10fdf6e..90becf0 100644 --- a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt @@ -2,19 +2,19 @@ package com.redmadrobot.konfeature.exception public sealed class KonfeatureException(messageProvider: () -> String) : Exception(messageProvider.invoke()) -public class GroupNameAlreadyExistException( +public class ConfigNameAlreadyExistException( name: String -) : KonfeatureException({ "feature group with name '$name' already registered" }) +) : KonfeatureException({ "feature config with name '$name' already registered" }) public class KeyDuplicationException( values: List, - group: String + config: String ) : KonfeatureException({ val duplicatedValues = values.joinToString(separator = ", ", transform = { "'$it'" }) - "values with keys <$duplicatedValues> are duplicated in group '$group'" + "values with keys <$duplicatedValues> are duplicated in config '$config'" }) -public class NoFeatureGroupException : KonfeatureException({ "No feature group added" }) +public class NoFeatureConfigException : KonfeatureException({ "No feature config added" }) public class SourceNameAlreadyExistException( name: String diff --git a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt index ddc16fb..db9a6e7 100644 --- a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt +++ b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt @@ -1,7 +1,7 @@ package com.redmadrobot.konfeature import com.redmadrobot.konfeature.builder.konfeature -import com.redmadrobot.konfeature.helper.TestFeatureGroup +import com.redmadrobot.konfeature.helper.TestFeatureConfig import com.redmadrobot.konfeature.helper.createTestSource import com.redmadrobot.konfeature.source.FeatureValueSource import com.redmadrobot.konfeature.source.Interceptor @@ -13,39 +13,39 @@ import kotlin.test.Test class KonfeatureTest { @Test - fun `when correct group added - then code should pass`() { - val featureGroup = TestFeatureGroup() + fun `when correct config added - then code should pass`() { + val featureConfig = TestFeatureConfig() konfeature { - register(featureGroup) + register(featureConfig) } } @Test - fun `when correct group added - should be correct spec`() { + fun `when correct config 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( + val featureConfig = TestFeatureConfig( cSourceSelectionStrategy = SourceSelectionStrategy.anyOf(selectedSource), ) // WHEN val toggleEase = konfeature { - register(featureGroup) + register(featureConfig) } // THEN toggleEase.spec.size shouldBe 1 - val group = toggleEase.spec.first() + val config = toggleEase.spec.first() - group.name shouldBe featureGroup.name - group.description shouldBe featureGroup.description - group.values.size shouldBe 3 + config.name shouldBe featureConfig.name + config.description shouldBe featureConfig.description + config.values.size shouldBe 3 - assertSoftly(group) { + assertSoftly(config) { values[0].apply { key shouldBe "a" description shouldBe "feature a desc" @@ -70,70 +70,70 @@ class KonfeatureTest { } @Test - fun `when source have value - group should return it`() { + fun `when source have value - config should return it`() { // GIVEN val source = createTestSource( name = "Test source", values = mapOf("a" to false), ) - val featureGroup = TestFeatureGroup() + val featureConfig = TestFeatureConfig() konfeature { addSource(source) - register(featureGroup) + register(featureConfig) } // WHEN - val a = featureGroup.a + val a = featureConfig.a // THEN a shouldBe false } @Test - fun `when source don't have value - group should return default value`() { + fun `when source don't have value - config should return default value`() { // GIVEN val source = createTestSource( name = "Test source", values = mapOf("b" to false), ) - val featureGroup = TestFeatureGroup() + val featureConfig = TestFeatureConfig() konfeature { addSource(source) - register(featureGroup) + register(featureConfig) } // WHEN - val a = featureGroup.a + val a = featureConfig.a // THEN a shouldBe true } @Test - fun `when source have value with unexpected type - group should return default value`() { + fun `when source have value with unexpected type - config should return default value`() { // GIVEN val source = createTestSource( name = "Test source", values = mapOf("a" to 5), ) - val featureGroup = TestFeatureGroup() + val featureConfig = TestFeatureConfig() konfeature { addSource(source) - register(featureGroup) + register(featureConfig) } // WHEN - val a = featureGroup.a + val a = featureConfig.a // THEN a shouldBe true } @Test - fun `when both sources contain same key - group should return value of first added source`() { + fun `when both sources contain same key - config should return value of first added source`() { // GIVEN val source1 = createTestSource( name = "Test source 1", @@ -144,23 +144,23 @@ class KonfeatureTest { values = mapOf("a" to true), ) - val featureGroup = TestFeatureGroup() + val featureConfig = TestFeatureConfig() // WHEN konfeature { addSource(source1) addSource(source2) - register(featureGroup) + register(featureConfig) } - val a = featureGroup.a + val a = featureConfig.a // THEN a shouldBe false } @Test - fun `when source specified by SourceSelectionStrategy - group should return value from it`() { + fun `when source specified by SourceSelectionStrategy - config should return value from it`() { // GIVEN val source1 = createTestSource( name = "Test source 1", @@ -171,7 +171,7 @@ class KonfeatureTest { values = mapOf("c" to "test_source_2_c"), ) - val featureGroup = TestFeatureGroup( + val featureConfig = TestFeatureConfig( cSourceSelectionStrategy = SourceSelectionStrategy.anyOf(source2.name), ) @@ -179,17 +179,17 @@ class KonfeatureTest { konfeature { addSource(source1) addSource(source2) - register(featureGroup) + register(featureConfig) } - val c = featureGroup.c + val c = featureConfig.c // THEN c shouldBe "test_source_2_c" } @Test - fun `when value changed by interceptor - group should return it`() { + fun `when value changed by interceptor - config should return it`() { // GIVEN val source = createTestSource( name = "Test source", @@ -210,17 +210,17 @@ class KonfeatureTest { } } - val featureGroup = TestFeatureGroup() + val featureConfig = TestFeatureConfig() // WHEN konfeature { addSource(source) addInterceptor(interceptor) - register(featureGroup) + register(featureConfig) } // THEN - assertSoftly(featureGroup) { + assertSoftly(featureConfig) { a shouldBe false b shouldBe true c shouldBe interceptedValue @@ -228,7 +228,7 @@ class KonfeatureTest { } @Test - fun `when value changed by interceptor but has unexpected type - group should return default`() { + fun `when value changed by interceptor but has unexpected type - config should return default`() { // GIVEN val interceptor = object : Interceptor { override val name: String = "test interceptor" @@ -238,15 +238,15 @@ class KonfeatureTest { } } - val featureGroup = TestFeatureGroup() + val featureConfig = TestFeatureConfig() // WHEN konfeature { addInterceptor(interceptor) - register(featureGroup) + register(featureConfig) } // THEN - featureGroup.c shouldBe "feature c" + featureConfig.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 index 4bdf1e0..b6fce2a 100644 --- a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilderTest.kt +++ b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilderTest.kt @@ -1,11 +1,11 @@ package com.redmadrobot.konfeature.builder -import com.redmadrobot.konfeature.exception.GroupNameAlreadyExistException +import com.redmadrobot.konfeature.exception.ConfigNameAlreadyExistException import com.redmadrobot.konfeature.exception.KeyDuplicationException -import com.redmadrobot.konfeature.exception.NoFeatureGroupException +import com.redmadrobot.konfeature.exception.NoFeatureConfigException import com.redmadrobot.konfeature.exception.SourceNameAlreadyExistException -import com.redmadrobot.konfeature.helper.TestFeatureGroup -import com.redmadrobot.konfeature.helper.createEmptyFeatureGroup +import com.redmadrobot.konfeature.helper.TestFeatureConfig +import com.redmadrobot.konfeature.helper.createEmptyFeatureConfig import com.redmadrobot.konfeature.helper.createTestSource import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -14,26 +14,26 @@ import kotlin.test.Test class KonfeatureBuilderTest { @Test - fun `when no any feature group registered - should throw exception`() { - shouldThrow { + fun `when no any feature config registered - should throw exception`() { + shouldThrow { KonfeatureBuilder().build() } } @Test - fun `when group with duplicated keys added - should throw exception`() { - val featureGroup = TestFeatureGroup(withDuplicates = true) + fun `when config with duplicated keys added - should throw exception`() { + val featureConfig = TestFeatureConfig(withDuplicates = true) val exception = shouldThrow { - KonfeatureBuilder().register(featureGroup).build() + KonfeatureBuilder().register(featureConfig).build() } - exception.message shouldBe "values with keys <'a'> are duplicated in group '${featureGroup.name}'" + exception.message shouldBe "values with keys <'a'> are duplicated in config '${featureConfig.name}'" } @Test fun `when source with same name added twice - should throw exception`() { - val featureGroupName = "Test Feature Group" + val featureConfigName = "Test Feature Config" val sourceName = "Test Source" @@ -41,7 +41,7 @@ class KonfeatureBuilderTest { konfeature { addSource(createTestSource(sourceName)) addSource(createTestSource(sourceName)) - register(createEmptyFeatureGroup(featureGroupName)) + register(createEmptyFeatureConfig(featureConfigName)) } } @@ -49,16 +49,16 @@ class KonfeatureBuilderTest { } @Test - fun `when feature group with same name registered twice - should throw exception`() { - val featureGroupName = "Test Feature Group" + fun `when feature config with same name registered twice - should throw exception`() { + val featureConfigName = "Test Feature Config" - val exception = shouldThrow { + val exception = shouldThrow { konfeature { - register(createEmptyFeatureGroup(featureGroupName)) - register(createEmptyFeatureGroup(featureGroupName)) + register(createEmptyFeatureConfig(featureConfigName)) + register(createEmptyFeatureConfig(featureConfigName)) } } - exception.message shouldBe "feature group with name '$featureGroupName' already registered" + exception.message shouldBe "feature config with name '$featureConfigName' 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 index 750471a..1b446e4 100644 --- a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/KonfeatureTestHelper.kt +++ b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/KonfeatureTestHelper.kt @@ -1,6 +1,6 @@ package com.redmadrobot.konfeature.helper -import com.redmadrobot.konfeature.delegate.FeatureGroup +import com.redmadrobot.konfeature.FeatureConfig import com.redmadrobot.konfeature.source.FeatureSource fun createTestSource( @@ -15,9 +15,9 @@ fun createTestSource( } } -fun createEmptyFeatureGroup( +fun createEmptyFeatureConfig( name: String, description: String = "test description for $name", -): FeatureGroup { - return object : FeatureGroup(name = name, description = description) {} +): FeatureConfig { + return object : FeatureConfig(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/TestFeatureConfig.kt similarity index 82% rename from konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureGroup.kt rename to konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureConfig.kt index 3bc6766..9fc51d8 100644 --- a/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureGroup.kt +++ b/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureConfig.kt @@ -1,14 +1,14 @@ package com.redmadrobot.konfeature.helper -import com.redmadrobot.konfeature.delegate.FeatureGroup +import com.redmadrobot.konfeature.FeatureConfig import com.redmadrobot.konfeature.source.SourceSelectionStrategy -class TestFeatureGroup( +class TestFeatureConfig( withDuplicates: Boolean = false, cSourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.Any, -) : FeatureGroup( - name = "TestFeatureGroup", - description = "TestFeatureGroup description", +) : FeatureConfig( + name = "TestFeatureConfig", + description = "TestFeatureConfig description", ) { val a by toggle( key = "a", diff --git a/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt index a29b156..5c1a944 100644 --- a/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt +++ b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt @@ -4,7 +4,7 @@ import com.redmadrobot.konfeature.Logger import com.redmadrobot.konfeature.builder.konfeature fun main() { - val featureGroup = SampleFeatureGroup() + val featureConfig = SampleFeatureConfig() val debugPanelInterceptor = FeatureToggleDebugPanelInterceptor() @@ -17,7 +17,7 @@ fun main() { val konfeature = konfeature { addSource(RemoteFeatureSource()) addSource(FirebaseFeatureSource()) - register(featureGroup) + register(featureConfig) addInterceptor(debugPanelInterceptor) setLogger(logger) } @@ -29,19 +29,19 @@ fun main() { println() val spec = konfeature.spec.first().values.first() - println("getFeatureToggleValue('${spec.key}') -> ${konfeature.getValue(spec)}") + println("getFeatureValue('${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) + println("feature1: " + featureConfig.isFeature1Enabled) + println("feature2: " + featureConfig.isFeature2Enabled) + println("feature3: " + featureConfig.isFeature3Enabled) + println("velocity: " + featureConfig.velocity) + println("puhFeature: " + featureConfig.puhFeature) debugPanelInterceptor.setFeatureValue("feature2", false) println() println("debugPanelInterceptor.setFeatureValue(\"feature2\", false)") - println("feature1: " + featureGroup.isFeature1Enabled) - println("feature2: " + featureGroup.isFeature2Enabled) - println("feature3: " + featureGroup.isFeature3Enabled) + println("feature1: " + featureConfig.isFeature1Enabled) + println("feature2: " + featureConfig.isFeature2Enabled) + println("feature3: " + featureConfig.isFeature3Enabled) } diff --git a/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureGroup.kt b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureConfig.kt similarity index 90% rename from sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureGroup.kt rename to sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureConfig.kt index 621ddf8..9555209 100644 --- a/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureGroup.kt +++ b/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureConfig.kt @@ -1,9 +1,9 @@ package com.redmadrobot.konfeature.sample -import com.redmadrobot.konfeature.delegate.FeatureGroup +import com.redmadrobot.konfeature.FeatureConfig import com.redmadrobot.konfeature.source.SourceSelectionStrategy -class SampleFeatureGroup : FeatureGroup( +class SampleFeatureConfig : FeatureConfig( name = "Sample", description = "simple sample set" ) { From de9c883f36b4bfdd75554548d1013428b3beb290 Mon Sep 17 00:00:00 2001 From: "a.tabolin" Date: Wed, 24 Jul 2024 12:08:55 +0300 Subject: [PATCH 3/3] [README] update --- README.md | 35 ++++++++++--------- .../konfeature/builder/KonfeatureBuilder.kt | 6 +--- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 54a5d9f..fed2998 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Working with remote configuration has become a standard part of the development - 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 +- support for multiple data sources for remote config - 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 @@ -18,7 +18,7 @@ We have made every effort to meet all these requirements in the development of K - [Installation](#installation) - [Usage](#usage) - - [FeatureGroup](#featuregroup) + - [FeatureConfig](#featureconfig) - [FeatureSource](#featuresource) - [SourceSelectionStrategy](#sourceselectionstrategy) - [Interceptor](#interceptor) @@ -45,7 +45,7 @@ dependencies { ## Usage -### FeatureGroup +### FeatureConfig Defines a set of configuration elements, where each element is defined using a delegate. There are two types of delegates: @@ -53,7 +53,7 @@ There are two types of delegates: - `by value(...)` - used for elements of any other type ```kotlin -class ProfileFeatureGroup : FeatureGroup( +class ProfileFeatureConfig : FeatureConfig( name = "profile_feature_config", description = "Config of features for profile usage" ) { @@ -92,10 +92,10 @@ Each configuration element requires specifying: After that, you need to register the configuration in `Konfeature`: ```kotlin -val profileFeatureGroup: FeatureGroup = ProfileFeatureGroup() +val profileFeatureConfig: FeatureConfig = ProfileFeatureConfig() val konfeatureInstance = konfeature { - register(profileFeatureGroup) + register(profileFeatureConfig) } ``` @@ -151,12 +151,12 @@ class FirebaseFeatureSource( After that, you need to add the `Source` in `Konfeature`: ```kotlin -val profileFeatureGroup: FeatureGroup = ProfileFeatureGroup() +val profileFeatureConfig: FeatureConfig = ProfileFeatureConfig() val source: FeatureSource = FirebaseFeatureSource(remoteConfig) val konfeatureInstance = konfeature { addSource(source) - register(profileFeatureGroup) + register(profileFeatureConfig) } ``` @@ -198,7 +198,8 @@ For most scenarios, predefined implementations will be sufficient: - `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!!!** +> [!IMPORTANT] +> By default, `SourceSelectionStrategy.None` is used! ### Interceptor @@ -242,13 +243,13 @@ class DebugPanelInterceptor : Interceptor { After that, you need to add the `Interceptor` in `Konfeature`: ```kotlin -val profileFeatureGroup: FeatureGroup = ProfileFeatureGroup() +val profileFeatureConfig: FeatureConfig = ProfileFeatureConfig() val source: FeatureSource = FirebaseFeatureSource(remoteConfig) val debugPanelInterceptor: Interceptor = DebugPanelInterceptor() val konfeatureInstance = konfeature { addSource(source) - register(profileFeatureGroup) + register(profileFeatureConfig) addInterceptor(debugPanelInterceptor) } ``` @@ -297,14 +298,14 @@ class TimberLogger: Logger { After that, you need to add the `Logger` in `Konfeature`: ```kotlin -val profileFeatureGroup: FeatureGroup = ProfileFeatureGroup() +val profileFeatureConfig: FeatureConfig = ProfileFeatureConfig() val source: FeatureSource = FirebaseFeatureSource(remoteConfig) val debugPanelInterceptor: Interceptor = DebugPanelInterceptor() val logger: Logger = TimberLogger() val konfeatureInstance = konfeature { addSource(source) - register(profileFeatureGroup) + register(profileFeatureConfig) addInterceptor(debugPanelInterceptor) setLogger(logger) } @@ -312,12 +313,12 @@ val konfeatureInstance = konfeature { ### Spec -Konfeature contains information about all registered `FeatureGroups` in the form of `spec`: +Konfeature contains information about all registered `FeatureConfig` in the form of `spec`: ```kotlin public interface Konfeature { - public val spec: List + public val spec: List public fun getValue(spec: FeatureValueSpec): FeatureValue } @@ -328,8 +329,8 @@ This allows you to obtain information about added configurations as well as the ```kotlin val konfeatureInstance = konfeature {...} -val featureGroupSpec = konfeatureInstance.spec[0] -val featureSpec = groupSpec.values[0] +val featureConfigSpec = konfeatureInstance.spec[0] +val featureSpec = featureConfigSpec.values[0] val featureValue = konfeatureInstance.getValue(featureSpec) ``` > This can be useful for use in the DebugPanel diff --git a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt index 10c4dd1..f2d6804 100644 --- a/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt +++ b/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt @@ -1,14 +1,10 @@ package com.redmadrobot.konfeature.builder -import com.redmadrobot.konfeature.Konfeature -import com.redmadrobot.konfeature.Logger -import com.redmadrobot.konfeature.FeatureConfig -import com.redmadrobot.konfeature.FeatureConfigSpec +import com.redmadrobot.konfeature.* import com.redmadrobot.konfeature.exception.ConfigNameAlreadyExistException import com.redmadrobot.konfeature.exception.KeyDuplicationException import com.redmadrobot.konfeature.exception.NoFeatureConfigException import com.redmadrobot.konfeature.exception.SourceNameAlreadyExistException -import com.redmadrobot.konfeature.logWarn import com.redmadrobot.konfeature.source.FeatureSource import com.redmadrobot.konfeature.source.Interceptor