Skip to content
Permalink
Browse files

LaunchDarkly modules (#1212)

* LaunchDarkly modules

* fixes for comments
  • Loading branch information...
mightyguava committed Oct 2, 2019
1 parent 7307653 commit e73ebdbcdb07b1eb3f36cce91508c00c0171d9ab
@@ -52,6 +52,7 @@ ext.dep = [
"kotlinxCoroutines": "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1",
"ktlintVersion": "0.33.0",
"kubernetesClient": "io.kubernetes:client-java:1.0.0",
"launchDarkly": "com.launchdarkly:launchdarkly-java-server-sdk:4.7.0",
"logbackClassic": "ch.qos.logback:logback-classic:1.2.3",
"logbackJsonCore": "ch.qos.logback.contrib:logback-json-core:0.1.5",
"loggingApi": "io.github.microutils:kotlin-logging:1.4.9",
@@ -0,0 +1,23 @@
buildscript {
dependencies {
classpath dep.kotlinNoArgPlugin
}
}

dependencies {
compile dep.guice
compile dep.kotlinStdLib
compile dep.kotlinReflection
compile project(':misk')
compile project(':misk-feature')
compile project(':misk-inject')

testCompile project(':misk-testing')
}

afterEvaluate { project ->
project.tasks.dokka {
outputDirectory = "$rootDir/docs/0.x"
outputFormat = 'gfm'
}
}
@@ -0,0 +1,4 @@
POM_ARTIFACT_ID=misk-feature-testing
POM_NAME=misk-feature-testing
POM_DESCRIPTION=A Misk module testing feature flags
POM_PACKAGING=jar
@@ -0,0 +1,65 @@
package misk.feature.testing

import com.google.common.util.concurrent.AbstractIdleService
import misk.feature.Attributes
import misk.feature.Feature
import misk.feature.FeatureFlags
import misk.feature.FeatureService
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton

/**
* In-memory test implementation of [FeatureFlags] that allows flags to be overridden.
*/
@Singleton
class FakeFeatureFlags @Inject constructor() : AbstractIdleService(),
FeatureFlags,
FeatureService {
override fun startUp() {}
override fun shutDown() {}

private val overrides = ConcurrentHashMap<Feature, Any>()

override fun getBoolean(feature: Feature, token: String, attributes: Attributes): Boolean {
return overrides.getOrDefault(feature, false) as Boolean
}

override fun getInt(feature: Feature, token: String, attributes: Attributes): Int =
overrides[feature] as? Int ?: throw IllegalArgumentException(
"Int flag $feature must be overridden with override() before use")

override fun getString(feature: Feature, token: String, attributes: Attributes): String =
overrides[feature] as? String ?: throw IllegalArgumentException(
"String flag $feature must be overridden with override() before use")

override fun <T : Enum<T>> getEnum(
feature: Feature,
token: String,
clazz: Class<T>,
attributes: Attributes
): T {
@Suppress("unchecked_cast")
return overrides.getOrDefault(feature, clazz.enumConstants[0]) as T
}

fun override(feature: Feature, value: Boolean) {
overrides[feature] = value
}

fun override(feature: Feature, value: Int) {
overrides[feature] = value
}

fun override(feature: Feature, value: String) {
overrides[feature] = value
}

fun override(feature: Feature, value: Enum<*>) {
overrides[feature] = value
}

fun reset() {
overrides.clear()
}
}
@@ -0,0 +1,41 @@
package misk.feature.testing

import misk.feature.FeatureFlags
import misk.feature.FeatureService
import misk.ServiceModule
import misk.inject.KAbstractModule
import misk.inject.toKey
import kotlin.reflect.KClass

/**
* Binds a [FakeFeatureFlags] that allows tests to override values.
*/
class FakeFeatureFlagsModule(
private val qualifier: KClass<out Annotation>? = null
) : KAbstractModule() {
private val testFeatureFlags = FakeFeatureFlags()

override fun configure() {
val key = FakeFeatureFlags::class.toKey(qualifier)
bind(key).toInstance(testFeatureFlags)
bind<FeatureFlags>().to(key)
bind<FeatureService>().to(key)
install(ServiceModule(FeatureService::class.toKey(qualifier)))
}

/**
* Add overrides for the feature flags. Allows flags to be overridden at module instantiation
* instead of within individual test classes.
*
* Usage:
* ```
* install(FakeFeatureFlagsModule().withOverrides { flags ->
* flags.overrideBool(Feature("foo"), true)
* })
* ```
*/
fun withOverrides(lambda: (FakeFeatureFlags.() -> Unit)): FakeFeatureFlagsModule {
lambda(testFeatureFlags)
return this
}
}
@@ -0,0 +1,19 @@
package misk.feature.testing

import com.google.inject.Guice
import misk.feature.Feature
import misk.feature.FeatureFlags
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class FakeFeatureFlagsModuleTest {
@Test
fun testModule() {
val injector = Guice.createInjector(FakeFeatureFlagsModule().withOverrides {
override(Feature("foo"), 24)
})

val flags = injector.getInstance(FeatureFlags::class.java)
assertEquals(24, flags.getInt(Feature("foo"), ""))
}
}
@@ -0,0 +1,50 @@
package misk.feature.testing

import misk.feature.Feature
import misk.feature.getEnum
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

internal class FakeFeatureFlagsTest {
val FEATURE = Feature("foo")
val OTHER_FEATURE = Feature("bar")
val TOKEN = "cust_abcdef123"

val subject = FakeFeatureFlags()

@Test
fun getInt() {
// Default throws.
assertThrows<RuntimeException> {
subject.getInt(FEATURE, TOKEN)
}

// Can be overridden
subject.override(FEATURE, 3)
subject.override(OTHER_FEATURE, 5)
assertThat(subject.getInt(FEATURE, TOKEN)).isEqualTo(3)
assertThat(subject.getInt(OTHER_FEATURE, TOKEN)).isEqualTo(5)
}

@Test
fun getEnum() {
// Defaults to first enum.
assertThat(subject.getEnum<Dinosaur>(FEATURE, TOKEN))
.isEqualTo(Dinosaur.PTERODACTYL)

// Can be overridden
subject.override(FEATURE, Dinosaur.TYRANNOSAURUS)
assertThat(subject.getEnum<Dinosaur>(FEATURE, TOKEN))
.isEqualTo(Dinosaur.TYRANNOSAURUS)

subject.reset()
assertThat(subject.getEnum<Dinosaur>(FEATURE, TOKEN))
.isEqualTo(Dinosaur.PTERODACTYL)
}

enum class Dinosaur {
PTERODACTYL,
TYRANNOSAURUS
}
}
@@ -0,0 +1,19 @@
buildscript {
dependencies {
classpath dep.kotlinNoArgPlugin
}
}

dependencies {
compile dep.kotlinStdLib
compile dep.guava

testCompile project(':misk-testing')
}

afterEvaluate { project ->
project.tasks.dokka {
outputDirectory = "$rootDir/docs/0.x"
outputFormat = 'gfm'
}
}
@@ -0,0 +1,4 @@
POM_ARTIFACT_ID=misk-feature
POM_NAME=misk-feature
POM_DESCRIPTION=A Misk module for handling feature flags
POM_PACKAGING=jar
@@ -0,0 +1,74 @@
package misk.feature

/**
* Interface for evaluating feature flags.
*/
interface FeatureFlags {

/**
* Calculates the value of an boolean feature flag for the given token and attributes.
* @see [getEnum] for param details
*/
fun getBoolean(
feature: Feature,
token: String,
attributes: Attributes = Attributes()
): Boolean

/**
* Calculates the value of an integer feature flag for the given token and attributes.
* @see [getEnum] for param details
*/
fun getInt(
feature: Feature,
token: String,
attributes: Attributes = Attributes()
): Int

/**
* Calculates the value of a string feature flag for the given token and attributes.
* @see [getEnum] for param details
*/
fun getString(
feature: Feature,
token: String,
attributes: Attributes = Attributes()
): String

/**
* Calculates the value of an enumerated feature flag for the given token and attributes.
* @param feature name of the feature flag to evaluate.
* @param token unique primary token for the entity the flag should be evaluated against.
* @param default default value to return if there was an error evaluating the flag or the flag
* does not exist.
* @param attributes additional attributes to provide to flag evaluation.
*/
fun <T : Enum<T>> getEnum(
feature: Feature,
token: String,
clazz: Class<T>,
attributes: Attributes = Attributes()
): T
}

inline fun <reified T : Enum<T>> FeatureFlags.getEnum(
feature: Feature,
token: String,
attributes: Attributes = Attributes()
): T = getEnum(feature, token, T::class.java, attributes)

/**
* Typed feature string.
*/
data class Feature(val name: String)

/**
* Extra attributes to be used for evaluating features.
*/
data class Attributes(
val text: Map<String, String> = mapOf(),
// NB: LaunchDarkly uses typed Gson attributes. We could leak that through, but that could make
// code unwieldly. Numerical attributes are likely to be rarely used, so we make it a separate,
// optional field rather than trying to account for multiple possible attribute types.
val number: Map<String, Number>? = null
)
@@ -0,0 +1,8 @@
package misk.feature

import com.google.common.util.concurrent.Service

/**
* Marker interface to integrate with the misk service graph.
*/
interface FeatureService : Service
@@ -0,0 +1,21 @@
buildscript {
dependencies {
classpath dep.kotlinNoArgPlugin
}
}

dependencies {
compile dep.guice
compile dep.kotlinStdLib
compile dep.launchDarkly
compile project(':misk-feature')

testCompile project(':misk-testing')
}

afterEvaluate { project ->
project.tasks.dokka {
outputDirectory = "$rootDir/docs/0.x"
outputFormat = 'gfm'
}
}
@@ -0,0 +1,4 @@
POM_ARTIFACT_ID=misk-launchdarkly-core
POM_NAME=misk-launchdarkly-core
POM_DESCRIPTION=A Misk module for adapting LaunchDarkly SDK to misk-feature, includes only the core interfaces
POM_PACKAGING=jar

0 comments on commit e73ebdb

Please sign in to comment.
You can’t perform that action at this time.