Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion buildSrc/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ pluginManagement {
dependencyResolutionManagement {
repositories {
google()
mavenLocal()
mavenCentral()
gradlePluginPortal()
}
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[versions]
kotlin = "2.2.10"
kotlinx-coroutines = "1.10.2"
open-feature-kotlin-sdk = "0.6.2"
android = "8.10.1"
open-feature-kotlin-sdk = "0.8.0"
android = "8.13.0"
ktor = "3.1.3"

[libraries]
Expand Down
4 changes: 3 additions & 1 deletion providers/env-var/api/env-var.api
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider : dev/openfeature/kotlin/sdk/StateManagingProvider {
public static final field Companion Ldev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider$Companion;
public fun <init> ()V
public fun <init> (Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway;Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer;)V
Expand All @@ -7,8 +7,10 @@ public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvide
public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getHooks ()Ljava/util/List;
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun observe ()Lkotlinx/coroutines/flow/Flow;
Expand Down
1 change: 1 addition & 0 deletions providers/env-var/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ kotlin {
sourceSets {
commonMain.dependencies {
api(libs.openfeature.kotlin.sdk)
api(libs.kotlinx.coroutines.core)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,59 @@
package dev.openfeature.kotlin.contrib.providers.envvar

import dev.openfeature.kotlin.sdk.EvaluationContext
import dev.openfeature.kotlin.sdk.FeatureProvider
import dev.openfeature.kotlin.sdk.Hook
import dev.openfeature.kotlin.sdk.OpenFeatureStatus
import dev.openfeature.kotlin.sdk.ProviderEvaluation
import dev.openfeature.kotlin.sdk.ProviderMetadata
import dev.openfeature.kotlin.sdk.ProviderStatusTracker
import dev.openfeature.kotlin.sdk.Reason
import dev.openfeature.kotlin.sdk.StateManagingProvider
import dev.openfeature.kotlin.sdk.Value
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
import dev.openfeature.kotlin.sdk.exceptions.ErrorCode
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.yield

/** EnvVarProvider is the Kotlin provider implementation for the environment variables. */
class EnvVarProvider(
private val environmentGateway: EnvironmentGateway = platformSpecificEnvironmentGateway(),
private val keyTransformer: EnvironmentKeyTransformer = EnvironmentKeyTransformer.doNothing(),
) : FeatureProvider {
) : StateManagingProvider {
private val statusTracker = ProviderStatusTracker()

override val hooks: List<Hook<*>> = emptyList()
override val metadata: ProviderMetadata
get() = Metadata

override val status: StateFlow<OpenFeatureStatus> = statusTracker.status

override suspend fun initialize(initialContext: EvaluationContext?) {
// Nothing to do here
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
}

override fun shutdown() {
// Nothing to do here
statusTracker.send(
OpenFeatureProviderEvents.ProviderError(
OpenFeatureProviderEvents.EventDetails(
message = "Environment Variables provider shut down; not ready for evaluation",
errorCode = ErrorCode.PROVIDER_NOT_READY,
),
),
)
}

override suspend fun onContextSet(
oldContext: EvaluationContext?,
newContext: EvaluationContext,
) {
// Nothing to do here
statusTracker.send(OpenFeatureProviderEvents.ProviderReconciling())
yield()
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
}

override fun observe() = statusTracker.observe()

override fun getBooleanEvaluation(
key: String,
defaultValue: Boolean,
Expand All @@ -51,6 +72,12 @@ class EnvVarProvider(
context: EvaluationContext?,
): ProviderEvaluation<Int> = evaluateEnvironmentVariable(key, String::toInt)

override fun getLongEvaluation(
key: String,
defaultValue: Long,
context: EvaluationContext?,
): ProviderEvaluation<Long> = evaluateEnvironmentVariable(key, String::toLong)

override fun getStringEvaluation(
key: String,
defaultValue: String,
Expand Down
4 changes: 3 additions & 1 deletion providers/ofrep/api/android/ofrep.api
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/StateManagingProvider {
public fun <init> (Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;)V
public fun getBooleanEvaluation (Ljava/lang/String;ZLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getHooks ()Ljava/util/List;
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun observe ()Lkotlinx/coroutines/flow/Flow;
Expand Down
4 changes: 3 additions & 1 deletion providers/ofrep/api/jvm/ofrep.api
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/StateManagingProvider {
public fun <init> (Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;)V
public fun getBooleanEvaluation (Ljava/lang/String;ZLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getHooks ()Ljava/util/List;
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getLongEvaluation (Ljava/lang/String;JLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun getStatus ()Lkotlinx/coroutines/flow/StateFlow;
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun observe ()Lkotlinx/coroutines/flow/Flow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,24 @@ import dev.openfeature.kotlin.contrib.providers.ofrep.controller.OfrepApi
import dev.openfeature.kotlin.contrib.providers.ofrep.enum.BulkEvaluationStatus
import dev.openfeature.kotlin.contrib.providers.ofrep.error.OfrepError
import dev.openfeature.kotlin.sdk.EvaluationContext
import dev.openfeature.kotlin.sdk.FeatureProvider
import dev.openfeature.kotlin.sdk.Hook
import dev.openfeature.kotlin.sdk.ImmutableContext
import dev.openfeature.kotlin.sdk.OpenFeatureStatus
import dev.openfeature.kotlin.sdk.ProviderEvaluation
import dev.openfeature.kotlin.sdk.ProviderMetadata
import dev.openfeature.kotlin.sdk.ProviderStatusTracker
import dev.openfeature.kotlin.sdk.StateManagingProvider
import dev.openfeature.kotlin.sdk.Value
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents.EventDetails
import dev.openfeature.kotlin.sdk.exceptions.ErrorCode
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.concurrent.Volatile
Expand All @@ -34,14 +37,18 @@ import kotlin.time.Instant
@OptIn(ExperimentalTime::class)
class OfrepProvider(
private val ofrepOptions: OfrepOptions,
) : FeatureProvider {
) : StateManagingProvider {
private val ofrepApi = OfrepApi(ofrepOptions)
override val hooks: List<Hook<*>>
get() = listOf()

override val metadata: ProviderMetadata
get() = OfrepProviderMetadata()

private val statusTracker = ProviderStatusTracker()

override val status: StateFlow<OpenFeatureStatus> = statusTracker.status

private var evaluationContext: EvaluationContext? = null

@Volatile
Expand All @@ -50,27 +57,32 @@ class OfrepProvider(
private val pollingScope: CoroutineScope = CoroutineScope(ofrepOptions.pollingDispatcher)
private var pollingJob: Job? = null

private val statusFlow = MutableSharedFlow<OpenFeatureProviderEvents>(replay = 1)
override fun observe(): Flow<OpenFeatureProviderEvents> = statusTracker.observe()

override fun observe(): Flow<OpenFeatureProviderEvents> = statusFlow
private fun providerError(error: Throwable): OpenFeatureProviderEvents.ProviderError {
val ofError =
error as? OpenFeatureError
?: OpenFeatureError.GeneralError(error.message ?: "Unknown error")
return OpenFeatureProviderEvents.ProviderError(
eventDetails =
EventDetails(
message = ofError.message,
errorCode = ofError.errorCode(),
),
)
}

override suspend fun initialize(initialContext: EvaluationContext?) {
this.evaluationContext = initialContext
try {
val bulkEvaluationStatus = evaluateFlags(initialContext ?: ImmutableContext())
if (bulkEvaluationStatus == BulkEvaluationStatus.RATE_LIMITED) {
statusFlow.emit(
OpenFeatureProviderEvents.ProviderError(
OpenFeatureError.GeneralError("Rate limited"),
),
)
statusTracker.send(providerError(OpenFeatureError.GeneralError("Rate limited")))
} else {
statusFlow.emit(OpenFeatureProviderEvents.ProviderReady)
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
}
} catch (e: OpenFeatureError) {
statusFlow.emit(OpenFeatureProviderEvents.ProviderError(e))
} catch (e: Exception) {
statusFlow.emit(OpenFeatureProviderEvents.ProviderError(OpenFeatureError.GeneralError(e.message ?: "Unknown error")))
} catch (e: Throwable) {
statusTracker.send(providerError(e))
}
startPolling()
}
Comment thread
maxnrp marked this conversation as resolved.
Expand Down Expand Up @@ -101,23 +113,17 @@ class OfrepProvider(
BulkEvaluationStatus.SUCCESS_UPDATED -> {
// TODO: we should migrate to configuration change event when it's available
// in the kotlin SDK
statusFlow.emit(OpenFeatureProviderEvents.ProviderReady)
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
}
}
} catch (e: CancellationException) {
// expected to happen when the job is cancelled, no need to report it via the
// statusFlow
// status tracker
} catch (e: OfrepError.ApiTooManyRequestsError) {
// in that case the provider is just stale because we were not able to
statusFlow.emit(OpenFeatureProviderEvents.ProviderStale)
statusTracker.send(OpenFeatureProviderEvents.ProviderStale())
} catch (e: Throwable) {
statusFlow.emit(
OpenFeatureProviderEvents.ProviderError(
OpenFeatureError.GeneralError(
e.message ?: "",
),
),
)
statusTracker.send(providerError(e))
}
}
}
Expand All @@ -141,6 +147,12 @@ class OfrepProvider(
context: EvaluationContext?,
): ProviderEvaluation<Int> = genericEvaluation(key, defaultValue)

override fun getLongEvaluation(
key: String,
defaultValue: Long,
context: EvaluationContext?,
): ProviderEvaluation<Long> = genericEvaluation(key, defaultValue)

override fun getObjectEvaluation(
key: String,
defaultValue: Value,
Expand All @@ -157,23 +169,31 @@ class OfrepProvider(
oldContext: EvaluationContext?,
newContext: EvaluationContext,
) {
this.statusFlow.emit(OpenFeatureProviderEvents.ProviderStale)
statusTracker.send(OpenFeatureProviderEvents.ProviderStale())
this.evaluationContext = newContext

try {
val postBulkEvaluateFlags = evaluateFlags(newContext)
// we don't emit event if the evaluation is rate limited because
// the provider is still stale
if (postBulkEvaluateFlags != BulkEvaluationStatus.RATE_LIMITED) {
statusFlow.emit(OpenFeatureProviderEvents.ProviderReady)
statusTracker.send(OpenFeatureProviderEvents.ProviderReady())
}
} catch (e: Throwable) {
statusFlow.emit(OpenFeatureProviderEvents.ProviderError(OpenFeatureError.GeneralError(e.message ?: "")))
statusTracker.send(providerError(e))
}
}

override fun shutdown() {
pollingJob?.cancel()
statusTracker.send(
OpenFeatureProviderEvents.ProviderError(
EventDetails(
message = "OFREP provider shut down; not ready for evaluation",
errorCode = ErrorCode.PROVIDER_NOT_READY,
),
),
)
}

private inline fun <reified T> genericEvaluation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ inline fun <reified T> Value.toPrimitive(): T {
Boolean::class -> asBoolean() as T?
String::class -> asString() as T?
Int::class -> asInteger() as T?
Long::class ->
(asLong() ?: asInteger()?.toLong()) as T?
Double::class ->
// doubles might have been serialized as integers
(asDouble() ?: asInteger()?.toDouble()) as T?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ internal object ValueSerializer : KSerializer<Value> {
override fun deserialize(decoder: Decoder): Value.Integer = Value.Integer(decoder.decodeInt())
}

private object LongValueSerializer : KSerializer<Value.Long> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("dev.openfeature.kotlin.sdk.Value.Long")

override fun serialize(
encoder: Encoder,
value: Value.Long,
) = encoder.encodeLong(value.long)

override fun deserialize(decoder: Decoder): Value.Long = Value.Long(decoder.decodeLong())
}

@OptIn(ExperimentalTime::class)
private object InstantValueSerializer : KSerializer<Value.Instant> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("dev.openfeature.kotlin.sdk.Value.Instant")
Expand Down Expand Up @@ -139,6 +150,7 @@ internal object ValueSerializer : KSerializer<Value> {
is Value.Boolean -> encoder.encodeSerializableValue(BooleanValueSerializer, value)
is Value.Double -> encoder.encodeSerializableValue(DoubleValueSerializer, value)
is Value.Integer -> encoder.encodeSerializableValue(IntValueSerializer, value)
is Value.Long -> encoder.encodeSerializableValue(LongValueSerializer, value)
is Value.Instant -> encoder.encodeSerializableValue(InstantValueSerializer, value)
is Value.List -> encoder.encodeSerializableValue(ListValueSerializer, value)
is Value.String -> encoder.encodeSerializableValue(StringValueSerializer, value)
Expand All @@ -160,19 +172,11 @@ internal object ValueSerializer : KSerializer<Value> {
// Order matters here: check for Int before Double to avoid loss of precision
// if a number is a whole number but represented as a double (e.g., 5.0)
element.longOrNull != null -> {
// If it fits in Int, use IntValueSerializer, otherwise could be an issue
// or you might need a Value.Long type. For now, assume it fits Int if it's an int.
// This part might need refinement based on how you handle large integers.
// If Value.Integer only holds Int, then a long might be an error or fallback to Double.
// Let's assume for now that if it has no decimal, it could be an Int or a long that
// should be treated as Int if it fits, or Double if it's too large for Int but fits Double.
val longVal = element.long
if (longVal >= Int.MIN_VALUE && longVal <= Int.MAX_VALUE) {
IntValueSerializer
} else {
// Fallback to Double if it's a long that doesn't fit Int
// or if your Value.Double can represent whole numbers.
DoubleValueSerializer
LongValueSerializer
}
}
element.doubleOrNull != null -> DoubleValueSerializer
Expand Down
Loading
Loading