Skip to content

Commit eaeb895

Browse files
committed
refactor: error messaging, tests, cleaning (#208)
Signed-off-by: Marcin Stepien <marcin.stepien@fluxon.com>
1 parent f9a501f commit eaeb895

7 files changed

Lines changed: 328 additions & 36 deletions

File tree

kotlin-sdk/api/android/kotlin-sdk.api

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ public abstract interface class dev/openfeature/kotlin/sdk/multiprovider/MultiPr
862862
}
863863

864864
public abstract interface class dev/openfeature/kotlin/sdk/providers/memory/ContextEvaluator {
865-
public abstract fun evaluate (Ldev/openfeature/kotlin/sdk/providers/memory/Flag;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ljava/lang/Object;
865+
public abstract fun evaluate (Ldev/openfeature/kotlin/sdk/providers/memory/Flag;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ljava/lang/String;
866866
}
867867

868868
public final class dev/openfeature/kotlin/sdk/providers/memory/Flag {
@@ -911,7 +911,6 @@ public final class dev/openfeature/kotlin/sdk/providers/memory/InMemoryProvider
911911
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
912912
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
913913
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
914-
public final fun getState ()Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
915914
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
916915
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
917916
public fun observe ()Lkotlinx/coroutines/flow/Flow;

kotlin-sdk/api/jvm/kotlin-sdk.api

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ public abstract interface class dev/openfeature/kotlin/sdk/multiprovider/MultiPr
862862
}
863863

864864
public abstract interface class dev/openfeature/kotlin/sdk/providers/memory/ContextEvaluator {
865-
public abstract fun evaluate (Ldev/openfeature/kotlin/sdk/providers/memory/Flag;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ljava/lang/Object;
865+
public abstract fun evaluate (Ldev/openfeature/kotlin/sdk/providers/memory/Flag;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ljava/lang/String;
866866
}
867867

868868
public final class dev/openfeature/kotlin/sdk/providers/memory/Flag {
@@ -911,7 +911,6 @@ public final class dev/openfeature/kotlin/sdk/providers/memory/InMemoryProvider
911911
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
912912
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
913913
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
914-
public final fun getState ()Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
915914
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
916915
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
917916
public fun observe ()Lkotlinx/coroutines/flow/Flow;

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/providers/memory/ContextEvaluator.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,13 @@ import dev.openfeature.kotlin.sdk.EvaluationContext
66
* Context evaluator - use for resolving flag according to evaluation context, for handling targeting.
77
*/
88
fun interface ContextEvaluator<T> {
9-
fun evaluate(flag: Flag<T>, evaluationContext: EvaluationContext?): T?
9+
/**
10+
* Evaluates the flag's specific variant based on the provided evaluation context.
11+
*
12+
* @param flag the feature flag representation
13+
* @param evaluationContext the context used for targeting
14+
* @return the resolved variant key (a string matching a key in the flag's variants map),
15+
* or null if no match was found.
16+
*/
17+
fun evaluate(flag: Flag<T>, evaluationContext: EvaluationContext?): String?
1018
}

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/providers/memory/Flag.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ data class Flag<T>(
1212
val flagMetadata: EvaluationMetadata? = null,
1313
val disabled: Boolean = false
1414
) {
15+
init {
16+
if (defaultVariant != null && !variants.containsKey(defaultVariant)) {
17+
throw IllegalArgumentException("defaultVariant ($defaultVariant) is not present in variants map")
18+
}
19+
}
20+
1521
companion object {
1622
fun <T> builder() = Builder<T>()
1723
}
@@ -31,11 +37,7 @@ data class Flag<T>(
3137
fun disabled(disabled: Boolean) = apply { this.disabled = disabled }
3238

3339
fun build(): Flag<T> {
34-
val dv = defaultVariant
35-
if (dv != null && !variants.containsKey(dv)) {
36-
throw IllegalArgumentException("defaultVariant ($dv) is not present in variants map")
37-
}
38-
return Flag(variants.toMap(), dv, contextEvaluator, flagMetadata, disabled)
40+
return Flag(variants.toMap(), defaultVariant, contextEvaluator, flagMetadata, disabled)
3941
}
4042
}
4143
}

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/providers/memory/InMemoryProvider.kt

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.openfeature.kotlin.sdk.providers.memory
22

33
import dev.openfeature.kotlin.sdk.EvaluationContext
4+
import dev.openfeature.kotlin.sdk.EvaluationMetadata
45
import dev.openfeature.kotlin.sdk.FeatureProvider
56
import dev.openfeature.kotlin.sdk.Hook
67
import dev.openfeature.kotlin.sdk.OpenFeatureStatus
@@ -12,6 +13,7 @@ import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
1213
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents.EventDetails
1314
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents.ProviderConfigurationChanged
1415
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents.ProviderReady
16+
import dev.openfeature.kotlin.sdk.exceptions.ErrorCode
1517
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.FlagNotFoundError
1618
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.ProviderNotReadyError
1719
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.TypeMismatchError
@@ -30,8 +32,7 @@ class InMemoryProvider(initialFlags: Map<String, Flag<*>> = emptyMap()) : Featur
3032
private val flagsState = MutableStateFlow<Map<String, Flag<*>>>(initialFlags.toMap())
3133

3234
@Volatile
33-
var state: OpenFeatureStatus = OpenFeatureStatus.NotReady
34-
private set
35+
private var state: OpenFeatureStatus = OpenFeatureStatus.NotReady
3536

3637
private val eventFlow = MutableSharedFlow<OpenFeatureProviderEvents>(extraBufferCapacity = 64)
3738

@@ -139,28 +140,42 @@ class InMemoryProvider(initialFlags: Map<String, Flag<*>> = emptyMap()) : Featur
139140
value = defaultValue,
140141
reason = Reason.DISABLED.toString(),
141142
metadata = flag.flagMetadata
142-
?: dev.openfeature.kotlin.sdk.EvaluationMetadata.EMPTY
143+
?: EvaluationMetadata.EMPTY
143144
)
144145
}
145146

146147
var value: Any? = null
147148
var reason = Reason.STATIC
149+
var errorCode: ErrorCode? = null
150+
var errorMessage: String? = null
151+
var variant: String? = flag.defaultVariant
148152

149153
if (flag.contextEvaluator != null) {
150154
try {
151155
@Suppress("UNCHECKED_CAST")
152-
value =
156+
val evaluatedVariant =
153157
(flag.contextEvaluator as ContextEvaluator<T>).evaluate(
154158
flag as Flag<T>,
155159
context
156160
)
157-
reason = Reason.TARGETING_MATCH
161+
if (evaluatedVariant != null) {
162+
if (flag.variants.containsKey(evaluatedVariant)) {
163+
value = flag.variants[evaluatedVariant]
164+
variant = evaluatedVariant
165+
reason = Reason.TARGETING_MATCH
166+
} else {
167+
errorCode = ErrorCode.GENERAL
168+
errorMessage = "Evaluated variant '$evaluatedVariant' not found in variants"
169+
}
170+
}
158171
} catch (e: Exception) {
159-
value = null
172+
errorCode = ErrorCode.GENERAL
173+
errorMessage = e.message ?: "Error evaluating context"
160174
}
161175
if (value == null) {
162176
value = flag.defaultVariant?.let { flag.variants[it] }
163-
reason = Reason.DEFAULT
177+
variant = flag.defaultVariant
178+
reason = if (errorCode != null) Reason.ERROR else Reason.DEFAULT
164179
}
165180
} else {
166181
value = flag.defaultVariant?.let { flag.variants[it] }
@@ -170,7 +185,10 @@ class InMemoryProvider(initialFlags: Map<String, Flag<*>> = emptyMap()) : Featur
170185
throw TypeMismatchError("flag $key evaluated to a type that does not match expected type")
171186
}
172187

173-
if (value == null) {
188+
if (value == null && errorMessage != null) {
189+
// Provide the caller with the actual failure reason
190+
value = defaultValue
191+
} else if (value == null) {
174192
// Check if expected class is actually instantiated by default, if we reach here there's
175193
// a problem
176194
throw TypeMismatchError("flag $key value could not be resolved or cast")
@@ -179,9 +197,11 @@ class InMemoryProvider(initialFlags: Map<String, Flag<*>> = emptyMap()) : Featur
179197
@Suppress("UNCHECKED_CAST")
180198
return ProviderEvaluation(
181199
value = value as T,
182-
variant = if (reason == Reason.DEFAULT) flag.defaultVariant else null,
200+
variant = variant,
183201
reason = reason.toString(),
184-
metadata = flag.flagMetadata ?: dev.openfeature.kotlin.sdk.EvaluationMetadata.EMPTY
202+
errorCode = errorCode,
203+
errorMessage = errorMessage,
204+
metadata = flag.flagMetadata ?: EvaluationMetadata.EMPTY
185205
)
186206
}
187207

0 commit comments

Comments
 (0)