Skip to content

Commit c299717

Browse files
committed
PR comments; remove redundant comments, fix test definitions, move strategy into MultiProvider
Signed-off-by: penguindan <[email protected]>
1 parent f834a43 commit c299717

File tree

7 files changed

+89
-120
lines changed

7 files changed

+89
-120
lines changed

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/FirstMatchStrategy.kt

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,15 @@ import dev.openfeature.kotlin.sdk.exceptions.ErrorCode
88
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
99

1010
/**
11-
* Return the first result returned by a provider. Skip providers that indicate they had no value due to FLAG_NOT_FOUND.
12-
* In all other cases, use the value returned by the provider. If any provider returns an error result other than
13-
* FLAG_NOT_FOUND, the whole evaluation should error and "bubble up" the individual provider's error in the result.
11+
* A [MultiProvider.Strategy] that returns the first result returned by a [FeatureProvider].
1412
*
15-
* As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call the
16-
* rest of the providers.
13+
* It skips providers that indicate they had no value due to [ErrorCode.FLAG_NOT_FOUND].
14+
* In all other cases, it uses the value returned by the provider.
15+
*
16+
* If any provider returns an error result other than [ErrorCode.FLAG_NOT_FOUND], the whole evaluation
17+
* returns the provider's error.
1718
*/
18-
class FirstMatchStrategy : Strategy {
19-
/**
20-
* Evaluates providers in sequence until finding one that has knowledge of the flag.
21-
*
22-
* @param providers List of providers to evaluate in order
23-
* @param key The feature flag key to look up
24-
* @param defaultValue Value to return if no provider knows about the flag
25-
* @param evaluationContext Optional context for evaluation
26-
* @param flagEval The specific evaluation method to call on each provider
27-
* @return ProviderEvaluation with the first match or default value
28-
*/
19+
class FirstMatchStrategy : MultiProvider.Strategy {
2920
override fun <T> evaluate(
3021
providers: List<FeatureProvider>,
3122
key: String,

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/FirstSuccessfulStrategy.kt

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,12 @@ import dev.openfeature.kotlin.sdk.ProviderEvaluation
66
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
77

88
/**
9-
* Similar to "First Match", except that errors from evaluated providers do not halt execution.
10-
* Instead, it will return the first successful result from a provider.
9+
* A [MultiProvider.Strategy] similar to the [FirstMatchStrategy], except that errors from evaluated
10+
* providers do not halt execution.
1111
*
12-
* If no provider successfully responds, it will throw an error result.
12+
* If no provider successfully responds, it throws an error result.
1313
*/
14-
class FirstSuccessfulStrategy : Strategy {
15-
/**
16-
* Evaluates providers in sequence until finding one that returns a successful result.
17-
*
18-
* @param providers List of providers to evaluate in order
19-
* @param key The feature flag key to evaluate
20-
* @param defaultValue Value to use in provider evaluations
21-
* @param evaluationContext Optional context for evaluation
22-
* @param flagEval The specific evaluation method to call on each provider
23-
* @return ProviderEvaluation with the first successful result
24-
* @throws OpenFeatureError.GeneralError if no provider returns a successful evaluation
25-
*/
14+
class FirstSuccessfulStrategy : MultiProvider.Strategy {
2615
override fun <T> evaluate(
2716
providers: List<FeatureProvider>,
2817
key: String,

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/MultiProvider.kt

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ import kotlinx.coroutines.flow.launchIn
2121
import kotlinx.coroutines.flow.onEach
2222
import kotlinx.coroutines.flow.update
2323

24+
/**
25+
* Type alias for a function that evaluates a feature flag using a FeatureProvider.
26+
* This represents an extension function on FeatureProvider that takes:
27+
* - key: The feature flag key to evaluate
28+
* - defaultValue: The default value to return if evaluation fails
29+
* - evaluationContext: Optional context for the evaluation
30+
* Returns a ProviderEvaluation containing the result
31+
*/
32+
typealias FlagEval<T> =
33+
FeatureProvider.(key: String, defaultValue: T, evaluationContext: EvaluationContext?) -> ProviderEvaluation<T>
34+
2435
/**
2536
* MultiProvider is a FeatureProvider implementation that delegates flag evaluations
2637
* to multiple underlying providers using a configurable strategy.
@@ -51,6 +62,41 @@ class MultiProvider(
5162
val name: String // Maybe there's a better variable name for this?
5263
) : FeatureProvider by implementation
5364

65+
/**
66+
* Strategy interface defines how multiple feature providers should be evaluated
67+
* to determine the final result for a feature flag evaluation.
68+
* Different strategies can implement different logic for combining or selecting
69+
* results from multiple providers.
70+
*/
71+
interface Strategy {
72+
/**
73+
* Evaluates a feature flag across multiple providers using the strategy's logic.
74+
* @param providers List of FeatureProvider instances to evaluate against
75+
* @param key The feature flag key to evaluate
76+
* @param defaultValue The default value to use if evaluation fails or no providers match
77+
* @param evaluationContext Optional context containing additional data for evaluation
78+
* @param flagEval Function reference to the specific evaluation method to call on each provider
79+
* @return ProviderEvaluation<T> containing the final evaluation result
80+
*/
81+
fun <T> evaluate(
82+
providers: List<FeatureProvider>,
83+
key: String,
84+
defaultValue: T,
85+
evaluationContext: EvaluationContext?,
86+
flagEval: FlagEval<T>
87+
): ProviderEvaluation<T>
88+
}
89+
90+
private val OpenFeatureStatus.precedence: Int
91+
get() = when (this) {
92+
is OpenFeatureStatus.Fatal -> 5
93+
is OpenFeatureStatus.NotReady -> 4
94+
is OpenFeatureStatus.Error -> 3
95+
is OpenFeatureStatus.Reconciling -> 2 // Not specified in precedence; treat similar to Stale
96+
is OpenFeatureStatus.Stale -> 2
97+
is OpenFeatureStatus.Ready -> 1
98+
}
99+
54100
// TODO: Support hooks
55101
override val hooks: List<Hook<*>> = emptyList()
56102
private val childFeatureProviders: List<ChildFeatureProvider> by lazy {
@@ -61,11 +107,7 @@ class MultiProvider(
61107
override val metadata: ProviderMetadata = object : ProviderMetadata {
62108
override val name: String? = MULTIPROVIDER_NAME
63109
override val originalMetadata: Map<String, ProviderMetadata> by lazy {
64-
constructOriginalMetadata()
65-
}
66-
67-
private fun constructOriginalMetadata(): Map<String, ProviderMetadata> {
68-
return childFeatureProviders.associate { it.name to it.metadata }
110+
childFeatureProviders.associate { it.name to it.metadata }
69111
}
70112

71113
override fun toString(): String {
@@ -114,7 +156,7 @@ class MultiProvider(
114156
/**
115157
* @return Number of unique providers
116158
*/
117-
fun getProviderCount(): Int = childFeatureProviders.size
159+
internal fun getProviderCount(): Int = childFeatureProviders.size
118160

119161
// TODO Add distinctUntilChanged operator once EventDetails have been added
120162
override fun observe(): Flow<OpenFeatureProviderEvents> = eventFlow.asSharedFlow()
@@ -145,12 +187,13 @@ class MultiProvider(
145187
}
146188

147189
private suspend fun handleProviderEvent(provider: ChildFeatureProvider, event: OpenFeatureProviderEvents) {
148-
if (event is OpenFeatureProviderEvents.ProviderConfigurationChanged) {
149-
eventFlow.emit(event)
150-
return
151-
}
152-
153190
val newChildStatus = when (event) {
191+
// ProviderConfigurationChanged events should always re-emit
192+
is OpenFeatureProviderEvents.ProviderConfigurationChanged -> {
193+
eventFlow.emit(event)
194+
return
195+
}
196+
154197
is OpenFeatureProviderEvents.ProviderReady -> OpenFeatureStatus.Ready
155198
is OpenFeatureProviderEvents.ProviderNotReady -> OpenFeatureStatus.NotReady
156199
is OpenFeatureProviderEvents.ProviderStale -> OpenFeatureStatus.Stale
@@ -160,8 +203,6 @@ class MultiProvider(
160203
} else {
161204
OpenFeatureStatus.Error(event.error)
162205
}
163-
164-
else -> error("Unexpected event $event")
165206
}
166207

167208
val previousStatus = _statusFlow.value
@@ -176,21 +217,10 @@ class MultiProvider(
176217
}
177218

178219
private fun calculateAggregateStatus(): OpenFeatureStatus {
179-
val highestPrecedenceStatus = childProviderStatuses.values.maxBy(::precedence)
220+
val highestPrecedenceStatus = childProviderStatuses.values.maxBy { it.precedence }
180221
return highestPrecedenceStatus
181222
}
182223

183-
private fun precedence(status: OpenFeatureStatus): Int {
184-
return when (status) {
185-
is OpenFeatureStatus.Fatal -> 5
186-
is OpenFeatureStatus.NotReady -> 4
187-
is OpenFeatureStatus.Error -> 3
188-
is OpenFeatureStatus.Reconciling -> 2 // Not specified in precedence; treat similar to Stale
189-
is OpenFeatureStatus.Stale -> 2
190-
is OpenFeatureStatus.Ready -> 1
191-
}
192-
}
193-
194224
/**
195225
* Shuts down all underlying providers.
196226
* This allows providers to clean up resources and complete any pending operations.

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/Strategy.kt

Lines changed: 0 additions & 41 deletions
This file was deleted.

kotlin-sdk/src/commonTest/kotlin/dev/openfeature/kotlin/sdk/multiprovider/FirstMatchStrategyTests.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import kotlin.test.assertFailsWith
1212
class FirstMatchStrategyTests {
1313

1414
@Test
15-
fun returns_first_success_without_calling_next_providers() {
15+
fun returnsFirstSuccessWithoutCallingNextProviders() {
1616
val strategy = FirstMatchStrategy()
1717
val first = RecordingBooleanProvider(
1818
name = "first",
@@ -38,7 +38,7 @@ class FirstMatchStrategyTests {
3838
}
3939

4040
@Test
41-
fun skips_flag_not_found_and_returns_next_match() {
41+
fun skipsFlagNotFoundAndReturnsNextMatch() {
4242
val strategy = FirstMatchStrategy()
4343
val notFoundProvider = RecordingBooleanProvider(
4444
name = "not-found",
@@ -70,7 +70,7 @@ class FirstMatchStrategyTests {
7070
}
7171

7272
@Test
73-
fun treats_FlagNotFound_exception_as_not_found_and_continues() {
73+
fun treatsFlagNotFoundExceptionAsNotFoundAndContinues() {
7474
val strategy = FirstMatchStrategy()
7575
val throwsNotFound = RecordingBooleanProvider(
7676
name = "throws-not-found",
@@ -95,7 +95,7 @@ class FirstMatchStrategyTests {
9595
}
9696

9797
@Test
98-
fun returns_error_result_other_than_not_found_and_short_circuits() {
98+
fun returnsErrorResultOtherThanNotFoundAndShortCircuits() {
9999
val strategy = FirstMatchStrategy()
100100
val errorProvider = RecordingBooleanProvider(
101101
name = "error",
@@ -121,7 +121,7 @@ class FirstMatchStrategyTests {
121121
}
122122

123123
@Test
124-
fun bubbles_up_non_not_found_exceptions() {
124+
fun bubblesUpNonNotFoundExceptions() {
125125
val strategy = FirstMatchStrategy()
126126
val throwsGeneral = RecordingBooleanProvider(
127127
name = "throws-general",
@@ -141,7 +141,7 @@ class FirstMatchStrategyTests {
141141
}
142142

143143
@Test
144-
fun returns_default_with_not_found_when_no_provider_matches() {
144+
fun returnsDefaultWithNotFoundWhenNoProviderMatches() {
145145
val strategy = FirstMatchStrategy()
146146
val p1 = RecordingBooleanProvider(
147147
name = "p1",

kotlin-sdk/src/commonTest/kotlin/dev/openfeature/kotlin/sdk/multiprovider/FirstSuccessfulStrategyTests.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import kotlin.test.assertFailsWith
1111
class FirstSuccessfulStrategyTests {
1212

1313
@Test
14-
fun returns_first_success_ignoring_prior_errors() {
14+
fun returnsFirstSuccessIgnoringPriorErrors() {
1515
val strategy = FirstSuccessfulStrategy()
1616
val error1 = RecordingBooleanProvider("e1") {
1717
throw OpenFeatureError.GeneralError("boom1")
@@ -43,7 +43,7 @@ class FirstSuccessfulStrategyTests {
4343
}
4444

4545
@Test
46-
fun skips_flag_not_found_error_and_result_until_success() {
46+
fun skipsFlagNotFoundErrorAndResultUntilSuccess() {
4747
val strategy = FirstSuccessfulStrategy()
4848
val notFoundThrow = RecordingBooleanProvider("nf-throw") {
4949
throw OpenFeatureError.FlagNotFoundError("flag")
@@ -70,7 +70,7 @@ class FirstSuccessfulStrategyTests {
7070
}
7171

7272
@Test
73-
fun throws_when_no_provider_returns_success() {
73+
fun throwsWhenNoProviderReturnsSuccess() {
7474
val strategy = FirstSuccessfulStrategy()
7575
val error1 = RecordingBooleanProvider("e1") {
7676
throw OpenFeatureError.GeneralError("boom1")

0 commit comments

Comments
 (0)