From 82c38c63a8928885df76da3fb05f14401b0903ce Mon Sep 17 00:00:00 2001 From: Mark Tonkonoh Date: Thu, 27 Aug 2026 18:21:44 +0200 Subject: [PATCH] feat: complete the provider event vocabulary Spec requirement 5.1.1 requires the provider event set to include PROVIDER_RECONCILING and PROVIDER_CONTEXT_CHANGED, and 5.2.3 requires event details to carry the provider name. Neither existed, so the SDK could not represent context reconciliation as events nor attribute an event to its provider. Signed-off-by: Mark Tonkonoh --- kotlin-sdk/api/android/kotlin-sdk.api | 36 ++++++++-- kotlin-sdk/api/jvm/kotlin-sdk.api | 36 ++++++++-- .../sdk/events/OpenFeatureProviderEvents.kt | 33 ++++++++- .../kotlin/sdk/multiprovider/MultiProvider.kt | 16 ++--- .../kotlin/sdk/EventDetailsTests.kt | 70 +++++++++++++++++++ 5 files changed, 171 insertions(+), 20 deletions(-) diff --git a/kotlin-sdk/api/android/kotlin-sdk.api b/kotlin-sdk/api/android/kotlin-sdk.api index 00d3bbe3..ca5741af 100644 --- a/kotlin-sdk/api/android/kotlin-sdk.api +++ b/kotlin-sdk/api/android/kotlin-sdk.api @@ -694,19 +694,21 @@ public abstract class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvent public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails { public fun ()V - public fun (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;)V - public synthetic fun (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;Ljava/lang/String;)V + public synthetic fun (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/util/Set; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode; public final fun component4 ()Ljava/util/Map; - public final fun copy (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; - public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;Ljava/lang/String;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; public fun equals (Ljava/lang/Object;)Z public final fun getErrorCode ()Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode; public final fun getEventMetadata ()Ljava/util/Map; public final fun getFlagsChanged ()Ljava/util/Set; public final fun getMessage ()Ljava/lang/String; + public final fun getProviderName ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -724,6 +726,19 @@ public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$P public fun toString ()Ljava/lang/String; } +public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderContextChanged : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents { + public fun ()V + public fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V + public synthetic fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public final fun copy (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderContextChanged; + public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderContextChanged;Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderContextChanged; + public fun equals (Ljava/lang/Object;)Z + public fun getEventDetails ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderError : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents { public fun ()V public fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V @@ -750,6 +765,19 @@ public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$P public fun toString ()Ljava/lang/String; } +public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents { + public fun ()V + public fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V + public synthetic fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public final fun copy (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling; + public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling;Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling; + public fun equals (Ljava/lang/Object;)Z + public fun getEventDetails ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderStale : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents { public fun ()V public fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V diff --git a/kotlin-sdk/api/jvm/kotlin-sdk.api b/kotlin-sdk/api/jvm/kotlin-sdk.api index 00d3bbe3..ca5741af 100644 --- a/kotlin-sdk/api/jvm/kotlin-sdk.api +++ b/kotlin-sdk/api/jvm/kotlin-sdk.api @@ -694,19 +694,21 @@ public abstract class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvent public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails { public fun ()V - public fun (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;)V - public synthetic fun (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;Ljava/lang/String;)V + public synthetic fun (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/util/Set; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode; public final fun component4 ()Ljava/util/Map; - public final fun copy (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; - public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;Ljava/lang/String;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;Ljava/util/Set;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; public fun equals (Ljava/lang/Object;)Z public final fun getErrorCode ()Ldev/openfeature/kotlin/sdk/exceptions/ErrorCode; public final fun getEventMetadata ()Ljava/util/Map; public final fun getFlagsChanged ()Ljava/util/Set; public final fun getMessage ()Ljava/lang/String; + public final fun getProviderName ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -724,6 +726,19 @@ public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$P public fun toString ()Ljava/lang/String; } +public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderContextChanged : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents { + public fun ()V + public fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V + public synthetic fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public final fun copy (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderContextChanged; + public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderContextChanged;Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderContextChanged; + public fun equals (Ljava/lang/Object;)Z + public fun getEventDetails ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderError : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents { public fun ()V public fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V @@ -750,6 +765,19 @@ public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$P public fun toString ()Ljava/lang/String; } +public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents { + public fun ()V + public fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V + public synthetic fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public final fun copy (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling; + public static synthetic fun copy$default (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling;Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;ILjava/lang/Object;)Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderReconciling; + public fun equals (Ljava/lang/Object;)Z + public fun getEventDetails ()Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$ProviderStale : dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents { public fun ()V public fun (Ldev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents$EventDetails;)V diff --git a/kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents.kt b/kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents.kt index 1b7d8681..eef6897d 100644 --- a/kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents.kt +++ b/kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/events/OpenFeatureProviderEvents.kt @@ -9,7 +9,8 @@ sealed class OpenFeatureProviderEvents { val flagsChanged: Set = emptySet(), val message: String? = null, val errorCode: ErrorCode? = null, - val eventMetadata: Map = emptyMap() + val eventMetadata: Map = emptyMap(), + val providerName: String? = null ) abstract val eventDetails: EventDetails? @@ -45,6 +46,36 @@ sealed class OpenFeatureProviderEvents { data class ProviderStale( override val eventDetails: EventDetails? = null ) : OpenFeatureProviderEvents() + + /** + * The provider started reconciling its state with a new [dev.openfeature.kotlin.sdk.EvaluationContext]. + * [eventDetails] may supply [EventDetails.flagsChanged], [EventDetails.message], [EventDetails.errorCode], and [EventDetails.eventMetadata] as applicable. + */ + data class ProviderReconciling( + override val eventDetails: EventDetails? = null + ) : OpenFeatureProviderEvents() + + /** + * The provider finished reconciling its state with a new [dev.openfeature.kotlin.sdk.EvaluationContext]. + * [eventDetails] may supply [EventDetails.flagsChanged], [EventDetails.message], [EventDetails.errorCode], and [EventDetails.eventMetadata] as applicable. + */ + data class ProviderContextChanged( + override val eventDetails: EventDetails? = null + ) : OpenFeatureProviderEvents() +} + +/** + * Status implied by an event, per the event/status association table in the specification. + * + * Returns null for events that carry no status transition, leaving the current status untouched. + */ +internal fun OpenFeatureProviderEvents.toOpenFeatureStatus(): OpenFeatureStatus? = when (this) { + is OpenFeatureProviderEvents.ProviderReady -> OpenFeatureStatus.Ready + is OpenFeatureProviderEvents.ProviderStale -> OpenFeatureStatus.Stale + is OpenFeatureProviderEvents.ProviderError -> toOpenFeatureStatusError() + is OpenFeatureProviderEvents.ProviderReconciling -> OpenFeatureStatus.Reconciling + is OpenFeatureProviderEvents.ProviderContextChanged -> OpenFeatureStatus.Ready + is OpenFeatureProviderEvents.ProviderConfigurationChanged -> null } internal fun OpenFeatureProviderEvents.ProviderError.toOpenFeatureStatusError(): OpenFeatureStatus { diff --git a/kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/MultiProvider.kt b/kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/MultiProvider.kt index 00e6d057..e32cbd6c 100644 --- a/kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/MultiProvider.kt +++ b/kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/multiprovider/MultiProvider.kt @@ -9,7 +9,7 @@ import dev.openfeature.kotlin.sdk.ProviderMetadata import dev.openfeature.kotlin.sdk.TrackingEventDetails import dev.openfeature.kotlin.sdk.Value import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents -import dev.openfeature.kotlin.sdk.events.toOpenFeatureStatusError +import dev.openfeature.kotlin.sdk.events.toOpenFeatureStatus import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -202,16 +202,10 @@ class MultiProvider( } private suspend fun handleProviderEvent(provider: ChildFeatureProvider, event: OpenFeatureProviderEvents) { - val newChildStatus = when (event) { - // ProviderConfigurationChanged events should always re-emit - is OpenFeatureProviderEvents.ProviderConfigurationChanged -> { - eventFlow.emit(event) - return - } - - is OpenFeatureProviderEvents.ProviderReady -> OpenFeatureStatus.Ready - is OpenFeatureProviderEvents.ProviderStale -> OpenFeatureStatus.Stale - is OpenFeatureProviderEvents.ProviderError -> event.toOpenFeatureStatusError() + // Events carrying no status transition (e.g. ProviderConfigurationChanged) always re-emit. + val newChildStatus = event.toOpenFeatureStatus() ?: run { + eventFlow.emit(event) + return } val previousStatus = _statusFlow.value diff --git a/kotlin-sdk/src/commonTest/kotlin/dev/openfeature/kotlin/sdk/EventDetailsTests.kt b/kotlin-sdk/src/commonTest/kotlin/dev/openfeature/kotlin/sdk/EventDetailsTests.kt index ccdfdd0c..cb69e22c 100644 --- a/kotlin-sdk/src/commonTest/kotlin/dev/openfeature/kotlin/sdk/EventDetailsTests.kt +++ b/kotlin-sdk/src/commonTest/kotlin/dev/openfeature/kotlin/sdk/EventDetailsTests.kt @@ -1,12 +1,14 @@ package dev.openfeature.kotlin.sdk import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents +import dev.openfeature.kotlin.sdk.events.toOpenFeatureStatus import dev.openfeature.kotlin.sdk.events.toOpenFeatureStatusError import dev.openfeature.kotlin.sdk.exceptions.ErrorCode import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs +import kotlin.test.assertNull class EventDetailsTests { @@ -79,4 +81,72 @@ class EventDetailsTests { val err = assertIs(errorStatus.error) assertEquals("test", err.message) } + + @Test + fun readyEventMapsToReadyStatus() { + assertEquals( + OpenFeatureStatus.Ready, + OpenFeatureProviderEvents.ProviderReady().toOpenFeatureStatus() + ) + } + + @Test + fun staleEventMapsToStaleStatus() { + assertEquals( + OpenFeatureStatus.Stale, + OpenFeatureProviderEvents.ProviderStale().toOpenFeatureStatus() + ) + } + + @Test + fun reconcilingEventMapsToReconcilingStatus() { + assertEquals( + OpenFeatureStatus.Reconciling, + OpenFeatureProviderEvents.ProviderReconciling().toOpenFeatureStatus() + ) + } + + @Test + fun contextChangedEventMapsToReadyStatus() { + assertEquals( + OpenFeatureStatus.Ready, + OpenFeatureProviderEvents.ProviderContextChanged().toOpenFeatureStatus() + ) + } + + @Test + fun configurationChangedEventMapsToNoStatusTransition() { + assertNull(OpenFeatureProviderEvents.ProviderConfigurationChanged().toOpenFeatureStatus()) + } + + @Test + fun errorEventMapsThroughToErrorStatus() { + val evt = OpenFeatureProviderEvents.ProviderError( + OpenFeatureProviderEvents.EventDetails( + message = "flag missing", + errorCode = ErrorCode.FLAG_NOT_FOUND + ) + ) + + val error = assertIs(evt.toOpenFeatureStatus()) + assertIs(error.error) + } + + @Test + fun errorEventWithFatalCodeMapsThroughToFatalStatus() { + val evt = OpenFeatureProviderEvents.ProviderError( + OpenFeatureProviderEvents.EventDetails( + message = "unrecoverable", + errorCode = ErrorCode.PROVIDER_FATAL + ) + ) + + assertIs(evt.toOpenFeatureStatus()) + } + + @Test + fun eventDetailsCarryProviderName() { + val details = OpenFeatureProviderEvents.EventDetails(providerName = "my-provider") + assertEquals("my-provider", details.providerName) + } } \ No newline at end of file