Skip to content

Commit ca9fcf8

Browse files
committed
Update Providers with SMP implementation
1 parent a8ef11f commit ca9fcf8

11 files changed

Lines changed: 92 additions & 36 deletions

File tree

buildSrc/settings.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ pluginManagement {
99
dependencyResolutionManagement {
1010
repositories {
1111
google()
12-
mavenLocal()
1312
mavenCentral()
1413
gradlePluginPortal()
1514
}

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
kotlin = "2.2.10"
33
kotlinx-coroutines = "1.10.2"
44
open-feature-kotlin-sdk = "0.8.0"
5-
android = "8.10.1"
5+
android = "8.13.0"
66
ktor = "3.1.3"
77

88
[libraries]

providers/env-var/api/env-var.api

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
1+
public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider : dev/openfeature/kotlin/sdk/StateManagingProvider {
22
public static final field Companion Ldev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider$Companion;
33
public fun <init> ()V
44
public fun <init> (Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway;Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer;)V
@@ -7,8 +7,10 @@ public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvide
77
public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
88
public fun getHooks ()Ljava/util/List;
99
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
10+
public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
1011
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
1112
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
13+
public fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
1214
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
1315
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1416
public fun observe ()Lkotlinx/coroutines/flow/Flow;

providers/env-var/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ kotlin {
2626
sourceSets {
2727
commonMain.dependencies {
2828
api(libs.openfeature.kotlin.sdk)
29+
api(libs.kotlinx.coroutines.core)
2930
}
3031
commonTest.dependencies {
3132
implementation(libs.kotlin.test)

providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider.kt

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,59 @@
11
package dev.openfeature.kotlin.contrib.providers.envvar
22

33
import dev.openfeature.kotlin.sdk.EvaluationContext
4-
import dev.openfeature.kotlin.sdk.FeatureProvider
54
import dev.openfeature.kotlin.sdk.Hook
5+
import dev.openfeature.kotlin.sdk.OpenFeatureStatus
66
import dev.openfeature.kotlin.sdk.ProviderEvaluation
77
import dev.openfeature.kotlin.sdk.ProviderMetadata
8+
import dev.openfeature.kotlin.sdk.ProviderStatusTracker
89
import dev.openfeature.kotlin.sdk.Reason
10+
import dev.openfeature.kotlin.sdk.StateManagingProvider
911
import dev.openfeature.kotlin.sdk.Value
12+
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
13+
import dev.openfeature.kotlin.sdk.exceptions.ErrorCode
1014
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
15+
import kotlinx.coroutines.flow.StateFlow
16+
import kotlinx.coroutines.yield
1117

1218
/** EnvVarProvider is the Kotlin provider implementation for the environment variables. */
1319
class EnvVarProvider(
1420
private val environmentGateway: EnvironmentGateway = platformSpecificEnvironmentGateway(),
1521
private val keyTransformer: EnvironmentKeyTransformer = EnvironmentKeyTransformer.doNothing(),
16-
) : FeatureProvider {
22+
) : StateManagingProvider {
23+
private val statusTracker = ProviderStatusTracker()
24+
1725
override val hooks: List<Hook<*>> = emptyList()
1826
override val metadata: ProviderMetadata
1927
get() = Metadata
2028

29+
override val status: StateFlow<OpenFeatureStatus> = statusTracker.status
30+
2131
override suspend fun initialize(initialContext: EvaluationContext?) {
22-
// Nothing to do here
32+
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
2333
}
2434

2535
override fun shutdown() {
26-
// Nothing to do here
36+
statusTracker.send(
37+
OpenFeatureProviderEvents.ProviderError(
38+
OpenFeatureProviderEvents.EventDetails(
39+
message = "Environment Variables provider shut down; not ready for evaluation",
40+
errorCode = ErrorCode.PROVIDER_NOT_READY,
41+
),
42+
),
43+
)
2744
}
2845

2946
override suspend fun onContextSet(
3047
oldContext: EvaluationContext?,
3148
newContext: EvaluationContext,
3249
) {
33-
// Nothing to do here
50+
statusTracker.send(OpenFeatureProviderEvents.ProviderReconciling())
51+
yield()
52+
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
3453
}
3554

55+
override fun observe() = statusTracker.observe()
56+
3657
override fun getBooleanEvaluation(
3758
key: String,
3859
defaultValue: Boolean,
@@ -51,6 +72,12 @@ class EnvVarProvider(
5172
context: EvaluationContext?,
5273
): ProviderEvaluation<Int> = evaluateEnvironmentVariable(key, String::toInt)
5374

75+
override fun getLongEvaluation(
76+
key: String,
77+
defaultValue: Long,
78+
context: EvaluationContext?,
79+
): ProviderEvaluation<Long> = evaluateEnvironmentVariable(key, String::toLong)
80+
5481
override fun getStringEvaluation(
5582
key: String,
5683
defaultValue: String,

providers/ofrep/api/android/ofrep.api

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
1+
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/StateManagingProvider {
22
public fun <init> (Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;)V
33
public fun getBooleanEvaluation (Ljava/lang/String;ZLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
44
public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
55
public fun getHooks ()Ljava/util/List;
66
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
7+
public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
78
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
89
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
10+
public fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
911
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
1012
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1113
public fun observe ()Lkotlinx/coroutines/flow/Flow;

providers/ofrep/api/jvm/ofrep.api

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
1+
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/StateManagingProvider {
22
public fun <init> (Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;)V
33
public fun getBooleanEvaluation (Ljava/lang/String;ZLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
44
public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
55
public fun getHooks ()Ljava/util/List;
66
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
7+
public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
78
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
89
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
10+
public fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
911
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
1012
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1113
public fun observe ()Lkotlinx/coroutines/flow/Flow;

providers/ofrep/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider.kt

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import dev.openfeature.kotlin.contrib.providers.ofrep.controller.OfrepApi
88
import dev.openfeature.kotlin.contrib.providers.ofrep.enum.BulkEvaluationStatus
99
import dev.openfeature.kotlin.contrib.providers.ofrep.error.OfrepError
1010
import dev.openfeature.kotlin.sdk.EvaluationContext
11-
import dev.openfeature.kotlin.sdk.FeatureProvider
1211
import dev.openfeature.kotlin.sdk.Hook
1312
import dev.openfeature.kotlin.sdk.ImmutableContext
13+
import dev.openfeature.kotlin.sdk.OpenFeatureStatus
1414
import dev.openfeature.kotlin.sdk.ProviderEvaluation
1515
import dev.openfeature.kotlin.sdk.ProviderMetadata
16+
import dev.openfeature.kotlin.sdk.ProviderStatusTracker
17+
import dev.openfeature.kotlin.sdk.StateManagingProvider
1618
import dev.openfeature.kotlin.sdk.Value
1719
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
1820
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents.EventDetails
@@ -23,7 +25,7 @@ import kotlinx.coroutines.CoroutineScope
2325
import kotlinx.coroutines.Job
2426
import kotlinx.coroutines.delay
2527
import kotlinx.coroutines.flow.Flow
26-
import kotlinx.coroutines.flow.MutableSharedFlow
28+
import kotlinx.coroutines.flow.StateFlow
2729
import kotlinx.coroutines.isActive
2830
import kotlinx.coroutines.launch
2931
import kotlin.concurrent.Volatile
@@ -35,14 +37,18 @@ import kotlin.time.Instant
3537
@OptIn(ExperimentalTime::class)
3638
class OfrepProvider(
3739
private val ofrepOptions: OfrepOptions,
38-
) : FeatureProvider {
40+
) : StateManagingProvider {
3941
private val ofrepApi = OfrepApi(ofrepOptions)
4042
override val hooks: List<Hook<*>>
4143
get() = listOf()
4244

4345
override val metadata: ProviderMetadata
4446
get() = OfrepProviderMetadata()
4547

48+
private val statusTracker = ProviderStatusTracker()
49+
50+
override val status: StateFlow<OpenFeatureStatus> = statusTracker.status
51+
4652
private var evaluationContext: EvaluationContext? = null
4753

4854
@Volatile
@@ -51,9 +57,7 @@ class OfrepProvider(
5157
private val pollingScope: CoroutineScope = CoroutineScope(ofrepOptions.pollingDispatcher)
5258
private var pollingJob: Job? = null
5359

54-
private val statusFlow = MutableSharedFlow<OpenFeatureProviderEvents>(replay = 1)
55-
56-
override fun observe(): Flow<OpenFeatureProviderEvents> = statusFlow
60+
override fun observe(): Flow<OpenFeatureProviderEvents> = statusTracker.observe()
5761

5862
private fun providerError(error: Throwable): OpenFeatureProviderEvents.ProviderError {
5963
val ofError =
@@ -73,12 +77,12 @@ class OfrepProvider(
7377
try {
7478
val bulkEvaluationStatus = evaluateFlags(initialContext ?: ImmutableContext())
7579
if (bulkEvaluationStatus == BulkEvaluationStatus.RATE_LIMITED) {
76-
statusFlow.emit(providerError(OpenFeatureError.GeneralError("Rate limited")))
80+
statusTracker.send(providerError(OpenFeatureError.GeneralError("Rate limited")))
7781
} else {
78-
statusFlow.emit(OpenFeatureProviderEvents.ProviderReady())
82+
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
7983
}
8084
} catch (e: Throwable) {
81-
statusFlow.emit(providerError(e))
85+
statusTracker.send(providerError(e))
8286
}
8387
startPolling()
8488
}
@@ -109,17 +113,17 @@ class OfrepProvider(
109113
BulkEvaluationStatus.SUCCESS_UPDATED -> {
110114
// TODO: we should migrate to configuration change event when it's available
111115
// in the kotlin SDK
112-
statusFlow.emit(OpenFeatureProviderEvents.ProviderReady())
116+
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
113117
}
114118
}
115119
} catch (e: CancellationException) {
116120
// expected to happen when the job is cancelled, no need to report it via the
117-
// statusFlow
121+
// status tracker
118122
} catch (e: OfrepError.ApiTooManyRequestsError) {
119123
// in that case the provider is just stale because we were not able to
120-
statusFlow.emit(OpenFeatureProviderEvents.ProviderStale())
124+
statusTracker.send(OpenFeatureProviderEvents.ProviderStale())
121125
} catch (e: Throwable) {
122-
statusFlow.emit(providerError(e))
126+
statusTracker.send(providerError(e))
123127
}
124128
}
125129
}
@@ -143,6 +147,12 @@ class OfrepProvider(
143147
context: EvaluationContext?,
144148
): ProviderEvaluation<Int> = genericEvaluation(key, defaultValue)
145149

150+
override fun getLongEvaluation(
151+
key: String,
152+
defaultValue: Long,
153+
context: EvaluationContext?,
154+
): ProviderEvaluation<Long> = genericEvaluation(key, defaultValue)
155+
146156
override fun getObjectEvaluation(
147157
key: String,
148158
defaultValue: Value,
@@ -159,23 +169,31 @@ class OfrepProvider(
159169
oldContext: EvaluationContext?,
160170
newContext: EvaluationContext,
161171
) {
162-
this.statusFlow.emit(OpenFeatureProviderEvents.ProviderStale())
172+
statusTracker.send(OpenFeatureProviderEvents.ProviderStale())
163173
this.evaluationContext = newContext
164174

165175
try {
166176
val postBulkEvaluateFlags = evaluateFlags(newContext)
167177
// we don't emit event if the evaluation is rate limited because
168178
// the provider is still stale
169179
if (postBulkEvaluateFlags != BulkEvaluationStatus.RATE_LIMITED) {
170-
statusFlow.emit(OpenFeatureProviderEvents.ProviderReady())
180+
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
171181
}
172182
} catch (e: Throwable) {
173-
statusFlow.emit(providerError(e))
183+
statusTracker.send(providerError(e))
174184
}
175185
}
176186

177187
override fun shutdown() {
178188
pollingJob?.cancel()
189+
statusTracker.send(
190+
OpenFeatureProviderEvents.ProviderError(
191+
EventDetails(
192+
message = "OFREP provider shut down; not ready for evaluation",
193+
errorCode = ErrorCode.PROVIDER_NOT_READY,
194+
),
195+
),
196+
)
179197
}
180198

181199
private inline fun <reified T> genericEvaluation(

providers/ofrep/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepApiResponse.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ inline fun <reified T> Value.toPrimitive(): T {
4242
Boolean::class -> asBoolean() as T?
4343
String::class -> asString() as T?
4444
Int::class -> asInteger() as T?
45+
Long::class ->
46+
(asLong() ?: asInteger()?.toLong()) as T?
4547
Double::class ->
4648
// doubles might have been serialized as integers
4749
(asDouble() ?: asInteger()?.toDouble()) as T?

providers/ofrep/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/ofrep/serialization/ValueSerializer.kt

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ internal object ValueSerializer : KSerializer<Value> {
6767
override fun deserialize(decoder: Decoder): Value.Integer = Value.Integer(decoder.decodeInt())
6868
}
6969

70+
private object LongValueSerializer : KSerializer<Value.Long> {
71+
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("dev.openfeature.kotlin.sdk.Value.Long")
72+
73+
override fun serialize(
74+
encoder: Encoder,
75+
value: Value.Long,
76+
) = encoder.encodeLong(value.long)
77+
78+
override fun deserialize(decoder: Decoder): Value.Long = Value.Long(decoder.decodeLong())
79+
}
80+
7081
@OptIn(ExperimentalTime::class)
7182
private object InstantValueSerializer : KSerializer<Value.Instant> {
7283
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("dev.openfeature.kotlin.sdk.Value.Instant")
@@ -139,6 +150,7 @@ internal object ValueSerializer : KSerializer<Value> {
139150
is Value.Boolean -> encoder.encodeSerializableValue(BooleanValueSerializer, value)
140151
is Value.Double -> encoder.encodeSerializableValue(DoubleValueSerializer, value)
141152
is Value.Integer -> encoder.encodeSerializableValue(IntValueSerializer, value)
153+
is Value.Long -> encoder.encodeSerializableValue(LongValueSerializer, value)
142154
is Value.Instant -> encoder.encodeSerializableValue(InstantValueSerializer, value)
143155
is Value.List -> encoder.encodeSerializableValue(ListValueSerializer, value)
144156
is Value.String -> encoder.encodeSerializableValue(StringValueSerializer, value)
@@ -160,19 +172,11 @@ internal object ValueSerializer : KSerializer<Value> {
160172
// Order matters here: check for Int before Double to avoid loss of precision
161173
// if a number is a whole number but represented as a double (e.g., 5.0)
162174
element.longOrNull != null -> {
163-
// If it fits in Int, use IntValueSerializer, otherwise could be an issue
164-
// or you might need a Value.Long type. For now, assume it fits Int if it's an int.
165-
// This part might need refinement based on how you handle large integers.
166-
// If Value.Integer only holds Int, then a long might be an error or fallback to Double.
167-
// Let's assume for now that if it has no decimal, it could be an Int or a long that
168-
// should be treated as Int if it fits, or Double if it's too large for Int but fits Double.
169175
val longVal = element.long
170176
if (longVal >= Int.MIN_VALUE && longVal <= Int.MAX_VALUE) {
171177
IntValueSerializer
172178
} else {
173-
// Fallback to Double if it's a long that doesn't fit Int
174-
// or if your Value.Double can represent whole numbers.
175-
DoubleValueSerializer
179+
LongValueSerializer
176180
}
177181
}
178182
element.doubleOrNull != null -> DoubleValueSerializer

0 commit comments

Comments
 (0)