From 873903e005b30256c4e6f59e13dc5b2e95416718 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Fri, 14 Apr 2023 11:04:46 +0200 Subject: [PATCH 01/56] Initial commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..317ac2b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# openfeature-kotlin-sdk \ No newline at end of file From 2812ece255caad3934fc4d626858d6bc1bfa626b Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 14 Apr 2023 13:24:48 +0200 Subject: [PATCH 02/56] Transfer codebase --- .gitignore | 46 ++++ OpenFeature/.gitignore | 1 + OpenFeature/build.gradle.kts | 53 ++++ OpenFeature/proguard-rules.pro | 21 ++ OpenFeature/src/main/AndroidManifest.xml | 3 + .../dev/openfeature/sdk/BaseEvaluation.kt | 11 + .../main/java/dev/openfeature/sdk/Client.kt | 8 + .../dev/openfeature/sdk/EvaluationContext.kt | 10 + .../dev/openfeature/sdk/FeatureProvider.kt | 16 ++ .../main/java/dev/openfeature/sdk/Features.kt | 24 ++ .../openfeature/sdk/FlagEvaluationDetails.kt | 25 ++ .../openfeature/sdk/FlagEvaluationOptions.kt | 6 + .../java/dev/openfeature/sdk/FlagValueType.kt | 9 + .../src/main/java/dev/openfeature/sdk/Hook.kt | 9 + .../java/dev/openfeature/sdk/HookContext.kt | 10 + .../java/dev/openfeature/sdk/HookSupport.kt | 206 ++++++++++++++++ .../main/java/dev/openfeature/sdk/Metadata.kt | 5 + .../dev/openfeature/sdk/MutableContext.kt | 52 ++++ .../dev/openfeature/sdk/MutableStructure.kt | 52 ++++ .../java/dev/openfeature/sdk/NoOpProvider.kt | 53 ++++ .../dev/openfeature/sdk/OpenFeatureAPI.kt | 53 ++++ .../dev/openfeature/sdk/OpenFeatureClient.kt | 233 ++++++++++++++++++ .../dev/openfeature/sdk/ProviderEvaluation.kt | 11 + .../main/java/dev/openfeature/sdk/Reason.kt | 22 ++ .../java/dev/openfeature/sdk/Structure.kt | 12 + .../main/java/dev/openfeature/sdk/Value.kt | 73 ++++++ .../openfeature/sdk/exceptions/ErrorCode.kt | 18 ++ .../sdk/exceptions/OpenFeatureError.kt | 43 ++++ .../sdk/DeveloperExperienceTests.kt | 61 +++++ .../dev/openfeature/sdk/EvalContextTests.kt | 156 ++++++++++++ .../openfeature/sdk/FlagEvaluationsTests.kt | 132 ++++++++++ .../java/dev/openfeature/sdk/HookSpecTests.kt | 70 ++++++ .../dev/openfeature/sdk/HookSupportTests.kt | 56 +++++ .../openfeature/sdk/OpenFeatureClientTests.kt | 18 ++ .../dev/openfeature/sdk/ProviderSpecTests.kt | 63 +++++ .../dev/openfeature/sdk/StructureTests.kt | 54 ++++ .../java/dev/openfeature/sdk/ValueTests.kt | 139 +++++++++++ .../sdk/helpers/AlwaysBrokenProvider.kt | 54 ++++ .../sdk/helpers/DoSomethingProvider.kt | 52 ++++ .../sdk/helpers/GenericSpyHookMock.kt | 44 ++++ README.md | 4 +- build.gradle.kts | 6 + gradle.properties | 25 ++ settings.gradle.kts | 18 ++ 44 files changed, 2036 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 OpenFeature/.gitignore create mode 100644 OpenFeature/build.gradle.kts create mode 100644 OpenFeature/proguard-rules.pro create mode 100644 OpenFeature/src/main/AndroidManifest.xml create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/BaseEvaluation.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/OpenFeatureClientTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..739e4e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +# Gradle files +.gradle/ +build/ +gradlew +gradlew.bat + +# Local configuration file (sdk path, etc) +local.properties + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Android Profiling +*.hprof + +# Other +*.DS_Store \ No newline at end of file diff --git a/OpenFeature/.gitignore b/OpenFeature/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/OpenFeature/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts new file mode 100644 index 0000000..64b422c --- /dev/null +++ b/OpenFeature/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("maven-publish") + kotlin("plugin.serialization") version "1.8.10" +} + +android { + namespace = "dev.openfeature.sdk" + compileSdk = 33 + + defaultConfig { + minSdk = 26 + version = "0.0.1" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } +} + +dependencies { + implementation("com.google.android.material:material:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") +} + +publishing { + publications { + register("release") { + groupId = "dev.openfeature" + artifactId = "kotlin-sdk" + version = "0.0.1-SNAPSHOT" + + afterEvaluate { + from(components["release"]) + } + } + } +} diff --git a/OpenFeature/proguard-rules.pro b/OpenFeature/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/OpenFeature/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/OpenFeature/src/main/AndroidManifest.xml b/OpenFeature/src/main/AndroidManifest.xml new file mode 100644 index 0000000..69fc412 --- /dev/null +++ b/OpenFeature/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/BaseEvaluation.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/BaseEvaluation.kt new file mode 100644 index 0000000..4aa2574 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/BaseEvaluation.kt @@ -0,0 +1,11 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.exceptions.ErrorCode + +interface BaseEvaluation { + val value: T + val variant: String? + val reason: String? + val errorCode: ErrorCode? + val errorMessage: String? +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt new file mode 100644 index 0000000..befb176 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt @@ -0,0 +1,8 @@ +package dev.openfeature.sdk + +interface Client: Features { + val metadata: Metadata + val hooks: List> + + fun addHooks(hooks: List>) +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt new file mode 100644 index 0000000..fcd0da8 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt @@ -0,0 +1,10 @@ +package dev.openfeature.sdk + +interface EvaluationContext: Structure { + fun getTargetingKey(): String + fun setTargetingKey(targetingKey: String) + + // Make sure these are implemented for correct object comparisons + override fun hashCode(): Int + override fun equals(other: Any?): Boolean +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt new file mode 100644 index 0000000..a7efcee --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt @@ -0,0 +1,16 @@ +package dev.openfeature.sdk + +interface FeatureProvider { + val hooks: List> + val metadata: Metadata + + // Called by OpenFeatureAPI whenever the new Provider is registered + suspend fun initialize(initialContext: EvaluationContext?) + // Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application + suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) + fun getBooleanEvaluation(key: String, defaultValue: Boolean): ProviderEvaluation + fun getStringEvaluation(key: String, defaultValue: String): ProviderEvaluation + fun getIntegerEvaluation(key: String, defaultValue: Int): ProviderEvaluation + fun getDoubleEvaluation(key: String, defaultValue: Double): ProviderEvaluation + fun getObjectEvaluation(key: String, defaultValue: Value): ProviderEvaluation +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt new file mode 100644 index 0000000..62b3784 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt @@ -0,0 +1,24 @@ +package dev.openfeature.sdk + +interface Features { + fun getBooleanValue(key: String, defaultValue: Boolean): Boolean + fun getBooleanValue(key: String, defaultValue: Boolean, options: FlagEvaluationOptions): Boolean + fun getBooleanDetails(key: String, defaultValue: Boolean): FlagEvaluationDetails + fun getBooleanDetails(key: String, defaultValue: Boolean, options: FlagEvaluationOptions): FlagEvaluationDetails + fun getStringValue(key: String, defaultValue: String): String + fun getStringValue(key: String, defaultValue: String, options: FlagEvaluationOptions): String + fun getStringDetails(key: String, defaultValue: String): FlagEvaluationDetails + fun getStringDetails(key: String, defaultValue: String, options: FlagEvaluationOptions): FlagEvaluationDetails + fun getIntegerValue(key: String, defaultValue: Int): Int + fun getIntegerValue(key: String, defaultValue: Int, options: FlagEvaluationOptions): Int + fun getIntegerDetails(key: String, defaultValue: Int): FlagEvaluationDetails + fun getIntegerDetails(key: String, defaultValue: Int, options: FlagEvaluationOptions): FlagEvaluationDetails + fun getDoubleValue(key: String, defaultValue: Double): Double + fun getDoubleValue(key: String, defaultValue: Double, options: FlagEvaluationOptions): Double + fun getDoubleDetails(key: String, defaultValue: Double): FlagEvaluationDetails + fun getDoubleDetails(key: String, defaultValue: Double, options: FlagEvaluationOptions): FlagEvaluationDetails + fun getObjectValue(key: String, defaultValue: Value): Value + fun getObjectValue(key: String, defaultValue: Value, options: FlagEvaluationOptions): Value + fun getObjectDetails(key: String, defaultValue: Value): FlagEvaluationDetails + fun getObjectDetails(key: String, defaultValue: Value, options: FlagEvaluationOptions): FlagEvaluationDetails +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt new file mode 100644 index 0000000..623bb12 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt @@ -0,0 +1,25 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.exceptions.ErrorCode + +data class FlagEvaluationDetails( + val flagKey: String, + override var value: T, + override var variant: String? = null, + override var reason: String? = null, + override var errorCode: ErrorCode? = null, + override var errorMessage: String? = null +) : BaseEvaluation { + companion object +} + +fun FlagEvaluationDetails.Companion.from(providerEval: ProviderEvaluation, flagKey: String): FlagEvaluationDetails { + return FlagEvaluationDetails( + flagKey, + providerEval.value, + providerEval.variant, + providerEval.reason, + providerEval.errorCode, + providerEval.errorMessage + ) +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.kt new file mode 100644 index 0000000..94659df --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.kt @@ -0,0 +1,6 @@ +package dev.openfeature.sdk + +data class FlagEvaluationOptions( + var hooks: List> = listOf(), + var hookHints: Map = mapOf() +) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt new file mode 100644 index 0000000..b463140 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt @@ -0,0 +1,9 @@ +package dev.openfeature.sdk +enum class FlagValueType { + BOOLEAN, + STRING, + INTEGER, + DOUBLE, + OBJECT; +} + diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt new file mode 100644 index 0000000..4b705e5 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt @@ -0,0 +1,9 @@ +package dev.openfeature.sdk + +interface Hook { + fun before(ctx: HookContext, hints: Map) = Unit + fun after(ctx: HookContext, details: FlagEvaluationDetails, hints: Map) = Unit + fun error(ctx: HookContext, error: Exception, hints: Map) = Unit + fun finallyAfter(ctx: HookContext, hints: Map) = Unit + fun supportsFlagValueType(flagValueType: FlagValueType): Boolean = true +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt new file mode 100644 index 0000000..7836784 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt @@ -0,0 +1,10 @@ +package dev.openfeature.sdk + +data class HookContext( + var flagKey: String, + val type: FlagValueType, + var defaultValue: T, + var ctx: EvaluationContext, + var clientMetadata: Metadata?, + var providerMetadata: Metadata +) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt new file mode 100644 index 0000000..a10d015 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt @@ -0,0 +1,206 @@ +package dev.openfeature.sdk + +@Suppress("UNCHECKED_CAST") // TODO can we do better here? +class HookSupport { + fun beforeHooks(flagValueType: FlagValueType, hookCtx: HookContext, hooks: List>, hints: Map) { + hooks + .reversed() + .filter { hook -> hook.supportsFlagValueType(flagValueType) } + .forEach { hook -> + when (flagValueType) { + FlagValueType.BOOLEAN -> { + safeLet(hook as? Hook, hookCtx as? HookContext) { booleanHook, booleanCtx -> + booleanHook.before(booleanCtx, hints) + } + } + FlagValueType.STRING -> { + safeLet(hook as? Hook, hookCtx as? HookContext) { stringHook, stringCtx -> + stringHook.before(stringCtx, hints) + } + } + FlagValueType.INTEGER -> { + safeLet(hook as? Hook, hookCtx as? HookContext) { integerHook, integerCtx -> + integerHook.before(integerCtx, hints) + } + } + FlagValueType.DOUBLE -> { + safeLet(hook as? Hook, hookCtx as? HookContext) { doubleHook, doubleCtx -> + doubleHook.before(doubleCtx, hints) + } + } + FlagValueType.OBJECT -> { + safeLet(hook as? Hook, hookCtx as? HookContext) { objectHook, objectCtx -> + objectHook.before(objectCtx, hints) + } + } + } + } + } + + fun afterHooks(flagValueType: FlagValueType, hookCtx: HookContext, details: FlagEvaluationDetails, hooks: List>, hints: Map) { + hooks + .filter { hook -> hook.supportsFlagValueType(flagValueType) } + .forEach { hook -> + run { + when (flagValueType) { + FlagValueType.BOOLEAN -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext, + details as? FlagEvaluationDetails + ) { booleanHook, booleanCtx, booleanDetails -> + booleanHook.after(booleanCtx, booleanDetails, hints) + } + } + FlagValueType.STRING -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext, + details as? FlagEvaluationDetails + ) { stringHook, stringCtx, stringDetails -> + stringHook.after(stringCtx, stringDetails, hints) + } + } + FlagValueType.INTEGER -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext, + details as? FlagEvaluationDetails + ) { integerHook, integerCtx, integerDetails -> + integerHook.after(integerCtx, integerDetails, hints) + } + } + FlagValueType.DOUBLE -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext, + details as? FlagEvaluationDetails + ) { doubleHook, doubleCtx, doubleDetails -> + doubleHook.after(doubleCtx, doubleDetails, hints) + } + } + FlagValueType.OBJECT -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext, + details as? FlagEvaluationDetails + ) { objectHook, objectCtx, objectDetails -> + objectHook.after(objectCtx, objectDetails, hints) + } + } + } + } + } + } + + fun afterAllHooks(flagValueType: FlagValueType, hookCtx: HookContext, hooks: List>, hints: Map) { + hooks + .filter { hook -> hook.supportsFlagValueType(flagValueType) } + .forEach { hook -> + run { + when (flagValueType) { + FlagValueType.BOOLEAN -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { booleanHook, booleanCtx -> + booleanHook.finallyAfter(booleanCtx, hints) + } + } + FlagValueType.STRING -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { stringHook, stringCtx -> + stringHook.finallyAfter(stringCtx, hints) + } + } + FlagValueType.INTEGER -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { integerHook, integerCtx -> + integerHook.finallyAfter(integerCtx, hints) + } + } + FlagValueType.DOUBLE -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { doubleHook, doubleCtx -> + doubleHook.finallyAfter(doubleCtx, hints) + } + } + FlagValueType.OBJECT -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { objectHook, objectCtx -> + objectHook.finallyAfter(objectCtx, hints) + } + } + } + } + } + } + + fun errorHooks(flagValueType: FlagValueType, hookCtx: HookContext, error: Exception, hooks: List>, hints: Map) { + hooks + .filter { hook -> hook.supportsFlagValueType(flagValueType) } + .forEach { hook -> + run { + when (flagValueType) { + FlagValueType.BOOLEAN -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { booleanHook, booleanCtx -> + booleanHook.error(booleanCtx, error, hints) + } + } + FlagValueType.STRING -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { stringHook, stringCtx -> + stringHook.error(stringCtx, error, hints) + } + } + FlagValueType.INTEGER -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { integerHook, integerCtx -> + integerHook.error(integerCtx, error, hints) + } + } + FlagValueType.DOUBLE -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { doubleHook, doubleCtx -> + doubleHook.error(doubleCtx, error, hints) + } + } + FlagValueType.OBJECT -> { + safeLet( + hook as? Hook, + hookCtx as? HookContext + ) { objectHook, objectCtx -> + objectHook.error(objectCtx, error, hints) + } + } + } + } + } + } + + + private inline fun safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? { + return if (p1 != null && p2 != null) block(p1, p2) else null + } + + private inline fun safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3)->R?): R? { + return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null + } +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt new file mode 100644 index 0000000..8fcc170 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt @@ -0,0 +1,5 @@ +package dev.openfeature.sdk + +interface Metadata { + val name: String? +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt new file mode 100644 index 0000000..c7e54f7 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt @@ -0,0 +1,52 @@ +package dev.openfeature.sdk + +class MutableContext + (private var targetingKey: String = "", attributes: MutableMap = mutableMapOf()) : EvaluationContext { + private var structure: MutableStructure = MutableStructure(attributes) + override fun getTargetingKey(): String { + return targetingKey + } + + override fun setTargetingKey(targetingKey: String) { + this.targetingKey = targetingKey + } + + override fun keySet(): Set { + return structure.keySet() + } + + override fun getValue(key: String): Value? { + return structure.getValue(key) + } + + override fun asMap(): MutableMap { + return structure.asMap() + } + + override fun asObjectMap(): Map { + return structure.asObjectMap() + } + + fun add(key: String, value: Value): MutableContext { + structure.add(key, value) + return this + } + + override fun hashCode(): Int { + var result = targetingKey.hashCode() + result = 31 * result + structure.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MutableContext + + if (targetingKey != other.targetingKey) return false + if (structure != other.structure) return false + + return true + } +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt new file mode 100644 index 0000000..7fff8db --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt @@ -0,0 +1,52 @@ +package dev.openfeature.sdk + +class MutableStructure(private var attributes: MutableMap = mutableMapOf()) : Structure { + override fun keySet(): Set { + return attributes.keys + } + + override fun getValue(key: String): Value? { + return attributes[key] + } + + override fun asMap(): MutableMap { + return attributes + } + + override fun asObjectMap(): Map { + return attributes.mapValues { convertValue(it.value) } + } + + fun add(key: String, value: Value): MutableStructure { + attributes[key] = value + return this + } + + private fun convertValue(value: Value): Any? { + return when(value) { + is Value.List -> value.list.map { t -> convertValue(t) } + is Value.Structure -> value.structure.mapValues { t -> convertValue(t.value) } + is Value.Null -> return null + is Value.String -> value.asString() + is Value.Boolean -> value.asBoolean() + is Value.Integer -> value.asInteger() + is Value.Instant -> value.asInstant() + is Value.Double -> value.asDouble() + } + } + + override fun hashCode(): Int { + return attributes.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MutableStructure + + if (attributes != other.attributes) return false + + return true + } +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt new file mode 100644 index 0000000..23dbfb3 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt @@ -0,0 +1,53 @@ +package dev.openfeature.sdk + +class NoOpProvider : FeatureProvider { + override var metadata: Metadata = NoOpMetadata("No-op provider") + override suspend fun initialize(initialContext: EvaluationContext?) { + // no-op + } + + override suspend fun onContextSet( + oldContext: EvaluationContext?, + newContext: EvaluationContext + ) { + // no-op + } + + override var hooks: List> = listOf() + override fun getBooleanEvaluation( + key: String, + defaultValue: Boolean + ): ProviderEvaluation { + return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) + } + + override fun getStringEvaluation( + key: String, + defaultValue: String + ): ProviderEvaluation { + return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) + } + + override fun getIntegerEvaluation( + key: String, + defaultValue: Int + ): ProviderEvaluation { + return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) + } + + override fun getDoubleEvaluation( + key: String, + defaultValue: Double + ): ProviderEvaluation { + return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) + } + + override fun getObjectEvaluation( + key: String, + defaultValue: Value + ): ProviderEvaluation { + return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) + } + + data class NoOpMetadata(override var name: String?) : Metadata +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt new file mode 100644 index 0000000..3cbce8e --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -0,0 +1,53 @@ +package dev.openfeature.sdk + +import kotlinx.coroutines.coroutineScope + +object OpenFeatureAPI { + private var provider: FeatureProvider? = null + private var context: EvaluationContext? = null + var hooks: List> = listOf() + private set + + suspend fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) = coroutineScope { + provider.initialize(initialContext ?: context) + this@OpenFeatureAPI.provider = provider + if (initialContext != null) context = initialContext + } + + fun getProvider(): FeatureProvider? { + return provider + } + + fun clearProvider() { + provider = null + } + + suspend fun setEvaluationContext(evaluationContext: EvaluationContext) { + getProvider()?.onContextSet(context, evaluationContext) + // A provider evaluation reading the global ctx at this point would fail due to stale cache. + // To prevent this, the provider should internally manage the ctx to use on each evaluation, and + // make sure it's aligned with the values in the cache at all times. If no guarantees are offered by + // the provider, the application can expect STALE resolves while setting a new global ctx + context = evaluationContext + } + + fun getEvaluationContext(): EvaluationContext? { + return context + } + + fun getProviderMetadata(): Metadata? { + return provider?.metadata + } + + fun getClient(name: String? = null, version: String? = null): Client { + return OpenFeatureClient(this, name, version) + } + + fun addHooks(hooks: List>) { + this.hooks += hooks + } + + fun clearHooks() { + this.hooks = listOf() + } +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt new file mode 100644 index 0000000..9a2e5ef --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt @@ -0,0 +1,233 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.FlagValueType.* +import dev.openfeature.sdk.exceptions.ErrorCode +import dev.openfeature.sdk.exceptions.OpenFeatureError +import dev.openfeature.sdk.exceptions.OpenFeatureError.GeneralError + +private val typeMatchingException = GeneralError("Unable to match default value type with flag value type") + +class OpenFeatureClient( + private val openFeatureAPI: OpenFeatureAPI, + name: String? = null, + version: String? = null, + override val hooks: MutableList> = mutableListOf() +) : Client { + override val metadata: Metadata = ClientMetadata(name) + private var hookSupport = HookSupport() + override fun addHooks(hooks: List>) { + this.hooks += hooks + } + + override fun getBooleanValue(key: String, defaultValue: Boolean): Boolean { + return getBooleanDetails(key, defaultValue).value + } + + override fun getBooleanValue( + key: String, + defaultValue: Boolean, + options: FlagEvaluationOptions + ): Boolean { + return getBooleanDetails(key, defaultValue, options).value + } + + override fun getBooleanDetails( + key: String, + defaultValue: Boolean + ): FlagEvaluationDetails { + return getBooleanDetails(key, defaultValue, FlagEvaluationOptions()) + } + + override fun getBooleanDetails( + key: String, + defaultValue: Boolean, + options: FlagEvaluationOptions + ): FlagEvaluationDetails { + return evaluateFlag(BOOLEAN, key, defaultValue, options) + } + + override fun getStringValue(key: String, defaultValue: String): String { + return getStringDetails(key, defaultValue).value + } + + override fun getStringValue( + key: String, + defaultValue: String, + options: FlagEvaluationOptions + ): String { + return getStringDetails(key, defaultValue, options).value + } + + override fun getStringDetails( + key: String, + defaultValue: String, + ): FlagEvaluationDetails { + return getStringDetails(key, defaultValue, FlagEvaluationOptions()) + } + + override fun getStringDetails( + key: String, + defaultValue: String, + options: FlagEvaluationOptions + ): FlagEvaluationDetails { + return evaluateFlag(STRING, key, defaultValue, options) + } + + override fun getIntegerValue(key: String, defaultValue: Int): Int { + return getIntegerDetails(key, defaultValue).value + } + + override fun getIntegerValue( + key: String, + defaultValue: Int, + options: FlagEvaluationOptions + ): Int { + return getIntegerDetails(key, defaultValue, options).value + } + + override fun getIntegerDetails( + key: String, + defaultValue: Int + ): FlagEvaluationDetails { + return getIntegerDetails(key, defaultValue, FlagEvaluationOptions()) + } + + override fun getIntegerDetails( + key: String, + defaultValue: Int, + options: FlagEvaluationOptions + ): FlagEvaluationDetails { + return evaluateFlag(INTEGER, key, defaultValue, options) + } + + override fun getDoubleValue(key: String, defaultValue: Double): Double { + return getDoubleDetails(key, defaultValue).value + } + + override fun getDoubleValue( + key: String, + defaultValue: Double, + options: FlagEvaluationOptions + ): Double { + return getDoubleDetails(key, defaultValue, options).value + } + + override fun getDoubleDetails( + key: String, + defaultValue: Double + ): FlagEvaluationDetails { + return evaluateFlag(DOUBLE, key, defaultValue, FlagEvaluationOptions()) + } + + override fun getDoubleDetails( + key: String, + defaultValue: Double, + options: FlagEvaluationOptions + ): FlagEvaluationDetails { + return evaluateFlag(DOUBLE, key, defaultValue, options) + } + + override fun getObjectValue(key: String, defaultValue: Value): Value { + return getObjectDetails(key, defaultValue).value + } + + override fun getObjectValue( + key: String, + defaultValue: Value, + options: FlagEvaluationOptions + ): Value { + return getObjectDetails(key, defaultValue, options).value + } + + override fun getObjectDetails( + key: String, + defaultValue: Value, + ): FlagEvaluationDetails { + return getObjectDetails(key, defaultValue, FlagEvaluationOptions()) + } + + override fun getObjectDetails( + key: String, + defaultValue: Value, + options: FlagEvaluationOptions + ): FlagEvaluationDetails { + return evaluateFlag(OBJECT, key, defaultValue, options) + } + + private fun evaluateFlag( + flagValueType: FlagValueType, + key: String, + defaultValue: T, + optionsIn: FlagEvaluationOptions? + ): FlagEvaluationDetails { + val options = optionsIn ?: FlagEvaluationOptions(listOf(), mapOf()) + val hints = options.hookHints + var details = FlagEvaluationDetails(key, defaultValue) + val provider = openFeatureAPI.getProvider() ?: NoOpProvider() + val mergedHooks: List> = provider.hooks + options.hooks + hooks + openFeatureAPI.hooks + val context = openFeatureAPI.getEvaluationContext() ?: MutableContext() + val hookCtx: HookContext = HookContext(key, flagValueType, defaultValue, context, this.metadata, provider.metadata) + try { + hookSupport.beforeHooks(flagValueType, hookCtx, mergedHooks, hints) + val providerEval = createProviderEvaluation( + flagValueType, + key, + defaultValue, + provider + ) + details = FlagEvaluationDetails.from(providerEval, key) + hookSupport.afterHooks(flagValueType, hookCtx, details, mergedHooks, hints) + } catch (error: Exception) { + if (error is OpenFeatureError) { + details.errorCode = error.errorCode() + } else { + details.errorCode = ErrorCode.GENERAL + } + + details.errorMessage = error.message + details.reason = Reason.ERROR.toString() + + hookSupport.errorHooks(flagValueType, hookCtx, error, mergedHooks, hints) + } + hookSupport.afterAllHooks(flagValueType, hookCtx, mergedHooks, hints) + return details + } + + @Suppress("UNCHECKED_CAST") // TODO can we do better here? + private fun createProviderEvaluation( + flagValueType: FlagValueType, + key: String, + defaultValue: V, + provider: FeatureProvider + ): ProviderEvaluation { + return when(flagValueType) { + BOOLEAN -> { + val defaultBoolean = defaultValue as? Boolean ?: throw typeMatchingException + val eval: ProviderEvaluation = provider.getBooleanEvaluation(key, defaultBoolean) + eval as? ProviderEvaluation ?: throw typeMatchingException + } + STRING -> { + val defaultString = defaultValue as? String ?: throw typeMatchingException + val eval: ProviderEvaluation = provider.getStringEvaluation(key, defaultString) + eval as? ProviderEvaluation ?: throw typeMatchingException + } + INTEGER -> { + val defaultInteger = defaultValue as? Int ?: throw typeMatchingException + val eval: ProviderEvaluation = provider.getIntegerEvaluation(key, defaultInteger) + eval as? ProviderEvaluation ?: throw typeMatchingException + } + DOUBLE -> { + val defaultDouble = defaultValue as? Double ?: throw typeMatchingException + val eval: ProviderEvaluation = provider.getDoubleEvaluation(key, defaultDouble) + eval as? ProviderEvaluation ?: throw typeMatchingException + } + OBJECT -> { + val defaultObject = defaultValue as? Value ?: throw typeMatchingException + val eval: ProviderEvaluation = provider.getObjectEvaluation(key, defaultObject) + eval as? ProviderEvaluation ?: throw typeMatchingException + } + } + } + + data class ClientMetadata(override var name: String?) : Metadata +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt new file mode 100644 index 0000000..082c160 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt @@ -0,0 +1,11 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.exceptions.ErrorCode + +data class ProviderEvaluation( + var value: T, + var variant: String? = null, + var reason: String? = null, + var errorCode: ErrorCode? = null, + var errorMessage: String? = null + ) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt new file mode 100644 index 0000000..dfca1a9 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt @@ -0,0 +1,22 @@ +package dev.openfeature.sdk + +enum class Reason { + // The resolved value is static (no dynamic evaluation). + STATIC, + // The resolved value fell back to a pre-configured value (no dynamic evaluation occurred or dynamic evaluation yielded no result). + DEFAULT, + // The resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting. + TARGETING_MATCH, + // The resolved value was the result of pseudorandom assignment. + SPLIT, + // The resolved value was retrieved from cache. + CACHED, + /// The resolved value was the result of the flag being disabled in the management system. + DISABLED, + /// The reason for the resolved value could not be determined. + UNKNOWN, + /// The resolved value is non-authoritative or possible out of date + STALE, + /// The resolved value was the result of an error. + ERROR +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt new file mode 100644 index 0000000..9681c58 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt @@ -0,0 +1,12 @@ +package dev.openfeature.sdk + +interface Structure { + fun keySet(): Set + fun getValue(key: String): Value? + fun asMap(): MutableMap + fun asObjectMap(): Map + + // Make sure these are implemented for correct object comparisons + override fun hashCode(): Int + override fun equals(other: Any?): Boolean +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt new file mode 100644 index 0000000..2228bc0 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt @@ -0,0 +1,73 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.exceptions.OpenFeatureError +import kotlinx.serialization.EncodeDefault.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonObject +import java.time.Instant +import java.util.* + +@Serializable(with = ValueSerializer::class) +sealed interface Value { + + fun asString(): kotlin.String? = if (this is String) string else null + fun asBoolean(): kotlin.Boolean? = if (this is Boolean) boolean else null + fun asInteger(): Int? = if (this is Integer) integer else null + fun asDouble(): kotlin.Double? = if (this is Double) double else null + fun asInstant(): java.time.Instant? = if (this is Instant) instant else null + fun asList(): kotlin.collections.List? = if (this is List) list else null + fun asStructure(): Map? = if (this is Structure) structure else null + fun isNull(): kotlin.Boolean = this is Null + + @Serializable + data class String(val string: kotlin.String) : Value + @Serializable + data class Boolean(val boolean: kotlin.Boolean) : Value + @Serializable + data class Integer(val integer: Int) : Value + @Serializable + data class Double(val double: kotlin.Double) : Value + @Serializable + data class Instant(@Serializable(InstantSerializer::class) val instant: java.time.Instant) : Value + @Serializable + data class Structure(val structure: Map) : Value + @Serializable + data class List(val list: kotlin.collections.List) : Value + @Serializable + object Null: Value { + override fun equals(other: Any?): kotlin.Boolean { + return other is Null + } + override fun hashCode(): Int { + return javaClass.hashCode() + } + } +} + + +object ValueSerializer: JsonContentPolymorphicSerializer(Value::class) { + override fun selectDeserializer(element: JsonElement) = when(element.jsonObject.keys) { + emptySet() -> Value.Null.serializer() + setOf("string") -> Value.String.serializer() + setOf("boolean") -> Value.Boolean.serializer() + setOf("integer") -> Value.Integer.serializer() + setOf("double") -> Value.Double.serializer() + setOf("instant") -> Value.Instant.serializer() + setOf("list") -> Value.List.serializer() + setOf("structure") -> Value.Structure.serializer() + else -> throw OpenFeatureError.ParseError("couldn't find deserialization key for Value") + } +} + +object InstantSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString()) +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt new file mode 100644 index 0000000..ed7ab8a --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt @@ -0,0 +1,18 @@ +package dev.openfeature.sdk.exceptions + +enum class ErrorCode { + // The value was resolved before the provider was ready. + PROVIDER_NOT_READY, + // The flag could not be found. + FLAG_NOT_FOUND, + // An error was encountered parsing data, such as a flag configuration. + PARSE_ERROR, + // The type of the flag value does not match the expected type. + TYPE_MISMATCH, + // The provider requires a targeting key and one was not provided in the evaluation context. + TARGETING_KEY_MISSING, + // The evaluation context does not meet provider requirements. + INVALID_CONTEXT, + // The error was for a reason not enumerated above. + GENERAL +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt new file mode 100644 index 0000000..be17b95 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt @@ -0,0 +1,43 @@ +package dev.openfeature.sdk.exceptions + +sealed class OpenFeatureError : Exception() { + abstract fun errorCode(): ErrorCode + + class GeneralError(override val message: String): OpenFeatureError() { + override fun errorCode(): ErrorCode { + return ErrorCode.GENERAL + } + } + + class FlagNotFoundError(flagKey: String): OpenFeatureError() { + override val message: String = "Could not find flag named: $flagKey" + override fun errorCode(): ErrorCode { + return ErrorCode.FLAG_NOT_FOUND + } + } + + class InvalidContextError( + override val message: String = "Invalid context"): OpenFeatureError() { + override fun errorCode(): ErrorCode { + return ErrorCode.INVALID_CONTEXT + } + } + + class ParseError(override val message: String): OpenFeatureError() { + override fun errorCode(): ErrorCode { + return ErrorCode.PARSE_ERROR + } + } + + class TargetingKeyMissingError(override val message: String = "Targeting key missing in evaluation context"): OpenFeatureError() { + override fun errorCode(): ErrorCode { + return ErrorCode.TARGETING_KEY_MISSING + } + } + + class ProviderNotReadyError(override val message: String = "The value was resolved before the provider was ready"): OpenFeatureError() { + override fun errorCode(): ErrorCode { + return ErrorCode.PROVIDER_NOT_READY + } + } +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt new file mode 100644 index 0000000..ca5303b --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt @@ -0,0 +1,61 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.exceptions.ErrorCode +import dev.openfeature.sdk.helpers.AlwaysBrokenProvider +import dev.openfeature.sdk.helpers.GenericSpyHookMock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class DeveloperExperienceTests { + @Test + fun testNoProviderSet() = runTest { + OpenFeatureAPI.clearProvider() + val stringValue = OpenFeatureAPI.getClient().getStringValue("test", "no-op") + Assert.assertEquals(stringValue, "no-op") + } + + @Test + fun testSimpleBooleanFlag() = runTest { + OpenFeatureAPI.setProvider(NoOpProvider()) + val booleanValue = OpenFeatureAPI.getClient().getBooleanValue("test", false) + Assert.assertFalse(booleanValue) + } + + @Test + fun testClientHooks() = runTest { + OpenFeatureAPI.setProvider(NoOpProvider()) + val client = OpenFeatureAPI.getClient() + + val hook = GenericSpyHookMock() + client.addHooks(listOf(hook)) + + client.getBooleanValue("test", false) + Assert.assertEquals(hook.finallyCalledAfter, 1) + } + + @Test + fun testEvalHooks() = runTest { + OpenFeatureAPI.setProvider(NoOpProvider()) + val client = OpenFeatureAPI.getClient() + + val hook = GenericSpyHookMock() + val options = FlagEvaluationOptions(listOf(hook)) + + client.getBooleanValue("test", false, options) + Assert.assertEquals(hook.finallyCalledAfter, 1) + } + + @Test + fun testBrokenProvider() = runTest { + OpenFeatureAPI.setProvider(AlwaysBrokenProvider()) + val client = OpenFeatureAPI.getClient() + + val details = client.getBooleanDetails("test", false) + Assert.assertEquals(ErrorCode.FLAG_NOT_FOUND, details.errorCode) + Assert.assertEquals("Could not find flag named: test", details.errorMessage) + Assert.assertEquals(Reason.ERROR.toString(), details.reason) + } +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt new file mode 100644 index 0000000..c17e750 --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt @@ -0,0 +1,156 @@ +package dev.openfeature.sdk + +import org.junit.Assert +import org.junit.Test +import java.time.Instant + +class EvalContextTests { + + @Test + fun testContextStoresTargetingKey() { + val ctx = MutableContext() + ctx.setTargetingKey("test") + Assert.assertEquals("test", ctx.getTargetingKey()) + } + + @Test + fun testContextStoresPrimitiveValues() { + val ctx = MutableContext() + val now = Instant.now() + + ctx.add("string", Value.String("value")) + Assert.assertEquals("value", ctx.getValue("string")?.asString()) + ctx.add("bool", Value.Boolean(true)) + Assert.assertEquals(true, ctx.getValue("bool")?.asBoolean()) + ctx.add("int", Value.Integer(3)) + Assert.assertEquals(3, ctx.getValue("int")?.asInteger()) + ctx.add("double", Value.Double(3.14)) + Assert.assertEquals(3.14, ctx.getValue("double")?.asDouble()) + ctx.add("instant", Value.Instant(now)) + Assert.assertEquals(now, ctx.getValue("instant")?.asInstant()) + } + @Test + fun testContextStoresLists() { + val ctx = MutableContext() + + ctx.add("list", Value.List(listOf( + Value.Integer(3), + Value.String("4")))) + Assert.assertEquals(3, ctx.getValue("list")?.asList()?.get(0)?.asInteger()) + Assert.assertEquals("4", ctx.getValue("list")?.asList()?.get(1)?.asString()) + } + + @Test + fun testContextStoresStructures() { + val ctx = MutableContext() + + ctx.add("struct", Value.Structure(mapOf( + "string" to Value.String("test"), + "int" to Value.Integer(3)))) + Assert.assertEquals("test", ctx.getValue("struct")?.asStructure()?.get("string")?.asString()) + Assert.assertEquals(3, ctx.getValue("struct")?.asStructure()?.get("int")?.asInteger()) + } + + @Test + fun testContextCanConvertToMap() { + val ctx = MutableContext() + val now = Instant.now() + ctx.add("str1", Value.String("test1")) + ctx.add("str2", Value.String("test2")) + ctx.add("bool1", Value.Boolean(true)) + ctx.add("bool2", Value.Boolean(false)) + ctx.add("int1", Value.Integer(4)) + ctx.add("int2", Value.Integer(2)) + ctx.add("dt", Value.Instant(now)) + ctx.add("obj", Value.Structure(mapOf("val1" to Value.Integer(1), "val2" to Value.String("2")))) + + val map = ctx.asMap() + val structure = map["obj"]?.asStructure() + Assert.assertEquals("test1", map["str1"]?.asString()) + Assert.assertEquals("test2", map["str2"]?.asString()) + Assert.assertEquals(true, map["bool1"]?.asBoolean()) + Assert.assertEquals(false, map["bool2"]?.asBoolean()) + Assert.assertEquals(4, map["int1"]?.asInteger()) + Assert.assertEquals(2, map["int2"]?.asInteger()) + Assert.assertEquals(now, map["dt"]?.asInstant()) + Assert.assertEquals(1, structure?.get("val1")?.asInteger()) + Assert.assertEquals("2", structure?.get("val2")?.asString()) + } + + @Test + fun testContextHasUniqueKeyAcrossTypes() { + val ctx = MutableContext() + + ctx.add("key", Value.String("val1")) + ctx.add("key", Value.String("val2")) + Assert.assertEquals("val2", ctx.getValue("key")?.asString()) + + ctx.add("key", Value.Integer(3)) + Assert.assertNull(ctx.getValue("key")?.asString()) + Assert.assertEquals(3, ctx.getValue("key")?.asInteger()) + } + + @Test + fun testContextCanChainAttributeAddition() { + val ctx = MutableContext() + + val result = + ctx.add("key1", Value.String("val1")) + ctx.add("key2", Value.String("val2")) + Assert.assertEquals("val1", result.getValue("key1")?.asString()) + Assert.assertEquals("val2", result.getValue("key2")?.asString()) + } + + @Test + fun testContextCanAddNull() { + val ctx = MutableContext() + + ctx.add("null", Value.Null) + Assert.assertEquals(true, ctx.getValue("null")?.isNull()) + Assert.assertNull(ctx.getValue("null")?.asString()) + } + + @Test + fun testContextConvertsToObjectMap() { + val key = "key1" + val now = Instant.now() + val ctx = MutableContext(key) + ctx.add("string", Value.String("value")) + ctx.add("bool", Value.Boolean(false)) + ctx.add("integer", Value.Integer(1)) + ctx.add("double", Value.Double(1.2)) + ctx.add("date", Value.Instant(now)) + ctx.add("null", Value.Null) + ctx.add("list", Value.List(listOf(Value.String("item1"), Value.Boolean(true)))) + ctx.add("structure", + Value.Structure( + mapOf( + "field1" to Value.Integer(3), + "field2" to Value.Double(3.14) + ) + ) + ) + + val expected = mapOf( + "string" to "value", + "bool" to false, + "integer" to 1, + "double" to 1.2, + "date" to now, + "null" to null, + "list" to listOf("item1", true), + "structure" to mapOf("field1" to 3, "field2" to 3.14), + ) + Assert.assertEquals(expected, ctx.asObjectMap()) + } + + @Test + fun compareContexts() { + val map: MutableMap = mutableMapOf("key" to Value.String("test")) + val map2: MutableMap = mutableMapOf("key" to Value.String("test")) + val ctx1 = MutableContext("user1", map) + val ctx2 = MutableContext("user1", map2) + + Assert.assertEquals(ctx1, ctx2) + } +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt new file mode 100644 index 0000000..024c06f --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt @@ -0,0 +1,132 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.exceptions.ErrorCode +import dev.openfeature.sdk.helpers.AlwaysBrokenProvider +import dev.openfeature.sdk.helpers.DoSomethingProvider +import dev.openfeature.sdk.helpers.GenericSpyHookMock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class FlagEvaluationsTests { + @Test + fun testApiSetsProvider() = runTest { + val provider = NoOpProvider() + + OpenFeatureAPI.setProvider(provider) + Assert.assertEquals(provider, OpenFeatureAPI.getProvider()) + } + + @Test + fun testHooksPersist() { + val hook1 = GenericSpyHookMock() + val hook2 = GenericSpyHookMock() + + OpenFeatureAPI.addHooks(listOf(hook1)) + Assert.assertEquals(1, OpenFeatureAPI.hooks.count()) + + OpenFeatureAPI.addHooks(listOf(hook2)) + Assert.assertEquals(2, OpenFeatureAPI.hooks.count()) + } + + @Test + fun testClientHooksPersist() { + val hook1 = GenericSpyHookMock() + val hook2 = GenericSpyHookMock() + + val client = OpenFeatureAPI.getClient() + client.addHooks(listOf(hook1)) + Assert.assertEquals(1, client.hooks.count()) + + client.addHooks(listOf(hook2)) + Assert.assertEquals(2, client.hooks.count()) + } + + @Test + fun testSimpleFlagEvaluation() = runTest { + OpenFeatureAPI.setProvider(DoSomethingProvider()) + val client = OpenFeatureAPI.getClient() + val key = "key" + + Assert.assertEquals(true, client.getBooleanValue(key, false)) + Assert.assertEquals(true, client.getBooleanValue(key, false, FlagEvaluationOptions())) + + Assert.assertEquals("test", client.getStringValue(key, "tset")) + Assert.assertEquals("test", client.getStringValue(key, "tset", FlagEvaluationOptions())) + + Assert.assertEquals(400, client.getIntegerValue(key, 4)) + Assert.assertEquals(400, client.getIntegerValue(key, 4, FlagEvaluationOptions())) + + Assert.assertEquals(40.0, client.getDoubleValue(key, 0.4),0.0) + Assert.assertEquals(40.0, client.getDoubleValue(key, 0.4, FlagEvaluationOptions()),0.0) + + Assert.assertEquals(Value.Null, client.getObjectValue(key, Value.Structure(mapOf()))) + Assert.assertEquals(Value.Null, client.getObjectValue(key, Value.Structure(mapOf()), FlagEvaluationOptions())) + } + + @Test + fun testDetailedFlagEvaluation() = runTest { + OpenFeatureAPI.setProvider(DoSomethingProvider()) + val client = OpenFeatureAPI.getClient() + val key = "key" + + val booleanDetails = FlagEvaluationDetails(key, true) + Assert.assertEquals(booleanDetails, client.getBooleanDetails(key, false)) + Assert.assertEquals(booleanDetails, client.getBooleanDetails(key, false, FlagEvaluationOptions())) + + val stringDetails = FlagEvaluationDetails(key, "tset") + Assert.assertEquals(stringDetails, client.getStringDetails(key, "test")) + Assert.assertEquals(stringDetails, client.getStringDetails(key, "test", FlagEvaluationOptions())) + + val integerDetails = FlagEvaluationDetails(key, 400) + Assert.assertEquals(integerDetails, client.getIntegerDetails(key, 4)) + Assert.assertEquals(integerDetails, client.getIntegerDetails(key, 4, FlagEvaluationOptions())) + + val doubleDetails = FlagEvaluationDetails(key, 40.0) + Assert.assertEquals(doubleDetails, client.getDoubleDetails(key, 0.4)) + Assert.assertEquals(doubleDetails, client.getDoubleDetails(key, 0.4, FlagEvaluationOptions())) + + val objectDetails = FlagEvaluationDetails(key, Value.Null) + Assert.assertEquals(objectDetails, client.getObjectDetails(key, Value.Structure(mapOf()))) + Assert.assertEquals(objectDetails, client.getObjectDetails(key, Value.Structure(mapOf()), FlagEvaluationOptions())) + } + + @Test + fun testHooksAreFired() = runTest { + OpenFeatureAPI.setProvider(NoOpProvider()) + val client = OpenFeatureAPI.getClient() + + val clientHook = GenericSpyHookMock() + val invocationHook = GenericSpyHookMock() + + client.addHooks(listOf(clientHook)) + client.getBooleanValue("key", false, FlagEvaluationOptions(listOf(invocationHook))) + + Assert.assertEquals(1, clientHook.beforeCalled) + Assert.assertEquals(1, invocationHook.beforeCalled) + } + + @Test + fun testBrokenProvider() = runTest { + OpenFeatureAPI.setProvider(AlwaysBrokenProvider()) + val client = OpenFeatureAPI.getClient() + + client.getBooleanValue("testKey", false) + val details = client.getBooleanDetails("testKey", false) + + Assert.assertEquals(ErrorCode.FLAG_NOT_FOUND, details.errorCode) + Assert.assertEquals(Reason.ERROR.toString(), details.reason) + Assert.assertEquals("Could not find flag named: testKey", details.errorMessage) + } + + @Test + fun testClientMetadata() { + val client1 = OpenFeatureAPI.getClient() + Assert.assertNull(client1.metadata.name) + + val client2 = OpenFeatureAPI.getClient("test") + Assert.assertEquals("test", client2.metadata.name) + } +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt new file mode 100644 index 0000000..b759a86 --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt @@ -0,0 +1,70 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.helpers.AlwaysBrokenProvider +import dev.openfeature.sdk.helpers.GenericSpyHookMock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class HookSpecTests { + + @Test + fun testNoErrorHookCalled() = runTest { + OpenFeatureAPI.setProvider(NoOpProvider()) + val client = OpenFeatureAPI.getClient() + val hook = GenericSpyHookMock() + + client.getBooleanValue("key", false, FlagEvaluationOptions(listOf(hook))) + + Assert.assertEquals(1, hook.beforeCalled) + Assert.assertEquals(1, hook.afterCalled) + Assert.assertEquals(0, hook.errorCalled) + Assert.assertEquals(1, hook.finallyCalledAfter) + } + + @Test + fun testErrorHookButNoAfterCalled() = runTest { + OpenFeatureAPI.setProvider(AlwaysBrokenProvider()) + val client = OpenFeatureAPI.getClient() + val hook = GenericSpyHookMock() + + client.getBooleanValue("key", false, FlagEvaluationOptions(listOf(hook))) + Assert.assertEquals(1, hook.beforeCalled) + Assert.assertEquals(0, hook.afterCalled) + Assert.assertEquals(1, hook.errorCalled) + Assert.assertEquals(1, hook.finallyCalledAfter) + } + + @Test + fun testHookEvaluationOrder() = runTest { + val provider = NoOpProvider() + val evalOrder: MutableList = mutableListOf() + val addEval: (String) -> Unit = { eval: String -> evalOrder += eval} + + provider.hooks = listOf(GenericSpyHookMock("provider", addEval)) + OpenFeatureAPI.setProvider(provider) + OpenFeatureAPI.addHooks(listOf(GenericSpyHookMock("api", addEval))) + val client = OpenFeatureAPI.getClient() + client.addHooks(listOf(GenericSpyHookMock("client", addEval))) + val flagOptions = FlagEvaluationOptions(listOf(GenericSpyHookMock("invocation", addEval))) + + client.getBooleanValue("key", false, flagOptions) + + Assert.assertEquals(listOf( + "api before", + "client before", + "invocation before", + "provider before", + "provider after", + "invocation after", + "client after", + "api after", + "provider finallyAfter", + "invocation finallyAfter", + "client finallyAfter", + "api finallyAfter" + ), evalOrder) + } +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt new file mode 100644 index 0000000..8791fc7 --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt @@ -0,0 +1,56 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.exceptions.OpenFeatureError.InvalidContextError +import dev.openfeature.sdk.helpers.GenericSpyHookMock +import org.junit.Assert +import org.junit.Test + +class HookSupportTests { + @Test + fun testShouldAlwaysCallGenericHook() { + val metadata = OpenFeatureAPI.getClient().metadata + val hook = GenericSpyHookMock() + val hookContext = HookContext( + "flagKey", + FlagValueType.BOOLEAN, + false, + MutableContext(), + metadata, + NoOpProvider().metadata + ) + + val hookSupport = HookSupport() + + hookSupport.beforeHooks( + FlagValueType.BOOLEAN, + hookContext, + listOf(hook), + mapOf() + ) + hookSupport.afterHooks( + FlagValueType.BOOLEAN, + hookContext, + FlagEvaluationDetails("", false), + listOf(hook), + mapOf() + ) + hookSupport.afterAllHooks( + FlagValueType.BOOLEAN, + hookContext, + listOf(hook), + mapOf() + ) + hookSupport.errorHooks( + FlagValueType.BOOLEAN, + hookContext, + InvalidContextError(), + listOf(hook), + mapOf() + ) + + Assert.assertEquals(1, hook.beforeCalled) + Assert.assertEquals(1, hook.afterCalled) + Assert.assertEquals(1, hook.finallyCalledAfter) + Assert.assertEquals(1, hook.errorCalled) + } +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/OpenFeatureClientTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/OpenFeatureClientTests.kt new file mode 100644 index 0000000..dce8c2f --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/OpenFeatureClientTests.kt @@ -0,0 +1,18 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.helpers.GenericSpyHookMock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class OpenFeatureClientTests { + @Test + fun testShouldNowThrowIfHookHasDifferentTypeArgument() = runTest { + OpenFeatureAPI.setProvider(NoOpProvider()) + OpenFeatureAPI.addHooks(listOf(GenericSpyHookMock())) + val stringValue = OpenFeatureAPI.getClient().getStringValue("test", "defaultTest") + assertEquals(stringValue, "defaultTest") + } +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt new file mode 100644 index 0000000..5e26e67 --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt @@ -0,0 +1,63 @@ +package dev.openfeature.sdk + +import org.junit.Assert +import org.junit.Test + +class ProviderSpecTests { + + @Test + fun testFlagValueSet() { + val provider = NoOpProvider() + + val boolResult = provider.getBooleanEvaluation("key", false) + Assert.assertNotNull(boolResult.value) + + val stringResult = provider.getStringEvaluation("key", "test") + Assert.assertNotNull(stringResult.value) + + val intResult = provider.getIntegerEvaluation("key", 4) + Assert.assertNotNull(intResult.value) + + val doubleResult = provider.getDoubleEvaluation("key", 0.4) + Assert.assertNotNull(doubleResult.value) + + val objectResult = provider.getObjectEvaluation("key", Value.Null) + Assert.assertNotNull(objectResult.value) + } + + @Test + fun testHasReason() { + val provider = NoOpProvider() + val boolResult = provider.getBooleanEvaluation("key", false) + + Assert.assertEquals(Reason.DEFAULT.toString(), boolResult.reason) + } + + @Test + fun testNoErrorCodeByDefault() { + val provider = NoOpProvider() + val boolResult = provider.getBooleanEvaluation("key", false) + + Assert.assertNull(boolResult.errorCode) + } + + @Test + fun testVariantIsSet() { + val provider = NoOpProvider() + + val boolResult = provider.getBooleanEvaluation("key", false) + Assert.assertNotNull(boolResult.variant) + + val stringResult = provider.getStringEvaluation("key", "test") + Assert.assertNotNull(stringResult.variant) + + val intResult = provider.getIntegerEvaluation("key", 4) + Assert.assertNotNull(intResult.variant) + + val doubleResult = provider.getDoubleEvaluation("key", 0.4) + Assert.assertNotNull(doubleResult.variant) + + val objectResult = provider.getObjectEvaluation("key", Value.Null) + Assert.assertNotNull(objectResult.variant) + } +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt new file mode 100644 index 0000000..fdab5e6 --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt @@ -0,0 +1,54 @@ +package dev.openfeature.sdk + +import org.junit.Assert +import org.junit.Test +import java.time.Instant + +class StructureTests { + + @Test + fun testNoArgIsEmpty() { + val structure = MutableContext() + Assert.assertTrue(structure.asMap().keys.isEmpty()) + } + + @Test + fun testArgShouldContainNewMap() { + val map: MutableMap = mutableMapOf("key" to Value.String("test")) + val structure = MutableStructure(map) + + Assert.assertEquals("test", structure.getValue("key")?.asString()) + Assert.assertEquals(map, structure.asMap()) + } + + @Test + fun testAddAndGetReturnValues() { + val now = Instant.now() + val structure = MutableStructure() + structure.add("bool", Value.Boolean(true)) + structure.add("string", Value.String("val")) + structure.add("int", Value.Integer(13)) + structure.add("double", Value.Double(0.5)) + structure.add("date", Value.Instant(now)) + structure.add("list", Value.List(listOf())) + structure.add("structure", Value.Structure(mapOf())) + + Assert.assertEquals(true, structure.getValue("bool")?.asBoolean()) + Assert.assertEquals("val", structure.getValue("string")?.asString()) + Assert.assertEquals(13, structure.getValue("int")?.asInteger()) + Assert.assertEquals(0.5, structure.getValue("double")?.asDouble()) + Assert.assertEquals(now, structure.getValue("date")?.asInstant()) + Assert.assertEquals(listOf(), structure.getValue("list")?.asList()) + Assert.assertEquals(mapOf(), structure.getValue("structure")?.asStructure()) + } + + @Test + fun testCompareStructure() { + val map: MutableMap = mutableMapOf("key" to Value.String("test")) + val map2: MutableMap = mutableMapOf("key" to Value.String("test")) + val structure1 = MutableStructure(map) + val structure2 = MutableStructure(map2) + + Assert.assertEquals(structure1, structure2) + } +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt new file mode 100644 index 0000000..ba0307a --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt @@ -0,0 +1,139 @@ +package dev.openfeature.sdk + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement +import org.junit.Assert +import org.junit.Test +import java.time.Instant + +class ValueTests { + + @Test + fun testNull() { + val value = Value.Null + Assert.assertTrue(value.isNull()) + } + + @Test + fun testIntShouldConvertToInt() { + val value = Value.Integer(3) + Assert.assertEquals(3, value.asInteger()) + } + + @Test + fun testDoubleShouldConvertToDouble() { + val value = Value.Double(3.14) + Assert.assertEquals(3.14, value.asDouble()!!, 0.0) + } + + @Test + fun testBoolShouldConvertToBool() { + val value = Value.Boolean(true) + Assert.assertEquals(true, value.asBoolean()) + } + + @Test + fun testStringShouldConvertToString() { + val value = Value.String("test") + Assert.assertEquals("test", value.asString()) + } + + @Test + fun testListShouldConvertToList() { + val value = Value.List(listOf(Value.Integer(3), Value.Integer(4))) + Assert.assertEquals(listOf(Value.Integer(3), Value.Integer(4)), value.asList()) + } + + @Test + fun testStructShouldConvertToStruct() { + val value = Value.Structure(mapOf("field1" to Value.Integer(3), "field2" to Value.String("test"))) + Assert.assertEquals(value.asStructure(), mapOf("field1" to Value.Integer(3), "field2" to Value.String("test"))) + } + + @Test + fun testEmptyListAllowed() { + val value = Value.List(listOf()) + Assert.assertEquals(listOf(), value.asList()) + } + + @Test + fun testEncodeDecode() { + val date = Instant.parse("2023-03-01T14:01:46Z") + val value = Value.Structure( + mapOf( + "null" to Value.Null, + "text" to Value.String("test"), + "bool" to Value.Boolean(true), + "int" to Value.Integer(3), + "double" to Value.Double(4.5), + "date" to Value.Instant(date), + "list" to Value.List(listOf(Value.Boolean(false), Value.Integer(4))), + "structure" to Value.Structure(mapOf("int" to Value.Integer(5))) + ) + ) + + val encodedValue = Json.encodeToJsonElement(value) + val decodedValue = Json.decodeFromJsonElement(encodedValue) + + Assert.assertEquals(value, decodedValue) + } + + @Test + fun testJsonDecode() { + val stringInstant = "2023-03-01T14:01:46Z" + val json = "{" + + " \"structure\": {" + + " \"null\": {}," + + " \"text\": {" + + " \"string\": \"test\"" + + " }," + + " \"bool\": {" + + " \"boolean\": true" + + " }," + + " \"int\": {" + + " \"integer\": 3" + + " }," + + " \"double\": {" + + " \"double\": 4.5" + + " }," + + " \"date\": {" + + " \"instant\": \"$stringInstant\"" + + " }," + + " \"list\": {" + + " \"list\": [" + + " {" + + " \"boolean\": false" + + " }," + + " {" + + " \"integer\": 4" + + " }" + + " ]" + + " }," + + " \"structure\": {" + + " \"structure\": {" + + " \"int\": {" + + " \"integer\": 5" + + " }" + + " }" + + " }" + + " }" + + "}" + + val expectedValue = Value.Structure( + mapOf( + "null" to Value.Null, + "text" to Value.String("test"), + "bool" to Value.Boolean(true), + "int" to Value.Integer(3), + "double" to Value.Double(4.5), + "date" to Value.Instant(Instant.parse(stringInstant)), + "list" to Value.List(listOf(Value.Boolean(false), Value.Integer(4))), + "structure" to Value.Structure(mapOf("int" to Value.Integer(5))) + ) + ) + + val decodedValue = Json.decodeFromString(Value.serializer(), json) + Assert.assertEquals(expectedValue, decodedValue) + } +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt new file mode 100644 index 0000000..98a8d3e --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt @@ -0,0 +1,54 @@ +package dev.openfeature.sdk.helpers + +import dev.openfeature.sdk.* +import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError + +class AlwaysBrokenProvider(override var hooks: List> = listOf(), override var metadata: Metadata = AlwaysBrokenMetadata()) : FeatureProvider { + override suspend fun initialize(initialContext: EvaluationContext?) { + // no-op + } + + override suspend fun onContextSet( + oldContext: EvaluationContext?, + newContext: EvaluationContext + ) { + // no-op + } + + override fun getBooleanEvaluation( + key: String, + defaultValue: Boolean + ): ProviderEvaluation { + throw FlagNotFoundError(key) + } + + override fun getStringEvaluation( + key: String, + defaultValue: String + ): ProviderEvaluation { + throw FlagNotFoundError(key) + } + + override fun getIntegerEvaluation( + key: String, + defaultValue: Int + ): ProviderEvaluation { + throw FlagNotFoundError(key) + } + + override fun getDoubleEvaluation( + key: String, + defaultValue: Double + ): ProviderEvaluation { + throw FlagNotFoundError(key) + } + + override fun getObjectEvaluation( + key: String, + defaultValue: Value + ): ProviderEvaluation { + throw FlagNotFoundError(key) + } + + class AlwaysBrokenMetadata(override var name: String? = "test") : Metadata +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt new file mode 100644 index 0000000..5184eef --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt @@ -0,0 +1,52 @@ +package dev.openfeature.sdk.helpers + +import dev.openfeature.sdk.* + +class DoSomethingProvider(override val hooks: List> = listOf(), override val metadata: Metadata = DoSomethingMetadata()) : FeatureProvider { + override suspend fun initialize(initialContext: EvaluationContext?) { + // no-op + } + + override suspend fun onContextSet( + oldContext: EvaluationContext?, + newContext: EvaluationContext + ) { + // no-op + } + + override fun getBooleanEvaluation( + key: String, + defaultValue: Boolean + ): ProviderEvaluation { + return ProviderEvaluation(!defaultValue) + } + + override fun getStringEvaluation( + key: String, + defaultValue: String + ): ProviderEvaluation { + return ProviderEvaluation(defaultValue.reversed()) + } + + override fun getIntegerEvaluation( + key: String, + defaultValue: Int + ): ProviderEvaluation { + return ProviderEvaluation(defaultValue * 100) + } + + override fun getDoubleEvaluation( + key: String, + defaultValue: Double + ): ProviderEvaluation { + return ProviderEvaluation(defaultValue * 100) + } + + override fun getObjectEvaluation( + key: String, + defaultValue: Value + ): ProviderEvaluation { + return ProviderEvaluation(Value.Null) + } + class DoSomethingMetadata(override var name: String? = "something") : Metadata +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt new file mode 100644 index 0000000..e6b179b --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt @@ -0,0 +1,44 @@ +package dev.openfeature.sdk.helpers + +import dev.openfeature.sdk.FlagEvaluationDetails +import dev.openfeature.sdk.Hook +import dev.openfeature.sdk.HookContext + +class GenericSpyHookMock(private var prefix: String = "", var addEval: (String) -> Unit = {}) : Hook { + var beforeCalled = 0 + var afterCalled = 0 + var finallyCalledAfter = 0 + var errorCalled = 0 + + override fun before( + ctx: HookContext, + hints: Map + ) { + beforeCalled += 1 + addEval("$prefix before") + } + + override fun after( + ctx: HookContext, + details: FlagEvaluationDetails, + hints: Map + ) { + afterCalled += 1 + addEval("$prefix after") + } + + + override fun error( + ctx: HookContext, + error: Exception, + hints: Map + ) { + errorCalled += 1 + addEval("$prefix error") + } + + override fun finallyAfter(ctx: HookContext, hints: Map) { + finallyCalledAfter += 1 + addEval("$prefix finallyAfter") + } +} \ No newline at end of file diff --git a/README.md b/README.md index 317ac2b..a48737e 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# openfeature-kotlin-sdk \ No newline at end of file +# OpenFeature Kotlin SDK + +Kotlin implementation of the OpenFeature SDK \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..e862768 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,6 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id("com.android.application").version("7.4.1").apply(false) + id("com.android.library").version("7.4.1").apply(false) + id("org.jetbrains.kotlin.android").version("1.8.0").apply(false) +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..0db0ea9 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,25 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +# Software Components will not be created automatically for Maven publishing from Android Gradle Plugin 8.0 +android.disableAutomaticComponentCreation=true \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..4673d67 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +import org.gradle.api.initialization.resolve.RepositoriesMode + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "OpenFeature" +include(":OpenFeature") \ No newline at end of file From c9257c66f388a0ffbb42ca3b8075447880213603 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 14 Apr 2023 13:58:31 +0200 Subject: [PATCH 03/56] update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 739e4e7..27d86e3 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ hs_err_pid* # Gradle files .gradle/ build/ +gradle/ gradlew gradlew.bat From 6d4b9a48178e3e2d798c3fd27019f75251d7c880 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 17 Apr 2023 11:44:19 +0200 Subject: [PATCH 04/56] Add gradlew to source control --- .gitignore | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 234 +++++++++++++++++++++++ gradlew.bat | 89 +++++++++ 5 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat diff --git a/.gitignore b/.gitignore index 27d86e3..c55d0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,9 +26,7 @@ hs_err_pid* # Gradle files .gradle/ build/ -gradle/ -gradlew -gradlew.bat +!gradle/wrapper/gradle-wrapper.jar # Local configuration file (sdk path, etc) local.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6d04661 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Apr 14 14:06:40 CEST 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 1bff4e79dcc9cd972e8f803b4ca7938b538b2c09 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 17 Apr 2023 11:25:43 +0200 Subject: [PATCH 05/56] Github Action --- .github/workflows/ci.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..b285d9d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: + - 'main' + pull_request: + branches: + - '*' + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup JDK + uses: actions/setup-java@v3 + with: + distribution: adopt + java-version: 11 + cache: gradle + + - name: Run checks + run: ./gradlew check --no-daemon --stacktrace \ No newline at end of file From a808f037c9d1ede2ba15b5b52e7e5b632d3e4b84 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 17 Apr 2023 15:24:42 +0200 Subject: [PATCH 06/56] Remove pinned Java version in CI --- .github/workflows/ci.yaml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b285d9d..9059480 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,13 +14,5 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - - name: Setup JDK - uses: actions/setup-java@v3 - with: - distribution: adopt - java-version: 11 - cache: gradle - - name: Run checks - run: ./gradlew check --no-daemon --stacktrace \ No newline at end of file + run: ./gradlew check --no-daemon --stacktrace From 8afce2e41a32360cedb330b25a139dfea79318a3 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 17 Apr 2023 15:31:32 +0200 Subject: [PATCH 07/56] Small workflow naming change --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9059480..5d6dd4b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ on: - '*' jobs: - verify: + Tests: runs-on: ubuntu-latest steps: - name: Checkout From 6626e8eb1f8d70c3c738b54876f7584177a01e4e Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 16 May 2023 16:03:06 +0200 Subject: [PATCH 08/56] update dependency to use coroutines-core we don't need all the stuff from material --- OpenFeature/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index 64b422c..81a2bf1 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -32,7 +32,7 @@ android { } dependencies { - implementation("com.google.android.material:material:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") testImplementation("junit:junit:4.13.2") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") From 16cefea3116effb2e21d8654a4e000ca676f631a Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 16 May 2023 16:14:55 +0200 Subject: [PATCH 09/56] align coroutines test framework version with main package --- OpenFeature/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index 81a2bf1..6321ba7 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") testImplementation("junit:junit:4.13.2") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") } publishing { From f63e098a5a31ced71a373dbdfe54e80f603310a6 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 19 May 2023 11:15:42 +0200 Subject: [PATCH 10/56] Global ctx passed into the provider via FeatureProvider --- .../dev/openfeature/sdk/FeatureProvider.kt | 10 ++++---- .../java/dev/openfeature/sdk/HookContext.kt | 2 +- .../java/dev/openfeature/sdk/NoOpProvider.kt | 15 ++++++++---- .../dev/openfeature/sdk/OpenFeatureClient.kt | 14 ++++++----- .../sdk/exceptions/OpenFeatureError.kt | 2 +- .../sdk/DeveloperExperienceTests.kt | 8 +++---- .../dev/openfeature/sdk/ProviderSpecTests.kt | 24 +++++++++---------- .../sdk/helpers/AlwaysBrokenProvider.kt | 15 ++++++++---- .../sdk/helpers/DoSomethingProvider.kt | 15 ++++++++---- 9 files changed, 61 insertions(+), 44 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt index a7efcee..aa7c534 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt @@ -8,9 +8,9 @@ interface FeatureProvider { suspend fun initialize(initialContext: EvaluationContext?) // Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) - fun getBooleanEvaluation(key: String, defaultValue: Boolean): ProviderEvaluation - fun getStringEvaluation(key: String, defaultValue: String): ProviderEvaluation - fun getIntegerEvaluation(key: String, defaultValue: Int): ProviderEvaluation - fun getDoubleEvaluation(key: String, defaultValue: Double): ProviderEvaluation - fun getObjectEvaluation(key: String, defaultValue: Value): ProviderEvaluation + fun getBooleanEvaluation(key: String, defaultValue: Boolean, context: EvaluationContext?): ProviderEvaluation + fun getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?): ProviderEvaluation + fun getIntegerEvaluation(key: String, defaultValue: Int, context: EvaluationContext?): ProviderEvaluation + fun getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?): ProviderEvaluation + fun getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?): ProviderEvaluation } \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt index 7836784..10e1d50 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt @@ -4,7 +4,7 @@ data class HookContext( var flagKey: String, val type: FlagValueType, var defaultValue: T, - var ctx: EvaluationContext, + var ctx: EvaluationContext?, var clientMetadata: Metadata?, var providerMetadata: Metadata ) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt index 23dbfb3..3e939d0 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt @@ -16,35 +16,40 @@ class NoOpProvider : FeatureProvider { override var hooks: List> = listOf() override fun getBooleanEvaluation( key: String, - defaultValue: Boolean + defaultValue: Boolean, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) } override fun getStringEvaluation( key: String, - defaultValue: String + defaultValue: String, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) } override fun getIntegerEvaluation( key: String, - defaultValue: Int + defaultValue: Int, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) } override fun getDoubleEvaluation( key: String, - defaultValue: Double + defaultValue: Double, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) } override fun getObjectEvaluation( key: String, - defaultValue: Value + defaultValue: Value, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) } diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt index 9a2e5ef..6c38254 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt @@ -165,13 +165,14 @@ class OpenFeatureClient( var details = FlagEvaluationDetails(key, defaultValue) val provider = openFeatureAPI.getProvider() ?: NoOpProvider() val mergedHooks: List> = provider.hooks + options.hooks + hooks + openFeatureAPI.hooks - val context = openFeatureAPI.getEvaluationContext() ?: MutableContext() + val context = openFeatureAPI.getEvaluationContext() val hookCtx: HookContext = HookContext(key, flagValueType, defaultValue, context, this.metadata, provider.metadata) try { hookSupport.beforeHooks(flagValueType, hookCtx, mergedHooks, hints) val providerEval = createProviderEvaluation( flagValueType, key, + context, defaultValue, provider ) @@ -197,33 +198,34 @@ class OpenFeatureClient( private fun createProviderEvaluation( flagValueType: FlagValueType, key: String, + context: EvaluationContext?, defaultValue: V, provider: FeatureProvider ): ProviderEvaluation { return when(flagValueType) { BOOLEAN -> { val defaultBoolean = defaultValue as? Boolean ?: throw typeMatchingException - val eval: ProviderEvaluation = provider.getBooleanEvaluation(key, defaultBoolean) + val eval: ProviderEvaluation = provider.getBooleanEvaluation(key, defaultBoolean, context) eval as? ProviderEvaluation ?: throw typeMatchingException } STRING -> { val defaultString = defaultValue as? String ?: throw typeMatchingException - val eval: ProviderEvaluation = provider.getStringEvaluation(key, defaultString) + val eval: ProviderEvaluation = provider.getStringEvaluation(key, defaultString, context) eval as? ProviderEvaluation ?: throw typeMatchingException } INTEGER -> { val defaultInteger = defaultValue as? Int ?: throw typeMatchingException - val eval: ProviderEvaluation = provider.getIntegerEvaluation(key, defaultInteger) + val eval: ProviderEvaluation = provider.getIntegerEvaluation(key, defaultInteger, context) eval as? ProviderEvaluation ?: throw typeMatchingException } DOUBLE -> { val defaultDouble = defaultValue as? Double ?: throw typeMatchingException - val eval: ProviderEvaluation = provider.getDoubleEvaluation(key, defaultDouble) + val eval: ProviderEvaluation = provider.getDoubleEvaluation(key, defaultDouble, context) eval as? ProviderEvaluation ?: throw typeMatchingException } OBJECT -> { val defaultObject = defaultValue as? Value ?: throw typeMatchingException - val eval: ProviderEvaluation = provider.getObjectEvaluation(key, defaultObject) + val eval: ProviderEvaluation = provider.getObjectEvaluation(key, defaultObject, context) eval as? ProviderEvaluation ?: throw typeMatchingException } } diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt index be17b95..9653ff7 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt @@ -17,7 +17,7 @@ sealed class OpenFeatureError : Exception() { } class InvalidContextError( - override val message: String = "Invalid context"): OpenFeatureError() { + override val message: String = "Invalid or missing context"): OpenFeatureError() { override fun errorCode(): ErrorCode { return ErrorCode.INVALID_CONTEXT } diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt index ca5303b..5ff94fc 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt @@ -19,14 +19,14 @@ class DeveloperExperienceTests { @Test fun testSimpleBooleanFlag() = runTest { - OpenFeatureAPI.setProvider(NoOpProvider()) + OpenFeatureAPI.setProvider(NoOpProvider(), MutableContext()) val booleanValue = OpenFeatureAPI.getClient().getBooleanValue("test", false) Assert.assertFalse(booleanValue) } @Test fun testClientHooks() = runTest { - OpenFeatureAPI.setProvider(NoOpProvider()) + OpenFeatureAPI.setProvider(NoOpProvider(), MutableContext()) val client = OpenFeatureAPI.getClient() val hook = GenericSpyHookMock() @@ -38,7 +38,7 @@ class DeveloperExperienceTests { @Test fun testEvalHooks() = runTest { - OpenFeatureAPI.setProvider(NoOpProvider()) + OpenFeatureAPI.setProvider(NoOpProvider(), MutableContext()) val client = OpenFeatureAPI.getClient() val hook = GenericSpyHookMock() @@ -50,7 +50,7 @@ class DeveloperExperienceTests { @Test fun testBrokenProvider() = runTest { - OpenFeatureAPI.setProvider(AlwaysBrokenProvider()) + OpenFeatureAPI.setProvider(AlwaysBrokenProvider(), MutableContext()) val client = OpenFeatureAPI.getClient() val details = client.getBooleanDetails("test", false) diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt index 5e26e67..91e1e19 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt @@ -9,26 +9,26 @@ class ProviderSpecTests { fun testFlagValueSet() { val provider = NoOpProvider() - val boolResult = provider.getBooleanEvaluation("key", false) + val boolResult = provider.getBooleanEvaluation("key", false, MutableContext()) Assert.assertNotNull(boolResult.value) - val stringResult = provider.getStringEvaluation("key", "test") + val stringResult = provider.getStringEvaluation("key", "test", MutableContext()) Assert.assertNotNull(stringResult.value) - val intResult = provider.getIntegerEvaluation("key", 4) + val intResult = provider.getIntegerEvaluation("key", 4, MutableContext()) Assert.assertNotNull(intResult.value) - val doubleResult = provider.getDoubleEvaluation("key", 0.4) + val doubleResult = provider.getDoubleEvaluation("key", 0.4, MutableContext()) Assert.assertNotNull(doubleResult.value) - val objectResult = provider.getObjectEvaluation("key", Value.Null) + val objectResult = provider.getObjectEvaluation("key", Value.Null, MutableContext()) Assert.assertNotNull(objectResult.value) } @Test fun testHasReason() { val provider = NoOpProvider() - val boolResult = provider.getBooleanEvaluation("key", false) + val boolResult = provider.getBooleanEvaluation("key", false, MutableContext()) Assert.assertEquals(Reason.DEFAULT.toString(), boolResult.reason) } @@ -36,7 +36,7 @@ class ProviderSpecTests { @Test fun testNoErrorCodeByDefault() { val provider = NoOpProvider() - val boolResult = provider.getBooleanEvaluation("key", false) + val boolResult = provider.getBooleanEvaluation("key", false, MutableContext()) Assert.assertNull(boolResult.errorCode) } @@ -45,19 +45,19 @@ class ProviderSpecTests { fun testVariantIsSet() { val provider = NoOpProvider() - val boolResult = provider.getBooleanEvaluation("key", false) + val boolResult = provider.getBooleanEvaluation("key", false, MutableContext()) Assert.assertNotNull(boolResult.variant) - val stringResult = provider.getStringEvaluation("key", "test") + val stringResult = provider.getStringEvaluation("key", "test", MutableContext()) Assert.assertNotNull(stringResult.variant) - val intResult = provider.getIntegerEvaluation("key", 4) + val intResult = provider.getIntegerEvaluation("key", 4, MutableContext()) Assert.assertNotNull(intResult.variant) - val doubleResult = provider.getDoubleEvaluation("key", 0.4) + val doubleResult = provider.getDoubleEvaluation("key", 0.4, MutableContext()) Assert.assertNotNull(doubleResult.variant) - val objectResult = provider.getObjectEvaluation("key", Value.Null) + val objectResult = provider.getObjectEvaluation("key", Value.Null, MutableContext()) Assert.assertNotNull(objectResult.variant) } } diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt index 98a8d3e..569d073 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt @@ -17,35 +17,40 @@ class AlwaysBrokenProvider(override var hooks: List> = listOf(), overrid override fun getBooleanEvaluation( key: String, - defaultValue: Boolean + defaultValue: Boolean, + context: EvaluationContext? ): ProviderEvaluation { throw FlagNotFoundError(key) } override fun getStringEvaluation( key: String, - defaultValue: String + defaultValue: String, + context: EvaluationContext? ): ProviderEvaluation { throw FlagNotFoundError(key) } override fun getIntegerEvaluation( key: String, - defaultValue: Int + defaultValue: Int, + context: EvaluationContext? ): ProviderEvaluation { throw FlagNotFoundError(key) } override fun getDoubleEvaluation( key: String, - defaultValue: Double + defaultValue: Double, + context: EvaluationContext? ): ProviderEvaluation { throw FlagNotFoundError(key) } override fun getObjectEvaluation( key: String, - defaultValue: Value + defaultValue: Value, + context: EvaluationContext? ): ProviderEvaluation { throw FlagNotFoundError(key) } diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt index 5184eef..09e4f6a 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt @@ -16,35 +16,40 @@ class DoSomethingProvider(override val hooks: List> = listOf(), override override fun getBooleanEvaluation( key: String, - defaultValue: Boolean + defaultValue: Boolean, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(!defaultValue) } override fun getStringEvaluation( key: String, - defaultValue: String + defaultValue: String, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(defaultValue.reversed()) } override fun getIntegerEvaluation( key: String, - defaultValue: Int + defaultValue: Int, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(defaultValue * 100) } override fun getDoubleEvaluation( key: String, - defaultValue: Double + defaultValue: Double, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(defaultValue * 100) } override fun getObjectEvaluation( key: String, - defaultValue: Value + defaultValue: Value, + context: EvaluationContext? ): ProviderEvaluation { return ProviderEvaluation(Value.Null) } From 31e577324f71f86f40cc76f5e0a486290d520131 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 30 May 2023 09:46:46 +0200 Subject: [PATCH 11/56] add ktlint plugin with default settings --- build.gradle.kts | 3 ++- settings.gradle.kts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e862768..21f7f58 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { id("com.android.application").version("7.4.1").apply(false) id("com.android.library").version("7.4.1").apply(false) id("org.jetbrains.kotlin.android").version("1.8.0").apply(false) -} \ No newline at end of file + id("org.jlleitschuh.gradle.ktlint").version("11.3.2").apply(true) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 4673d67..0962e3d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,4 +15,4 @@ dependencyResolutionManagement { } } rootProject.name = "OpenFeature" -include(":OpenFeature") \ No newline at end of file +include(":OpenFeature") From da4f2e0a9c5a446277e77e700bc386b406055204 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 30 May 2023 09:54:02 +0200 Subject: [PATCH 12/56] formatted with ktlint --- .../main/java/dev/openfeature/sdk/Client.kt | 4 +- .../dev/openfeature/sdk/EvaluationContext.kt | 2 +- .../dev/openfeature/sdk/FeatureProvider.kt | 3 +- .../main/java/dev/openfeature/sdk/Features.kt | 2 +- .../openfeature/sdk/FlagEvaluationDetails.kt | 2 +- .../java/dev/openfeature/sdk/FlagValueType.kt | 1 - .../src/main/java/dev/openfeature/sdk/Hook.kt | 2 +- .../java/dev/openfeature/sdk/HookSupport.kt | 7 +- .../main/java/dev/openfeature/sdk/Metadata.kt | 2 +- .../dev/openfeature/sdk/MutableContext.kt | 4 +- .../dev/openfeature/sdk/MutableStructure.kt | 2 +- .../java/dev/openfeature/sdk/NoOpProvider.kt | 2 +- .../dev/openfeature/sdk/OpenFeatureAPI.kt | 2 +- .../dev/openfeature/sdk/OpenFeatureClient.kt | 14 ++-- .../dev/openfeature/sdk/ProviderEvaluation.kt | 2 +- .../main/java/dev/openfeature/sdk/Reason.kt | 18 +++-- .../main/java/dev/openfeature/sdk/Value.kt | 16 +++-- .../openfeature/sdk/exceptions/ErrorCode.kt | 6 ++ .../sdk/exceptions/OpenFeatureError.kt | 15 ++-- .../sdk/DeveloperExperienceTests.kt | 2 +- .../dev/openfeature/sdk/EvalContextTests.kt | 42 +++++++---- .../openfeature/sdk/FlagEvaluationsTests.kt | 4 +- .../java/dev/openfeature/sdk/HookSpecTests.kt | 35 ++++----- .../dev/openfeature/sdk/HookSupportTests.kt | 2 +- .../dev/openfeature/sdk/StructureTests.kt | 2 +- .../java/dev/openfeature/sdk/ValueTests.kt | 72 +++++++++---------- .../sdk/helpers/AlwaysBrokenProvider.kt | 12 +++- .../sdk/helpers/DoSomethingProvider.kt | 7 +- .../sdk/helpers/GenericSpyHookMock.kt | 3 +- 29 files changed, 168 insertions(+), 119 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt index befb176..2c6a246 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt @@ -1,8 +1,8 @@ package dev.openfeature.sdk -interface Client: Features { +interface Client : Features { val metadata: Metadata val hooks: List> fun addHooks(hooks: List>) -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt index fcd0da8..ac447ae 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt @@ -1,6 +1,6 @@ package dev.openfeature.sdk -interface EvaluationContext: Structure { +interface EvaluationContext : Structure { fun getTargetingKey(): String fun setTargetingKey(targetingKey: String) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt index aa7c534..1b12f00 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt @@ -6,6 +6,7 @@ interface FeatureProvider { // Called by OpenFeatureAPI whenever the new Provider is registered suspend fun initialize(initialContext: EvaluationContext?) + // Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) fun getBooleanEvaluation(key: String, defaultValue: Boolean, context: EvaluationContext?): ProviderEvaluation @@ -13,4 +14,4 @@ interface FeatureProvider { fun getIntegerEvaluation(key: String, defaultValue: Int, context: EvaluationContext?): ProviderEvaluation fun getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?): ProviderEvaluation fun getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?): ProviderEvaluation -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt index 62b3784..84c2830 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt @@ -21,4 +21,4 @@ interface Features { fun getObjectValue(key: String, defaultValue: Value, options: FlagEvaluationOptions): Value fun getObjectDetails(key: String, defaultValue: Value): FlagEvaluationDetails fun getObjectDetails(key: String, defaultValue: Value, options: FlagEvaluationOptions): FlagEvaluationDetails -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt index 623bb12..3b42a2e 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt @@ -22,4 +22,4 @@ fun FlagEvaluationDetails.Companion.from(providerEval: ProviderEvaluation providerEval.errorCode, providerEval.errorMessage ) -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt index b463140..ab3ef2b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt @@ -6,4 +6,3 @@ enum class FlagValueType { DOUBLE, OBJECT; } - diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt index 4b705e5..9361c49 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt @@ -6,4 +6,4 @@ interface Hook { fun error(ctx: HookContext, error: Exception, hints: Map) = Unit fun finallyAfter(ctx: HookContext, hints: Map) = Unit fun supportsFlagValueType(flagValueType: FlagValueType): Boolean = true -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt index a10d015..d062cd9 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt @@ -195,12 +195,11 @@ class HookSupport { } } - - private inline fun safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? { + private inline fun safeLet(p1: T1?, p2: T2?, block: (T1, T2) -> R?): R? { return if (p1 != null && p2 != null) block(p1, p2) else null } - private inline fun safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3)->R?): R? { + private inline fun safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3) -> R?): R? { return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null } -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt index 8fcc170..8382181 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt @@ -2,4 +2,4 @@ package dev.openfeature.sdk interface Metadata { val name: String? -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt index c7e54f7..2e29b8e 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt @@ -1,7 +1,7 @@ package dev.openfeature.sdk class MutableContext - (private var targetingKey: String = "", attributes: MutableMap = mutableMapOf()) : EvaluationContext { +(private var targetingKey: String = "", attributes: MutableMap = mutableMapOf()) : EvaluationContext { private var structure: MutableStructure = MutableStructure(attributes) override fun getTargetingKey(): String { return targetingKey @@ -49,4 +49,4 @@ class MutableContext return true } -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt index 7fff8db..6dcbff6 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt @@ -23,7 +23,7 @@ class MutableStructure(private var attributes: MutableMap = mutab } private fun convertValue(value: Value): Any? { - return when(value) { + return when (value) { is Value.List -> value.list.map { t -> convertValue(t) } is Value.Structure -> value.structure.mapValues { t -> convertValue(t.value) } is Value.Null -> return null diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt index 3e939d0..316a1c0 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt @@ -55,4 +55,4 @@ class NoOpProvider : FeatureProvider { } data class NoOpMetadata(override var name: String?) : Metadata -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index 3cbce8e..8626b19 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -50,4 +50,4 @@ object OpenFeatureAPI { fun clearHooks() { this.hooks = listOf() } -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt index 6c38254..68c32ec 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt @@ -1,6 +1,10 @@ package dev.openfeature.sdk -import dev.openfeature.sdk.FlagValueType.* +import dev.openfeature.sdk.FlagValueType.BOOLEAN +import dev.openfeature.sdk.FlagValueType.DOUBLE +import dev.openfeature.sdk.FlagValueType.INTEGER +import dev.openfeature.sdk.FlagValueType.OBJECT +import dev.openfeature.sdk.FlagValueType.STRING import dev.openfeature.sdk.exceptions.ErrorCode import dev.openfeature.sdk.exceptions.OpenFeatureError import dev.openfeature.sdk.exceptions.OpenFeatureError.GeneralError @@ -60,7 +64,7 @@ class OpenFeatureClient( override fun getStringDetails( key: String, - defaultValue: String, + defaultValue: String ): FlagEvaluationDetails { return getStringDetails(key, defaultValue, FlagEvaluationOptions()) } @@ -141,7 +145,7 @@ class OpenFeatureClient( override fun getObjectDetails( key: String, - defaultValue: Value, + defaultValue: Value ): FlagEvaluationDetails { return getObjectDetails(key, defaultValue, FlagEvaluationOptions()) } @@ -202,7 +206,7 @@ class OpenFeatureClient( defaultValue: V, provider: FeatureProvider ): ProviderEvaluation { - return when(flagValueType) { + return when (flagValueType) { BOOLEAN -> { val defaultBoolean = defaultValue as? Boolean ?: throw typeMatchingException val eval: ProviderEvaluation = provider.getBooleanEvaluation(key, defaultBoolean, context) @@ -232,4 +236,4 @@ class OpenFeatureClient( } data class ClientMetadata(override var name: String?) : Metadata -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt index 082c160..0bed120 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt @@ -8,4 +8,4 @@ data class ProviderEvaluation( var reason: String? = null, var errorCode: ErrorCode? = null, var errorMessage: String? = null - ) +) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt index dfca1a9..ef57797 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt @@ -3,20 +3,28 @@ package dev.openfeature.sdk enum class Reason { // The resolved value is static (no dynamic evaluation). STATIC, + // The resolved value fell back to a pre-configured value (no dynamic evaluation occurred or dynamic evaluation yielded no result). DEFAULT, + // The resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting. TARGETING_MATCH, + // The resolved value was the result of pseudorandom assignment. SPLIT, + // The resolved value was retrieved from cache. CACHED, - /// The resolved value was the result of the flag being disabled in the management system. + + // / The resolved value was the result of the flag being disabled in the management system. DISABLED, - /// The reason for the resolved value could not be determined. + + // / The reason for the resolved value could not be determined. UNKNOWN, - /// The resolved value is non-authoritative or possible out of date + + // / The resolved value is non-authoritative or possible out of date STALE, - /// The resolved value was the result of an error. + + // / The resolved value was the result of an error. ERROR -} \ No newline at end of file +} diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt index 2228bc0..8caf282 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt @@ -1,7 +1,6 @@ package dev.openfeature.sdk import dev.openfeature.sdk.exceptions.OpenFeatureError -import kotlinx.serialization.EncodeDefault.* import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -12,7 +11,6 @@ import kotlinx.serialization.json.JsonContentPolymorphicSerializer import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonObject import java.time.Instant -import java.util.* @Serializable(with = ValueSerializer::class) sealed interface Value { @@ -28,20 +26,27 @@ sealed interface Value { @Serializable data class String(val string: kotlin.String) : Value + @Serializable data class Boolean(val boolean: kotlin.Boolean) : Value + @Serializable data class Integer(val integer: Int) : Value + @Serializable data class Double(val double: kotlin.Double) : Value + @Serializable data class Instant(@Serializable(InstantSerializer::class) val instant: java.time.Instant) : Value + @Serializable data class Structure(val structure: Map) : Value + @Serializable data class List(val list: kotlin.collections.List) : Value + @Serializable - object Null: Value { + object Null : Value { override fun equals(other: Any?): kotlin.Boolean { return other is Null } @@ -51,9 +56,8 @@ sealed interface Value { } } - -object ValueSerializer: JsonContentPolymorphicSerializer(Value::class) { - override fun selectDeserializer(element: JsonElement) = when(element.jsonObject.keys) { +object ValueSerializer : JsonContentPolymorphicSerializer(Value::class) { + override fun selectDeserializer(element: JsonElement) = when (element.jsonObject.keys) { emptySet() -> Value.Null.serializer() setOf("string") -> Value.String.serializer() setOf("boolean") -> Value.Boolean.serializer() diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt index ed7ab8a..401e15f 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt @@ -3,16 +3,22 @@ package dev.openfeature.sdk.exceptions enum class ErrorCode { // The value was resolved before the provider was ready. PROVIDER_NOT_READY, + // The flag could not be found. FLAG_NOT_FOUND, + // An error was encountered parsing data, such as a flag configuration. PARSE_ERROR, + // The type of the flag value does not match the expected type. TYPE_MISMATCH, + // The provider requires a targeting key and one was not provided in the evaluation context. TARGETING_KEY_MISSING, + // The evaluation context does not meet provider requirements. INVALID_CONTEXT, + // The error was for a reason not enumerated above. GENERAL } diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt index 9653ff7..42ed596 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt @@ -3,13 +3,13 @@ package dev.openfeature.sdk.exceptions sealed class OpenFeatureError : Exception() { abstract fun errorCode(): ErrorCode - class GeneralError(override val message: String): OpenFeatureError() { + class GeneralError(override val message: String) : OpenFeatureError() { override fun errorCode(): ErrorCode { return ErrorCode.GENERAL } } - class FlagNotFoundError(flagKey: String): OpenFeatureError() { + class FlagNotFoundError(flagKey: String) : OpenFeatureError() { override val message: String = "Could not find flag named: $flagKey" override fun errorCode(): ErrorCode { return ErrorCode.FLAG_NOT_FOUND @@ -17,27 +17,28 @@ sealed class OpenFeatureError : Exception() { } class InvalidContextError( - override val message: String = "Invalid or missing context"): OpenFeatureError() { + override val message: String = "Invalid or missing context" + ) : OpenFeatureError() { override fun errorCode(): ErrorCode { return ErrorCode.INVALID_CONTEXT } } - class ParseError(override val message: String): OpenFeatureError() { + class ParseError(override val message: String) : OpenFeatureError() { override fun errorCode(): ErrorCode { return ErrorCode.PARSE_ERROR } } - class TargetingKeyMissingError(override val message: String = "Targeting key missing in evaluation context"): OpenFeatureError() { + class TargetingKeyMissingError(override val message: String = "Targeting key missing in evaluation context") : OpenFeatureError() { override fun errorCode(): ErrorCode { return ErrorCode.TARGETING_KEY_MISSING } } - class ProviderNotReadyError(override val message: String = "The value was resolved before the provider was ready"): OpenFeatureError() { + class ProviderNotReadyError(override val message: String = "The value was resolved before the provider was ready") : OpenFeatureError() { override fun errorCode(): ErrorCode { return ErrorCode.PROVIDER_NOT_READY } } -} \ No newline at end of file +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt index 5ff94fc..3072732 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt @@ -58,4 +58,4 @@ class DeveloperExperienceTests { Assert.assertEquals("Could not find flag named: test", details.errorMessage) Assert.assertEquals(Reason.ERROR.toString(), details.reason) } -} \ No newline at end of file +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt index c17e750..936880c 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt @@ -29,13 +29,20 @@ class EvalContextTests { ctx.add("instant", Value.Instant(now)) Assert.assertEquals(now, ctx.getValue("instant")?.asInstant()) } + @Test fun testContextStoresLists() { val ctx = MutableContext() - ctx.add("list", Value.List(listOf( - Value.Integer(3), - Value.String("4")))) + ctx.add( + "list", + Value.List( + listOf( + Value.Integer(3), + Value.String("4") + ) + ) + ) Assert.assertEquals(3, ctx.getValue("list")?.asList()?.get(0)?.asInteger()) Assert.assertEquals("4", ctx.getValue("list")?.asList()?.get(1)?.asString()) } @@ -44,9 +51,15 @@ class EvalContextTests { fun testContextStoresStructures() { val ctx = MutableContext() - ctx.add("struct", Value.Structure(mapOf( - "string" to Value.String("test"), - "int" to Value.Integer(3)))) + ctx.add( + "struct", + Value.Structure( + mapOf( + "string" to Value.String("test"), + "int" to Value.Integer(3) + ) + ) + ) Assert.assertEquals("test", ctx.getValue("struct")?.asStructure()?.get("string")?.asString()) Assert.assertEquals(3, ctx.getValue("struct")?.asStructure()?.get("int")?.asInteger()) } @@ -96,7 +109,7 @@ class EvalContextTests { val result = ctx.add("key1", Value.String("val1")) - ctx.add("key2", Value.String("val2")) + ctx.add("key2", Value.String("val2")) Assert.assertEquals("val1", result.getValue("key1")?.asString()) Assert.assertEquals("val2", result.getValue("key2")?.asString()) } @@ -122,7 +135,8 @@ class EvalContextTests { ctx.add("date", Value.Instant(now)) ctx.add("null", Value.Null) ctx.add("list", Value.List(listOf(Value.String("item1"), Value.Boolean(true)))) - ctx.add("structure", + ctx.add( + "structure", Value.Structure( mapOf( "field1" to Value.Integer(3), @@ -139,18 +153,18 @@ class EvalContextTests { "date" to now, "null" to null, "list" to listOf("item1", true), - "structure" to mapOf("field1" to 3, "field2" to 3.14), + "structure" to mapOf("field1" to 3, "field2" to 3.14) ) Assert.assertEquals(expected, ctx.asObjectMap()) } @Test fun compareContexts() { - val map: MutableMap = mutableMapOf("key" to Value.String("test")) - val map2: MutableMap = mutableMapOf("key" to Value.String("test")) - val ctx1 = MutableContext("user1", map) - val ctx2 = MutableContext("user1", map2) + val map: MutableMap = mutableMapOf("key" to Value.String("test")) + val map2: MutableMap = mutableMapOf("key" to Value.String("test")) + val ctx1 = MutableContext("user1", map) + val ctx2 = MutableContext("user1", map2) - Assert.assertEquals(ctx1, ctx2) + Assert.assertEquals(ctx1, ctx2) } } diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt index 024c06f..3f9abd9 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt @@ -59,8 +59,8 @@ class FlagEvaluationsTests { Assert.assertEquals(400, client.getIntegerValue(key, 4)) Assert.assertEquals(400, client.getIntegerValue(key, 4, FlagEvaluationOptions())) - Assert.assertEquals(40.0, client.getDoubleValue(key, 0.4),0.0) - Assert.assertEquals(40.0, client.getDoubleValue(key, 0.4, FlagEvaluationOptions()),0.0) + Assert.assertEquals(40.0, client.getDoubleValue(key, 0.4), 0.0) + Assert.assertEquals(40.0, client.getDoubleValue(key, 0.4, FlagEvaluationOptions()), 0.0) Assert.assertEquals(Value.Null, client.getObjectValue(key, Value.Structure(mapOf()))) Assert.assertEquals(Value.Null, client.getObjectValue(key, Value.Structure(mapOf()), FlagEvaluationOptions())) diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt index b759a86..227ccac 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt @@ -41,7 +41,7 @@ class HookSpecTests { fun testHookEvaluationOrder() = runTest { val provider = NoOpProvider() val evalOrder: MutableList = mutableListOf() - val addEval: (String) -> Unit = { eval: String -> evalOrder += eval} + val addEval: (String) -> Unit = { eval: String -> evalOrder += eval } provider.hooks = listOf(GenericSpyHookMock("provider", addEval)) OpenFeatureAPI.setProvider(provider) @@ -52,19 +52,22 @@ class HookSpecTests { client.getBooleanValue("key", false, flagOptions) - Assert.assertEquals(listOf( - "api before", - "client before", - "invocation before", - "provider before", - "provider after", - "invocation after", - "client after", - "api after", - "provider finallyAfter", - "invocation finallyAfter", - "client finallyAfter", - "api finallyAfter" - ), evalOrder) + Assert.assertEquals( + listOf( + "api before", + "client before", + "invocation before", + "provider before", + "provider after", + "invocation after", + "client after", + "api after", + "provider finallyAfter", + "invocation finallyAfter", + "client finallyAfter", + "api finallyAfter" + ), + evalOrder + ) } -} \ No newline at end of file +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt index 8791fc7..8814d3b 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt @@ -53,4 +53,4 @@ class HookSupportTests { Assert.assertEquals(1, hook.finallyCalledAfter) Assert.assertEquals(1, hook.errorCalled) } -} \ No newline at end of file +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt index fdab5e6..2e7d467 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt @@ -51,4 +51,4 @@ class StructureTests { Assert.assertEquals(structure1, structure2) } -} \ No newline at end of file +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt index ba0307a..48cbf73 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt @@ -83,42 +83,42 @@ class ValueTests { fun testJsonDecode() { val stringInstant = "2023-03-01T14:01:46Z" val json = "{" + - " \"structure\": {" + - " \"null\": {}," + - " \"text\": {" + - " \"string\": \"test\"" + - " }," + - " \"bool\": {" + - " \"boolean\": true" + - " }," + - " \"int\": {" + - " \"integer\": 3" + - " }," + - " \"double\": {" + - " \"double\": 4.5" + - " }," + - " \"date\": {" + - " \"instant\": \"$stringInstant\"" + - " }," + - " \"list\": {" + - " \"list\": [" + - " {" + - " \"boolean\": false" + - " }," + - " {" + - " \"integer\": 4" + - " }" + - " ]" + - " }," + - " \"structure\": {" + - " \"structure\": {" + - " \"int\": {" + - " \"integer\": 5" + - " }" + - " }" + - " }" + - " }" + - "}" + " \"structure\": {" + + " \"null\": {}," + + " \"text\": {" + + " \"string\": \"test\"" + + " }," + + " \"bool\": {" + + " \"boolean\": true" + + " }," + + " \"int\": {" + + " \"integer\": 3" + + " }," + + " \"double\": {" + + " \"double\": 4.5" + + " }," + + " \"date\": {" + + " \"instant\": \"$stringInstant\"" + + " }," + + " \"list\": {" + + " \"list\": [" + + " {" + + " \"boolean\": false" + + " }," + + " {" + + " \"integer\": 4" + + " }" + + " ]" + + " }," + + " \"structure\": {" + + " \"structure\": {" + + " \"int\": {" + + " \"integer\": 5" + + " }" + + " }" + + " }" + + " }" + + "}" val expectedValue = Value.Structure( mapOf( diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt index 569d073..7e1e18b 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt @@ -1,9 +1,15 @@ package dev.openfeature.sdk.helpers -import dev.openfeature.sdk.* +import dev.openfeature.sdk.EvaluationContext +import dev.openfeature.sdk.FeatureProvider +import dev.openfeature.sdk.Hook +import dev.openfeature.sdk.Metadata +import dev.openfeature.sdk.ProviderEvaluation +import dev.openfeature.sdk.Value import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError -class AlwaysBrokenProvider(override var hooks: List> = listOf(), override var metadata: Metadata = AlwaysBrokenMetadata()) : FeatureProvider { +class AlwaysBrokenProvider(override var hooks: List> = listOf(), override var metadata: Metadata = AlwaysBrokenMetadata()) : + FeatureProvider { override suspend fun initialize(initialContext: EvaluationContext?) { // no-op } @@ -56,4 +62,4 @@ class AlwaysBrokenProvider(override var hooks: List> = listOf(), overrid } class AlwaysBrokenMetadata(override var name: String? = "test") : Metadata -} \ No newline at end of file +} diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt index 09e4f6a..0220f9b 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt @@ -1,6 +1,11 @@ package dev.openfeature.sdk.helpers -import dev.openfeature.sdk.* +import dev.openfeature.sdk.EvaluationContext +import dev.openfeature.sdk.FeatureProvider +import dev.openfeature.sdk.Hook +import dev.openfeature.sdk.Metadata +import dev.openfeature.sdk.ProviderEvaluation +import dev.openfeature.sdk.Value class DoSomethingProvider(override val hooks: List> = listOf(), override val metadata: Metadata = DoSomethingMetadata()) : FeatureProvider { override suspend fun initialize(initialContext: EvaluationContext?) { diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt index e6b179b..e21061d 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt @@ -27,7 +27,6 @@ class GenericSpyHookMock(private var prefix: String = "", var addEval: (String) addEval("$prefix after") } - override fun error( ctx: HookContext, error: Exception, @@ -41,4 +40,4 @@ class GenericSpyHookMock(private var prefix: String = "", var addEval: (String) finallyCalledAfter += 1 addEval("$prefix finallyAfter") } -} \ No newline at end of file +} From fb0cac2f08cffdb0658209c2384ce9f9e34b6db7 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 30 May 2023 10:33:35 +0200 Subject: [PATCH 13/56] Add formatting info in Readme --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a48737e..2cb3b28 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ # OpenFeature Kotlin SDK -Kotlin implementation of the OpenFeature SDK \ No newline at end of file +Kotlin implementation of the OpenFeature SDK + +## Formatting + +This repo uses [ktlint](https://github.com/JLLeitschuh/ktlint-gradle) for formatting. + +Please consider adding a pre-commit hook for formatting using + +``` +./gradlew :addKtlintCheckGitPreCommitHook +``` +Manual formatting is done by invoking +``` +./gradlew :ktlintFormat +``` From 0322112260d3ffce8b0b47106badc4d516a2c3ad Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 30 May 2023 14:07:00 +0200 Subject: [PATCH 14/56] make sure the plugin runs on the OpenFeature module --- OpenFeature/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index 6321ba7..1a95545 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("maven-publish") + id("org.jlleitschuh.gradle.ktlint") kotlin("plugin.serialization") version "1.8.10" } From 8374ef3eeeb8e52830c5c6693d8d3c72a5967c68 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 30 May 2023 14:25:58 +0200 Subject: [PATCH 15/56] ran ./gradlew ktlintFormat --- .editorconfig | 133 ++++++++++++++++++ OpenFeature/build.gradle.kts | 7 +- .../dev/openfeature/sdk/BaseEvaluation.kt | 2 +- .../main/java/dev/openfeature/sdk/Client.kt | 2 +- .../dev/openfeature/sdk/EvaluationContext.kt | 2 +- .../dev/openfeature/sdk/FeatureProvider.kt | 9 +- .../main/java/dev/openfeature/sdk/Features.kt | 23 ++- .../openfeature/sdk/FlagEvaluationDetails.kt | 7 +- .../openfeature/sdk/FlagEvaluationOptions.kt | 2 +- .../java/dev/openfeature/sdk/FlagValueType.kt | 2 +- .../src/main/java/dev/openfeature/sdk/Hook.kt | 2 +- .../java/dev/openfeature/sdk/HookContext.kt | 2 +- .../java/dev/openfeature/sdk/HookSupport.kt | 39 ++++- .../main/java/dev/openfeature/sdk/Metadata.kt | 2 +- .../dev/openfeature/sdk/MutableContext.kt | 2 +- .../dev/openfeature/sdk/MutableStructure.kt | 2 +- .../java/dev/openfeature/sdk/NoOpProvider.kt | 2 +- .../dev/openfeature/sdk/OpenFeatureAPI.kt | 2 +- .../dev/openfeature/sdk/OpenFeatureClient.kt | 11 +- .../dev/openfeature/sdk/ProviderEvaluation.kt | 2 +- .../main/java/dev/openfeature/sdk/Reason.kt | 2 +- .../java/dev/openfeature/sdk/Structure.kt | 2 +- .../main/java/dev/openfeature/sdk/Value.kt | 2 +- .../openfeature/sdk/exceptions/ErrorCode.kt | 2 +- .../sdk/exceptions/OpenFeatureError.kt | 8 +- .../sdk/DeveloperExperienceTests.kt | 2 +- .../dev/openfeature/sdk/EvalContextTests.kt | 2 +- .../openfeature/sdk/FlagEvaluationsTests.kt | 2 +- .../java/dev/openfeature/sdk/HookSpecTests.kt | 2 +- .../dev/openfeature/sdk/HookSupportTests.kt | 2 +- .../openfeature/sdk/OpenFeatureClientTests.kt | 2 +- .../dev/openfeature/sdk/ProviderSpecTests.kt | 2 +- .../dev/openfeature/sdk/StructureTests.kt | 2 +- .../java/dev/openfeature/sdk/ValueTests.kt | 2 +- .../sdk/helpers/AlwaysBrokenProvider.kt | 2 +- .../sdk/helpers/DoSomethingProvider.kt | 2 +- .../sdk/helpers/GenericSpyHookMock.kt | 2 +- build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 39 files changed, 247 insertions(+), 52 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ea41f61 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,133 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 100 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = unset +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_use_custom_formatting_for_modifiers = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + + +# Copied from internal settings +[*.kt] +continuation_indent_size = 4 +ktlint_standard_annotation = disabled # Annotations don't have to be on separate lines +ktlint_standard_annotation-spacing = disabled # It is ok with a comment between annotation and declaration +ktlint_standard_argument-list-wrapping = disabled # It is ok to have multiple args on one line +ktlint_standard_spacing-between-declarations-with-annotations = disabled # No need for empty line between these +ktlint_standard_spacing-between-declarations-with-comments = disabled # No need for empty line between these +ktlint_standard_trailing-comma-on-call-site = disabled # Trailing commas are ok +ktlint_standard_trailing-comma-on-declaration-site = disabled # Trailing commas are ok + +# There is a bug in ktlint making multiline typealias indent in the wrong way: +# https://github.com/pinterest/ktlint/issues/1788 - enable this one this is fixed. +ktlint_standard_indent = disabled + +ktlint_experimental_class-naming = enabled +ktlint_experimental_fun-keyword-spacing = enabled +ktlint_experimental_function-type-reference-spacing = enabled +ktlint_experimental_nullable-type-spacing = enabled +ktlint_experimental_spacing-between-function-name-and-opening-parenthesis = enabled +ktlint_experimental_unnecessary-parentheses-before-trailing-lambda = enabled + +# Setting max line for Kotlin files, exactly as Detekt config (120 by default) to insure IDE and ktlint formatter +# format the code that is valid for Detekt, avoiding mismatch. +max_line_length=120 + +# Ignore max line for kotlin test files, the same as Detekt. +[**/{test,androidTest}/**/*.kt] +max_line_length=off diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index 1a95545..e9c4c78 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -20,7 +20,10 @@ android { buildTypes { getByName("release") { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } compileOptions { @@ -51,4 +54,4 @@ publishing { } } } -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/BaseEvaluation.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/BaseEvaluation.kt index 4aa2574..b8667d7 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/BaseEvaluation.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/BaseEvaluation.kt @@ -8,4 +8,4 @@ interface BaseEvaluation { val reason: String? val errorCode: ErrorCode? val errorMessage: String? -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt index 2c6a246..aedcb58 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt @@ -5,4 +5,4 @@ interface Client : Features { val hooks: List> fun addHooks(hooks: List>) -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt index ac447ae..3141278 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/EvaluationContext.kt @@ -7,4 +7,4 @@ interface EvaluationContext : Structure { // Make sure these are implemented for correct object comparisons override fun hashCode(): Int override fun equals(other: Any?): Boolean -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt index 1b12f00..70e7ae3 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt @@ -9,9 +9,14 @@ interface FeatureProvider { // Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) - fun getBooleanEvaluation(key: String, defaultValue: Boolean, context: EvaluationContext?): ProviderEvaluation + fun getBooleanEvaluation( + key: String, + defaultValue: Boolean, + context: EvaluationContext? + ): ProviderEvaluation + fun getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?): ProviderEvaluation fun getIntegerEvaluation(key: String, defaultValue: Int, context: EvaluationContext?): ProviderEvaluation fun getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?): ProviderEvaluation fun getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?): ProviderEvaluation -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt index 84c2830..ae8dcbe 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Features.kt @@ -4,11 +4,21 @@ interface Features { fun getBooleanValue(key: String, defaultValue: Boolean): Boolean fun getBooleanValue(key: String, defaultValue: Boolean, options: FlagEvaluationOptions): Boolean fun getBooleanDetails(key: String, defaultValue: Boolean): FlagEvaluationDetails - fun getBooleanDetails(key: String, defaultValue: Boolean, options: FlagEvaluationOptions): FlagEvaluationDetails + fun getBooleanDetails( + key: String, + defaultValue: Boolean, + options: FlagEvaluationOptions + ): FlagEvaluationDetails + fun getStringValue(key: String, defaultValue: String): String fun getStringValue(key: String, defaultValue: String, options: FlagEvaluationOptions): String fun getStringDetails(key: String, defaultValue: String): FlagEvaluationDetails - fun getStringDetails(key: String, defaultValue: String, options: FlagEvaluationOptions): FlagEvaluationDetails + fun getStringDetails( + key: String, + defaultValue: String, + options: FlagEvaluationOptions + ): FlagEvaluationDetails + fun getIntegerValue(key: String, defaultValue: Int): Int fun getIntegerValue(key: String, defaultValue: Int, options: FlagEvaluationOptions): Int fun getIntegerDetails(key: String, defaultValue: Int): FlagEvaluationDetails @@ -16,9 +26,14 @@ interface Features { fun getDoubleValue(key: String, defaultValue: Double): Double fun getDoubleValue(key: String, defaultValue: Double, options: FlagEvaluationOptions): Double fun getDoubleDetails(key: String, defaultValue: Double): FlagEvaluationDetails - fun getDoubleDetails(key: String, defaultValue: Double, options: FlagEvaluationOptions): FlagEvaluationDetails + fun getDoubleDetails( + key: String, + defaultValue: Double, + options: FlagEvaluationOptions + ): FlagEvaluationDetails + fun getObjectValue(key: String, defaultValue: Value): Value fun getObjectValue(key: String, defaultValue: Value, options: FlagEvaluationOptions): Value fun getObjectDetails(key: String, defaultValue: Value): FlagEvaluationDetails fun getObjectDetails(key: String, defaultValue: Value, options: FlagEvaluationOptions): FlagEvaluationDetails -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt index 3b42a2e..b9f5af9 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.kt @@ -13,7 +13,10 @@ data class FlagEvaluationDetails( companion object } -fun FlagEvaluationDetails.Companion.from(providerEval: ProviderEvaluation, flagKey: String): FlagEvaluationDetails { +fun FlagEvaluationDetails.Companion.from( + providerEval: ProviderEvaluation, + flagKey: String +): FlagEvaluationDetails { return FlagEvaluationDetails( flagKey, providerEval.value, @@ -22,4 +25,4 @@ fun FlagEvaluationDetails.Companion.from(providerEval: ProviderEvaluation providerEval.errorCode, providerEval.errorMessage ) -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.kt index 94659df..5fb0cc3 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.kt @@ -3,4 +3,4 @@ package dev.openfeature.sdk data class FlagEvaluationOptions( var hooks: List> = listOf(), var hookHints: Map = mapOf() -) +) \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt index ab3ef2b..382ba9d 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FlagValueType.kt @@ -5,4 +5,4 @@ enum class FlagValueType { INTEGER, DOUBLE, OBJECT; -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt index 9361c49..4b705e5 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Hook.kt @@ -6,4 +6,4 @@ interface Hook { fun error(ctx: HookContext, error: Exception, hints: Map) = Unit fun finallyAfter(ctx: HookContext, hints: Map) = Unit fun supportsFlagValueType(flagValueType: FlagValueType): Boolean = true -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt index 10e1d50..8811d75 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt @@ -7,4 +7,4 @@ data class HookContext( var ctx: EvaluationContext?, var clientMetadata: Metadata?, var providerMetadata: Metadata -) +) \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt index d062cd9..2f7aaa9 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/HookSupport.kt @@ -2,7 +2,12 @@ package dev.openfeature.sdk @Suppress("UNCHECKED_CAST") // TODO can we do better here? class HookSupport { - fun beforeHooks(flagValueType: FlagValueType, hookCtx: HookContext, hooks: List>, hints: Map) { + fun beforeHooks( + flagValueType: FlagValueType, + hookCtx: HookContext, + hooks: List>, + hints: Map + ) { hooks .reversed() .filter { hook -> hook.supportsFlagValueType(flagValueType) } @@ -37,7 +42,13 @@ class HookSupport { } } - fun afterHooks(flagValueType: FlagValueType, hookCtx: HookContext, details: FlagEvaluationDetails, hooks: List>, hints: Map) { + fun afterHooks( + flagValueType: FlagValueType, + hookCtx: HookContext, + details: FlagEvaluationDetails, + hooks: List>, + hints: Map + ) { hooks .filter { hook -> hook.supportsFlagValueType(flagValueType) } .forEach { hook -> @@ -93,7 +104,12 @@ class HookSupport { } } - fun afterAllHooks(flagValueType: FlagValueType, hookCtx: HookContext, hooks: List>, hints: Map) { + fun afterAllHooks( + flagValueType: FlagValueType, + hookCtx: HookContext, + hooks: List>, + hints: Map + ) { hooks .filter { hook -> hook.supportsFlagValueType(flagValueType) } .forEach { hook -> @@ -144,7 +160,13 @@ class HookSupport { } } - fun errorHooks(flagValueType: FlagValueType, hookCtx: HookContext, error: Exception, hooks: List>, hints: Map) { + fun errorHooks( + flagValueType: FlagValueType, + hookCtx: HookContext, + error: Exception, + hooks: List>, + hints: Map + ) { hooks .filter { hook -> hook.supportsFlagValueType(flagValueType) } .forEach { hook -> @@ -199,7 +221,12 @@ class HookSupport { return if (p1 != null && p2 != null) block(p1, p2) else null } - private inline fun safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3) -> R?): R? { + private inline fun safeLet( + p1: T1?, + p2: T2?, + p3: T3?, + block: (T1, T2, T3) -> R? + ): R? { return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null } -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt index 8382181..8fcc170 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt @@ -2,4 +2,4 @@ package dev.openfeature.sdk interface Metadata { val name: String? -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt index 2e29b8e..c1882ca 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt @@ -49,4 +49,4 @@ class MutableContext return true } -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt index 6dcbff6..003fc54 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt @@ -49,4 +49,4 @@ class MutableStructure(private var attributes: MutableMap = mutab return true } -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt index 316a1c0..3e939d0 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt @@ -55,4 +55,4 @@ class NoOpProvider : FeatureProvider { } data class NoOpMetadata(override var name: String?) : Metadata -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index 8626b19..3cbce8e 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -50,4 +50,4 @@ object OpenFeatureAPI { fun clearHooks() { this.hooks = listOf() } -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt index 68c32ec..a0cdd5b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt @@ -170,7 +170,14 @@ class OpenFeatureClient( val provider = openFeatureAPI.getProvider() ?: NoOpProvider() val mergedHooks: List> = provider.hooks + options.hooks + hooks + openFeatureAPI.hooks val context = openFeatureAPI.getEvaluationContext() - val hookCtx: HookContext = HookContext(key, flagValueType, defaultValue, context, this.metadata, provider.metadata) + val hookCtx: HookContext = HookContext( + key, + flagValueType, + defaultValue, + context, + this.metadata, + provider.metadata + ) try { hookSupport.beforeHooks(flagValueType, hookCtx, mergedHooks, hints) val providerEval = createProviderEvaluation( @@ -236,4 +243,4 @@ class OpenFeatureClient( } data class ClientMetadata(override var name: String?) : Metadata -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt index 0bed120..251ec74 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderEvaluation.kt @@ -8,4 +8,4 @@ data class ProviderEvaluation( var reason: String? = null, var errorCode: ErrorCode? = null, var errorMessage: String? = null -) +) \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt index ef57797..39b1175 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Reason.kt @@ -27,4 +27,4 @@ enum class Reason { // / The resolved value was the result of an error. ERROR -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt index 9681c58..31e5e2e 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt @@ -9,4 +9,4 @@ interface Structure { // Make sure these are implemented for correct object comparisons override fun hashCode(): Int override fun equals(other: Any?): Boolean -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt index 8caf282..05e5b9d 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt @@ -74,4 +74,4 @@ object InstantSerializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString()) override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString()) -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt index 401e15f..c282706 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/ErrorCode.kt @@ -21,4 +21,4 @@ enum class ErrorCode { // The error was for a reason not enumerated above. GENERAL -} +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt index 42ed596..4fd565b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/exceptions/OpenFeatureError.kt @@ -30,15 +30,17 @@ sealed class OpenFeatureError : Exception() { } } - class TargetingKeyMissingError(override val message: String = "Targeting key missing in evaluation context") : OpenFeatureError() { + class TargetingKeyMissingError(override val message: String = "Targeting key missing in evaluation context") : + OpenFeatureError() { override fun errorCode(): ErrorCode { return ErrorCode.TARGETING_KEY_MISSING } } - class ProviderNotReadyError(override val message: String = "The value was resolved before the provider was ready") : OpenFeatureError() { + class ProviderNotReadyError(override val message: String = "The value was resolved before the provider was ready") : + OpenFeatureError() { override fun errorCode(): ErrorCode { return ErrorCode.PROVIDER_NOT_READY } } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt index 3072732..5ff94fc 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt @@ -58,4 +58,4 @@ class DeveloperExperienceTests { Assert.assertEquals("Could not find flag named: test", details.errorMessage) Assert.assertEquals(Reason.ERROR.toString(), details.reason) } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt index 936880c..fe2e276 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt @@ -167,4 +167,4 @@ class EvalContextTests { Assert.assertEquals(ctx1, ctx2) } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt index 3f9abd9..9b3991b 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/FlagEvaluationsTests.kt @@ -129,4 +129,4 @@ class FlagEvaluationsTests { val client2 = OpenFeatureAPI.getClient("test") Assert.assertEquals("test", client2.metadata.name) } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt index 227ccac..c0d05ed 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSpecTests.kt @@ -70,4 +70,4 @@ class HookSpecTests { evalOrder ) } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt index 8814d3b..8791fc7 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt @@ -53,4 +53,4 @@ class HookSupportTests { Assert.assertEquals(1, hook.finallyCalledAfter) Assert.assertEquals(1, hook.errorCalled) } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/OpenFeatureClientTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/OpenFeatureClientTests.kt index dce8c2f..592e3c7 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/OpenFeatureClientTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/OpenFeatureClientTests.kt @@ -15,4 +15,4 @@ class OpenFeatureClientTests { val stringValue = OpenFeatureAPI.getClient().getStringValue("test", "defaultTest") assertEquals(stringValue, "defaultTest") } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt index 91e1e19..586ee22 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt @@ -60,4 +60,4 @@ class ProviderSpecTests { val objectResult = provider.getObjectEvaluation("key", Value.Null, MutableContext()) Assert.assertNotNull(objectResult.variant) } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt index 2e7d467..fdab5e6 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt @@ -51,4 +51,4 @@ class StructureTests { Assert.assertEquals(structure1, structure2) } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt index 48cbf73..6264bd2 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt @@ -136,4 +136,4 @@ class ValueTests { val decodedValue = Json.decodeFromString(Value.serializer(), json) Assert.assertEquals(expectedValue, decodedValue) } -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt index 7e1e18b..3467a77 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt @@ -62,4 +62,4 @@ class AlwaysBrokenProvider(override var hooks: List> = listOf(), overrid } class AlwaysBrokenMetadata(override var name: String? = "test") : Metadata -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt index 0220f9b..b0da3c1 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt @@ -59,4 +59,4 @@ class DoSomethingProvider(override val hooks: List> = listOf(), override return ProviderEvaluation(Value.Null) } class DoSomethingMetadata(override var name: String? = "something") : Metadata -} +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt index e21061d..6e1f180 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/GenericSpyHookMock.kt @@ -40,4 +40,4 @@ class GenericSpyHookMock(private var prefix: String = "", var addEval: (String) finallyCalledAfter += 1 addEval("$prefix finallyAfter") } -} +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 21f7f58..be9a85f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,4 +4,4 @@ plugins { id("com.android.library").version("7.4.1").apply(false) id("org.jetbrains.kotlin.android").version("1.8.0").apply(false) id("org.jlleitschuh.gradle.ktlint").version("11.3.2").apply(true) -} +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0962e3d..4673d67 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,4 +15,4 @@ dependencyResolutionManagement { } } rootProject.name = "OpenFeature" -include(":OpenFeature") +include(":OpenFeature") \ No newline at end of file From 7d4030e3eb992a360ff382dd5e4b952034749dd6 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 30 May 2023 16:29:49 +0200 Subject: [PATCH 16/56] Update to readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2cb3b28..5d32270 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ This repo uses [ktlint](https://github.com/JLLeitschuh/ktlint-gradle) for format Please consider adding a pre-commit hook for formatting using ``` -./gradlew :addKtlintCheckGitPreCommitHook +./gradlew addKtlintCheckGitPreCommitHook ``` Manual formatting is done by invoking ``` -./gradlew :ktlintFormat +./gradlew ktlintFormat ``` From 0f8230c84f238964fbe7315887e897dfb2460672 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Thu, 1 Jun 2023 09:31:37 +0200 Subject: [PATCH 17/56] publish sdk sources --- OpenFeature/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index e9c4c78..cbf23bc 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -33,6 +33,12 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } } dependencies { From 576a4be647a57c5e80c000185687468806ce98b0 Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Mon, 26 Jun 2023 11:01:37 +0200 Subject: [PATCH 18/56] decrease minSDK to 21 --- OpenFeature/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index cbf23bc..c0a3ed1 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -11,7 +11,7 @@ android { compileSdk = 33 defaultConfig { - minSdk = 26 + minSdk = 21 version = "0.0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 0a598fa2f1682ef6672dc5a9ced645e4ec4860ca Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Wed, 28 Jun 2023 10:04:51 +0200 Subject: [PATCH 19/56] Move away from java.time.Instant For the main source set we want to back the Instant with a Date object to allow for a lower minSDK without desugaring --- .../main/java/dev/openfeature/sdk/Value.kt | 21 +++++++++++++------ .../dev/openfeature/sdk/EvalContextTests.kt | 8 +++---- .../dev/openfeature/sdk/StructureTests.kt | 4 ++-- .../java/dev/openfeature/sdk/ValueTests.kt | 5 +++-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt index 05e5b9d..ad25cb6 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt @@ -1,5 +1,6 @@ package dev.openfeature.sdk +import android.annotation.SuppressLint import dev.openfeature.sdk.exceptions.OpenFeatureError import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -10,7 +11,9 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonContentPolymorphicSerializer import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonObject -import java.time.Instant +import java.text.SimpleDateFormat +import java.util.Date +import java.util.TimeZone @Serializable(with = ValueSerializer::class) sealed interface Value { @@ -19,7 +22,7 @@ sealed interface Value { fun asBoolean(): kotlin.Boolean? = if (this is Boolean) boolean else null fun asInteger(): Int? = if (this is Integer) integer else null fun asDouble(): kotlin.Double? = if (this is Double) double else null - fun asInstant(): java.time.Instant? = if (this is Instant) instant else null + fun asInstant(): Date? = if (this is Instant) instant else null fun asList(): kotlin.collections.List? = if (this is List) list else null fun asStructure(): Map? = if (this is Structure) structure else null fun isNull(): kotlin.Boolean = this is Null @@ -37,7 +40,7 @@ sealed interface Value { data class Double(val double: kotlin.Double) : Value @Serializable - data class Instant(@Serializable(InstantSerializer::class) val instant: java.time.Instant) : Value + data class Instant(@Serializable(DateSerializer::class) val instant: Date) : Value @Serializable data class Structure(val structure: Map) : Value @@ -70,8 +73,14 @@ object ValueSerializer : JsonContentPolymorphicSerializer(Value::class) { } } -object InstantSerializer : KSerializer { +object DateSerializer : KSerializer { + @SuppressLint("SimpleDateFormat") + private val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { timeZone = TimeZone.getTimeZone("UTC") } override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString()) - override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString()) + override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(df.format(value)) + override fun deserialize(decoder: Decoder): Date = try { + df.parse(decoder.decodeString()) ?: throw IllegalArgumentException("unable to parse ${decoder.decodeString()}") + } catch (e: Exception) { + throw IllegalArgumentException(e) + } } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt index fe2e276..ff3c63b 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt @@ -2,7 +2,7 @@ package dev.openfeature.sdk import org.junit.Assert import org.junit.Test -import java.time.Instant +import java.util.Date class EvalContextTests { @@ -16,7 +16,7 @@ class EvalContextTests { @Test fun testContextStoresPrimitiveValues() { val ctx = MutableContext() - val now = Instant.now() + val now = Date() ctx.add("string", Value.String("value")) Assert.assertEquals("value", ctx.getValue("string")?.asString()) @@ -67,7 +67,7 @@ class EvalContextTests { @Test fun testContextCanConvertToMap() { val ctx = MutableContext() - val now = Instant.now() + val now = Date() ctx.add("str1", Value.String("test1")) ctx.add("str2", Value.String("test2")) ctx.add("bool1", Value.Boolean(true)) @@ -126,7 +126,7 @@ class EvalContextTests { @Test fun testContextConvertsToObjectMap() { val key = "key1" - val now = Instant.now() + val now = Date() val ctx = MutableContext(key) ctx.add("string", Value.String("value")) ctx.add("bool", Value.Boolean(false)) diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt index fdab5e6..d15cf9e 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt @@ -2,7 +2,7 @@ package dev.openfeature.sdk import org.junit.Assert import org.junit.Test -import java.time.Instant +import java.util.Date class StructureTests { @@ -23,7 +23,7 @@ class StructureTests { @Test fun testAddAndGetReturnValues() { - val now = Instant.now() + val now = Date() val structure = MutableStructure() structure.add("bool", Value.Boolean(true)) structure.add("string", Value.String("val")) diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt index 6264bd2..91e8d62 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.json.encodeToJsonElement import org.junit.Assert import org.junit.Test import java.time.Instant +import java.util.Date class ValueTests { @@ -59,7 +60,7 @@ class ValueTests { @Test fun testEncodeDecode() { - val date = Instant.parse("2023-03-01T14:01:46Z") + val date = Date.from(Instant.parse("2023-03-01T14:01:46Z")) val value = Value.Structure( mapOf( "null" to Value.Null, @@ -127,7 +128,7 @@ class ValueTests { "bool" to Value.Boolean(true), "int" to Value.Integer(3), "double" to Value.Double(4.5), - "date" to Value.Instant(Instant.parse(stringInstant)), + "date" to Value.Instant(Date.from(Instant.parse(stringInstant))), "list" to Value.List(listOf(Value.Boolean(false), Value.Integer(4))), "structure" to Value.Structure(mapOf("int" to Value.Integer(5))) ) From 5f8f8bb4ca00e5c1226ae1271eb72248562f6401 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Fri, 30 Jun 2023 07:52:51 +0200 Subject: [PATCH 20/56] Revert "Decrease minSDK to 21" --- OpenFeature/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index c0a3ed1..cbf23bc 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -11,7 +11,7 @@ android { compileSdk = 33 defaultConfig { - minSdk = 21 + minSdk = 26 version = "0.0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 0c2cafe4814b00417bb65d002441ee0fee6ad16e Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 30 Jun 2023 11:39:16 +0200 Subject: [PATCH 21/56] Add FOSS docs --- .github/CODE_OF_CONDUCT.md | 3 + CONTRIBUTING.md | 17 ++++ LICENSE | 202 +++++++++++++++++++++++++++++++++++++ OWNERS.md | 10 ++ README.md | 51 ++++++++-- SECURITY.md | 16 +++ catalog-info.yaml | 7 ++ 7 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 OWNERS.md create mode 100644 SECURITY.md create mode 100644 catalog-info.yaml diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8709cb6 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +Please refer to the [OpenFeature community page](https://openfeature.dev/community/#code-of-conduct) for more information on Code of Conduct. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..048b5b8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +## Getting Started + +OpenFeature is not keen on vendor-specific stuff in this library, but if there are changes that need to happen in the spec to enable vendor-specific stuff in user code or other extension points, check out [the spec](https://github.com/open-feature/spec). + +## Formatting + +This repo uses [ktlint](https://github.com/JLLeitschuh/ktlint-gradle) for formatting. + +Please consider adding a pre-commit hook for formatting using + +``` +./gradlew addKtlintCheckGitPreCommitHook +``` +Manual formatting is done by invoking +``` +./gradlew ktlintFormat +``` \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/OWNERS.md b/OWNERS.md new file mode 100644 index 0000000..27208a8 --- /dev/null +++ b/OWNERS.md @@ -0,0 +1,10 @@ +# Owners + +- See [CONTRIBUTING.md](CONTRIBUTING.md) for general contribution guidelines. + +## Core Developers + +- Fabrizio Demaria (fabriziodemaria, Spotify) +- Nicklas Lundin (nicklasl, Spotify) +- Vahid Torkaman (vahidlazio, Spotify) +- Nicky Bondarenko (nickybondarenko, Spotify) \ No newline at end of file diff --git a/README.md b/README.md index 5d32270..9a0149b 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,54 @@ # OpenFeature Kotlin SDK -Kotlin implementation of the OpenFeature SDK +![Status](https://img.shields.io/badge/lifecycle-alpha-a0c3d2.svg) -## Formatting +What is OpenFeature? +[OpenFeature][openfeature-website] is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. -This repo uses [ktlint](https://github.com/JLLeitschuh/ktlint-gradle) for formatting. +Why standardize feature flags? +Standardizing feature flags unifies tools and vendors behind a common interface which avoids vendor lock-in at the code level. Additionally, it offers a framework for building extensions and integrations and allows providers to focus on their unique value proposition. -Please consider adding a pre-commit hook for formatting using +This Kotlin implementation of an OpenFeature SDK has been developed at Spotify, and currently made available and maintained within the Spotify Open Source Software organization. Part of our roadmap is for the OpenFeature community to evaluate this implementation and potentially include it in the existing ecosystem of [OpenFeature SDKs][openfeature-sdks]. +## Requirements + +- The Android minSdk version supported is: `26`. + +Note that this library is intended to be used in a mobile context, and has not been evaluated for use in other type of applications (e.g. server applications). + + +## Usage + +### Adding the library dependency (WORK IN PROGRESS ⚠️) + +This library is not published to central repositories yet. +Clone this repository and run the following to install the library locally: ``` -./gradlew addKtlintCheckGitPreCommitHook +./gradlew publishToMavenLocal ``` -Manual formatting is done by invoking +The Android project must include `mavenLocal()` in `settings.gradle`. + +You can now add the OpenFeature SDK dependency: ``` -./gradlew ktlintFormat +implementation(""dev.openfeature:kotlin-sdk:0.0.1-SNAPSHOT") ``` + +### Resolving a flag +```kotlin +import dev.openfeature.sdk.* + +// Change NoOpProvider with your actual provider +OpenFeatureAPI.setProvider(NoOpProvider(), MutableContext()) +val flagValue = OpenFeatureAPI.getClient().getBooleanValue("boolFlag", false) +``` +Setting a new provider or setting a new evaluation context are asynchronous operations. The provider might execute I/O operations as part of these method calls (e.g. fetching flag evaluations from the backend and store them in a local cache). It's advised to not interact with the OpenFeature client until the `setProvider()` or `setEvaluationContext()` functions have returned successfully. + +Please refer to our [documentation on static-context APIs](https://github.com/open-feature/spec/pull/171) for further information on how these APIs are structured for the use-case of mobile clients. + + +### Providers + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. You’ll then need to write the provider itself. This can be accomplished by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +[openfeature-website]: https://openfeature.dev +[openfeature-sdks]: https://openfeature.dev/docs/reference/technologies/ \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1bfe7e6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +We're big believers in protecting your privacy and security. As a company, we not only have a vested interest, but also a deep desire to see the Internet remain as safe as possible for us all. + +So, needless to say, we take security issues very seriously. + +In our opinion, the practice of 'responsible disclosure' is the best way to safeguard the Internet. It allows individuals to notify companies like Spotify of any security threats before going public with the information. This gives us a fighting chance to resolve the problem before the criminally-minded become aware of it. + +Responsible disclosure is the industry best practice, and we recommend it as a procedure to anyone researching security vulnerabilities. + +## Reporting a Vulnerability + +If you have discovered a vulnerability in this open source project or another serious security issue, +please submit it to the Spotify bounty program hosted by HackerOne. + +https://hackerone.com/spotify \ No newline at end of file diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..ab5905f --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,7 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: openfeature-kotlin-sdk +spec: + type: library + owner: hawkeye From 18417216623f241a7f10efd1b3fe67b62355c609 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 3 Jul 2023 11:37:40 +0200 Subject: [PATCH 22/56] Update minSDK --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a0149b..171088b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This Kotlin implementation of an OpenFeature SDK has been developed at Spotify, ## Requirements -- The Android minSdk version supported is: `26`. +- The Android minSdk version supported is: `21`. Note that this library is intended to be used in a mobile context, and has not been evaluated for use in other type of applications (e.g. server applications). From 786e10fc5192acb2af47bae244644dd12c015c53 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Mon, 3 Jul 2023 11:35:22 +0200 Subject: [PATCH 23/56] minSDK is 21 --- OpenFeature/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index cbf23bc..c0a3ed1 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -11,7 +11,7 @@ android { compileSdk = 33 defaultConfig { - minSdk = 26 + minSdk = 21 version = "0.0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 669bd4eb685d2f0b76ec4a459d9b9e490f489e43 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 4 Jul 2023 17:27:33 +0200 Subject: [PATCH 24/56] Smaller README typo fix --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 171088b..fc4e81a 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,10 @@ Clone this repository and run the following to install the library locally: The Android project must include `mavenLocal()` in `settings.gradle`. You can now add the OpenFeature SDK dependency: -``` -implementation(""dev.openfeature:kotlin-sdk:0.0.1-SNAPSHOT") +```kotlin +dependencies { + implementation("dev.openfeature:kotlin-sdk:0.0.1-SNAPSHOT") +} ``` ### Resolving a flag From 23594442d69efc7c0bb974edcedcb06be9f4bc0d Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Wed, 5 Jul 2023 21:58:32 +0200 Subject: [PATCH 25/56] Format ISO8601 dates with milliseconds. Serializes Date objects (backing of Value.Instant) as ISO8601 WITH milliseconds. Also adds fallback parsing support where milliseconds are not passed. --- .../main/java/dev/openfeature/sdk/Value.kt | 22 +++++++++++++------ .../java/dev/openfeature/sdk/ValueTests.kt | 10 ++++++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt index ad25cb6..20fa25c 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt @@ -53,6 +53,7 @@ sealed interface Value { override fun equals(other: Any?): kotlin.Boolean { return other is Null } + override fun hashCode(): Int { return javaClass.hashCode() } @@ -73,14 +74,21 @@ object ValueSerializer : JsonContentPolymorphicSerializer(Value::class) { } } +@SuppressLint("SimpleDateFormat") object DateSerializer : KSerializer { - @SuppressLint("SimpleDateFormat") - private val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { timeZone = TimeZone.getTimeZone("UTC") } + private val dateFormatter = + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").apply { timeZone = TimeZone.getTimeZone("UTC") } + private val fallbackDateFormatter = + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { timeZone = TimeZone.getTimeZone("UTC") } override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(df.format(value)) - override fun deserialize(decoder: Decoder): Date = try { - df.parse(decoder.decodeString()) ?: throw IllegalArgumentException("unable to parse ${decoder.decodeString()}") - } catch (e: Exception) { - throw IllegalArgumentException(e) + override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(dateFormatter.format(value)) + override fun deserialize(decoder: Decoder): Date = with(decoder.decodeString()) { + try { + dateFormatter.parse(this) + ?: throw IllegalArgumentException("unable to parse $this") + } catch (e: Exception) { + fallbackDateFormatter.parse(this) + ?: throw IllegalArgumentException("unable to parse $this") + } } } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt index 91e8d62..e71727e 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt @@ -48,8 +48,12 @@ class ValueTests { @Test fun testStructShouldConvertToStruct() { - val value = Value.Structure(mapOf("field1" to Value.Integer(3), "field2" to Value.String("test"))) - Assert.assertEquals(value.asStructure(), mapOf("field1" to Value.Integer(3), "field2" to Value.String("test"))) + val value = + Value.Structure(mapOf("field1" to Value.Integer(3), "field2" to Value.String("test"))) + Assert.assertEquals( + value.asStructure(), + mapOf("field1" to Value.Integer(3), "field2" to Value.String("test")) + ) } @Test @@ -82,7 +86,7 @@ class ValueTests { @Test fun testJsonDecode() { - val stringInstant = "2023-03-01T14:01:46Z" + val stringInstant = "2023-03-01T14:01:46.321Z" val json = "{" + " \"structure\": {" + " \"null\": {}," + From 2a1e54355898335ea4a847c4e184ad3c8f77fd64 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Thu, 6 Jul 2023 11:29:31 +0200 Subject: [PATCH 26/56] Set JavaVersion to 1.8 ... for kotlinOptions and compileOptions, to verify that and > Java1.8 issues are found in compile time --- OpenFeature/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index c0a3ed1..c1bdd10 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -27,11 +27,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_1_8.toString() } publishing { singleVariant("release") { From 258046befd50d4149807515ddb89ab8ae8b2c352 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Tue, 4 Jul 2023 13:28:40 +0200 Subject: [PATCH 27/56] Rename the Instant value to Date Aligns with the Swift SDK API breaking change --- .../main/java/dev/openfeature/sdk/MutableStructure.kt | 2 +- OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt | 6 +++--- .../test/java/dev/openfeature/sdk/EvalContextTests.kt | 10 +++++----- .../test/java/dev/openfeature/sdk/StructureTests.kt | 4 ++-- .../src/test/java/dev/openfeature/sdk/ValueTests.kt | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt index 003fc54..fb498cd 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt @@ -30,7 +30,7 @@ class MutableStructure(private var attributes: MutableMap = mutab is Value.String -> value.asString() is Value.Boolean -> value.asBoolean() is Value.Integer -> value.asInteger() - is Value.Instant -> value.asInstant() + is Value.Date -> value.asDate() is Value.Double -> value.asDouble() } } diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt index 20fa25c..0cd35b6 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Value.kt @@ -22,7 +22,7 @@ sealed interface Value { fun asBoolean(): kotlin.Boolean? = if (this is Boolean) boolean else null fun asInteger(): Int? = if (this is Integer) integer else null fun asDouble(): kotlin.Double? = if (this is Double) double else null - fun asInstant(): Date? = if (this is Instant) instant else null + fun asDate(): java.util.Date? = if (this is Date) date else null fun asList(): kotlin.collections.List? = if (this is List) list else null fun asStructure(): Map? = if (this is Structure) structure else null fun isNull(): kotlin.Boolean = this is Null @@ -40,7 +40,7 @@ sealed interface Value { data class Double(val double: kotlin.Double) : Value @Serializable - data class Instant(@Serializable(DateSerializer::class) val instant: Date) : Value + data class Date(@Serializable(DateSerializer::class) val date: java.util.Date) : Value @Serializable data class Structure(val structure: Map) : Value @@ -67,7 +67,7 @@ object ValueSerializer : JsonContentPolymorphicSerializer(Value::class) { setOf("boolean") -> Value.Boolean.serializer() setOf("integer") -> Value.Integer.serializer() setOf("double") -> Value.Double.serializer() - setOf("instant") -> Value.Instant.serializer() + setOf("date") -> Value.Date.serializer() setOf("list") -> Value.List.serializer() setOf("structure") -> Value.Structure.serializer() else -> throw OpenFeatureError.ParseError("couldn't find deserialization key for Value") diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt index ff3c63b..5c09aa0 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt @@ -26,8 +26,8 @@ class EvalContextTests { Assert.assertEquals(3, ctx.getValue("int")?.asInteger()) ctx.add("double", Value.Double(3.14)) Assert.assertEquals(3.14, ctx.getValue("double")?.asDouble()) - ctx.add("instant", Value.Instant(now)) - Assert.assertEquals(now, ctx.getValue("instant")?.asInstant()) + ctx.add("date", Value.Date(now)) + Assert.assertEquals(now, ctx.getValue("date")?.asDate()) } @Test @@ -74,7 +74,7 @@ class EvalContextTests { ctx.add("bool2", Value.Boolean(false)) ctx.add("int1", Value.Integer(4)) ctx.add("int2", Value.Integer(2)) - ctx.add("dt", Value.Instant(now)) + ctx.add("dt", Value.Date(now)) ctx.add("obj", Value.Structure(mapOf("val1" to Value.Integer(1), "val2" to Value.String("2")))) val map = ctx.asMap() @@ -85,7 +85,7 @@ class EvalContextTests { Assert.assertEquals(false, map["bool2"]?.asBoolean()) Assert.assertEquals(4, map["int1"]?.asInteger()) Assert.assertEquals(2, map["int2"]?.asInteger()) - Assert.assertEquals(now, map["dt"]?.asInstant()) + Assert.assertEquals(now, map["dt"]?.asDate()) Assert.assertEquals(1, structure?.get("val1")?.asInteger()) Assert.assertEquals("2", structure?.get("val2")?.asString()) } @@ -132,7 +132,7 @@ class EvalContextTests { ctx.add("bool", Value.Boolean(false)) ctx.add("integer", Value.Integer(1)) ctx.add("double", Value.Double(1.2)) - ctx.add("date", Value.Instant(now)) + ctx.add("date", Value.Date(now)) ctx.add("null", Value.Null) ctx.add("list", Value.List(listOf(Value.String("item1"), Value.Boolean(true)))) ctx.add( diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt index d15cf9e..340344f 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt @@ -29,7 +29,7 @@ class StructureTests { structure.add("string", Value.String("val")) structure.add("int", Value.Integer(13)) structure.add("double", Value.Double(0.5)) - structure.add("date", Value.Instant(now)) + structure.add("date", Value.Date(now)) structure.add("list", Value.List(listOf())) structure.add("structure", Value.Structure(mapOf())) @@ -37,7 +37,7 @@ class StructureTests { Assert.assertEquals("val", structure.getValue("string")?.asString()) Assert.assertEquals(13, structure.getValue("int")?.asInteger()) Assert.assertEquals(0.5, structure.getValue("double")?.asDouble()) - Assert.assertEquals(now, structure.getValue("date")?.asInstant()) + Assert.assertEquals(now, structure.getValue("date")?.asDate()) Assert.assertEquals(listOf(), structure.getValue("list")?.asList()) Assert.assertEquals(mapOf(), structure.getValue("structure")?.asStructure()) } diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt index e71727e..4004036 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ValueTests.kt @@ -72,7 +72,7 @@ class ValueTests { "bool" to Value.Boolean(true), "int" to Value.Integer(3), "double" to Value.Double(4.5), - "date" to Value.Instant(date), + "date" to Value.Date(date), "list" to Value.List(listOf(Value.Boolean(false), Value.Integer(4))), "structure" to Value.Structure(mapOf("int" to Value.Integer(5))) ) @@ -86,7 +86,7 @@ class ValueTests { @Test fun testJsonDecode() { - val stringInstant = "2023-03-01T14:01:46.321Z" + val stringDateTime = "2023-03-01T14:01:46Z" val json = "{" + " \"structure\": {" + " \"null\": {}," + @@ -103,7 +103,7 @@ class ValueTests { " \"double\": 4.5" + " }," + " \"date\": {" + - " \"instant\": \"$stringInstant\"" + + " \"date\": \"$stringDateTime\"" + " }," + " \"list\": {" + " \"list\": [" + @@ -132,7 +132,7 @@ class ValueTests { "bool" to Value.Boolean(true), "int" to Value.Integer(3), "double" to Value.Double(4.5), - "date" to Value.Instant(Date.from(Instant.parse(stringInstant))), + "date" to Value.Date(Date.from(Instant.parse(stringDateTime))), "list" to Value.List(listOf(Value.Boolean(false), Value.Integer(4))), "structure" to Value.Structure(mapOf("int" to Value.Integer(5))) ) From 573c8a0694ac81103226af9161d27c0d290fcb89 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Fri, 7 Jul 2023 08:47:26 +0200 Subject: [PATCH 28/56] chore: Bump gradle wrapper to 8.2 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6d04661..f1f0a50 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 14 14:06:40 CEST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 8316ddc50dbc65aec52f7bbcc6102ccf7ceea233 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Fri, 7 Jul 2023 09:00:48 +0200 Subject: [PATCH 29/56] fix: drop android application plugin --- build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index be9a85f..9c79167 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application").version("7.4.1").apply(false) id("com.android.library").version("7.4.1").apply(false) id("org.jetbrains.kotlin.android").version("1.8.0").apply(false) id("org.jlleitschuh.gradle.ktlint").version("11.3.2").apply(true) From c47bedc21139689d4f46f27831f4c329fdfbb915 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 12 Jul 2023 11:40:37 +0200 Subject: [PATCH 30/56] Make Structure and Context impl immutable --- ...{MutableContext.kt => ImmutableContext.kt} | 15 +- ...ableStructure.kt => ImmutableStructure.kt} | 11 +- .../java/dev/openfeature/sdk/Structure.kt | 2 +- .../sdk/DeveloperExperienceTests.kt | 8 +- .../dev/openfeature/sdk/EvalContextTests.kt | 142 +++++++++--------- .../dev/openfeature/sdk/HookSupportTests.kt | 2 +- .../dev/openfeature/sdk/ProviderSpecTests.kt | 24 +-- .../dev/openfeature/sdk/StructureTests.kt | 29 ++-- 8 files changed, 113 insertions(+), 120 deletions(-) rename OpenFeature/src/main/java/dev/openfeature/sdk/{MutableContext.kt => ImmutableContext.kt} (70%) rename OpenFeature/src/main/java/dev/openfeature/sdk/{MutableStructure.kt => ImmutableStructure.kt} (79%) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/ImmutableContext.kt similarity index 70% rename from OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt rename to OpenFeature/src/main/java/dev/openfeature/sdk/ImmutableContext.kt index c1882ca..2d906fa 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableContext.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/ImmutableContext.kt @@ -1,8 +1,8 @@ package dev.openfeature.sdk -class MutableContext -(private var targetingKey: String = "", attributes: MutableMap = mutableMapOf()) : EvaluationContext { - private var structure: MutableStructure = MutableStructure(attributes) +class ImmutableContext +(private var targetingKey: String = "", attributes: Map = mapOf()) : EvaluationContext { + private var structure: ImmutableStructure = ImmutableStructure(attributes) override fun getTargetingKey(): String { return targetingKey } @@ -19,7 +19,7 @@ class MutableContext return structure.getValue(key) } - override fun asMap(): MutableMap { + override fun asMap(): Map { return structure.asMap() } @@ -27,11 +27,6 @@ class MutableContext return structure.asObjectMap() } - fun add(key: String, value: Value): MutableContext { - structure.add(key, value) - return this - } - override fun hashCode(): Int { var result = targetingKey.hashCode() result = 31 * result + structure.hashCode() @@ -42,7 +37,7 @@ class MutableContext if (this === other) return true if (javaClass != other?.javaClass) return false - other as MutableContext + other as ImmutableContext if (targetingKey != other.targetingKey) return false if (structure != other.structure) return false diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/ImmutableStructure.kt similarity index 79% rename from OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt rename to OpenFeature/src/main/java/dev/openfeature/sdk/ImmutableStructure.kt index fb498cd..09f20d5 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/MutableStructure.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/ImmutableStructure.kt @@ -1,6 +1,6 @@ package dev.openfeature.sdk -class MutableStructure(private var attributes: MutableMap = mutableMapOf()) : Structure { +class ImmutableStructure(private var attributes: Map = mapOf()) : Structure { override fun keySet(): Set { return attributes.keys } @@ -9,7 +9,7 @@ class MutableStructure(private var attributes: MutableMap = mutab return attributes[key] } - override fun asMap(): MutableMap { + override fun asMap(): Map { return attributes } @@ -17,11 +17,6 @@ class MutableStructure(private var attributes: MutableMap = mutab return attributes.mapValues { convertValue(it.value) } } - fun add(key: String, value: Value): MutableStructure { - attributes[key] = value - return this - } - private fun convertValue(value: Value): Any? { return when (value) { is Value.List -> value.list.map { t -> convertValue(t) } @@ -43,7 +38,7 @@ class MutableStructure(private var attributes: MutableMap = mutab if (this === other) return true if (javaClass != other?.javaClass) return false - other as MutableStructure + other as ImmutableStructure if (attributes != other.attributes) return false diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt index 31e5e2e..b96d7d7 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Structure.kt @@ -3,7 +3,7 @@ package dev.openfeature.sdk interface Structure { fun keySet(): Set fun getValue(key: String): Value? - fun asMap(): MutableMap + fun asMap(): Map fun asObjectMap(): Map // Make sure these are implemented for correct object comparisons diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt index 5ff94fc..3e6c7ba 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/DeveloperExperienceTests.kt @@ -19,14 +19,14 @@ class DeveloperExperienceTests { @Test fun testSimpleBooleanFlag() = runTest { - OpenFeatureAPI.setProvider(NoOpProvider(), MutableContext()) + OpenFeatureAPI.setProvider(NoOpProvider(), ImmutableContext()) val booleanValue = OpenFeatureAPI.getClient().getBooleanValue("test", false) Assert.assertFalse(booleanValue) } @Test fun testClientHooks() = runTest { - OpenFeatureAPI.setProvider(NoOpProvider(), MutableContext()) + OpenFeatureAPI.setProvider(NoOpProvider(), ImmutableContext()) val client = OpenFeatureAPI.getClient() val hook = GenericSpyHookMock() @@ -38,7 +38,7 @@ class DeveloperExperienceTests { @Test fun testEvalHooks() = runTest { - OpenFeatureAPI.setProvider(NoOpProvider(), MutableContext()) + OpenFeatureAPI.setProvider(NoOpProvider(), ImmutableContext()) val client = OpenFeatureAPI.getClient() val hook = GenericSpyHookMock() @@ -50,7 +50,7 @@ class DeveloperExperienceTests { @Test fun testBrokenProvider() = runTest { - OpenFeatureAPI.setProvider(AlwaysBrokenProvider(), MutableContext()) + OpenFeatureAPI.setProvider(AlwaysBrokenProvider(), ImmutableContext()) val client = OpenFeatureAPI.getClient() val details = client.getBooleanDetails("test", false) diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt index 5c09aa0..7fcc622 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EvalContextTests.kt @@ -8,39 +8,42 @@ class EvalContextTests { @Test fun testContextStoresTargetingKey() { - val ctx = MutableContext() + val ctx = ImmutableContext() ctx.setTargetingKey("test") Assert.assertEquals("test", ctx.getTargetingKey()) } @Test fun testContextStoresPrimitiveValues() { - val ctx = MutableContext() val now = Date() + val ctx = ImmutableContext( + attributes = mapOf( + "string" to Value.String("value"), + "bool" to Value.Boolean(true), + "int" to Value.Integer(3), + "double" to Value.Double(3.14), + "date" to Value.Date(now) + ) + ) - ctx.add("string", Value.String("value")) Assert.assertEquals("value", ctx.getValue("string")?.asString()) - ctx.add("bool", Value.Boolean(true)) Assert.assertEquals(true, ctx.getValue("bool")?.asBoolean()) - ctx.add("int", Value.Integer(3)) Assert.assertEquals(3, ctx.getValue("int")?.asInteger()) - ctx.add("double", Value.Double(3.14)) Assert.assertEquals(3.14, ctx.getValue("double")?.asDouble()) - ctx.add("date", Value.Date(now)) Assert.assertEquals(now, ctx.getValue("date")?.asDate()) } @Test fun testContextStoresLists() { - val ctx = MutableContext() - - ctx.add( - "list", - Value.List( - listOf( - Value.Integer(3), - Value.String("4") - ) + val ctx = ImmutableContext( + attributes = mapOf( + "list" to + Value.List( + listOf( + Value.Integer(3), + Value.String("4") + ) + ) ) ) Assert.assertEquals(3, ctx.getValue("list")?.asList()?.get(0)?.asInteger()) @@ -49,15 +52,15 @@ class EvalContextTests { @Test fun testContextStoresStructures() { - val ctx = MutableContext() - - ctx.add( - "struct", - Value.Structure( - mapOf( - "string" to Value.String("test"), - "int" to Value.Integer(3) - ) + val ctx = ImmutableContext( + attributes = mapOf( + "struct" to + Value.Structure( + mapOf( + "string" to Value.String("test"), + "int" to Value.Integer(3) + ) + ) ) ) Assert.assertEquals("test", ctx.getValue("struct")?.asStructure()?.get("string")?.asString()) @@ -66,16 +69,20 @@ class EvalContextTests { @Test fun testContextCanConvertToMap() { - val ctx = MutableContext() val now = Date() - ctx.add("str1", Value.String("test1")) - ctx.add("str2", Value.String("test2")) - ctx.add("bool1", Value.Boolean(true)) - ctx.add("bool2", Value.Boolean(false)) - ctx.add("int1", Value.Integer(4)) - ctx.add("int2", Value.Integer(2)) - ctx.add("dt", Value.Date(now)) - ctx.add("obj", Value.Structure(mapOf("val1" to Value.Integer(1), "val2" to Value.String("2")))) + val ctx = ImmutableContext( + attributes = mapOf( + "str1" to Value.String("test1"), + "str2" to Value.String("test2"), + "bool1" to Value.Boolean(true), + "bool2" to Value.Boolean(false), + "int1" to Value.Integer(4), + "int2" to Value.Integer(2), + "double" to Value.Double(3.14), + "dt" to Value.Date(now), + "obj" to Value.Structure(mapOf("val1" to Value.Integer(1), "val2" to Value.String("2"))) + ) + ) val map = ctx.asMap() val structure = map["obj"]?.asStructure() @@ -92,33 +99,23 @@ class EvalContextTests { @Test fun testContextHasUniqueKeyAcrossTypes() { - val ctx = MutableContext() - - ctx.add("key", Value.String("val1")) - ctx.add("key", Value.String("val2")) - Assert.assertEquals("val2", ctx.getValue("key")?.asString()) - - ctx.add("key", Value.Integer(3)) + val ctx = ImmutableContext( + attributes = mapOf( + "key" to Value.String("val1"), + "key" to Value.Integer(3) + ) + ) Assert.assertNull(ctx.getValue("key")?.asString()) Assert.assertEquals(3, ctx.getValue("key")?.asInteger()) } @Test - fun testContextCanChainAttributeAddition() { - val ctx = MutableContext() - - val result = - ctx.add("key1", Value.String("val1")) - ctx.add("key2", Value.String("val2")) - Assert.assertEquals("val1", result.getValue("key1")?.asString()) - Assert.assertEquals("val2", result.getValue("key2")?.asString()) - } - - @Test - fun testContextCanAddNull() { - val ctx = MutableContext() - - ctx.add("null", Value.Null) + fun testContextStoresNull() { + val ctx = ImmutableContext( + attributes = mapOf( + "null" to Value.Null + ) + ) Assert.assertEquals(true, ctx.getValue("null")?.isNull()) Assert.assertNull(ctx.getValue("null")?.asString()) } @@ -127,20 +124,21 @@ class EvalContextTests { fun testContextConvertsToObjectMap() { val key = "key1" val now = Date() - val ctx = MutableContext(key) - ctx.add("string", Value.String("value")) - ctx.add("bool", Value.Boolean(false)) - ctx.add("integer", Value.Integer(1)) - ctx.add("double", Value.Double(1.2)) - ctx.add("date", Value.Date(now)) - ctx.add("null", Value.Null) - ctx.add("list", Value.List(listOf(Value.String("item1"), Value.Boolean(true)))) - ctx.add( - "structure", - Value.Structure( - mapOf( - "field1" to Value.Integer(3), - "field2" to Value.Double(3.14) + val ctx = ImmutableContext( + key, + mapOf( + "string" to Value.String("value"), + "bool" to Value.Boolean(false), + "integer" to Value.Integer(1), + "double" to Value.Double(1.2), + "date" to Value.Date(now), + "null" to Value.Null, + "list" to Value.List(listOf(Value.String("item1"), Value.Boolean(true))), + "structure" to Value.Structure( + mapOf( + "field1" to Value.Integer(3), + "field2" to Value.Double(3.14) + ) ) ) ) @@ -162,8 +160,8 @@ class EvalContextTests { fun compareContexts() { val map: MutableMap = mutableMapOf("key" to Value.String("test")) val map2: MutableMap = mutableMapOf("key" to Value.String("test")) - val ctx1 = MutableContext("user1", map) - val ctx2 = MutableContext("user1", map2) + val ctx1 = ImmutableContext("user1", map) + val ctx2 = ImmutableContext("user1", map2) Assert.assertEquals(ctx1, ctx2) } diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt index 8791fc7..f4e9443 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/HookSupportTests.kt @@ -14,7 +14,7 @@ class HookSupportTests { "flagKey", FlagValueType.BOOLEAN, false, - MutableContext(), + ImmutableContext(), metadata, NoOpProvider().metadata ) diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt index 586ee22..30e1a3d 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/ProviderSpecTests.kt @@ -9,26 +9,26 @@ class ProviderSpecTests { fun testFlagValueSet() { val provider = NoOpProvider() - val boolResult = provider.getBooleanEvaluation("key", false, MutableContext()) + val boolResult = provider.getBooleanEvaluation("key", false, ImmutableContext()) Assert.assertNotNull(boolResult.value) - val stringResult = provider.getStringEvaluation("key", "test", MutableContext()) + val stringResult = provider.getStringEvaluation("key", "test", ImmutableContext()) Assert.assertNotNull(stringResult.value) - val intResult = provider.getIntegerEvaluation("key", 4, MutableContext()) + val intResult = provider.getIntegerEvaluation("key", 4, ImmutableContext()) Assert.assertNotNull(intResult.value) - val doubleResult = provider.getDoubleEvaluation("key", 0.4, MutableContext()) + val doubleResult = provider.getDoubleEvaluation("key", 0.4, ImmutableContext()) Assert.assertNotNull(doubleResult.value) - val objectResult = provider.getObjectEvaluation("key", Value.Null, MutableContext()) + val objectResult = provider.getObjectEvaluation("key", Value.Null, ImmutableContext()) Assert.assertNotNull(objectResult.value) } @Test fun testHasReason() { val provider = NoOpProvider() - val boolResult = provider.getBooleanEvaluation("key", false, MutableContext()) + val boolResult = provider.getBooleanEvaluation("key", false, ImmutableContext()) Assert.assertEquals(Reason.DEFAULT.toString(), boolResult.reason) } @@ -36,7 +36,7 @@ class ProviderSpecTests { @Test fun testNoErrorCodeByDefault() { val provider = NoOpProvider() - val boolResult = provider.getBooleanEvaluation("key", false, MutableContext()) + val boolResult = provider.getBooleanEvaluation("key", false, ImmutableContext()) Assert.assertNull(boolResult.errorCode) } @@ -45,19 +45,19 @@ class ProviderSpecTests { fun testVariantIsSet() { val provider = NoOpProvider() - val boolResult = provider.getBooleanEvaluation("key", false, MutableContext()) + val boolResult = provider.getBooleanEvaluation("key", false, ImmutableContext()) Assert.assertNotNull(boolResult.variant) - val stringResult = provider.getStringEvaluation("key", "test", MutableContext()) + val stringResult = provider.getStringEvaluation("key", "test", ImmutableContext()) Assert.assertNotNull(stringResult.variant) - val intResult = provider.getIntegerEvaluation("key", 4, MutableContext()) + val intResult = provider.getIntegerEvaluation("key", 4, ImmutableContext()) Assert.assertNotNull(intResult.variant) - val doubleResult = provider.getDoubleEvaluation("key", 0.4, MutableContext()) + val doubleResult = provider.getDoubleEvaluation("key", 0.4, ImmutableContext()) Assert.assertNotNull(doubleResult.variant) - val objectResult = provider.getObjectEvaluation("key", Value.Null, MutableContext()) + val objectResult = provider.getObjectEvaluation("key", Value.Null, ImmutableContext()) Assert.assertNotNull(objectResult.variant) } } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt index 340344f..0933687 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/StructureTests.kt @@ -8,14 +8,14 @@ class StructureTests { @Test fun testNoArgIsEmpty() { - val structure = MutableContext() + val structure = ImmutableContext() Assert.assertTrue(structure.asMap().keys.isEmpty()) } @Test fun testArgShouldContainNewMap() { val map: MutableMap = mutableMapOf("key" to Value.String("test")) - val structure = MutableStructure(map) + val structure = ImmutableStructure(map) Assert.assertEquals("test", structure.getValue("key")?.asString()) Assert.assertEquals(map, structure.asMap()) @@ -24,14 +24,19 @@ class StructureTests { @Test fun testAddAndGetReturnValues() { val now = Date() - val structure = MutableStructure() - structure.add("bool", Value.Boolean(true)) - structure.add("string", Value.String("val")) - structure.add("int", Value.Integer(13)) - structure.add("double", Value.Double(0.5)) - structure.add("date", Value.Date(now)) - structure.add("list", Value.List(listOf())) - structure.add("structure", Value.Structure(mapOf())) + val structure = ImmutableStructure( + mapOf( + "string" to Value.String("val"), + "bool" to Value.Boolean(true), + "int" to Value.Integer(13), + "double" to Value.Double(0.5), + "date" to Value.Date(now), + "list" to Value.List(listOf()), + "structure" to Value.Structure( + mapOf() + ) + ) + ) Assert.assertEquals(true, structure.getValue("bool")?.asBoolean()) Assert.assertEquals("val", structure.getValue("string")?.asString()) @@ -46,8 +51,8 @@ class StructureTests { fun testCompareStructure() { val map: MutableMap = mutableMapOf("key" to Value.String("test")) val map2: MutableMap = mutableMapOf("key" to Value.String("test")) - val structure1 = MutableStructure(map) - val structure2 = MutableStructure(map2) + val structure1 = ImmutableStructure(map) + val structure2 = ImmutableStructure(map2) Assert.assertEquals(structure1, structure2) } From a12c8a0a116da09c19adb246392afcfdacab5375 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 12 Jul 2023 11:48:49 +0200 Subject: [PATCH 31/56] Mention ImmutableContext in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc4e81a..effb789 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ dependencies { import dev.openfeature.sdk.* // Change NoOpProvider with your actual provider -OpenFeatureAPI.setProvider(NoOpProvider(), MutableContext()) +OpenFeatureAPI.setProvider(NoOpProvider(), ImmutableContext()) val flagValue = OpenFeatureAPI.getClient().getBooleanValue("boolFlag", false) ``` Setting a new provider or setting a new evaluation context are asynchronous operations. The provider might execute I/O operations as part of these method calls (e.g. fetching flag evaluations from the backend and store them in a local cache). It's advised to not interact with the OpenFeature client until the `setProvider()` or `setEvaluationContext()` functions have returned successfully. From 3993db3fa20011741a40c04ff815ad15513fe960 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 12 Jul 2023 12:03:38 +0200 Subject: [PATCH 32/56] Different Metadata interfaces client/provider --- OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt | 2 +- .../dev/openfeature/sdk/{Metadata.kt => ClientMetadata.kt} | 2 +- .../src/main/java/dev/openfeature/sdk/FeatureProvider.kt | 2 +- .../src/main/java/dev/openfeature/sdk/HookContext.kt | 4 ++-- .../src/main/java/dev/openfeature/sdk/NoOpProvider.kt | 4 ++-- .../src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt | 2 +- .../src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt | 4 ++-- .../src/main/java/dev/openfeature/sdk/ProviderMetadata.kt | 5 +++++ .../dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt | 6 +++--- .../java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt | 6 +++--- 10 files changed, 21 insertions(+), 16 deletions(-) rename OpenFeature/src/main/java/dev/openfeature/sdk/{Metadata.kt => ClientMetadata.kt} (65%) create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/ProviderMetadata.kt diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt index aedcb58..e87d89b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/Client.kt @@ -1,7 +1,7 @@ package dev.openfeature.sdk interface Client : Features { - val metadata: Metadata + val metadata: ClientMetadata val hooks: List> fun addHooks(hooks: List>) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/ClientMetadata.kt similarity index 65% rename from OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt rename to OpenFeature/src/main/java/dev/openfeature/sdk/ClientMetadata.kt index 8fcc170..7977cf5 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/Metadata.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/ClientMetadata.kt @@ -1,5 +1,5 @@ package dev.openfeature.sdk -interface Metadata { +interface ClientMetadata { val name: String? } \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt index 70e7ae3..f0b6338 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt @@ -2,7 +2,7 @@ package dev.openfeature.sdk interface FeatureProvider { val hooks: List> - val metadata: Metadata + val metadata: ProviderMetadata // Called by OpenFeatureAPI whenever the new Provider is registered suspend fun initialize(initialContext: EvaluationContext?) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt index 8811d75..ac500fd 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/HookContext.kt @@ -5,6 +5,6 @@ data class HookContext( val type: FlagValueType, var defaultValue: T, var ctx: EvaluationContext?, - var clientMetadata: Metadata?, - var providerMetadata: Metadata + var clientMetadata: ClientMetadata?, + var providerMetadata: ProviderMetadata ) \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt index 3e939d0..fbb46fe 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt @@ -1,7 +1,7 @@ package dev.openfeature.sdk class NoOpProvider : FeatureProvider { - override var metadata: Metadata = NoOpMetadata("No-op provider") + override var metadata: ProviderMetadata = NoOpProviderMetadata("No-op provider") override suspend fun initialize(initialContext: EvaluationContext?) { // no-op } @@ -54,5 +54,5 @@ class NoOpProvider : FeatureProvider { return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) } - data class NoOpMetadata(override var name: String?) : Metadata + data class NoOpProviderMetadata(override var name: String?) : ProviderMetadata } \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index 3cbce8e..a4e7f2b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -35,7 +35,7 @@ object OpenFeatureAPI { return context } - fun getProviderMetadata(): Metadata? { + fun getProviderMetadata(): ProviderMetadata? { return provider?.metadata } diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt index a0cdd5b..9e9c433 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt @@ -17,7 +17,7 @@ class OpenFeatureClient( version: String? = null, override val hooks: MutableList> = mutableListOf() ) : Client { - override val metadata: Metadata = ClientMetadata(name) + override val metadata: ClientMetadata = Metadata(name) private var hookSupport = HookSupport() override fun addHooks(hooks: List>) { this.hooks += hooks @@ -242,5 +242,5 @@ class OpenFeatureClient( } } - data class ClientMetadata(override var name: String?) : Metadata + data class Metadata(override var name: String?) : ClientMetadata } \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderMetadata.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderMetadata.kt new file mode 100644 index 0000000..1a2c53e --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/ProviderMetadata.kt @@ -0,0 +1,5 @@ +package dev.openfeature.sdk + +interface ProviderMetadata { + val name: String? +} \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt index 3467a77..4bac97b 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt @@ -3,12 +3,12 @@ package dev.openfeature.sdk.helpers import dev.openfeature.sdk.EvaluationContext import dev.openfeature.sdk.FeatureProvider import dev.openfeature.sdk.Hook -import dev.openfeature.sdk.Metadata import dev.openfeature.sdk.ProviderEvaluation +import dev.openfeature.sdk.ProviderMetadata import dev.openfeature.sdk.Value import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError -class AlwaysBrokenProvider(override var hooks: List> = listOf(), override var metadata: Metadata = AlwaysBrokenMetadata()) : +class AlwaysBrokenProvider(override var hooks: List> = listOf(), override var metadata: ProviderMetadata = AlwaysBrokenProviderMetadata()) : FeatureProvider { override suspend fun initialize(initialContext: EvaluationContext?) { // no-op @@ -61,5 +61,5 @@ class AlwaysBrokenProvider(override var hooks: List> = listOf(), overrid throw FlagNotFoundError(key) } - class AlwaysBrokenMetadata(override var name: String? = "test") : Metadata + class AlwaysBrokenProviderMetadata(override var name: String? = "test") : ProviderMetadata } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt index b0da3c1..06f5007 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt @@ -3,11 +3,11 @@ package dev.openfeature.sdk.helpers import dev.openfeature.sdk.EvaluationContext import dev.openfeature.sdk.FeatureProvider import dev.openfeature.sdk.Hook -import dev.openfeature.sdk.Metadata import dev.openfeature.sdk.ProviderEvaluation +import dev.openfeature.sdk.ProviderMetadata import dev.openfeature.sdk.Value -class DoSomethingProvider(override val hooks: List> = listOf(), override val metadata: Metadata = DoSomethingMetadata()) : FeatureProvider { +class DoSomethingProvider(override val hooks: List> = listOf(), override val metadata: ProviderMetadata = DoSomethingProviderMetadata()) : FeatureProvider { override suspend fun initialize(initialContext: EvaluationContext?) { // no-op } @@ -58,5 +58,5 @@ class DoSomethingProvider(override val hooks: List> = listOf(), override ): ProviderEvaluation { return ProviderEvaluation(Value.Null) } - class DoSomethingMetadata(override var name: String? = "something") : Metadata + class DoSomethingProviderMetadata(override var name: String? = "something") : ProviderMetadata } \ No newline at end of file From 2fdaafec822f3316434f94be3b9f8c101b2904ad Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 7 Jul 2023 10:10:12 +0200 Subject: [PATCH 33/56] add the publishing to gpr and release pipeline --- .github/workflows/release.yaml | 54 ++++++++++++++++++++++++++++++++++ OpenFeature/build.gradle.kts | 36 +++++++++++++++-------- OpenFeature/jitpack.yaml | 2 ++ build.gradle.kts | 2 +- 4 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/release.yaml create mode 100644 OpenFeature/jitpack.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..251d59e --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,54 @@ +name: Release +on: + push: + tags: + - 'v*' + +jobs: + publish: + name: Release Openfeature SDK + runs-on: ubuntu-latest + + steps: + - name: Cache Gradle and wrapper + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - uses: actions/checkout@v1 + + - name: Set up JDK 12 + uses: actions/setup-java@v1 + with: + java-version: 12 + + - name: Grant Permission for Gradlew to Execute + run: chmod +x gradlew + + - name: Build AAR ⚙️🛠 + run: bash ./gradlew :openfeature:assemble + - name: Create Release ✅ + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.TOKEN_PUBLISH }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: true + prerelease: false + + - name: Upload Openfeature SDK AAR 🗳 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.TOKEN_PUBLISH }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: OpenFeature/build/outputs/aar/OpenFeature-release.aar + asset_name: openfeature-sdk.aar + asset_content_type: application/aar \ No newline at end of file diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index c1bdd10..e9374ca 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -1,3 +1,4 @@ +// ktlint-disable max-line-length plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -33,12 +34,6 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } - publishing { - singleVariant("release") { - withSourcesJar() - withJavadocJar() - } - } } dependencies { @@ -48,16 +43,31 @@ dependencies { testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") } -publishing { - publications { - register("release") { - groupId = "dev.openfeature" - artifactId = "kotlin-sdk" - version = "0.0.1-SNAPSHOT" +afterEvaluate { + publishing { + publications { + register("release") { + groupId = "dev.openfeature" + artifactId = "kotlin-sdk" + version = "0.0.1-SNAPSHOT" - afterEvaluate { from(components["release"]) + artifact(androidSourcesJar.get()) + + pom { + name.set("OpenfeatureSDK") + } } } } +} + +val androidSourcesJar by tasks.registering(Jar::class) { + archiveClassifier.set("sources") + from(android.sourceSets.getByName("main").java.srcDirs) +} + +// Assembling should be performed before publishing package +tasks.named("publish") { + dependsOn("assemble") } \ No newline at end of file diff --git a/OpenFeature/jitpack.yaml b/OpenFeature/jitpack.yaml new file mode 100644 index 0000000..46c8529 --- /dev/null +++ b/OpenFeature/jitpack.yaml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 9c79167..7c1df9e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.library").version("7.4.1").apply(false) + id("com.android.library").version("7.4.2").apply(false) id("org.jetbrains.kotlin.android").version("1.8.0").apply(false) id("org.jlleitschuh.gradle.ktlint").version("11.3.2").apply(true) } \ No newline at end of file From 764dba41abc336a33173e1f66638889b6da1b42a Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 12 Jul 2023 17:38:41 +0200 Subject: [PATCH 34/56] agp version 7.4 is not compatible with java 1.8 --- OpenFeature/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index e9374ca..c60bf7f 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -28,11 +28,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() + jvmTarget = JavaVersion.VERSION_11.toString() } } From 35610e557847cd3b3bb0bc9669e4a11348a01dbb Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 13 Jul 2023 14:00:21 +0200 Subject: [PATCH 35/56] update readme about using the openfeature sdk --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fc4e81a..9c76065 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![](https://jitpack.io/v/spotify/openfeature-kotlin-sdk.svg)](https://jitpack.io/#spotify/openfeature-kotlin-sdk) + # OpenFeature Kotlin SDK ![Status](https://img.shields.io/badge/lifecycle-alpha-a0c3d2.svg) @@ -16,24 +18,24 @@ This Kotlin implementation of an OpenFeature SDK has been developed at Spotify, Note that this library is intended to be used in a mobile context, and has not been evaluated for use in other type of applications (e.g. server applications). - ## Usage -### Adding the library dependency (WORK IN PROGRESS ⚠️) +### Adding the library dependency -This library is not published to central repositories yet. -Clone this repository and run the following to install the library locally: -``` -./gradlew publishToMavenLocal -``` -The Android project must include `mavenLocal()` in `settings.gradle`. +The Android project must include `maven("https://jitpack.io")` in `settings.gradle`. You can now add the OpenFeature SDK dependency: ```kotlin dependencies { - implementation("dev.openfeature:kotlin-sdk:0.0.1-SNAPSHOT") + api("com.github.spotify:openfeature-kotlin-sdk:") } ``` +Please note that the `` can be any `Commit SHA` or a version based off a branch as following: +``` +api("com.github.spotify:openfeature-kotlin-sdk:[ANY_BRANCH]-SNAPSHOT") +``` + +This will get a build from the head of the mentioned branch. ### Resolving a flag ```kotlin From d0e20169775be8c42f6d2dcf3cb22f46c46af8c9 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 18 Jul 2023 11:42:50 +0200 Subject: [PATCH 36/56] Restructure setters in OpenFeatureAPI --- .../src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index a4e7f2b..b348d57 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -9,9 +9,9 @@ object OpenFeatureAPI { private set suspend fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) = coroutineScope { - provider.initialize(initialContext ?: context) this@OpenFeatureAPI.provider = provider if (initialContext != null) context = initialContext + provider.initialize(context) } fun getProvider(): FeatureProvider? { @@ -23,12 +23,8 @@ object OpenFeatureAPI { } suspend fun setEvaluationContext(evaluationContext: EvaluationContext) { - getProvider()?.onContextSet(context, evaluationContext) - // A provider evaluation reading the global ctx at this point would fail due to stale cache. - // To prevent this, the provider should internally manage the ctx to use on each evaluation, and - // make sure it's aligned with the values in the cache at all times. If no guarantees are offered by - // the provider, the application can expect STALE resolves while setting a new global ctx context = evaluationContext + getProvider()?.onContextSet(context, evaluationContext) } fun getEvaluationContext(): EvaluationContext? { From 0049fb4fdb8a7eba60a7f3c414da7a6e8b4513d0 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 19 Jul 2023 13:50:01 +0200 Subject: [PATCH 37/56] add basics for adding the events, add extension to convert the client to the async client --- .../dev/openfeature/sdk/async/AsyncClient.kt | 45 +++++++++++ .../dev/openfeature/sdk/async/Extensions.kt | 30 ++++++++ .../openfeature/sdk/events/EventPublisher.kt | 77 +++++++++++++++++++ .../sdk/events/OpenFeatureEvents.kt | 9 +++ 4 files changed, 161 insertions(+) create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt create mode 100644 OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt new file mode 100644 index 0000000..27192cb --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt @@ -0,0 +1,45 @@ +package dev.openfeature.sdk.async + +import dev.openfeature.sdk.OpenFeatureClient +import dev.openfeature.sdk.events.EventObserver +import dev.openfeature.sdk.events.OpenFeatureEvents +import dev.openfeature.sdk.events.ProviderStatus +import dev.openfeature.sdk.events.observe +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +interface AsyncClient { + fun observeBooleanValue(key: String, default: Boolean): Flow + fun observeIntValue(key: String, default: Int): Flow + fun observeStringValue(key: String, default: String): Flow +} + +internal class AsyncClientImpl( + private val client: OpenFeatureClient, + private val eventsObserver: EventObserver, + private val providerStatus: ProviderStatus +) : AsyncClient { + private fun observeEvents(callback: () -> T) = eventsObserver + .observe() + .onStart { + if (providerStatus.isProviderReady()) { + this.emit(OpenFeatureEvents.ProviderReady) + } + } + .map { callback() } + .distinctUntilChanged() + + override fun observeBooleanValue(key: String, default: Boolean) = observeEvents { + client.getBooleanValue(key, default) + } + + override fun observeIntValue(key: String, default: Int) = observeEvents { + client.getIntegerValue(key, default) + } + + override fun observeStringValue(key: String, default: String) = observeEvents { + client.getStringValue(key, default) + } +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt new file mode 100644 index 0000000..3873665 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt @@ -0,0 +1,30 @@ +package dev.openfeature.sdk.async + +import dev.openfeature.sdk.OpenFeatureClient +import dev.openfeature.sdk.events.EventHandler +import dev.openfeature.sdk.events.EventObserver +import dev.openfeature.sdk.events.OpenFeatureEvents +import dev.openfeature.sdk.events.ProviderStatus +import dev.openfeature.sdk.events.observe +import kotlinx.coroutines.flow.onStart + +fun OpenFeatureClient.toAsync(): AsyncClient { + val eventsObserver: EventObserver = EventHandler.eventsObserver() + val providerStatus: ProviderStatus = EventHandler.providerStatus() + + return AsyncClientImpl( + this, + eventsObserver, + providerStatus + ) +} + +fun observeProviderStatus() = observeProviderEvents() + .observe() + .onStart { + if (EventHandler.providerStatus().isProviderReady()) { + this.emit(OpenFeatureEvents.ProviderReady) + } + } + +fun observeProviderEvents() = EventHandler.eventsObserver() \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt new file mode 100644 index 0000000..4434120 --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt @@ -0,0 +1,77 @@ +package dev.openfeature.sdk.events + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch +import kotlin.reflect.KClass + +interface ProviderStatus { + fun isProviderReady(): Boolean +} + +interface EventObserver { + fun observe(kClass: KClass): Flow +} + +interface EventsPublisher { + fun publish(event: OpenFeatureEvents) +} + +inline fun EventObserver.observe() = observe(T::class) + +class EventHandler : EventObserver, EventsPublisher, ProviderStatus { + private val sharedFlow: MutableSharedFlow = MutableSharedFlow() + private val isProviderReady = MutableStateFlow(false) + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + init { + coroutineScope.launch { + sharedFlow.collect { + when (it) { + is OpenFeatureEvents.ProviderReady -> isProviderReady.value = true + is OpenFeatureEvents.ProviderShutDown -> { + isProviderReady.value = false + coroutineScope.cancel() + } + else -> { + // do nothing + } + } + } + } + } + + override fun publish(event: OpenFeatureEvents) { + coroutineScope.launch { + sharedFlow.emit(event) + } + } + + override fun isProviderReady(): Boolean { + return isProviderReady.value + } + + override fun observe(kClass: KClass): Flow = sharedFlow + .filterIsInstance(kClass) + + companion object { + @Volatile + private var instance: EventHandler? = null + + private fun getInstance() = + instance ?: synchronized(this) { + instance ?: create().also { instance = it } + } + + fun eventsObserver(): EventObserver = getInstance() + fun providerStatus(): ProviderStatus = getInstance() + fun eventsPublisher(): EventsPublisher = getInstance() + + private fun create() = EventHandler() + } +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt new file mode 100644 index 0000000..56d4e8c --- /dev/null +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt @@ -0,0 +1,9 @@ +package dev.openfeature.sdk.events + +sealed class OpenFeatureEvents { + object ProviderReady : OpenFeatureEvents() + object ProviderConfigurationChanged : OpenFeatureEvents() + object ProviderError : OpenFeatureEvents() + object ProviderStale : OpenFeatureEvents() + object ProviderShutDown : OpenFeatureEvents() +} \ No newline at end of file From 06c1cd3c44e67ff6d79af512f7bfe5e0bac2bc3a Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 21 Jul 2023 12:46:38 +0200 Subject: [PATCH 38/56] add tests to support the events implementations --- OpenFeature/build.gradle.kts | 7 +- .../dev/openfeature/sdk/async/Extensions.kt | 6 +- .../openfeature/sdk/events/EventPublisher.kt | 26 ++- .../dev/openfeature/sdk/EventsHandlerTest.kt | 209 ++++++++++++++++++ 4 files changed, 230 insertions(+), 18 deletions(-) create mode 100644 OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt diff --git a/OpenFeature/build.gradle.kts b/OpenFeature/build.gradle.kts index c60bf7f..4d6dc84 100644 --- a/OpenFeature/build.gradle.kts +++ b/OpenFeature/build.gradle.kts @@ -37,10 +37,11 @@ android { } dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") testImplementation("junit:junit:4.13.2") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2") } afterEvaluate { diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt index c3b27fb..60194aa 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt @@ -10,12 +10,10 @@ fun OpenFeatureClient.toAsync(): AsyncClient { return AsyncClientImpl(this) } -fun observeProviderReady() = observeProviderEvents() +fun observeProviderReady() = EventHandler.eventsObserver() .observe() .onStart { if (EventHandler.providerStatus().isProviderReady()) { this.emit(OpenFeatureEvents.ProviderReady) } - } - -fun observeProviderEvents() = EventHandler.eventsObserver() \ No newline at end of file + } \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt index 4434120..130c52b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt @@ -1,5 +1,6 @@ package dev.openfeature.sdk.events +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -24,10 +25,10 @@ interface EventsPublisher { inline fun EventObserver.observe() = observe(T::class) -class EventHandler : EventObserver, EventsPublisher, ProviderStatus { +class EventHandler(dispatcher: CoroutineDispatcher) : EventObserver, EventsPublisher, ProviderStatus { private val sharedFlow: MutableSharedFlow = MutableSharedFlow() private val isProviderReady = MutableStateFlow(false) - private val coroutineScope = CoroutineScope(Dispatchers.IO) + private val coroutineScope = CoroutineScope(dispatcher) init { coroutineScope.launch { @@ -52,26 +53,29 @@ class EventHandler : EventObserver, EventsPublisher, ProviderStatus { } } + override fun observe(kClass: KClass): Flow = sharedFlow + .filterIsInstance(kClass) + override fun isProviderReady(): Boolean { return isProviderReady.value } - override fun observe(kClass: KClass): Flow = sharedFlow - .filterIsInstance(kClass) - companion object { @Volatile private var instance: EventHandler? = null - private fun getInstance() = + private fun getInstance(dispatcher: CoroutineDispatcher) = instance ?: synchronized(this) { - instance ?: create().also { instance = it } + instance ?: create(dispatcher).also { instance = it } } - fun eventsObserver(): EventObserver = getInstance() - fun providerStatus(): ProviderStatus = getInstance() - fun eventsPublisher(): EventsPublisher = getInstance() + fun eventsObserver(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventObserver = + getInstance(dispatcher) + fun providerStatus(dispatcher: CoroutineDispatcher = Dispatchers.IO): ProviderStatus = + getInstance(dispatcher) + fun eventsPublisher(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventsPublisher = + getInstance(dispatcher) - private fun create() = EventHandler() + private fun create(dispatcher: CoroutineDispatcher) = EventHandler(dispatcher) } } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt new file mode 100644 index 0000000..d0c4a9a --- /dev/null +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt @@ -0,0 +1,209 @@ +package dev.openfeature.sdk + +import dev.openfeature.sdk.async.observeProviderReady +import dev.openfeature.sdk.async.toAsync +import dev.openfeature.sdk.events.EventHandler +import dev.openfeature.sdk.events.OpenFeatureEvents +import dev.openfeature.sdk.events.observe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.timeout +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito.`when` +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import kotlin.time.Duration.Companion.milliseconds + +@OptIn(ExperimentalCoroutinesApi::class) +class EventsHandlerTest { + + @Test + fun observing_event_observer_works() = runTest { + val eventObserver = EventHandler.eventsObserver() + val eventPublisher = EventHandler.eventsPublisher() + var emitted = false + + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + eventObserver.observe() + .take(1) + .collect { + emitted = true + } + } + + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + job.join() + Assert.assertTrue(emitted) + } + + @Test + fun multiple_subscribers_works() = runTest { + val eventObserver = EventHandler.eventsObserver() + val eventPublisher = EventHandler.eventsPublisher() + val numberOfSubscribers = 10 + val parentJob = Job() + var emitted = 0 + + repeat(numberOfSubscribers) { + CoroutineScope(parentJob).launch(UnconfinedTestDispatcher(testScheduler)) { + eventObserver.observe() + .take(1) + .collect { + emitted += 1 + } + } + } + + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + parentJob.children.forEach { it.join() } + Assert.assertTrue(emitted == 10) + } + + @Test + fun canceling_one_subscriber_does_not_cancel_others() = runTest { + val eventObserver = EventHandler.eventsObserver() + val eventPublisher = EventHandler.eventsPublisher() + val numberOfSubscribers = 10 + val parentJob = Job() + var emitted = 0 + + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + eventObserver.observe() + .take(1) + .collect {} + } + + repeat(numberOfSubscribers) { + CoroutineScope(parentJob).launch(UnconfinedTestDispatcher(testScheduler)) { + eventObserver.observe() + .take(1) + .collect { + emitted += 1 + } + } + } + job.cancel() + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + parentJob.children.forEach { it.join() } + Assert.assertTrue(emitted == 10) + } + + @Test + fun the_provider_status_stream_works() = runTest { + val eventPublisher = EventHandler.eventsPublisher() + var isProviderReady = false + + // observing the provider status after the provider ready event is published + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + observeProviderReady() + .take(1) + .collect { + isProviderReady = true + } + } + + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + job.join() + Assert.assertTrue(isProviderReady) + } + + @Test + fun the_provider_status_stream_not_emitting_without_event_published() = runTest { + var isProviderReady = false + + // observing the provider status after the provider ready event is published + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + observeProviderReady() + .timeout(10L.milliseconds) + .collect { + isProviderReady = true + } + } + + job.join() + Assert.assertTrue(!isProviderReady) + } + + @Test + fun the_provider_status_stream_is_replays_current_status() = runTest { + val eventPublisher = EventHandler.eventsPublisher() + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + var isProviderReady = false + + // observing the provider status after the provider ready event is published + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + observeProviderReady() + .take(1) + .collect { + isProviderReady = true + } + } + + job.join() + Assert.assertTrue(isProviderReady) + } + + @Test + fun observe_string_value_from_client_works() = runTest { + val testDispatcher = UnconfinedTestDispatcher(testScheduler) + val eventPublisher = EventHandler.eventsPublisher(testDispatcher) + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + val key = "mykey" + val default = "default" + val resultTexts = mutableListOf() + + val mockOpenFeatureClient = mock { + on { getStringValue(key, default) } doReturn "text1" + } + + // observing the provider status after the provider ready event is published + val job = backgroundScope.launch(testDispatcher) { + mockOpenFeatureClient.toAsync() + .observeStringValue(key, default) + .take(2) + .collect { + resultTexts.add(it) + } + } + + `when`(mockOpenFeatureClient.getStringValue(key, default)) + .thenReturn("text2") + + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + job.join() + Assert.assertEquals(listOf("text1", "text2"), resultTexts) + } + + @Test + fun observe_string_value_from_client_waits_until_provider_ready() = runTest { + val testDispatcher = UnconfinedTestDispatcher(testScheduler) + val eventPublisher = EventHandler.eventsPublisher(testDispatcher) + val key = "mykey" + val default = "default" + val resultTexts = mutableListOf() + + val mockOpenFeatureClient = mock { + on { getStringValue(key, default) } doReturn "text1" + } + + // observing the provider status after the provider ready event is published + val job = backgroundScope.launch(testDispatcher) { + mockOpenFeatureClient.toAsync() + .observeStringValue(key, default) + .take(1) + .collect { + resultTexts.add(it) + } + } + + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + job.join() + Assert.assertEquals(listOf("text1"), resultTexts) + } +} \ No newline at end of file From a61f6d02eaaf6c55718a0567cbdebc970a190fdb Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 21 Jul 2023 14:45:25 +0200 Subject: [PATCH 39/56] adding shutdown method to shutdown the provider. non-suspending context set and initialize for setting provider --- .../main/java/dev/openfeature/sdk/FeatureProvider.kt | 8 ++++++-- .../src/main/java/dev/openfeature/sdk/NoOpProvider.kt | 8 ++++++-- .../main/java/dev/openfeature/sdk/OpenFeatureAPI.kt | 10 ++++++---- .../openfeature/sdk/helpers/AlwaysBrokenProvider.kt | 8 ++++++-- .../dev/openfeature/sdk/helpers/DoSomethingProvider.kt | 8 ++++++-- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt index f0b6338..81ed906 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt @@ -5,10 +5,14 @@ interface FeatureProvider { val metadata: ProviderMetadata // Called by OpenFeatureAPI whenever the new Provider is registered - suspend fun initialize(initialContext: EvaluationContext?) + fun initialize(initialContext: EvaluationContext?) + + // called when the lifecycle of the OpenFeatureClient is over + // to release resources/threads. + fun shutdown() // Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application - suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) + fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) fun getBooleanEvaluation( key: String, defaultValue: Boolean, diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt index fbb46fe..45320c0 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt @@ -2,11 +2,15 @@ package dev.openfeature.sdk class NoOpProvider : FeatureProvider { override var metadata: ProviderMetadata = NoOpProviderMetadata("No-op provider") - override suspend fun initialize(initialContext: EvaluationContext?) { + override fun initialize(initialContext: EvaluationContext?) { // no-op } - override suspend fun onContextSet( + override fun shutdown() { + // no-op + } + + override fun onContextSet( oldContext: EvaluationContext?, newContext: EvaluationContext ) { diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index b348d57..ac34ba5 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -1,14 +1,12 @@ package dev.openfeature.sdk -import kotlinx.coroutines.coroutineScope - object OpenFeatureAPI { private var provider: FeatureProvider? = null private var context: EvaluationContext? = null var hooks: List> = listOf() private set - suspend fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) = coroutineScope { + fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) { this@OpenFeatureAPI.provider = provider if (initialContext != null) context = initialContext provider.initialize(context) @@ -22,7 +20,7 @@ object OpenFeatureAPI { provider = null } - suspend fun setEvaluationContext(evaluationContext: EvaluationContext) { + fun setEvaluationContext(evaluationContext: EvaluationContext) { context = evaluationContext getProvider()?.onContextSet(context, evaluationContext) } @@ -46,4 +44,8 @@ object OpenFeatureAPI { fun clearHooks() { this.hooks = listOf() } + + fun shutdown() { + provider?.shutdown() + } } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt index 4bac97b..93e9c7f 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt @@ -10,11 +10,15 @@ import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError class AlwaysBrokenProvider(override var hooks: List> = listOf(), override var metadata: ProviderMetadata = AlwaysBrokenProviderMetadata()) : FeatureProvider { - override suspend fun initialize(initialContext: EvaluationContext?) { + override fun initialize(initialContext: EvaluationContext?) { // no-op } - override suspend fun onContextSet( + override fun shutdown() { + // no-op + } + + override fun onContextSet( oldContext: EvaluationContext?, newContext: EvaluationContext ) { diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt index 06f5007..b53d693 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt @@ -8,11 +8,15 @@ import dev.openfeature.sdk.ProviderMetadata import dev.openfeature.sdk.Value class DoSomethingProvider(override val hooks: List> = listOf(), override val metadata: ProviderMetadata = DoSomethingProviderMetadata()) : FeatureProvider { - override suspend fun initialize(initialContext: EvaluationContext?) { + override fun initialize(initialContext: EvaluationContext?) { // no-op } - override suspend fun onContextSet( + override fun shutdown() { + // no-op + } + + override fun onContextSet( oldContext: EvaluationContext?, newContext: EvaluationContext ) { From 66ad36dd0ae4192d226dd5b4c0d5a5fa18d7a4a8 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 21 Jul 2023 14:58:52 +0200 Subject: [PATCH 40/56] add suspending function for ease of use the provider ready event --- .../dev/openfeature/sdk/async/Extensions.kt | 34 +++++++++++++++++-- .../sdk/events/OpenFeatureEvents.kt | 2 +- .../dev/openfeature/sdk/EventsHandlerTest.kt | 6 ++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt index 60194aa..bcb299b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt @@ -4,16 +4,46 @@ import dev.openfeature.sdk.OpenFeatureClient import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.events.observe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine fun OpenFeatureClient.toAsync(): AsyncClient { return AsyncClientImpl(this) } -fun observeProviderReady() = EventHandler.eventsObserver() +internal fun observeProviderReady() = EventHandler.eventsObserver() .observe() .onStart { if (EventHandler.providerStatus().isProviderReady()) { this.emit(OpenFeatureEvents.ProviderReady) } - } \ No newline at end of file + } + +suspend fun awaitProviderReady() = suspendCancellableCoroutine { continuation -> + val coroutineScope = CoroutineScope(Dispatchers.IO) + coroutineScope.launch { + observeProviderReady() + .take(1) + .collect { + continuation.resumeWith(Result.success(Unit)) + } + } + + coroutineScope.launch { + EventHandler.eventsObserver() + .observe() + .take(1) + .collect { + continuation.resumeWith(Result.failure(it.error)) + } + } + + continuation.invokeOnCancellation { + coroutineScope.cancel() + } +} \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt index 56d4e8c..e6d0e79 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt @@ -3,7 +3,7 @@ package dev.openfeature.sdk.events sealed class OpenFeatureEvents { object ProviderReady : OpenFeatureEvents() object ProviderConfigurationChanged : OpenFeatureEvents() - object ProviderError : OpenFeatureEvents() + data class ProviderError(val error: Throwable) : OpenFeatureEvents() object ProviderStale : OpenFeatureEvents() object ProviderShutDown : OpenFeatureEvents() } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt index d0c4a9a..4b42af0 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt @@ -101,7 +101,8 @@ class EventsHandlerTest { // observing the provider status after the provider ready event is published val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - observeProviderReady() + EventHandler.eventsObserver() + .observe() .take(1) .collect { isProviderReady = true @@ -119,7 +120,8 @@ class EventsHandlerTest { // observing the provider status after the provider ready event is published val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - observeProviderReady() + EventHandler.eventsObserver() + .observe() .timeout(10L.milliseconds) .collect { isProviderReady = true From 9c2b389163807b822768ed8250c7be8bdf75bf40 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Fri, 21 Jul 2023 16:05:04 +0200 Subject: [PATCH 41/56] add double and value for the async client --- .../java/dev/openfeature/sdk/async/AsyncClient.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt index 1005c0f..38a2cb4 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt @@ -1,6 +1,7 @@ package dev.openfeature.sdk.async import dev.openfeature.sdk.OpenFeatureClient +import dev.openfeature.sdk.Value import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -9,6 +10,8 @@ interface AsyncClient { fun observeBooleanValue(key: String, default: Boolean): Flow fun observeIntValue(key: String, default: Int): Flow fun observeStringValue(key: String, default: String): Flow + fun observeDoubleValue(key: String, default: Double): Flow + fun observeValue(key: String, default: Value): Flow } internal class AsyncClientImpl( @@ -29,4 +32,12 @@ internal class AsyncClientImpl( override fun observeStringValue(key: String, default: String) = observeEvents { client.getStringValue(key, default) } + + override fun observeDoubleValue(key: String, default: Double): Flow = observeEvents { + client.getDoubleValue(key, default) + } + + override fun observeValue(key: String, default: Value): Flow = observeEvents { + client.getObjectValue(key, default) + } } \ No newline at end of file From be364024810db70d6ac44c5368506c02d8af679d Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Mon, 24 Jul 2023 16:18:09 +0200 Subject: [PATCH 42/56] cancel children instead of cancel to be able to set provider again --- .../java/dev/openfeature/sdk/events/EventPublisher.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt index 130c52b..f951b26 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt @@ -3,7 +3,8 @@ package dev.openfeature.sdk.events import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -28,7 +29,8 @@ inline fun EventObserver.observe() = observe(T:: class EventHandler(dispatcher: CoroutineDispatcher) : EventObserver, EventsPublisher, ProviderStatus { private val sharedFlow: MutableSharedFlow = MutableSharedFlow() private val isProviderReady = MutableStateFlow(false) - private val coroutineScope = CoroutineScope(dispatcher) + private val job = Job() + private val coroutineScope = CoroutineScope(job + dispatcher) init { coroutineScope.launch { @@ -37,7 +39,7 @@ class EventHandler(dispatcher: CoroutineDispatcher) : EventObserver, EventsPubli is OpenFeatureEvents.ProviderReady -> isProviderReady.value = true is OpenFeatureEvents.ProviderShutDown -> { isProviderReady.value = false - coroutineScope.cancel() + job.cancelChildren() } else -> { // do nothing From 264fe9c9818b336ca327cc52ac589c8cac785ba2 Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Mon, 24 Jul 2023 16:28:38 +0200 Subject: [PATCH 43/56] update README.md --- README.md | 174 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 153 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ac75e98..8032727 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,33 @@ -[![](https://jitpack.io/v/spotify/openfeature-kotlin-sdk.svg)](https://jitpack.io/#spotify/openfeature-kotlin-sdk) + +

+ + + + OpenFeature Logo + +

-# OpenFeature Kotlin SDK +

OpenFeature Kotlin SDKs

![Status](https://img.shields.io/badge/lifecycle-alpha-a0c3d2.svg) -What is OpenFeature? +## 👋 Hey there! Thanks for checking out the OpenFeature Kotlin SDK + +### What is OpenFeature? + [OpenFeature][openfeature-website] is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. -Why standardize feature flags? -Standardizing feature flags unifies tools and vendors behind a common interface which avoids vendor lock-in at the code level. Additionally, it offers a framework for building extensions and integrations and allows providers to focus on their unique value proposition. +### Why standardize feature flags? -This Kotlin implementation of an OpenFeature SDK has been developed at Spotify, and currently made available and maintained within the Spotify Open Source Software organization. Part of our roadmap is for the OpenFeature community to evaluate this implementation and potentially include it in the existing ecosystem of [OpenFeature SDKs][openfeature-sdks]. +Standardizing feature flags unifies tools and vendors behind a common interface which avoids vendor lock-in at the code level. Additionally, it offers a framework for building extensions and integrations and allows providers to focus on their unique value proposition. -## Requirements +## 🔍 Requirements - The Android minSdk version supported is: `21`. Note that this library is intended to be used in a mobile context, and has not been evaluated for use in other type of applications (e.g. server applications). -## Usage - -### Adding the library dependency +## 📦 Installation The Android project must include `maven("https://jitpack.io")` in `settings.gradle`. @@ -37,22 +44,147 @@ api("com.github.spotify:openfeature-kotlin-sdk:[ANY_BRANCH]-SNAPSHOT") This will get a build from the head of the mentioned branch. -### Resolving a flag +## 🌟 Features + +- support for various backend [providers](https://openfeature.dev/docs/reference/concepts/provider) +- easy integration and extension via [hooks](https://openfeature.dev/docs/reference/concepts/hooks) +- bool, string, numeric, and object flag types +- [context-aware](https://openfeature.dev/docs/reference/concepts/evaluation-context) evaluation + +## 🚀 Usage + +```kotlin + // configure a provider and get client + OpenFeatureAPI.setProvider( + CustomProvider.initialise() + ) + val client = OpenFeatureAPI.getClient() + + // get a bool flag value + client.getBooleanValue("boolFlag", default = false) + + // get a bool flag value async + openFeatureClient + .toAsync() + .observeBooleanValue(key, default) + .collect { + // do something with boolean + } + + // get bool flag with compose + val myBoolProperty = openFeatureClient + .toAsync() + .observeBooleanValue(key, default) + .collectAsState() +``` + +### Context-aware evaluation + +Sometimes the value of a flag must take into account some dynamic criteria about the application or user, such as the user location, IP, email address, or the location of the server. +In OpenFeature, we refer to this as [`targeting`](https://openfeature.dev/specification/glossary#targeting). +If the flag system you're using supports targeting, you can provide the input data using the `EvaluationContext`. + + + +### Events + +Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. +Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. +Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. +Please refer to the documentation of the provider you're using to see what events are supported. + +```kotlin + // to listen to PROVIDER_READY event + CoroutineScope(Dispatchers.IO).launch { + awaitProviderReady() + // now provider is ready, read the properties + } +``` + +### Providers: + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization. +Finally, you’ll then need to write the provider itself. +This can be accomplished by implementing the `Provider` interface exported by the OpenFeature SDK. + ```kotlin -import dev.openfeature.sdk.* +class NewProvider(override val hooks: List>, override val metadata: Metadata) : FeatureProvider { + override fun getBooleanEvaluation( + key: String, + defaultValue: Boolean, + context: EvaluationContext? + ): ProviderEvaluation { + // resolve a boolean flag value + } + + override fun getDoubleEvaluation( + key: String, + defaultValue: Double, + context: EvaluationContext? + ): ProviderEvaluation { + // resolve a double flag value + } + + override fun getIntegerEvaluation( + key: String, + defaultValue: Int, + context: EvaluationContext? + ): ProviderEvaluation { + // resolve an integer flag value + } + + override fun getObjectEvaluation( + key: String, + defaultValue: Value, + context: EvaluationContext? + ): ProviderEvaluation { + // resolve an object flag value + } + + override fun getStringEvaluation( + key: String, + defaultValue: String, + context: EvaluationContext? + ): ProviderEvaluation { + // resolve a string flag value + } + + override suspend fun initialize(initialContext: EvaluationContext?) { + // add context-aware provider initialisation + } + + override suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) { + // add necessary changes on context change + } -// Change NoOpProvider with your actual provider -OpenFeatureAPI.setProvider(NoOpProvider(), ImmutableContext()) -val flagValue = OpenFeatureAPI.getClient().getBooleanValue("boolFlag", false) +} ``` -Setting a new provider or setting a new evaluation context are asynchronous operations. The provider might execute I/O operations as part of these method calls (e.g. fetching flag evaluations from the backend and store them in a local cache). It's advised to not interact with the OpenFeature client until the `setProvider()` or `setEvaluationContext()` functions have returned successfully. -Please refer to our [documentation on static-context APIs](https://github.com/open-feature/spec/pull/171) for further information on how these APIs are structured for the use-case of mobile clients. +## ⭐️ Support the project + +- Give this repo a ⭐️! +- Follow us on social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more check out our [community page](https://openfeature.dev/community/) + +## 🤝 Contributing + +Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. + +### Thanks to everyone that has already contributed + + + Pictures of the folks who have contributed to the project + + +Made with [contrib.rocks](https://contrib.rocks). -### Providers +## 📜 License -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. You’ll then need to write the provider itself. This can be accomplished by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. +[Apache License 2.0](LICENSE) -[openfeature-website]: https://openfeature.dev -[openfeature-sdks]: https://openfeature.dev/docs/reference/technologies/ \ No newline at end of file +[openfeature-website]: https://openfeature.dev \ No newline at end of file From a12b96fc5fbe6d892641bcf4c5ad1d069580e003 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 25 Jul 2023 11:27:39 +0200 Subject: [PATCH 44/56] expose observing the events from the openfeature api --- .../main/java/dev/openfeature/sdk/OpenFeatureAPI.kt | 11 +++++++++++ .../main/java/dev/openfeature/sdk/async/Extensions.kt | 2 ++ .../sdk/events/{EventPublisher.kt => EventHandler.kt} | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) rename OpenFeature/src/main/java/dev/openfeature/sdk/events/{EventPublisher.kt => EventHandler.kt} (96%) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index ac34ba5..e767d3a 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -1,8 +1,15 @@ package dev.openfeature.sdk +import dev.openfeature.sdk.events.EventHandler +import dev.openfeature.sdk.events.OpenFeatureEvents +import dev.openfeature.sdk.events.observe +import kotlinx.coroutines.CoroutineDispatcher + +@Suppress("TooManyFunctions") object OpenFeatureAPI { private var provider: FeatureProvider? = null private var context: EvaluationContext? = null + var hooks: List> = listOf() private set @@ -16,6 +23,10 @@ object OpenFeatureAPI { return provider } + inline fun observeEvents(dispatcher: CoroutineDispatcher) = + EventHandler.eventsObserver(dispatcher) + .observe() + fun clearProvider() { provider = null } diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt index bcb299b..b993c88 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt @@ -1,9 +1,11 @@ package dev.openfeature.sdk.async +import dev.openfeature.sdk.OpenFeatureAPI import dev.openfeature.sdk.OpenFeatureClient import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.events.observe +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt similarity index 96% rename from OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt rename to OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt index f951b26..45fa3fe 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventPublisher.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt @@ -73,7 +73,7 @@ class EventHandler(dispatcher: CoroutineDispatcher) : EventObserver, EventsPubli fun eventsObserver(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventObserver = getInstance(dispatcher) - fun providerStatus(dispatcher: CoroutineDispatcher = Dispatchers.IO): ProviderStatus = + internal fun providerStatus(dispatcher: CoroutineDispatcher = Dispatchers.IO): ProviderStatus = getInstance(dispatcher) fun eventsPublisher(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventsPublisher = getInstance(dispatcher) From 69b94b6adf53e0d7eca6f4b4575c10a565fa47a7 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 25 Jul 2023 11:28:03 +0200 Subject: [PATCH 45/56] expose observing the events from the openfeature api --- .../src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt | 4 ++-- .../src/main/java/dev/openfeature/sdk/async/Extensions.kt | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index e767d3a..4e941a7 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -23,9 +23,9 @@ object OpenFeatureAPI { return provider } - inline fun observeEvents(dispatcher: CoroutineDispatcher) = + inline fun observeEvents(dispatcher: CoroutineDispatcher) = EventHandler.eventsObserver(dispatcher) - .observe() + .observe() fun clearProvider() { provider = null diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt index b993c88..bcb299b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt @@ -1,11 +1,9 @@ package dev.openfeature.sdk.async -import dev.openfeature.sdk.OpenFeatureAPI import dev.openfeature.sdk.OpenFeatureClient import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.events.observe -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel From 8f3f15fb716abe4c5d9f4d1094e399c6943961d1 Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Tue, 25 Jul 2023 15:05:11 +0200 Subject: [PATCH 46/56] update code snippets in README.md --- README.md | 48 +++++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8032727..6d4ffc3 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Note that this library is intended to be used in a mobile context, and has not b ## 📦 Installation +### Jitpack + The Android project must include `maven("https://jitpack.io")` in `settings.gradle`. You can now add the OpenFeature SDK dependency: @@ -44,6 +46,10 @@ api("com.github.spotify:openfeature-kotlin-sdk:[ANY_BRANCH]-SNAPSHOT") This will get a build from the head of the mentioned branch. +### Maven + +Installation via Maven Central is currently WIP + ## 🌟 Features - support for various backend [providers](https://openfeature.dev/docs/reference/concepts/provider) @@ -55,37 +61,21 @@ This will get a build from the head of the mentioned branch. ```kotlin // configure a provider and get client - OpenFeatureAPI.setProvider( - CustomProvider.initialise() - ) + OpenFeatureAPI.setProvider(customProvider) val client = OpenFeatureAPI.getClient() // get a bool flag value client.getBooleanValue("boolFlag", default = false) // get a bool flag value async - openFeatureClient - .toAsync() - .observeBooleanValue(key, default) - .collect { - // do something with boolean + coroutineScope.launch { + WithContext(Dispatchers.IO) { + client.awaitProviderReady() } - - // get bool flag with compose - val myBoolProperty = openFeatureClient - .toAsync() - .observeBooleanValue(key, default) - .collectAsState() + client.getBooleanValue("boolFlag", default = false) + } ``` -### Context-aware evaluation - -Sometimes the value of a flag must take into account some dynamic criteria about the application or user, such as the user location, IP, email address, or the location of the server. -In OpenFeature, we refer to this as [`targeting`](https://openfeature.dev/specification/glossary#targeting). -If the flag system you're using supports targeting, you can provide the input data using the `EvaluationContext`. - - - ### Events Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. @@ -94,11 +84,11 @@ Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGE Please refer to the documentation of the provider you're using to see what events are supported. ```kotlin - // to listen to PROVIDER_READY event - CoroutineScope(Dispatchers.IO).launch { - awaitProviderReady() - // now provider is ready, read the properties - } + OpenFeatureAPI.eventsObserver() + .observe() + .collect { + // do something once the provider is ready + } ``` ### Providers: @@ -150,11 +140,11 @@ class NewProvider(override val hooks: List>, override val metadata: Meta // resolve a string flag value } - override suspend fun initialize(initialContext: EvaluationContext?) { + override fun initialize(initialContext: EvaluationContext?) { // add context-aware provider initialisation } - override suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) { + override fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) { // add necessary changes on context change } From f22fffab94bd47bd20e65a3e76066311b1e6860d Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Tue, 25 Jul 2023 16:14:45 +0200 Subject: [PATCH 47/56] add a badge to README and remove wrong link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d4ffc3..7760f99 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

OpenFeature Kotlin SDKs

-![Status](https://img.shields.io/badge/lifecycle-alpha-a0c3d2.svg) +![Status](https://img.shields.io/badge/lifecycle-alpha-a0c3d2.svg) [![](https://jitpack.io/v/spotify/openfeature-kotlin-sdk.svg)](https://jitpack.io/#spotify/openfeature-kotlin-sdk) ## 👋 Hey there! Thanks for checking out the OpenFeature Kotlin SDK @@ -94,7 +94,7 @@ Please refer to the documentation of the provider you're using to see what event ### Providers: To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. -This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization. +This can be a new repository or included in the existing contrib repository available under the OpenFeature organization. Finally, you’ll then need to write the provider itself. This can be accomplished by implementing the `Provider` interface exported by the OpenFeature SDK. From 3850c5d0d5bf0c9ec394deda02d9044efc4ce6ec Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 26 Jul 2023 13:40:01 +0200 Subject: [PATCH 48/56] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7760f99..7a4f33f 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Please refer to the documentation of the provider you're using to see what event } ``` -### Providers: +### Providers To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in the existing contrib repository available under the OpenFeature organization. @@ -177,4 +177,4 @@ Made with [contrib.rocks](https://contrib.rocks). [Apache License 2.0](LICENSE) -[openfeature-website]: https://openfeature.dev \ No newline at end of file +[openfeature-website]: https://openfeature.dev From 77fcb38c073b82e3dad506510863e21c5cd40496 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 27 Jul 2023 15:46:10 +0200 Subject: [PATCH 49/56] add dispatchers to the extensions --- .../java/dev/openfeature/sdk/async/AsyncClient.kt | 6 ++++-- .../java/dev/openfeature/sdk/async/Extensions.kt | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt index 38a2cb4..d730d6e 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt @@ -2,6 +2,7 @@ package dev.openfeature.sdk.async import dev.openfeature.sdk.OpenFeatureClient import dev.openfeature.sdk.Value +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -15,9 +16,10 @@ interface AsyncClient { } internal class AsyncClientImpl( - private val client: OpenFeatureClient + private val client: OpenFeatureClient, + private val dispatcher: CoroutineDispatcher ) : AsyncClient { - private fun observeEvents(callback: () -> T) = observeProviderReady() + private fun observeEvents(callback: () -> T) = observeProviderReady(dispatcher) .map { callback() } .distinctUntilChanged() diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt index bcb299b..52645cc 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt @@ -4,6 +4,7 @@ import dev.openfeature.sdk.OpenFeatureClient import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.events.observe +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -12,11 +13,13 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -fun OpenFeatureClient.toAsync(): AsyncClient { - return AsyncClientImpl(this) +fun OpenFeatureClient.toAsync(dispatcher: CoroutineDispatcher = Dispatchers.IO): AsyncClient { + return AsyncClientImpl(this, dispatcher) } -internal fun observeProviderReady() = EventHandler.eventsObserver() +internal fun observeProviderReady( + dispatcher: CoroutineDispatcher = Dispatchers.IO +) = EventHandler.eventsObserver(dispatcher) .observe() .onStart { if (EventHandler.providerStatus().isProviderReady()) { @@ -24,8 +27,10 @@ internal fun observeProviderReady() = EventHandler.eventsObserver() } } -suspend fun awaitProviderReady() = suspendCancellableCoroutine { continuation -> - val coroutineScope = CoroutineScope(Dispatchers.IO) +suspend fun awaitProviderReady( + dispatcher: CoroutineDispatcher = Dispatchers.IO +) = suspendCancellableCoroutine { continuation -> + val coroutineScope = CoroutineScope(dispatcher) coroutineScope.launch { observeProviderReady() .take(1) From 2d5c39adfbe54fe34bb9d37bb278ca868f88d509 Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Mon, 31 Jul 2023 15:29:09 +0200 Subject: [PATCH 50/56] handle ProviderStale event and remove ProviderConfigurationChanged event --- .../openfeature/sdk/events/EventHandler.kt | 1 + .../sdk/events/OpenFeatureEvents.kt | 1 - .../dev/openfeature/sdk/EventsHandlerTest.kt | 27 ++++++++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt index 45fa3fe..d5813a0 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt @@ -37,6 +37,7 @@ class EventHandler(dispatcher: CoroutineDispatcher) : EventObserver, EventsPubli sharedFlow.collect { when (it) { is OpenFeatureEvents.ProviderReady -> isProviderReady.value = true + is OpenFeatureEvents.ProviderStale -> isProviderReady.value = false is OpenFeatureEvents.ProviderShutDown -> { isProviderReady.value = false job.cancelChildren() diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt index e6d0e79..90a1a8c 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/OpenFeatureEvents.kt @@ -2,7 +2,6 @@ package dev.openfeature.sdk.events sealed class OpenFeatureEvents { object ProviderReady : OpenFeatureEvents() - object ProviderConfigurationChanged : OpenFeatureEvents() data class ProviderError(val error: Throwable) : OpenFeatureEvents() object ProviderStale : OpenFeatureEvents() object ProviderShutDown : OpenFeatureEvents() diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt index 4b42af0..397e1dd 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt @@ -5,12 +5,10 @@ import dev.openfeature.sdk.async.toAsync import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.events.observe -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.timeout -import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Assert @@ -151,6 +149,27 @@ class EventsHandlerTest { Assert.assertTrue(isProviderReady) } + @Test + fun the_provider_becomes_stale() = runTest { + val eventPublisher = EventHandler.eventsPublisher() + var isProviderStale = false + + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + EventHandler.eventsObserver() + .observe() + .take(1) + .collect { + isProviderStale = true + } + } + + eventPublisher.publish(OpenFeatureEvents.ProviderReady) + eventPublisher.publish(OpenFeatureEvents.ProviderStale) + job.join() + + Assert.assertTrue(isProviderStale) + } + @Test fun observe_string_value_from_client_works() = runTest { val testDispatcher = UnconfinedTestDispatcher(testScheduler) From 87cd97a645615f34818d4f32719394f4b00b8bcd Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Mon, 31 Jul 2023 15:45:35 +0200 Subject: [PATCH 51/56] remove wildcard import --- .../src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt index 397e1dd..e019bc1 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt @@ -5,10 +5,12 @@ import dev.openfeature.sdk.async.toAsync import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.events.observe -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.timeout +import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Assert From 7545f78c01add80f63b7d2c5ffdcd76f024755e9 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 2 Aug 2023 11:41:22 +0200 Subject: [PATCH 52/56] Bug fix in setEvaluationContext --- .../src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index 4e941a7..649d83b 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -32,8 +32,9 @@ object OpenFeatureAPI { } fun setEvaluationContext(evaluationContext: EvaluationContext) { + val oldContext = context context = evaluationContext - getProvider()?.onContextSet(context, evaluationContext) + getProvider()?.onContextSet(oldContext, evaluationContext) } fun getEvaluationContext(): EvaluationContext? { From 3081a655dbe84e56805a4627bfd56030f63fa3f0 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Fri, 7 Jul 2023 16:30:53 -0400 Subject: [PATCH 53/56] Initial commit --- LICENSE | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 7a4a3ea..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -199,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. From 853591b942ce885fa2b94c91fbb58ef2a0b1360e Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Tue, 8 Aug 2023 16:22:36 +0200 Subject: [PATCH 54/56] remove comment from code to document it in issues Signed-off-by: Nicky Bondarenko --- .../src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt index 9e9c433..3e2d7d2 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt @@ -205,7 +205,7 @@ class OpenFeatureClient( return details } - @Suppress("UNCHECKED_CAST") // TODO can we do better here? + @Suppress("UNCHECKED_CAST") private fun createProviderEvaluation( flagValueType: FlagValueType, key: String, From 0781caff0c40e2711c3e20d064eab650e21589bf Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Tue, 8 Aug 2023 16:28:09 +0200 Subject: [PATCH 55/56] remove mention of HackerOne Signed-off-by: Nicky Bondarenko --- SECURITY.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 1bfe7e6..b561b6c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,10 +7,3 @@ So, needless to say, we take security issues very seriously. In our opinion, the practice of 'responsible disclosure' is the best way to safeguard the Internet. It allows individuals to notify companies like Spotify of any security threats before going public with the information. This gives us a fighting chance to resolve the problem before the criminally-minded become aware of it. Responsible disclosure is the industry best practice, and we recommend it as a procedure to anyone researching security vulnerabilities. - -## Reporting a Vulnerability - -If you have discovered a vulnerability in this open source project or another serious security issue, -please submit it to the Spotify bounty program hosted by HackerOne. - -https://hackerone.com/spotify \ No newline at end of file From 163c22358ee9711d25d506d75959724b5c33800c Mon Sep 17 00:00:00 2001 From: Nicky Bondarenko Date: Tue, 8 Aug 2023 16:28:44 +0200 Subject: [PATCH 56/56] remove unnecessary yaml file Signed-off-by: Nicky Bondarenko --- catalog-info.yaml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 catalog-info.yaml diff --git a/catalog-info.yaml b/catalog-info.yaml deleted file mode 100644 index ab5905f..0000000 --- a/catalog-info.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: openfeature-kotlin-sdk -spec: - type: library - owner: hawkeye