Skip to content

Commit b220355

Browse files
committed
Progress on the provider
1 parent a6b512a commit b220355

File tree

7 files changed

+201
-6
lines changed

7 files changed

+201
-6
lines changed

build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ plugins {
1010
}
1111

1212
dependencies {
13-
implementation(libs.configcat)
14-
implementation(libs.openfeature)
13+
api(libs.configcat)
14+
api(libs.openfeature)
1515
implementation(libs.atomicfu)
16+
implementation(libs.coroutines)
1617
implementation(libs.serialization.json)
1718
testImplementation(libs.kotlin.test)
1819
testImplementation(libs.coroutines.test)

gradle/libs.versions.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[versions]
22
kotlin = "2.1.10"
33
android-gradle-plugin = "8.7.3"
4-
configcat = "4.2.0"
4+
configcat = "4.2.0-SNAPSHOT"
55
openfeature = "0.4.1"
66
ktor = "3.0.0"
77
kotlinx-serialization = "1.7.3"
@@ -16,6 +16,7 @@ atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu"
1616
configcat = { module = "com.configcat:configcat-kotlin-client", version.ref = "configcat" }
1717
openfeature = { module = "dev.openfeature:android-sdk", version.ref = "openfeature" }
1818
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
19+
coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
1920
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
2021
ktor-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
2122

settings.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import java.net.URI
2+
13
pluginManagement {
24
repositories {
35
google()
@@ -12,6 +14,10 @@ dependencyResolutionManagement {
1214
google()
1315
mavenCentral()
1416
mavenLocal()
17+
maven {
18+
name = "Central Portal Snapshots"
19+
url = URI("https://central.sonatype.com/repository/maven-snapshots/")
20+
}
1521
}
1622
}
1723

src/main/kotlin/com/configcat/ConfigCatProvider.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import dev.openfeature.sdk.ProviderEvaluation
77
import dev.openfeature.sdk.ProviderMetadata
88
import dev.openfeature.sdk.Reason
99
import dev.openfeature.sdk.Value
10+
import dev.openfeature.sdk.events.OpenFeatureProviderEvents
1011
import dev.openfeature.sdk.exceptions.ErrorCode
12+
import kotlinx.atomicfu.AtomicBoolean
1113
import kotlinx.atomicfu.AtomicRef
1214
import kotlinx.atomicfu.atomic
15+
import kotlinx.coroutines.flow.Flow
16+
import kotlinx.coroutines.flow.MutableSharedFlow
1317
import kotlinx.serialization.json.Json
1418
import kotlinx.serialization.json.JsonArray
1519
import kotlinx.serialization.json.JsonElement
@@ -28,7 +32,6 @@ import kotlinx.serialization.json.intOrNull
2832
import java.util.Date
2933
import kotlin.collections.component1
3034
import kotlin.collections.component2
31-
import kotlin.div
3235

3336
/**
3437
* Describes the ConfigCat OpenFeature provider.
@@ -42,22 +45,30 @@ class ConfigCatProvider(
4245
override var hooks: List<Hook<*>> = listOf()
4346
override val metadata: ProviderMetadata = ConfigCatProviderMetadata()
4447

48+
private val events = MutableSharedFlow<OpenFeatureProviderEvents>(replay = 1, extraBufferCapacity = 5)
4549
private val snapshot: AtomicRef<ConfigCatClientSnapshot?> = atomic(null)
4650
private val user: AtomicRef<ConfigCatUser?> = atomic(null)
51+
private val initialized: AtomicBoolean = atomic(false)
4752
private val client: ConfigCatClient
4853

4954
init {
5055
options.hooks.addOnConfigChanged {
5156
val sn = client.snapshot()
5257
snapshot.value = sn
58+
if (!initialized.value && sn.cacheState != ClientCacheState.NO_FLAG_DATA) {
59+
setInitialized()
60+
}
5361
}
5462
client = ConfigCatClient(sdkKey, options)
5563
}
5664

5765
override suspend fun initialize(initialContext: EvaluationContext?) {
5866
val initialUser = initialContext?.toConfigCatUser()
5967
user.value = initialUser
60-
client.waitForReady()
68+
val state = client.waitForReady()
69+
if (!initialized.value && state != ClientCacheState.NO_FLAG_DATA) {
70+
setInitialized()
71+
}
6172
}
6273

6374
override suspend fun onContextSet(
@@ -131,6 +142,16 @@ class ConfigCatProvider(
131142
client.close()
132143
}
133144

145+
override fun observe(): Flow<OpenFeatureProviderEvents> = events
146+
147+
private fun setInitialized() {
148+
if (initialized.compareAndSet(expect = false, update = true)) {
149+
if (!events.tryEmit(OpenFeatureProviderEvents.ProviderReady)) {
150+
initialized.value = false
151+
}
152+
}
153+
}
154+
134155
private inline fun <reified T> eval(
135156
key: String,
136157
defaultValue: T,

src/test/kotlin/com/configcat/ClassPathResourceOverrideDataSource.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ class ClassPathResourceOverrideDataSource(name: String) : OverrideDataSource {
1010
private val settings: Map<String, Setting>
1111

1212
init {
13-
val content = this::class.java.classLoader!!.getResource(name)!!.readText()
13+
val content = readResource(name)
1414
settings = json.decodeFromString<Config>(content).settings!!
1515
}
1616

1717
override fun getOverrides(): Map<String, Setting> = settings
1818
}
19+
20+
fun readResource(name: String): String = ClassPathResourceOverrideDataSource::class.java.classLoader!!.getResource(name)!!.readText()

src/test/kotlin/com/configcat/ProviderTests.kt

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,28 @@ package com.configcat
33
import com.configcat.override.OverrideBehavior
44
import com.configcat.override.OverrideDataSource
55
import dev.openfeature.sdk.ImmutableContext
6+
import dev.openfeature.sdk.OpenFeatureAPI
67
import dev.openfeature.sdk.Reason
78
import dev.openfeature.sdk.Value
9+
import dev.openfeature.sdk.events.OpenFeatureProviderEvents
810
import dev.openfeature.sdk.exceptions.ErrorCode
11+
import io.ktor.client.engine.mock.MockEngine
12+
import io.ktor.client.engine.mock.MockEngine.Companion.invoke
13+
import io.ktor.client.engine.mock.respond
14+
import io.ktor.http.HttpStatusCode
15+
import kotlinx.coroutines.cancelAndJoin
16+
import kotlinx.coroutines.launch
17+
import kotlinx.coroutines.test.runTest
918
import java.time.Instant
1019
import java.util.Date
20+
import kotlin.compareTo
1121
import kotlin.test.Test
1222
import kotlin.test.assertContains
1323
import kotlin.test.assertEquals
1424
import kotlin.test.assertFalse
1525
import kotlin.test.assertTrue
26+
import kotlin.time.Duration.Companion.seconds
27+
import kotlin.time.TimeSource
1628

1729
class ProviderTests {
1830
@Test
@@ -164,4 +176,128 @@ class ProviderTests {
164176

165177
provider.shutdown()
166178
}
179+
180+
@Test
181+
fun testInitialize() =
182+
runTest {
183+
val mockEngine =
184+
MockEngine {
185+
respond(
186+
content = readResource("test_json_complex.json"),
187+
status = HttpStatusCode.OK,
188+
)
189+
}
190+
191+
val provider =
192+
ConfigCatProvider(randomSdkKey()) {
193+
httpEngine = mockEngine
194+
}
195+
196+
provider.initialize(null)
197+
198+
var ready = false
199+
val collectJob =
200+
launch {
201+
provider.observe().collect {
202+
if (it == OpenFeatureProviderEvents.ProviderReady) {
203+
ready = true
204+
}
205+
}
206+
}
207+
208+
awaitUntil {
209+
ready
210+
}
211+
212+
val boolVal = provider.getBooleanEvaluation("enabledFeature", false, null)
213+
assertTrue(boolVal.value)
214+
assertEquals("v-enabled", boolVal.variant)
215+
assertEquals(Reason.DEFAULT.name, boolVal.reason)
216+
217+
provider.shutdown()
218+
collectJob.cancelAndJoin()
219+
}
220+
221+
@Test
222+
fun testReadyOnConfigChange() =
223+
runTest {
224+
val mockEngine =
225+
MockEngine.create {
226+
this.addHandler {
227+
respond(
228+
content = "",
229+
status = HttpStatusCode.BadRequest,
230+
)
231+
}
232+
this.addHandler {
233+
respond(
234+
content = readResource("test_json_complex.json"),
235+
status = HttpStatusCode.OK,
236+
)
237+
}
238+
}
239+
240+
val provider =
241+
ConfigCatProvider(randomSdkKey()) {
242+
httpEngine = mockEngine
243+
pollingMode = autoPoll { pollingInterval = 1.seconds }
244+
}
245+
246+
val ts = TimeSource.Monotonic
247+
val start = ts.markNow()
248+
provider.initialize(null)
249+
250+
var ready = false
251+
val collectJob =
252+
launch {
253+
provider.observe().collect {
254+
if (it == OpenFeatureProviderEvents.ProviderReady) {
255+
ready = true
256+
}
257+
}
258+
}
259+
260+
awaitUntil {
261+
ready
262+
}
263+
264+
val elapsed = ts.markNow() - start
265+
assertTrue { elapsed >= 1.seconds }
266+
267+
val boolVal = provider.getBooleanEvaluation("enabledFeature", false, null)
268+
assertTrue(boolVal.value)
269+
assertEquals("v-enabled", boolVal.variant)
270+
assertEquals(Reason.DEFAULT.name, boolVal.reason)
271+
272+
provider.shutdown()
273+
collectJob.cancelAndJoin()
274+
}
275+
276+
@Test
277+
fun testOpenFeatureAPI() =
278+
runTest {
279+
val mockEngine =
280+
MockEngine {
281+
respond(
282+
content = readResource("test_json_complex.json"),
283+
status = HttpStatusCode.OK,
284+
)
285+
}
286+
287+
val provider =
288+
ConfigCatProvider(randomSdkKey()) {
289+
httpEngine = mockEngine
290+
pollingMode = autoPoll { pollingInterval = 1.seconds }
291+
}
292+
293+
OpenFeatureAPI.setProviderAndWait(provider)
294+
val client = OpenFeatureAPI.getClient()
295+
296+
val boolVal = client.getBooleanDetails("enabledFeature", false)
297+
assertTrue(boolVal.value)
298+
assertEquals("v-enabled", boolVal.variant)
299+
assertEquals(Reason.DEFAULT.name, boolVal.reason)
300+
301+
OpenFeatureAPI.shutdown()
302+
}
167303
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.configcat
2+
3+
import kotlinx.coroutines.delay
4+
import kotlin.time.Duration
5+
import kotlin.time.Duration.Companion.seconds
6+
import kotlin.time.TimeSource
7+
8+
fun randomSdkKey(): String = "${randomSdkKeySegment()}/${randomSdkKeySegment()}"
9+
10+
fun randomSdkKeySegment(): String =
11+
(1..22)
12+
.map { (('A'..'Z') + ('a'..'z') + ('0'..'9')).random() }
13+
.joinToString("")
14+
15+
suspend fun awaitUntil(
16+
timeout: Duration = 5.seconds,
17+
condTarget: suspend () -> Boolean,
18+
): Boolean {
19+
val timeSource = TimeSource.Monotonic
20+
val deadline = timeSource.markNow() + timeout
21+
while (!condTarget()) {
22+
delay(200)
23+
if (deadline.hasPassedNow()) {
24+
throw Exception("Test await timed out.")
25+
}
26+
}
27+
return deadline.hasPassedNow()
28+
}

0 commit comments

Comments
 (0)