diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 6bc3c67..8a3b284 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -3,4 +3,7 @@ object Versions { const val serializationRuntime = "1.4.1" const val npmPublishPlugin = "2.0.2" const val kotlinLint = "10.2.1" + + // Testing + const val ktorVersion = "2.3.1" } diff --git a/evaluation-core/build.gradle.kts b/evaluation-core/build.gradle.kts index af36070..b37bb74 100644 --- a/evaluation-core/build.gradle.kts +++ b/evaluation-core/build.gradle.kts @@ -1,3 +1,5 @@ +import Versions.ktorVersion + plugins { kotlin("multiplatform") kotlin("plugin.serialization") version Versions.serializationPlugin @@ -36,6 +38,8 @@ kotlin { val commonTest by getting { dependencies { implementation(kotlin("test")) + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-cio:$ktorVersion") } } } diff --git a/evaluation-core/src/commonMain/kotlin/EvaluationEngine.kt b/evaluation-core/src/commonMain/kotlin/EvaluationEngine.kt index 5df7f0a..d4c8486 100644 --- a/evaluation-core/src/commonMain/kotlin/EvaluationEngine.kt +++ b/evaluation-core/src/commonMain/kotlin/EvaluationEngine.kt @@ -7,7 +7,6 @@ import kotlinx.serialization.json.JsonArray private const val MAX_HASH_VALUE = 4294967295L private const val MAX_VARIANT_HASH_VALUE = MAX_HASH_VALUE.floorDiv(100) -private const val VERSION = "version" interface EvaluationEngine { fun evaluate( @@ -59,7 +58,7 @@ class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) : Evaluat if (result != null) { // Merge all metadata into the result val metadata = mergeMetadata(flag.metadata, segment.metadata, result.metadata) - result = EvaluationVariant(result.key, result.value, metadata) + result = EvaluationVariant(result.key, result.value, result.payload, metadata) log?.verbose { "Flag evaluation returned result $result on segment $segment." } break } @@ -104,8 +103,18 @@ class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) : Evaluat private fun matchCondition(target: EvaluationTarget, condition: EvaluationCondition): Boolean { val propValue = target.select(condition.selector) - val op = transformOperator(condition.op, condition.selector) - return match(propValue, op, condition.values) + // We need special matching for null properties and set type prop values + // and operators. All other values are matched as strings, since the + // filter values are always strings. + if (propValue == null) { + return matchNull(condition.op, condition.values) + } else if (isSetOperator(condition.op)) { + val propValueStringList = coerceStringList(propValue) ?: return false + return matchSet(propValueStringList, condition.op, condition.values) + } else { + val propValueString = coerceString(propValue) ?: return false + return matchString(propValueString, condition.op, condition.values) + } } private fun getHash(key: String): Long { @@ -174,39 +183,6 @@ class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) : Evaluat } } - private fun transformOperator(op: String, selector: List?): String { - var operator = op - if (selector != null && selector.isNotEmpty()) { - // if it's a version field, map the operator into an operator that supports semantic versioning. Nova - // doesn't have to do this, because dash does it while creating a nova query - if (selector[selector.size - 1].contains(VERSION)) { - operator = when (op) { - EvaluationOperator.LESS_THAN -> EvaluationOperator.VERSION_LESS_THAN - EvaluationOperator.LESS_THAN_EQUALS -> EvaluationOperator.VERSION_LESS_THAN_EQUALS - EvaluationOperator.GREATER_THAN -> EvaluationOperator.VERSION_GREATER_THAN - EvaluationOperator.GREATER_THAN_EQUALS -> EvaluationOperator.VERSION_GREATER_THAN_EQUALS - else -> op - } - } - } - return operator - } - - private fun match(propValue: Any?, op: String, filterValues: Set): Boolean { - // We need special matching for null properties and set type prop values - // and operators. All other values are matched as strings, since the - // filter values are always strings. - if (propValue == null) { - return matchNull(op, filterValues) - } else if (isSetOperator(op)) { - val propValueStringList = coerceStringList(propValue) ?: return false - return matchSet(propValueStringList, op, filterValues) - } else { - val propValueString = coerceString(propValue) ?: return false - return matchString(propValueString, op, filterValues) - } - } - private fun matchNull(op: String, filterValues: Set): Boolean { val containsNone = containsNone(filterValues) return when (op) { @@ -266,7 +242,7 @@ class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) : Evaluat private fun matchesContains(propValue: String, filterValues: Set): Boolean { for (filterValue in filterValues) { - if (filterValue.lowercase().contains(propValue.lowercase())) { + if (propValue.lowercase().contains(filterValue.lowercase())) { return true } } diff --git a/evaluation-core/src/commonTest/kotlin/EvaluationIntegrationTest.kt b/evaluation-core/src/commonTest/kotlin/EvaluationIntegrationTest.kt new file mode 100644 index 0000000..b3da1f8 --- /dev/null +++ b/evaluation-core/src/commonTest/kotlin/EvaluationIntegrationTest.kt @@ -0,0 +1,817 @@ +package com.amplitude.experiment.evaluation + +import com.amplitude.experiment.evaluation.util.FlagApi +import kotlinx.coroutines.runBlocking +import kotlin.test.DefaultAsserter +import kotlin.test.Test + +private const val DEPLOYMENT_KEY = "server-NgJxxvg8OGwwBsWVXqyxQbdiflbhvugy" + +private fun userContext( + userId: String? = null, + deviceId: String? = null, + amplitudeId: String? = null, + userProperties: Map? = null +): EvaluationContext { + return EvaluationContext().apply { + put("user", mutableMapOf().apply { + if (userId != null) put("user_id", userId) + if (deviceId != null) put("device_id", deviceId) + if (amplitudeId != null) put("amplitude_id", amplitudeId) + if (userProperties != null) put("user_properties", userProperties) + }) + } +} + +private fun groupContext( + groupType: String, + groupName: String, + groupProperties: Map? = null +): EvaluationContext { + return EvaluationContext().apply { + put("groups", mutableMapOf().apply { + put(groupType, mutableMapOf().apply { + put("group_name", groupName) + if (groupProperties != null) { + put("group_properties", groupProperties) + } + }) + }) + } +} + +class EvaluationIntegrationTest { + + private val engine: EvaluationEngine = EvaluationEngineImpl() + private val flags: List + + init { + val flagApi = FlagApi("http://localhost:3034") + flags = runBlocking { flagApi.getFlagConfigs(DEPLOYMENT_KEY) } + } + + // Basic Tests + + @Test + fun `test off`() { + val user = userContext(userId = "user_id", deviceId = "device_id") + val result = engine.evaluate(user, flags)["test-off"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "off", + result?.key + ) + } + + @Test + fun `test on`() { + val user = userContext(userId = "user_id", deviceId = "device_id") + val result = engine.evaluate(user, flags)["test-on"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + // Opinionated Segment Tests + + @Test + fun `test individual inclusions match`() { + // Match User ID + var user = userContext(userId = "user_id") + var result = engine.evaluate(user, flags)["test-individual-inclusions"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + DefaultAsserter.assertEquals( + "Unexpected segment", + "individual-inclusions", + result?.metadata?.get("segmentName") + ) + // Match Device ID + user = userContext(deviceId = "device_id") + result = engine.evaluate(user, flags)["test-individual-inclusions"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + DefaultAsserter.assertEquals( + "Unexpected segment", + "individual-inclusions", + result?.metadata?.get("segmentName") + ) + // Doesn't Match User ID + user = userContext(userId = "not_user_id") + result = engine.evaluate(user, flags)["test-individual-inclusions"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "off", + result?.key + ) + // Doesn't Match Device ID + user = userContext(deviceId = "not_device_id") + result = engine.evaluate(user, flags)["test-individual-inclusions"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "off", + result?.key + ) + } + + @Test + fun `test flag dependencies on`() { + val user = userContext(userId = "user_id", deviceId = "device_id") + val result = engine.evaluate(user, flags)["test-flag-dependencies-on"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test flag dependencies off`() { + val user = userContext(userId = "user_id", deviceId = "device_id") + val result = engine.evaluate(user, flags)["test-flag-dependencies-off"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "off", + result?.key + ) + DefaultAsserter.assertEquals( + "Unexpected segment", + "flag-dependencies", + result?.metadata?.get("segmentName") + ) + } + + @Test + fun `test sticky bucketing`() { + // On + var user = userContext( + userId = "user_id", + deviceId = "device_id", + userProperties = mapOf( + "[Experiment] test-sticky-bucketing" to "on", + ) + ) + var result = engine.evaluate(user, flags)["test-sticky-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + DefaultAsserter.assertEquals( + "Unexpected segment", + "sticky-bucketing", + result?.metadata?.get("segmentName") + ) + // Off + user = userContext( + userId = "user_id", + deviceId = "device_id", + userProperties = mapOf( + "[Experiment] test-sticky-bucketing" to "off", + ) + ) + result = engine.evaluate(user, flags)["test-sticky-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result: $result", + "off", + result?.key + ) + DefaultAsserter.assertEquals( + "Unexpected segment", + "All Other Users", + result?.metadata?.get("segmentName") + ) + // Non-variant + user = userContext( + userId = "user_id", + deviceId = "device_id", + userProperties = mapOf( + "[Experiment] test-sticky-bucketing" to "not-a-variant", + ) + ) + result = engine.evaluate(user, flags)["test-sticky-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result: $result", + "off", + result?.key + ) + } + + // Experiment and Flag Segment Tests + + @Test + fun `test experiment`() { + val user = userContext(userId = "user_id", deviceId = "device_id") + val result = engine.evaluate(user, flags)["test-experiment"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + DefaultAsserter.assertEquals( + "Unexpected experiment key", + "exp-1", + result?.metadata?.get("experimentKey") + ) + } + + @Test + fun `test flag`() { + val user = userContext(userId = "user_id", deviceId = "device_id") + val result = engine.evaluate(user, flags)["test-flag"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + DefaultAsserter.assertEquals( + "Unexpected experiment key", + null, + result?.metadata?.get("experimentKey") + ) + } + + // Conditional Logic Tests + + @Test + fun `test multiple conditions and values`() { + // All match, on + var user = userContext( + userProperties = mapOf( + "key-1" to "value-1", + "key-2" to "value-2", + "key-3" to "value-3", + ) + ) + var result = engine.evaluate(user, flags)["test-multiple-conditions-and-values"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + // Some match, off + user = userContext( + userProperties = mapOf( + "key-1" to "value-1", + "key-2" to "value-2", + ) + ) + result = engine.evaluate(user, flags)["test-multiple-conditions-and-values"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "off", + result?.key + ) + } + + // Condition Property Targeting Tests + + @Test + fun `test amplitude property targeting`() { + val user = userContext(userId = "user_id") + val result = engine.evaluate(user, flags)["test-amplitude-property-targeting"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test group name targeting`() { + val user = groupContext("org name", "amplitude") + val result = engine.evaluate(user, flags)["test-group-name-targeting"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test group property targeting`() { + val user = groupContext("org name", "amplitude", mapOf("org plan" to "enterprise2")) + val result = engine.evaluate(user, flags)["test-group-property-targeting"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + // Bucketing Unit Tests + + @Test + fun `test amplitude id bucketing`() { + val user = userContext(amplitudeId = "1234567890") + val result = engine.evaluate(user, flags)["test-amplitude-id-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test user id bucketing`() { + val user = userContext(userId = "user_id") + val result = engine.evaluate(user, flags)["test-user-id-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test device id bucketing`() { + val user = userContext(userId = "device_id") + val result = engine.evaluate(user, flags)["test-user-id-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test custom user property bucketing`() { + val user = userContext(userProperties = mapOf("key" to "value")) + val result = engine.evaluate(user, flags)["test-custom-user-property-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test group name bucketing`() { + val user = groupContext("org name", "amplitude") + val result = engine.evaluate(user, flags)["test-group-name-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test group property bucketing`() { + val user = groupContext("org name", "amplitude", mapOf("org plan" to "enterprise2")) + val result = engine.evaluate(user, flags)["test-group-property-bucketing"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + // Bucketing Allocation Tests + + @Test + fun `test 1 percent allocation`() { + var on = 0 + repeat(10000) { i -> + val user = userContext(deviceId = "${i + 1}") + val result = engine.evaluate(user, flags)["test-1-percent-allocation"] + if (result?.key == "on") { + on++ + } + } + DefaultAsserter.assertEquals( + "Unexpected assignments", + 107, + on + ) + } + + @Test + fun `test 50 percent allocation`() { + var on = 0 + repeat(10000) { i -> + val user = userContext(deviceId = "${i + 1}") + val result = engine.evaluate(user, flags)["test-50-percent-allocation"] + if (result?.key == "on") { + on++ + } + } + DefaultAsserter.assertEquals( + "Unexpected assignments", + 5009, + on + ) + } + + @Test + fun `test 99 percent allocation`() { + var on = 0 + repeat(10000) { i -> + val user = userContext(deviceId = "${i + 1}") + val result = engine.evaluate(user, flags)["test-99-percent-allocation"] + if (result?.key == "on") { + on++ + } + } + DefaultAsserter.assertEquals( + "Unexpected assignments", + 9900, + on + ) + } + + // Bucketing Distribution Tests + + @Test + fun `test 1 percent distribution`() { + var control = 0 + var treatment = 0 + repeat(10000) { i -> + val user = userContext(deviceId = "${i + 1}") + val result = engine.evaluate(user, flags)["test-1-percent-distribution"] + if (result?.key == "control") { + control++ + } else if (result?.key == "treatment") { + treatment++ + } + } + DefaultAsserter.assertEquals( + "Unexpected assignments", + 106, + control + ) + DefaultAsserter.assertEquals( + "Unexpected assignments", + 9894, + treatment + ) + } + + @Test + fun `test 50 percent distribution`() { + var control = 0 + var treatment = 0 + repeat(10000) { i -> + val user = userContext(deviceId = "${i + 1}") + val result = engine.evaluate(user, flags)["test-50-percent-distribution"] + if (result?.key == "control") { + control++ + } else if (result?.key == "treatment") { + treatment++ + } + } + DefaultAsserter.assertEquals( + "Unexpected assignments", + 4990, + control + ) + DefaultAsserter.assertEquals( + "Unexpected assignments", + 5010, + treatment + ) + } + + @Test + fun `test 99 percent distribution`() { + var control = 0 + var treatment = 0 + repeat(10000) { i -> + val user = userContext(deviceId = "${i + 1}") + val result = engine.evaluate(user, flags)["test-99-percent-distribution"] + if (result?.key == "control") { + control++ + } else if (result?.key == "treatment") { + treatment++ + } + } + DefaultAsserter.assertEquals( + "Unexpected assignments", + 9909, + control + ) + DefaultAsserter.assertEquals( + "Unexpected assignments", + 91, + treatment + ) + } + + @Test + fun `test multiple distributions`() { + var a = 0 + var b = 0 + var c = 0 + var d = 0 + repeat(10000) { i -> + val user = userContext(deviceId = "${i + 1}") + val result = engine.evaluate(user, flags)["test-multiple-distributions"] + when (result?.key) { + "a" -> a++ + "b" -> b++ + "c" -> c++ + "d" -> d++ + } + } + DefaultAsserter.assertEquals( + "Unexpected assignments", + 2444, + a + ) + DefaultAsserter.assertEquals( + "Unexpected assignments", + 2634, + b + ) + DefaultAsserter.assertEquals( + "Unexpected assignments", + 2447, + c + ) + DefaultAsserter.assertEquals( + "Unexpected assignments", + 2475, + d + ) + } + + // Operator Tests + + @Test + fun `test is`() { + val user = userContext( + userProperties = mapOf( + "key" to "value" + ) + ) + val result = engine.evaluate(user, flags)["test-is"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test is not`() { + val user = userContext( + userProperties = mapOf( + "key" to "value" + ) + ) + val result = engine.evaluate(user, flags)["test-is-not"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test contains`() { + val user = userContext( + userProperties = mapOf( + "key" to "value" + ) + ) + val result = engine.evaluate(user, flags)["test-contains"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test does not contain`() { + val user = userContext( + userProperties = mapOf( + "key" to "value" + ) + ) + val result = engine.evaluate(user, flags)["test-does-not-contain"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test less`() { + val user = userContext( + userProperties = mapOf( + "key" to "-1" + ) + ) + val result = engine.evaluate(user, flags)["test-less"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test less or equal`() { + val user = userContext( + userProperties = mapOf( + "key" to "0" + ) + ) + val result = engine.evaluate(user, flags)["test-less-or-equal"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test greater`() { + val user = userContext( + userProperties = mapOf( + "key" to "1" + ) + ) + val result = engine.evaluate(user, flags)["test-greater"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test greater or equal`() { + val user = userContext( + userProperties = mapOf( + "key" to "0" + ) + ) + val result = engine.evaluate(user, flags)["test-greater-or-equal"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test version less`() { + val user = userContext( + userProperties = mapOf( + "version" to "1.9.0" + ) + ) + val result = engine.evaluate(user, flags)["test-version-less"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test version less or equal`() { + val user = userContext( + userProperties = mapOf( + "version" to "1.10.0" + ) + ) + val result = engine.evaluate(user, flags)["test-version-less-or-equal"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test version greater`() { + val user = userContext( + userProperties = mapOf( + "version" to "1.10.0" + ) + ) + val result = engine.evaluate(user, flags)["test-version-greater"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test version greater or equal`() { + val user = userContext( + userProperties = mapOf( + "version" to "1.9.0" + ) + ) + val result = engine.evaluate(user, flags)["test-version-greater-or-equal"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test set is`() { + val user = userContext( + userProperties = mapOf( + "key" to listOf("1", "2", "3") + ) + ) + val result = engine.evaluate(user, flags)["test-set-is"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test set is not`() { + val user = userContext( + userProperties = mapOf( + "key" to listOf("1", "2") + ) + ) + val result = engine.evaluate(user, flags)["test-set-is-not"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test set contains`() { + val user = userContext( + userProperties = mapOf( + "key" to listOf("1", "2") + ) + ) + val result = engine.evaluate(user, flags)["test-set-contains"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test set does not contain`() { + val user = userContext( + userProperties = mapOf( + "key" to listOf("1", "2", "4") + ) + ) + val result = engine.evaluate(user, flags)["test-set-does-not-contain"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test glob match`() { + val user = userContext( + userProperties = mapOf( + "key" to "/path/1/2/3/end" + ) + ) + val result = engine.evaluate(user, flags)["test-glob-match"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } + + @Test + fun `test glob does not match`() { + val user = userContext( + userProperties = mapOf( + "key" to "/path/1/2/3" + ) + ) + val result = engine.evaluate(user, flags)["test-glob-does-not-match"] + DefaultAsserter.assertEquals( + "Unexpected evaluation result", + "on", + result?.key + ) + } +} diff --git a/evaluation-core/src/commonTest/kotlin/util/FlagApi.kt b/evaluation-core/src/commonTest/kotlin/util/FlagApi.kt new file mode 100644 index 0000000..4c29d0e --- /dev/null +++ b/evaluation-core/src/commonTest/kotlin/util/FlagApi.kt @@ -0,0 +1,61 @@ +package com.amplitude.experiment.evaluation.util + +import com.amplitude.experiment.evaluation.EvaluationFlag +import com.amplitude.experiment.evaluation.json +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.request +import io.ktor.client.request.url +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.client.request.headers +import io.ktor.http.isSuccess +import io.ktor.http.path +import kotlinx.serialization.decodeFromString + +internal class HttpErrorResponseException( + statusCode: HttpStatusCode +) : Exception("HTTP error response: code=$statusCode, message=${statusCode.description}") + +suspend fun HttpClient.get( + url: String, + path: String, + block: HttpRequestBuilder.() -> Unit +): HttpResponse { + return request(HttpMethod.Get, url, path, block) +} + +suspend fun HttpClient.request( + method: HttpMethod, + url: String, + path: String, + block: HttpRequestBuilder.() -> Unit +): HttpResponse { + return request { + this.method = method + url { + url(url) + path(path) + } + block.invoke(this) + } +} + +class FlagApi(private val serverUrl: String = "https://api.lab.amplitude.com") { + private val client = HttpClient(CIO) + suspend fun getFlagConfigs(deploymentKey: String): List { + val response = client.get(serverUrl, "/sdk/v2/flags") { + headers { + set("Authorization", "Api-Key $deploymentKey") + } + } + if (!response.status.isSuccess()) { + throw HttpErrorResponseException(response.status) + } + val body: String = response.body() + return json.decodeFromString(body) + } +}