Skip to content

Commit

Permalink
feat(kotlin-provider): Support flag metadata (#2862)
Browse files Browse the repository at this point in the history
* feat(kotlin-provider): Support flag metadata

Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org>

* Update doc

Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org>

---------

Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org>
  • Loading branch information
thomaspoignant authored Dec 24, 2024
1 parent 6d1f25e commit 5a6108d
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 39 deletions.
16 changes: 8 additions & 8 deletions openfeature/providers/kotlin-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,14 @@ coroutineScope.launch {

## Features status

| Status | Feature | Description |
|--------|--------------------|--------------------------------------------------------------------------------------|
| βœ… | Flag evaluation | It is possible to evaluate all the type of flags |
| βœ… | Cache invalidation | Websocket mechanism is in place to refresh the cache in case of configuration change |
| ❌ | Logging | Not supported by the SDK |
| ❌ | Flag Metadata | Not supported by the SDK |
| βœ… | Event Streaming | Not implemented |
| βœ… | Unit test | Not implemented |
| Status | Feature | Description |
|--------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| βœ… | Flag evaluation | It is possible to evaluate all the type of flags |
| βœ… | Cache invalidation | A polling mechanism is in place to refresh the cache in case of configuration change |
| ❌ | Logging | Not supported by the SDK |
| βœ… | Flag Metadata | You have access to your flag metadata |
| βœ… | Event Streaming | You can register to receive some internal event from the provider |
| βœ… | Unit test | The test are running one by one, but we still have an [issue open](https://github.com/open-feature/kotlin-sdk/issues/108) to enable fully the tests |

<sub>Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌</sub>

Expand Down
6 changes: 3 additions & 3 deletions openfeature/providers/kotlin-provider/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.5.1" apply false
id("com.android.application") version "8.5.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
id("com.android.library") version "8.5.1" apply false
id("com.android.library") version "8.5.2" apply false
id("org.jlleitschuh.gradle.ktlint") version "11.6.1" apply true
id("io.github.gradle-nexus.publish-plugin") version "1.3.0" apply true
id("io.github.gradle-nexus.publish-plugin") version "2.0.0" apply true
}

allprojects {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ dependencies {
api("dev.openfeature:android-sdk:0.3.2")
api("com.squareup.okhttp3:okhttp:4.12.0")
api("com.google.code.gson:gson:2.11.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
testImplementation("org.skyscreamer:jsonassert:1.5.3")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,7 @@ class OfrepProvider(
defaultValue: Int,
context: EvaluationContext?
): ProviderEvaluation<Int> {
val res = genericEvaluation<Int>(key, Int::class)
return ProviderEvaluation<Int>(
value = res.value,
reason = res.reason,
variant = res.variant,
errorCode = res.errorCode,
errorMessage = res.errorMessage
)
return genericEvaluation<Int>(key, Int::class)
}

override fun getObjectEvaluation(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dev.openfeature.sdk.EvaluationMetadata
import dev.openfeature.sdk.ProviderEvaluation
import dev.openfeature.sdk.Value
import dev.openfeature.sdk.exceptions.ErrorCode
Expand All @@ -16,7 +17,8 @@ data class FlagDto(
val reason: String,
val variant: String,
val errorCode: ErrorCode?,
val errorDetails: String?
val errorDetails: String?,
val metadata: Map<String, Any>? = emptyMap()
) {
fun isError(): Boolean {
return errorCode != null
Expand All @@ -38,7 +40,8 @@ data class FlagDto(
reason = reason,
variant = variant,
errorCode = errorCode,
errorMessage = errorDetails
errorMessage = errorDetails,
metadata = convertMetadata(metadata)
)
}

Expand All @@ -50,7 +53,8 @@ data class FlagDto(
reason = reason,
variant = variant,
errorCode = errorCode,
errorMessage = errorDetails
errorMessage = errorDetails,
metadata = convertMetadata(metadata)
)
} else if (value is Map<*, *>) {
val typedValue = convertObjectToStructure(value)
Expand All @@ -59,7 +63,8 @@ data class FlagDto(
reason = reason,
variant = variant,
errorCode = errorCode,
errorMessage = errorDetails
errorMessage = errorDetails,
metadata = convertMetadata(metadata)
)
} else {
throw IllegalArgumentException("Unsupported type for: $value")
Expand All @@ -74,10 +79,46 @@ data class FlagDto(
reason = reason,
variant = variant,
errorCode = errorCode,
errorMessage = errorDetails
errorMessage = errorDetails,
metadata = convertMetadata(metadata)
)
}

private fun convertMetadata(inputMap: Map<String, Any>?): EvaluationMetadata {
//check that inputMap is null or empty
if (inputMap.isNullOrEmpty()) {
return EvaluationMetadata.EMPTY
}

val metadataBuilder = EvaluationMetadata.builder()
inputMap.forEach { entry ->
// switch case on entry.value types
when (entry.value) {
is String -> {
metadataBuilder.putString(entry.key, entry.value as String)
}

is Boolean -> {
metadataBuilder.putBoolean(entry.key, entry.value as Boolean)
}

is Int -> {
metadataBuilder.putInt(entry.key, entry.value as Int)
}

is Long -> {
metadataBuilder.putInt(entry.key, (entry.value as Long).toInt())
}

is Double -> {
metadataBuilder.putDouble(entry.key, entry.value as Double)
}
}
}

return metadataBuilder.build()
}

private fun convertList(inputList: List<*>): List<Value> {
return inputList.map { item ->
when (item) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.gofeatureflag.openfeature.ofrep

import dev.openfeature.sdk.EvaluationContext
import dev.openfeature.sdk.EvaluationMetadata
import dev.openfeature.sdk.FlagEvaluationDetails
import dev.openfeature.sdk.ImmutableContext
import dev.openfeature.sdk.OpenFeatureAPI
Expand Down Expand Up @@ -230,6 +231,10 @@ class OfrepProviderTest {
reason = "DEFAULT",
errorCode = null,
errorMessage = null,
metadata = EvaluationMetadata.builder()
.putString("description", "This flag controls the title of the feature flag")
.putString("title", "Feature Flag Title")
.build()
)
assertEquals(want, got)
}
Expand Down Expand Up @@ -312,7 +317,7 @@ class OfrepProviderTest {
}

@Test
fun `should return a valid evaluation for Boolean`() = runBlocking {
fun `should return a valid evaluation for Boolean`(): Unit = runBlocking {
enqueueMockResponse("org.gofeatureflag.openfeature.ofrep/valid_api_response.json", 200)
val provider = OfrepProvider(OfrepOptions(endpoint = mockWebServer?.url("/").toString()))
OpenFeatureAPI.setProviderAndWait(provider, Dispatchers.IO, defaultEvalCtx)
Expand All @@ -325,6 +330,11 @@ class OfrepProviderTest {
reason = "TARGETING_MATCH",
errorCode = null,
errorMessage = null,
metadata = EvaluationMetadata.builder()
.putBoolean("additionalProp1", true)
.putString("additionalProp2", "value")
.putInt("additionalProp3", 123)
.build()
)
assertEquals(want, got)
}
Expand All @@ -343,6 +353,11 @@ class OfrepProviderTest {
reason = "TARGETING_MATCH",
errorCode = null,
errorMessage = null,
metadata = EvaluationMetadata.builder()
.putBoolean("additionalProp1", true)
.putString("additionalProp2", "value")
.putInt("additionalProp3", 123)
.build()
)
assertEquals(want, got)
}
Expand All @@ -361,6 +376,11 @@ class OfrepProviderTest {
reason = "TARGETING_MATCH",
errorCode = null,
errorMessage = null,
metadata = EvaluationMetadata.builder()
.putBoolean("additionalProp1", true)
.putString("additionalProp2", "value")
.putInt("additionalProp3", 123)
.build()
)
assertEquals(want, got)
}
Expand All @@ -379,6 +399,11 @@ class OfrepProviderTest {
reason = "TARGETING_MATCH",
errorCode = null,
errorMessage = null,
metadata = EvaluationMetadata.builder()
.putBoolean("additionalProp1", true)
.putString("additionalProp2", "value")
.putInt("additionalProp3", 123)
.build()
)
assertEquals(want, got)
}
Expand All @@ -401,6 +426,11 @@ class OfrepProviderTest {
reason = "TARGETING_MATCH",
errorCode = null,
errorMessage = null,
metadata = EvaluationMetadata.builder()
.putBoolean("additionalProp1", true)
.putString("additionalProp2", "value")
.putInt("additionalProp3", 123)
.build()
)
assertEquals(want, got)
}
Expand Down Expand Up @@ -435,6 +465,11 @@ class OfrepProviderTest {
reason = "TARGETING_MATCH",
errorCode = null,
errorMessage = null,
metadata = EvaluationMetadata.builder()
.putBoolean("additionalProp1", true)
.putString("additionalProp2", "value")
.putInt("additionalProp3", 123)
.build()
)
assertEquals(want, got)
}
Expand Down Expand Up @@ -551,4 +586,4 @@ class OfrepProviderTest {
}
mockWebServer!!.enqueue(resp)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class OfrepApiTest {
)
val res = ofrepApi.postBulkEvaluateFlags(ctx)
assertEquals(200, res.httpResponse.code)

val expected = OfrepApiResponse(
flags = listOf(
FlagDto(
Expand All @@ -67,23 +68,29 @@ class OfrepApiTest {
reason = "DEFAULT",
variant = "nocolor",
errorCode = null,
errorDetails = null
errorDetails = null,
metadata = null
),
FlagDto(
key = "hide-logo",
value = false,
reason = "STATIC",
variant = "var_false",
errorCode = null,
errorDetails = null
errorDetails = null,
metadata = null
),
FlagDto(
key = "title-flag",
value = "GO Feature Flag",
reason = "DEFAULT",
variant = "default_title",
errorCode = null,
errorDetails = null
errorDetails = null,
metadata = hashMapOf<String, Any>(
"description" to "This flag controls the title of the feature flag",
"title" to "Feature Flag Title"
)
)
), null, null
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,14 @@ coroutineScope.launch {

## Features status

| Status | Feature | Description |
|--------|--------------------|--------------------------------------------------------------------------------------|
| βœ… | Flag evaluation | It is possible to evaluate all the type of flags |
| βœ… | Cache invalidation | Websocket mechanism is in place to refresh the cache in case of configuration change |
| ❌ | Logging | Not supported by the SDK |
| ❌ | Flag Metadata | Not supported by the SDK |
| βœ… | Event Streaming | Not implemented |
| βœ… | Unit test | Not implemented |

| Status | Feature | Description |
|--------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| βœ… | Flag evaluation | It is possible to evaluate all the type of flags |
| βœ… | Cache invalidation | A polling mechanism is in place to refresh the cache in case of configuration change |
| ❌ | Logging | Not supported by the SDK |
| βœ… | Flag Metadata | You have access to your flag metadata |
| βœ… | Event Streaming | You can register to receive some internal event from the provider |
| βœ… | Unit test | The test are running one by one, but we still have an [issue open](https://github.com/open-feature/kotlin-sdk/issues/108) to enable fully the tests |

<sub>Implemented: βœ… | In-progress: ⚠️ | Not implemented yet: ❌</sub>

0 comments on commit 5a6108d

Please sign in to comment.