Skip to content

Commit deb4242

Browse files
authored
feat: Multiplatform OFREP provider (#6)
## This PR Implements an [OFREP](https://openfeature.dev/specification/appendix-c) provider for Kotlin Multiplatform. The implementation was initially based on @thomaspoignant's [OFREP implementation](https://github.com/thomaspoignant/go-feature-flag/tree/main/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/ofrep), then I made a bunch of changes to make it compatible with multiple platforms: * Replaced Java APIs with Kotlin APIs * Replaced jUnit with kotlin-test * Replaced OkHttp with Ktor * Replaced GSON with kotlinx-serialization * Replaced the Java Timer with Kotlin Coroutines * ... Now the project is compatible with multiple platforms, and can be easily extended to other ones by changing the targets in the `build.gradle.kts`: | Supported | Platform | Supported versions | |-----------|----------------------|--------------------------------------------------------------------------------| | ✅ | Android | | | ✅ | JVM | JDK 11+ | | ✅ | Native | Linux x64 | | ❌ | Native | [Other native targets](https://kotlinlang.org/docs/native-target-support.html) | | ✅ | Javascript (Node.js) | | | ✅ | Javascript (Browser) | | | ❌ | Wasm | | ### Notes I have based these changes on the unmerged open-feature/kotlin-sdk#148, which brings in multiplatform support for the SDK. ### Prerequisites - [x] Merge open-feature/kotlin-sdk#148 first ### How to test <!-- if applicable, add testing instructions under this section --> For manual testing: set up a local OFREP provider and try out various evaluations. --------- Signed-off-by: Bence Hornák <[email protected]>
1 parent db30d94 commit deb4242

File tree

39 files changed

+4591
-8
lines changed

39 files changed

+4591
-8
lines changed

buildSrc/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies {
88
implementation(plugin(libs.plugins.kotlin.multiplatform))
99
implementation(plugin(libs.plugins.dokka))
1010
implementation(plugin(libs.plugins.vanniktech.maven.publish))
11+
implementation(plugin(libs.plugins.android.library))
1112
}
1213

1314
/**
@@ -16,4 +17,4 @@ dependencies {
1617
* https://docs.gradle.org/current/userguide/version_catalogs.html#sec:buildsrc-version-catalog
1718
*/
1819
private fun DependencyHandlerScope.plugin(plugin: Provider<PluginDependency>) =
19-
plugin.map { "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}" }
20+
plugin.map { "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}" }

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
version=0.1.0
22
org.gradle.configuration-cache=true
3+
org.gradle.jvmargs=-Xmx1g "-XX:MaxMetaspaceSize=768m
34

45
# This seems to be necessary for Coroutines to work on JS. Otherwise getting the following error:
56
# > Task :providers:env-var:compileTestDevelopmentExecutableKotlinJs FAILED

gradle/libs.versions.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22
kotlin = "2.2.10"
33
kotlinx-coroutines = "1.10.2"
44
open-feature-kotlin-sdk = "0.6.2"
5+
android = "8.10.1"
6+
ktor = "3.1.3"
57

68
[libraries]
9+
ktor-core = { module="io.ktor:ktor-client-core", version.ref="ktor" }
10+
ktor-cio = { module="io.ktor:ktor-client-cio", version.ref="ktor" }
11+
ktor-js = { module="io.ktor:ktor-client-js", version.ref="ktor" }
12+
ktor-client-content-negotiation = { module="io.ktor:ktor-client-content-negotiation", version.ref="ktor" }
13+
ktor-serialization-kotlinx-json = { module="io.ktor:ktor-serialization-kotlinx-json", version.ref="ktor" }
14+
ktor-client-mock = { module="io.ktor:ktor-client-mock", version.ref="ktor" }
715
openfeature-kotlin-sdk = { group="dev.openfeature", name="kotlin-sdk", version.ref="open-feature-kotlin-sdk" }
816
kotlin-test = { group="org.jetbrains.kotlin", name="kotlin-test", version.ref="kotlin" }
917
kotlinx-coroutines-core = { group="org.jetbrains.kotlinx", name="kotlinx-coroutines-core", version.ref="kotlinx-coroutines" }
@@ -13,7 +21,10 @@ kotlinx-coroutines-test = { group="org.jetbrains.kotlinx", name="kotlinx-corouti
1321
dokka = { id="org.jetbrains.dokka", version="2.0.0" }
1422
kotlin-multiplatform = { id="org.jetbrains.kotlin.multiplatform", version.ref="kotlin" }
1523
kotlinx-atomicfu = { id="org.jetbrains.kotlinx.atomicfu", version="0.29.0" }
24+
kotlinx-serialization = { id="org.jetbrains.kotlin.plugin.serialization", version="2.1.21" }
1625
ktlint = { id="org.jlleitschuh.gradle.ktlint", version="13.1.0" }
1726
nexus-publish = { id="io.github.gradle-nexus.publish-plugin", version="2.0.0" }
1827
binary-compatibility-validator = { id="org.jetbrains.kotlinx.binary-compatibility-validator", version="0.18.1" }
1928
vanniktech-maven-publish = { id="com.vanniktech.maven.publish", version="0.34.0" }
29+
android-library = { id="com.android.library", version.ref="android" }
30+
docker-compose = { id="com.avast.gradle.docker-compose", version="0.17.12" }

kotlin-js-store/yarn.lock

Lines changed: 1633 additions & 7 deletions
Large diffs are not rendered by default.

providers/ofrep/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Kotlin OFREP Provider
2+
3+
This provider is designed to use the [OpenFeature Remote Evaluation Protocol (OFREP)](https://openfeature.dev/specification/appendix-c).
4+
5+
## Supported platforms
6+
7+
| Supported | Platform | Supported versions |
8+
|-----------|----------------------|--------------------------------------------------------------------------------|
9+
|| Android | |
10+
|| JVM | JDK 11+ |
11+
|| Native | Linux x64 |
12+
|| Native | [Other native targets](https://kotlinlang.org/docs/native-target-support.html) |
13+
|| Javascript (Node.js) | |
14+
|| Javascript (Browser) | |
15+
|| Wasm | |
16+
17+
18+
## Installation
19+
20+
```kotlin
21+
implementation("dev.openfeature.kotlin.contrib.providers:ofrep:0.1.0")
22+
```
23+
24+
## Usage
25+
26+
To use the `OfrepProvider` create an instance and use it as a provider:
27+
28+
```kotlin
29+
val options = OfrepOptions(endpoint="https://localhost:8080")
30+
val provider = OfrepProvider(options)
31+
OpenFeatureAPI.setProviderAndWait(provider)
32+
```
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
2+
public fun <init> (Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;)V
3+
public fun getBooleanEvaluation (Ljava/lang/String;ZLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
4+
public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
5+
public fun getHooks ()Ljava/util/List;
6+
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
7+
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
8+
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
9+
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
10+
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
11+
public fun observe ()Lkotlinx/coroutines/flow/Flow;
12+
public fun onContextSet (Ldev/openfeature/kotlin/sdk/EvaluationContext;Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
13+
public fun shutdown ()V
14+
public fun track (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;Ldev/openfeature/kotlin/sdk/TrackingEventDetails;)V
15+
}
16+
17+
public final class dev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions {
18+
public synthetic fun <init> (Ljava/lang/String;JIJLjava/util/Map;JLio/ktor/client/engine/HttpClientEngine;Lkotlinx/coroutines/CoroutineDispatcher;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
19+
public synthetic fun <init> (Ljava/lang/String;JIJLjava/util/Map;JLio/ktor/client/engine/HttpClientEngine;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
20+
public final fun component1 ()Ljava/lang/String;
21+
public final fun component2-UwyO8pc ()J
22+
public final fun component3 ()I
23+
public final fun component4-UwyO8pc ()J
24+
public final fun component5 ()Ljava/util/Map;
25+
public final fun component6-UwyO8pc ()J
26+
public final fun component7 ()Lio/ktor/client/engine/HttpClientEngine;
27+
public final fun component8 ()Lkotlinx/coroutines/CoroutineDispatcher;
28+
public final fun copy-Sx1y28s (Ljava/lang/String;JIJLjava/util/Map;JLio/ktor/client/engine/HttpClientEngine;Lkotlinx/coroutines/CoroutineDispatcher;)Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;
29+
public static synthetic fun copy-Sx1y28s$default (Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;Ljava/lang/String;JIJLjava/util/Map;JLio/ktor/client/engine/HttpClientEngine;Lkotlinx/coroutines/CoroutineDispatcher;ILjava/lang/Object;)Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;
30+
public fun equals (Ljava/lang/Object;)Z
31+
public final fun getEndpoint ()Ljava/lang/String;
32+
public final fun getHeaders ()Ljava/util/Map;
33+
public final fun getHttpClientEngine ()Lio/ktor/client/engine/HttpClientEngine;
34+
public final fun getKeepAliveDuration-UwyO8pc ()J
35+
public final fun getMaxIdleConnections ()I
36+
public final fun getPollingDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher;
37+
public final fun getPollingInterval-UwyO8pc ()J
38+
public final fun getTimeout-UwyO8pc ()J
39+
public fun hashCode ()I
40+
public fun toString ()Ljava/lang/String;
41+
}
42+

providers/ofrep/api/jvm/ofrep.api

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
public final class dev/openfeature/kotlin/contrib/providers/ofrep/OfrepProvider : dev/openfeature/kotlin/sdk/FeatureProvider {
2+
public fun <init> (Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;)V
3+
public fun getBooleanEvaluation (Ljava/lang/String;ZLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
4+
public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
5+
public fun getHooks ()Ljava/util/List;
6+
public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
7+
public fun getMetadata ()Ldev/openfeature/kotlin/sdk/ProviderMetadata;
8+
public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
9+
public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;)Ldev/openfeature/kotlin/sdk/ProviderEvaluation;
10+
public fun initialize (Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
11+
public fun observe ()Lkotlinx/coroutines/flow/Flow;
12+
public fun onContextSet (Ldev/openfeature/kotlin/sdk/EvaluationContext;Ldev/openfeature/kotlin/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
13+
public fun shutdown ()V
14+
public fun track (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/EvaluationContext;Ldev/openfeature/kotlin/sdk/TrackingEventDetails;)V
15+
}
16+
17+
public final class dev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions {
18+
public synthetic fun <init> (Ljava/lang/String;JIJLjava/util/Map;JLio/ktor/client/engine/HttpClientEngine;Lkotlinx/coroutines/CoroutineDispatcher;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
19+
public synthetic fun <init> (Ljava/lang/String;JIJLjava/util/Map;JLio/ktor/client/engine/HttpClientEngine;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
20+
public final fun component1 ()Ljava/lang/String;
21+
public final fun component2-UwyO8pc ()J
22+
public final fun component3 ()I
23+
public final fun component4-UwyO8pc ()J
24+
public final fun component5 ()Ljava/util/Map;
25+
public final fun component6-UwyO8pc ()J
26+
public final fun component7 ()Lio/ktor/client/engine/HttpClientEngine;
27+
public final fun component8 ()Lkotlinx/coroutines/CoroutineDispatcher;
28+
public final fun copy-Sx1y28s (Ljava/lang/String;JIJLjava/util/Map;JLio/ktor/client/engine/HttpClientEngine;Lkotlinx/coroutines/CoroutineDispatcher;)Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;
29+
public static synthetic fun copy-Sx1y28s$default (Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;Ljava/lang/String;JIJLjava/util/Map;JLio/ktor/client/engine/HttpClientEngine;Lkotlinx/coroutines/CoroutineDispatcher;ILjava/lang/Object;)Ldev/openfeature/kotlin/contrib/providers/ofrep/bean/OfrepOptions;
30+
public fun equals (Ljava/lang/Object;)Z
31+
public final fun getEndpoint ()Ljava/lang/String;
32+
public final fun getHeaders ()Ljava/util/Map;
33+
public final fun getHttpClientEngine ()Lio/ktor/client/engine/HttpClientEngine;
34+
public final fun getKeepAliveDuration-UwyO8pc ()J
35+
public final fun getMaxIdleConnections ()I
36+
public final fun getPollingDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher;
37+
public final fun getPollingInterval-UwyO8pc ()J
38+
public final fun getTimeout-UwyO8pc ()J
39+
public fun hashCode ()I
40+
public fun toString ()Ljava/lang/String;
41+
}
42+

providers/ofrep/build.gradle.kts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinTest
3+
4+
plugins {
5+
id("com.android.library")
6+
id("dev.openfeature.provider-conventions")
7+
alias(libs.plugins.kotlinx.serialization)
8+
// Needed for the JS coroutine support for the tests
9+
alias(libs.plugins.kotlinx.atomicfu)
10+
alias(libs.plugins.docker.compose)
11+
}
12+
13+
kotlin {
14+
androidTarget()
15+
jvm {
16+
compilations.all {
17+
compileTaskProvider.configure {
18+
compilerOptions {
19+
jvmTarget.set(JvmTarget.JVM_11)
20+
}
21+
}
22+
}
23+
}
24+
linuxX64 {}
25+
js {
26+
nodejs {}
27+
browser {
28+
testTask {
29+
useKarma {
30+
useChromeHeadless()
31+
}
32+
}
33+
}
34+
}
35+
36+
sourceSets {
37+
val commonMain by getting {
38+
dependencies {
39+
api(libs.openfeature.kotlin.sdk)
40+
41+
api(libs.kotlinx.coroutines.core)
42+
api(libs.ktor.core)
43+
implementation(libs.ktor.client.content.negotiation)
44+
implementation(libs.ktor.serialization.kotlinx.json)
45+
}
46+
}
47+
commonTest.dependencies {
48+
implementation(libs.kotlin.test)
49+
implementation(libs.kotlinx.coroutines.test)
50+
implementation(libs.ktor.client.mock)
51+
}
52+
val nonJsMain by creating {
53+
dependsOn(commonMain)
54+
dependencies {
55+
implementation(libs.ktor.cio)
56+
}
57+
}
58+
androidMain.get().dependsOn(nonJsMain)
59+
jvmMain.get().dependsOn(nonJsMain)
60+
linuxX64Main.get().dependsOn(nonJsMain)
61+
62+
jsMain.dependencies {
63+
implementation(libs.ktor.js)
64+
}
65+
}
66+
}
67+
68+
android {
69+
namespace = "dev.openfeature.kotlin.contrib.providers.ofrep"
70+
compileSdk = 35
71+
72+
defaultConfig {
73+
minSdk = 21
74+
}
75+
76+
compileOptions {
77+
sourceCompatibility = JavaVersion.VERSION_11
78+
targetCompatibility = JavaVersion.VERSION_11
79+
}
80+
81+
publishing {
82+
singleVariant("release") {
83+
withJavadocJar()
84+
withSourcesJar()
85+
}
86+
}
87+
88+
testOptions {
89+
unitTests {
90+
isIncludeAndroidResources = true
91+
}
92+
}
93+
}
94+
95+
// Launch test container for IntegrationTest.kt
96+
dockerCompose {
97+
useComposeFiles = listOf("src/integrationTest/docker-compose.yaml")
98+
}
99+
// Note: some Kotlin test targets extend the Test (e.g. JVM), some others the KotlinTest class (e.g. Native, JS)
100+
tasks.withType(Test::class) {
101+
dependsOn(tasks.named("composeUp"))
102+
finalizedBy(tasks.named("composeDown"))
103+
}
104+
tasks.withType(KotlinTest::class) {
105+
dependsOn(tasks.named("composeUp"))
106+
finalizedBy(tasks.named("composeDown"))
107+
}

providers/ofrep/gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Disable the default source set hierarchy to allow creating the nonJsMain source set
2+
kotlin.mpp.applyDefaultHierarchyTemplate=false

0 commit comments

Comments
 (0)