Skip to content

Commit

Permalink
Add rudimentary iOS LaunchDarkly support (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevincianfarini authored Mar 4, 2024
1 parent f44c971 commit ad48eda
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 2 deletions.
5 changes: 4 additions & 1 deletion integrations/launch-darkly/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ android {

kotlin {
explicitApi()
jvmToolchain(17)
jvmToolchain(19)

iosArm64()
iosSimulatorArm64()
iosX64()
androidTarget {
publishLibraryVariants("release")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import kotlin.test.*
class LaunchDarklyFeatureFlagDataStoreTest {

@Test fun unpopulated_boolean_returns_default() {
error("boo")
val (dataStore, _) = sut()
assertFalse(dataStore.getBoolean("key", false))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.kevincianfarini.monarch.launchdarkly

/**
* A temporary, experimental shim to allow iOS consumers of Monarch to wire their own LDClient
* as a data store using [LaunchDarklyClientShim.asFeatureFlagDataStore]. This interface will be
* removed in future versions of this library when future, first-party support of LaunchDarkly
* is available.
*/
public interface LaunchDarklyClientShim {

public fun boolVariation(forKey: String, default: Boolean): Boolean

public fun intVariation(forKey: String, default: Int): Int

public fun doubleVariation(forKey: String, default: Double): Double

public fun stringVariation(forKey: String, default: String): String

public fun jsonStringVariation(forKey: String, default: String?): String?

public fun observe(key: String, owner: Any, handler: () -> Unit)

public fun stopObserving(owner: Any)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.github.kevincianfarini.monarch.launchdarkly

import io.github.kevincianfarini.monarch.ObservableFeatureFlagDataStore
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.pin
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate

public fun LaunchDarklyClientShim.asFeatureFlagDataStore(): ObservableFeatureFlagDataStore {
return LaunchDarklyFeatureFlagDataStore(this)
}

private class LaunchDarklyFeatureFlagDataStore(
private val shim: LaunchDarklyClientShim
) : ObservableFeatureFlagDataStore {

override fun getBoolean(key: String, default: Boolean): Boolean {
return shim.getValue(key, default)
}

override fun getString(key: String, default: String): String {
return shim.getValue(key, default)
}

override fun getDouble(key: String, default: Double): Double {
return shim.getValue(key, default)
}

override fun getLong(key: String, default: Long): Long {
return shim.getValue(key, default)
}

override fun getByteArray(key: String, default: ByteArray): ByteArray {
throw NotImplementedError("LaunchDarkly does not support ByteArray flags.")
}

override fun observeString(key: String, default: String): Flow<String> {
return shim.observeValue(key, default)
}

override fun observeBoolean(key: String, default: Boolean): Flow<Boolean> {
return shim.observeValue(key, default)
}

override fun observeDouble(key: String, default: Double): Flow<Double> {
return shim.observeValue(key, default)
}

override fun observeLong(key: String, default: Long): Flow<Long> {
return shim.observeValue(key, default)
}

override fun observeByteArray(key: String, default: ByteArray): Flow<ByteArray> {
throw NotImplementedError("LaunchDarkly does not support ByteArray flags.")
}
}

@OptIn(ExperimentalForeignApi::class)
private inline fun <reified T : Any> LaunchDarklyClientShim.observeValue(key: String, default: T): Flow<T> {
return callbackFlow {
trySend(getValue<T>(key, default)).getOrThrow()
val owner = Any().pin()
observe(key, owner.get()) { trySend(getValue<T>(key, default)).getOrThrow() }
awaitClose {
stopObserving(owner.get())
owner.unpin()
}
}.conflate()
}

private inline fun <reified T : Any> LaunchDarklyClientShim.getValue(key: String, default: T): T {
return when (val clazz = T::class) {
Boolean::class -> boolVariation(key, default as Boolean) as T
String::class -> {
val jsonString = jsonStringVariation(key, null)
when (jsonString) {
null -> stringVariation(key, default as String)
else -> jsonString
} as T
}
Double::class -> doubleVariation(key, default as Double) as T
Long::class -> intVariation(key, (default as Long).toInt()).toLong() as T
else -> throw IllegalArgumentException("Illegal type for getValue: $clazz")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.github.kevincianfarini.monarch.launchdarkly

import io.github.kevincianfarini.monarch.ObservableFeatureFlagDataStore
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.Json

actual fun sut(): Pair<ObservableFeatureFlagDataStore, MutableLDClientInterface> {
val client = FakeLDShim()
return Pair(client.asFeatureFlagDataStore(), client)
}

private class FakeLDShim : LaunchDarklyClientShim, MutableLDClientInterface {

private val flagValues = mutableMapOf<String, Any>()
private val listeners = mutableSetOf<FlagListener>()

override fun setVariation(flagKey: String, value: Boolean) {
flagValues[flagKey] = value
listeners.filter { it.key == flagKey }.forEach { it.handler() }
}

override fun setVariation(flagKey: String, value: String) {
flagValues[flagKey] = value
listeners.filter { it.key == flagKey }.forEach { it.handler() }
}

override fun setVariation(flagKey: String, value: Double) {
flagValues[flagKey] = value
listeners.filter { it.key == flagKey }.forEach { it.handler() }
}

override fun setVariation(flagKey: String, value: Int) {
flagValues[flagKey] = value
listeners.filter { it.key == flagKey }.forEach { it.handler() }
}

override fun <T> setVariation(flagKey: String, value: T, serialzer: SerializationStrategy<T>) {
flagValues[flagKey] = JsonValue(Json.Default.encodeToString(serialzer, value))
listeners.filter { it.key == flagKey }.forEach { it.handler() }
}

override fun boolVariation(forKey: String, default: Boolean): Boolean {
return (flagValues[forKey] as? Boolean) ?: default
}

override fun intVariation(forKey: String, default: Int): Int {
return (flagValues[forKey] as? Int) ?: default
}

override fun doubleVariation(forKey: String, default: Double): Double {
return (flagValues[forKey] as? Double) ?: default
}

override fun stringVariation(forKey: String, default: String): String {
return (flagValues[forKey] as? String) ?: default
}

override fun jsonStringVariation(forKey: String, default: String?): String? {
return (flagValues[forKey] as? JsonValue)?.jsonString ?: default
}

override fun observe(key: String, owner: Any, handler: () -> Unit) {
listeners.add(FlagListener(key, owner, handler))
}

override fun stopObserving(owner: Any) {
listeners.removeAll { it.owner === owner }
}
}

private data class FlagListener(
val key: String,
val owner: Any,
val handler: () -> Unit,
)

private class JsonValue(val jsonString: String)

0 comments on commit ad48eda

Please sign in to comment.