Skip to content

Commit 5a6108d

Browse files
feat(kotlin-provider): Support flag metadata (#2862)
* feat(kotlin-provider): Support flag metadata Signed-off-by: Thomas Poignant <[email protected]> * Update doc Signed-off-by: Thomas Poignant <[email protected]> --------- Signed-off-by: Thomas Poignant <[email protected]>
1 parent 6d1f25e commit 5a6108d

File tree

8 files changed

+116
-39
lines changed

8 files changed

+116
-39
lines changed

β€Žopenfeature/providers/kotlin-provider/README.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,14 @@ coroutineScope.launch {
154154

155155
## Features status
156156

157-
| Status | Feature | Description |
158-
|--------|--------------------|--------------------------------------------------------------------------------------|
159-
| βœ… | Flag evaluation | It is possible to evaluate all the type of flags |
160-
| βœ… | Cache invalidation | Websocket mechanism is in place to refresh the cache in case of configuration change |
161-
| ❌ | Logging | Not supported by the SDK |
162-
| ❌ | Flag Metadata | Not supported by the SDK |
163-
| βœ… | Event Streaming | Not implemented |
164-
| βœ… | Unit test | Not implemented |
157+
| Status | Feature | Description |
158+
|--------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
159+
| βœ… | Flag evaluation | It is possible to evaluate all the type of flags |
160+
| βœ… | Cache invalidation | A polling mechanism is in place to refresh the cache in case of configuration change |
161+
| ❌ | Logging | Not supported by the SDK |
162+
| βœ… | Flag Metadata | You have access to your flag metadata |
163+
| βœ… | Event Streaming | You can register to receive some internal event from the provider |
164+
| βœ… | 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 |
165165

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

β€Žopenfeature/providers/kotlin-provider/build.gradle.kts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22
plugins {
3-
id("com.android.application") version "8.5.1" apply false
3+
id("com.android.application") version "8.5.2" apply false
44
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
5-
id("com.android.library") version "8.5.1" apply false
5+
id("com.android.library") version "8.5.2" apply false
66
id("org.jlleitschuh.gradle.ktlint") version "11.6.1" apply true
7-
id("io.github.gradle-nexus.publish-plugin") version "1.3.0" apply true
7+
id("io.github.gradle-nexus.publish-plugin") version "2.0.0" apply true
88
}
99

1010
allprojects {

β€Žopenfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ dependencies {
9696
api("dev.openfeature:android-sdk:0.3.2")
9797
api("com.squareup.okhttp3:okhttp:4.12.0")
9898
api("com.google.code.gson:gson:2.11.0")
99-
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
99+
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
100100
testImplementation("junit:junit:4.13.2")
101-
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
101+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
102102
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
103103
testImplementation("org.skyscreamer:jsonassert:1.5.3")
104104
}

β€Žopenfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/ofrep/OfrepProvider.kt

+1-8
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,7 @@ class OfrepProvider(
138138
defaultValue: Int,
139139
context: EvaluationContext?
140140
): ProviderEvaluation<Int> {
141-
val res = genericEvaluation<Int>(key, Int::class)
142-
return ProviderEvaluation<Int>(
143-
value = res.value,
144-
reason = res.reason,
145-
variant = res.variant,
146-
errorCode = res.errorCode,
147-
errorMessage = res.errorMessage
148-
)
141+
return genericEvaluation<Int>(key, Int::class)
149142
}
150143

151144
override fun getObjectEvaluation(

β€Žopenfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/ofrep/bean/OfrepApiResponse.kt

+46-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dev.openfeature.sdk.EvaluationMetadata
12
import dev.openfeature.sdk.ProviderEvaluation
23
import dev.openfeature.sdk.Value
34
import dev.openfeature.sdk.exceptions.ErrorCode
@@ -16,7 +17,8 @@ data class FlagDto(
1617
val reason: String,
1718
val variant: String,
1819
val errorCode: ErrorCode?,
19-
val errorDetails: String?
20+
val errorDetails: String?,
21+
val metadata: Map<String, Any>? = emptyMap()
2022
) {
2123
fun isError(): Boolean {
2224
return errorCode != null
@@ -38,7 +40,8 @@ data class FlagDto(
3840
reason = reason,
3941
variant = variant,
4042
errorCode = errorCode,
41-
errorMessage = errorDetails
43+
errorMessage = errorDetails,
44+
metadata = convertMetadata(metadata)
4245
)
4346
}
4447

@@ -50,7 +53,8 @@ data class FlagDto(
5053
reason = reason,
5154
variant = variant,
5255
errorCode = errorCode,
53-
errorMessage = errorDetails
56+
errorMessage = errorDetails,
57+
metadata = convertMetadata(metadata)
5458
)
5559
} else if (value is Map<*, *>) {
5660
val typedValue = convertObjectToStructure(value)
@@ -59,7 +63,8 @@ data class FlagDto(
5963
reason = reason,
6064
variant = variant,
6165
errorCode = errorCode,
62-
errorMessage = errorDetails
66+
errorMessage = errorDetails,
67+
metadata = convertMetadata(metadata)
6368
)
6469
} else {
6570
throw IllegalArgumentException("Unsupported type for: $value")
@@ -74,10 +79,46 @@ data class FlagDto(
7479
reason = reason,
7580
variant = variant,
7681
errorCode = errorCode,
77-
errorMessage = errorDetails
82+
errorMessage = errorDetails,
83+
metadata = convertMetadata(metadata)
7884
)
7985
}
8086

87+
private fun convertMetadata(inputMap: Map<String, Any>?): EvaluationMetadata {
88+
//check that inputMap is null or empty
89+
if (inputMap.isNullOrEmpty()) {
90+
return EvaluationMetadata.EMPTY
91+
}
92+
93+
val metadataBuilder = EvaluationMetadata.builder()
94+
inputMap.forEach { entry ->
95+
// switch case on entry.value types
96+
when (entry.value) {
97+
is String -> {
98+
metadataBuilder.putString(entry.key, entry.value as String)
99+
}
100+
101+
is Boolean -> {
102+
metadataBuilder.putBoolean(entry.key, entry.value as Boolean)
103+
}
104+
105+
is Int -> {
106+
metadataBuilder.putInt(entry.key, entry.value as Int)
107+
}
108+
109+
is Long -> {
110+
metadataBuilder.putInt(entry.key, (entry.value as Long).toInt())
111+
}
112+
113+
is Double -> {
114+
metadataBuilder.putDouble(entry.key, entry.value as Double)
115+
}
116+
}
117+
}
118+
119+
return metadataBuilder.build()
120+
}
121+
81122
private fun convertList(inputList: List<*>): List<Value> {
82123
return inputList.map { item ->
83124
when (item) {

β€Žopenfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/ofrep/OfrepProviderTest.kt

+37-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.gofeatureflag.openfeature.ofrep
22

33
import dev.openfeature.sdk.EvaluationContext
4+
import dev.openfeature.sdk.EvaluationMetadata
45
import dev.openfeature.sdk.FlagEvaluationDetails
56
import dev.openfeature.sdk.ImmutableContext
67
import dev.openfeature.sdk.OpenFeatureAPI
@@ -230,6 +231,10 @@ class OfrepProviderTest {
230231
reason = "DEFAULT",
231232
errorCode = null,
232233
errorMessage = null,
234+
metadata = EvaluationMetadata.builder()
235+
.putString("description", "This flag controls the title of the feature flag")
236+
.putString("title", "Feature Flag Title")
237+
.build()
233238
)
234239
assertEquals(want, got)
235240
}
@@ -312,7 +317,7 @@ class OfrepProviderTest {
312317
}
313318

314319
@Test
315-
fun `should return a valid evaluation for Boolean`() = runBlocking {
320+
fun `should return a valid evaluation for Boolean`(): Unit = runBlocking {
316321
enqueueMockResponse("org.gofeatureflag.openfeature.ofrep/valid_api_response.json", 200)
317322
val provider = OfrepProvider(OfrepOptions(endpoint = mockWebServer?.url("/").toString()))
318323
OpenFeatureAPI.setProviderAndWait(provider, Dispatchers.IO, defaultEvalCtx)
@@ -325,6 +330,11 @@ class OfrepProviderTest {
325330
reason = "TARGETING_MATCH",
326331
errorCode = null,
327332
errorMessage = null,
333+
metadata = EvaluationMetadata.builder()
334+
.putBoolean("additionalProp1", true)
335+
.putString("additionalProp2", "value")
336+
.putInt("additionalProp3", 123)
337+
.build()
328338
)
329339
assertEquals(want, got)
330340
}
@@ -343,6 +353,11 @@ class OfrepProviderTest {
343353
reason = "TARGETING_MATCH",
344354
errorCode = null,
345355
errorMessage = null,
356+
metadata = EvaluationMetadata.builder()
357+
.putBoolean("additionalProp1", true)
358+
.putString("additionalProp2", "value")
359+
.putInt("additionalProp3", 123)
360+
.build()
346361
)
347362
assertEquals(want, got)
348363
}
@@ -361,6 +376,11 @@ class OfrepProviderTest {
361376
reason = "TARGETING_MATCH",
362377
errorCode = null,
363378
errorMessage = null,
379+
metadata = EvaluationMetadata.builder()
380+
.putBoolean("additionalProp1", true)
381+
.putString("additionalProp2", "value")
382+
.putInt("additionalProp3", 123)
383+
.build()
364384
)
365385
assertEquals(want, got)
366386
}
@@ -379,6 +399,11 @@ class OfrepProviderTest {
379399
reason = "TARGETING_MATCH",
380400
errorCode = null,
381401
errorMessage = null,
402+
metadata = EvaluationMetadata.builder()
403+
.putBoolean("additionalProp1", true)
404+
.putString("additionalProp2", "value")
405+
.putInt("additionalProp3", 123)
406+
.build()
382407
)
383408
assertEquals(want, got)
384409
}
@@ -401,6 +426,11 @@ class OfrepProviderTest {
401426
reason = "TARGETING_MATCH",
402427
errorCode = null,
403428
errorMessage = null,
429+
metadata = EvaluationMetadata.builder()
430+
.putBoolean("additionalProp1", true)
431+
.putString("additionalProp2", "value")
432+
.putInt("additionalProp3", 123)
433+
.build()
404434
)
405435
assertEquals(want, got)
406436
}
@@ -435,6 +465,11 @@ class OfrepProviderTest {
435465
reason = "TARGETING_MATCH",
436466
errorCode = null,
437467
errorMessage = null,
468+
metadata = EvaluationMetadata.builder()
469+
.putBoolean("additionalProp1", true)
470+
.putString("additionalProp2", "value")
471+
.putInt("additionalProp3", 123)
472+
.build()
438473
)
439474
assertEquals(want, got)
440475
}
@@ -551,4 +586,4 @@ class OfrepProviderTest {
551586
}
552587
mockWebServer!!.enqueue(resp)
553588
}
554-
}
589+
}

β€Žopenfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/ofrep/controller/OfrepApiTest.kt

+10-3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class OfrepApiTest {
5959
)
6060
val res = ofrepApi.postBulkEvaluateFlags(ctx)
6161
assertEquals(200, res.httpResponse.code)
62+
6263
val expected = OfrepApiResponse(
6364
flags = listOf(
6465
FlagDto(
@@ -67,23 +68,29 @@ class OfrepApiTest {
6768
reason = "DEFAULT",
6869
variant = "nocolor",
6970
errorCode = null,
70-
errorDetails = null
71+
errorDetails = null,
72+
metadata = null
7173
),
7274
FlagDto(
7375
key = "hide-logo",
7476
value = false,
7577
reason = "STATIC",
7678
variant = "var_false",
7779
errorCode = null,
78-
errorDetails = null
80+
errorDetails = null,
81+
metadata = null
7982
),
8083
FlagDto(
8184
key = "title-flag",
8285
value = "GO Feature Flag",
8386
reason = "DEFAULT",
8487
variant = "default_title",
8588
errorCode = null,
86-
errorDetails = null
89+
errorDetails = null,
90+
metadata = hashMapOf<String, Any>(
91+
"description" to "This flag controls the title of the feature flag",
92+
"title" to "Feature Flag Title"
93+
)
8794
)
8895
), null, null
8996
)

β€Žwebsite/docs/openfeature_sdk/client_providers/openfeature_android.mdx

+9-8
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,14 @@ coroutineScope.launch {
197197

198198
## Features status
199199

200-
| Status | Feature | Description |
201-
|--------|--------------------|--------------------------------------------------------------------------------------|
202-
| βœ… | Flag evaluation | It is possible to evaluate all the type of flags |
203-
| βœ… | Cache invalidation | Websocket mechanism is in place to refresh the cache in case of configuration change |
204-
| ❌ | Logging | Not supported by the SDK |
205-
| ❌ | Flag Metadata | Not supported by the SDK |
206-
| βœ… | Event Streaming | Not implemented |
207-
| βœ… | Unit test | Not implemented |
200+
201+
| Status | Feature | Description |
202+
|--------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
203+
| βœ… | Flag evaluation | It is possible to evaluate all the type of flags |
204+
| βœ… | Cache invalidation | A polling mechanism is in place to refresh the cache in case of configuration change |
205+
| ❌ | Logging | Not supported by the SDK |
206+
| βœ… | Flag Metadata | You have access to your flag metadata |
207+
| βœ… | Event Streaming | You can register to receive some internal event from the provider |
208+
| βœ… | 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 |
208209

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

0 commit comments

Comments
Β (0)