Skip to content

Commit ab963b2

Browse files
authored
Fixup user serialization issue (#52)
* Make anyserializer exclusive for user object serialization / handle more types * Format / User object printing fix * Downgrade kotlinx.serialization to 1.7.1 * Revert "Downgrade kotlinx.serialization to 1.7.1" This reverts commit cd27522. * Switch to version catalog * Update Constants.kt * Revert version bump * Increase coverage * Update ConfigCatUserTests.kt * Update ConfigCatUserTests.kt
1 parent 4a0720e commit ab963b2

File tree

8 files changed

+144
-171
lines changed

8 files changed

+144
-171
lines changed

build.gradle.kts

+26-45
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,20 @@ import io.gitlab.arturbosch.detekt.Detekt
22
import org.jetbrains.dokka.gradle.DokkaTask
33
import java.net.URL
44

5-
buildscript {
6-
val kotlinVersion by extra("2.0.21")
7-
val atomicfuVersion: String by project
8-
dependencies {
9-
classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfuVersion")
10-
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
11-
classpath("org.jetbrains.kotlin.android:org.jetbrains.kotlin.android.gradle.plugin:$kotlinVersion")
12-
}
13-
}
14-
15-
apply(plugin = "kotlinx-atomicfu")
16-
175
plugins {
18-
kotlin("multiplatform") version "2.0.21"
19-
kotlin("plugin.serialization") version "2.0.21"
20-
id("com.android.library")
6+
alias(libs.plugins.kotlinMultiplatform)
7+
alias(libs.plugins.serialization)
8+
alias(libs.plugins.atomicfu)
9+
alias(libs.plugins.androidLibrary)
10+
alias(libs.plugins.dokka)
11+
alias(libs.plugins.sonarqube)
12+
alias(libs.plugins.kover)
13+
alias(libs.plugins.detekt)
14+
alias(libs.plugins.ktlint)
2115
id("maven-publish")
2216
id("signing")
23-
id("org.jetbrains.dokka") version "1.9.20"
24-
id("org.sonarqube") version "5.0.0.4638"
25-
id("org.jetbrains.kotlinx.kover") version "0.7.6"
26-
id("io.gitlab.arturbosch.detekt") version "1.23.6"
27-
id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
2817
}
2918

30-
val atomicfuVersion: String by project
31-
val ktorVersion: String by project
32-
val kotlinxSerializationVersion: String by project
33-
val kotlinxCoroutinesVersion: String by project
34-
val klockVersion: String by project
35-
val kryptoVersion: String by project
36-
val semverVersion: String by project
37-
3819
val buildNumber: String get() = System.getenv("BUILD_NUMBER") ?: ""
3920
val isSnapshot: Boolean get() = System.getProperty("snapshot") != null
4021

@@ -102,39 +83,39 @@ kotlin {
10283

10384
sourceSets {
10485
commonMain.dependencies {
105-
implementation("io.ktor:ktor-client-core:$ktorVersion")
106-
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinxSerializationVersion")
107-
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
108-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
109-
implementation("com.soywiz.korlibs.klock:klock:$klockVersion")
110-
implementation("com.soywiz.korlibs.krypto:krypto:$kryptoVersion")
111-
implementation("io.github.z4kn4fein:semver:$semverVersion")
86+
implementation(libs.ktor)
87+
implementation(libs.serialization.core)
88+
implementation(libs.serialization.json)
89+
implementation(libs.coroutines.core)
90+
implementation(libs.klock)
91+
implementation(libs.krypto)
92+
implementation(libs.semver)
11293
}
11394
commonTest.dependencies {
114-
implementation(kotlin("test"))
115-
implementation("io.ktor:ktor-client-mock:$ktorVersion")
116-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
95+
implementation(libs.kotlin.test)
96+
implementation(libs.ktor.mock)
97+
implementation(libs.coroutines.test)
11798
}
11899

119100
jvmMain.dependencies {
120-
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
101+
implementation(libs.ktor.okhttp)
121102
}
122103

123104
jsMain.dependencies {
124-
implementation("io.ktor:ktor-client-js:$ktorVersion")
105+
implementation(libs.ktor.js)
125106
}
126107

127108
androidMain.dependencies {
128-
implementation("io.ktor:ktor-client-android:$ktorVersion")
129-
implementation("org.jetbrains.kotlinx:atomicfu:$atomicfuVersion")
109+
implementation(libs.ktor.android)
110+
implementation(libs.atomicfu)
130111
}
131112

132113
appleMain.dependencies {
133-
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
114+
implementation(libs.ktor.darwin)
134115
}
135116

136117
appleTest.dependencies {
137-
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
118+
implementation(libs.ktor.darwin)
138119
}
139120

140121
val nativeRestMain by creating {
@@ -143,7 +124,7 @@ kotlin {
143124
val nativeRestTest by creating {
144125
dependsOn(commonTest.get())
145126
dependencies {
146-
implementation("io.ktor:ktor-client-cio:$ktorVersion")
127+
implementation(libs.ktor.cio)
147128
}
148129
}
149130

gradle.properties

-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
group=com.configcat
22
version=4.1.1
33

4-
ktorVersion=3.0.0
5-
kotlinxSerializationVersion=1.7.3
6-
kotlinxCoroutinesVersion=1.9.0
7-
klockVersion=4.0.10
8-
kryptoVersion=4.0.10
9-
atomicfuVersion=0.23.1
10-
android_gradle_plugin=8.7.0
11-
12-
semverVersion=2.0.0
13-
144
kotlin.code.style=official
155

166
kotlin.native.ignoreIncorrectDependencies=true

gradle/libs.versions.toml

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[versions]
2+
kotlin = "2.0.21"
3+
android-gradle-plugin = "8.7.3"
4+
ktor = "3.0.0"
5+
kotlinx-serialization = "1.7.3"
6+
kotlinx-coroutines= "1.9.0"
7+
klock = "4.0.10"
8+
krypto = "4.0.10"
9+
atomicfu = "0.26.1"
10+
semver = "2.0.0"
11+
dokka = "1.9.20"
12+
sonarqube = "5.0.0.4638"
13+
kover = "0.7.6"
14+
detekt = "1.23.6"
15+
ktlint = "12.1.0"
16+
17+
[libraries]
18+
ktor = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
19+
ktor-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
20+
ktor-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
21+
ktor-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
22+
ktor-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
23+
ktor-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
24+
ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
25+
serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
26+
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
27+
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
28+
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
29+
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
30+
krypto = { module = "com.soywiz.korlibs.krypto:krypto", version.ref = "krypto" }
31+
semver = { module = "io.github.z4kn4fein:semver", version.ref = "semver" }
32+
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
33+
atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" }
34+
35+
[plugins]
36+
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
37+
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
38+
androidLibrary = { id = "com.android.library", version.ref = "android-gradle-plugin" }
39+
atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" }
40+
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
41+
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
42+
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
43+
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
44+
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }

src/commonMain/kotlin/com/configcat/ConfigCatClient.kt

+15-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.configcat.override.OverrideBehavior
1515
import io.ktor.client.engine.HttpClientEngine
1616
import io.ktor.client.engine.ProxyConfig
1717
import korlibs.time.DateTime
18+
import kotlinx.atomicfu.AtomicRef
1819
import kotlinx.atomicfu.atomic
1920
import kotlinx.atomicfu.locks.reentrantLock
2021
import kotlinx.atomicfu.locks.withLock
@@ -328,7 +329,7 @@ internal class Client private constructor(
328329
private val evaluator: Evaluator
329330
private val logLevel: LogLevel
330331
private val logger: InternalLogger
331-
private var defaultUser: ConfigCatUser?
332+
private val defaultUser: AtomicRef<ConfigCatUser?> = atomic(null)
332333
private val isClosed = atomic(false)
333334

334335
override val hooks: Hooks
@@ -338,7 +339,7 @@ internal class Client private constructor(
338339
logger = InternalLogger(options.logger, options.logLevel, options.hooks)
339340
logLevel = options.logLevel
340341
hooks = options.hooks
341-
defaultUser = options.defaultUser
342+
defaultUser.value = options.defaultUser
342343
flagOverrides = options.flagOverrides?.let { FlagOverrides().apply(it) }
343344
service =
344345
if (flagOverrides != null && flagOverrides.behavior == OverrideBehavior.LOCAL_ONLY) {
@@ -359,7 +360,7 @@ internal class Client private constructor(
359360
require(key.isNotEmpty()) { "'key' cannot be empty." }
360361

361362
val settingResult = getSettings()
362-
val evalUser = user ?: defaultUser
363+
val evalUser = user ?: defaultUser.value
363364
val checkSettingAvailable = checkSettingAvailable(settingResult, key, defaultValue)
364365
val setting = checkSettingAvailable.second
365366
if (setting == null) {
@@ -403,7 +404,7 @@ internal class Client private constructor(
403404
require(key.isNotEmpty()) { "'key' cannot be empty." }
404405

405406
val settingResult = getSettings()
406-
val evalUser = user ?: defaultUser
407+
val evalUser = user ?: defaultUser.value
407408

408409
val checkSettingAvailable = checkSettingAvailable(settingResult, key, defaultValue)
409410
val setting = checkSettingAvailable.second
@@ -447,7 +448,7 @@ internal class Client private constructor(
447448
}
448449
return try {
449450
settingResult.settings.map {
450-
evaluate(it.value, it.key, user ?: defaultUser, settingResult.fetchTime, settingResult.settings)
451+
evaluate(it.value, it.key, user ?: defaultUser.value, settingResult.fetchTime, settingResult.settings)
451452
}
452453
} catch (exception: Exception) {
453454
val errorMessage =
@@ -528,7 +529,13 @@ internal class Client private constructor(
528529
return try {
529530
return settingResult.settings.map {
530531
val evaluated =
531-
evaluate(it.value, it.key, user ?: defaultUser, settingResult.fetchTime, settingResult.settings)
532+
evaluate(
533+
it.value,
534+
it.key,
535+
user ?: defaultUser.value,
536+
settingResult.fetchTime,
537+
settingResult.settings,
538+
)
532539
it.key to evaluated.value
533540
}.toMap()
534541
} catch (exception: Exception) {
@@ -577,7 +584,7 @@ internal class Client private constructor(
577584
)
578585
return
579586
}
580-
defaultUser = user
587+
defaultUser.value = user
581588
}
582589

583590
override fun clearDefaultUser() {
@@ -588,7 +595,7 @@ internal class Client private constructor(
588595
)
589596
return
590597
}
591-
defaultUser = null
598+
defaultUser.value = null
592599
}
593600

594601
override fun close() {

src/commonMain/kotlin/com/configcat/ConfigCatUser.kt

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package com.configcat
22

33
import kotlinx.serialization.encodeToString
4+
import kotlinx.serialization.json.JsonArray
5+
import kotlinx.serialization.json.JsonElement
6+
import kotlinx.serialization.json.JsonObject
7+
import kotlinx.serialization.json.JsonPrimitive
48

59
/**
610
* An object containing attributes to properly identify a given user for variation evaluation.
@@ -83,6 +87,25 @@ public class ConfigCatUser(
8387
}
8488

8589
override fun toString(): String {
86-
return Constants.json.encodeToString(attributes)
90+
return Constants.json.encodeToString(toJsonElement(attributes))
8791
}
92+
93+
private fun toJsonElement(value: Any): JsonElement =
94+
when (value) {
95+
is JsonElement -> value
96+
is Number -> JsonPrimitive(value)
97+
is String -> JsonPrimitive(value)
98+
is Boolean -> JsonPrimitive(value)
99+
is Enum<*> -> JsonPrimitive(value.toString())
100+
is Array<*> -> JsonArray(value.map { toJsonElement(it ?: "") })
101+
is Iterable<*> -> JsonArray(value.map { toJsonElement(it ?: "") })
102+
is Map<*, *> ->
103+
JsonObject(
104+
value.map {
105+
(key, value) ->
106+
key as String to toJsonElement(value ?: "")
107+
}.toMap(),
108+
)
109+
else -> JsonPrimitive(value.toString())
110+
}
88111
}

src/commonMain/kotlin/com/configcat/Constants.kt

-51
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,7 @@ import com.configcat.model.Config
55
import com.configcat.model.SettingType
66
import com.configcat.model.SettingValue
77
import korlibs.time.DateTime
8-
import kotlinx.serialization.ContextualSerializer
9-
import kotlinx.serialization.ExperimentalSerializationApi
10-
import kotlinx.serialization.KSerializer
11-
import kotlinx.serialization.descriptors.SerialDescriptor
12-
import kotlinx.serialization.encoding.Decoder
13-
import kotlinx.serialization.encoding.Encoder
148
import kotlinx.serialization.json.Json
15-
import kotlinx.serialization.json.JsonDecoder
16-
import kotlinx.serialization.json.JsonElement
17-
import kotlinx.serialization.json.JsonEncoder
18-
import kotlinx.serialization.json.JsonPrimitive
19-
import kotlinx.serialization.modules.SerializersModule
209

2110
internal interface Closeable {
2211
fun close()
@@ -37,47 +26,7 @@ internal object Constants {
3726
val json =
3827
Json {
3928
ignoreUnknownKeys = true
40-
serializersModule =
41-
SerializersModule {
42-
contextual(Any::class, FlagValueSerializer)
43-
}
4429
}
45-
46-
internal object FlagValueSerializer : KSerializer<Any> {
47-
override fun deserialize(decoder: Decoder): Any {
48-
val json =
49-
decoder as? JsonDecoder
50-
?: error("Only JsonDecoder is supported.")
51-
val element = json.decodeJsonElement()
52-
val primitive = element as? JsonPrimitive ?: error("Unable to decode $element")
53-
return when (primitive.content) {
54-
"true", "false" -> primitive.content == "true"
55-
else -> primitive.content.toIntOrNull() ?: primitive.content.toDoubleOrNull() ?: primitive.content
56-
}
57-
}
58-
59-
override fun serialize(
60-
encoder: Encoder,
61-
value: Any,
62-
) {
63-
val json =
64-
encoder as? JsonEncoder
65-
?: error("Only JsonEncoder is supported.")
66-
val element: JsonElement =
67-
when (value) {
68-
is String -> JsonPrimitive(value)
69-
is Number -> JsonPrimitive(value)
70-
is Boolean -> JsonPrimitive(value)
71-
is JsonElement -> value
72-
else -> throw IllegalArgumentException("Unable to encode $value")
73-
}
74-
json.encodeJsonElement(element)
75-
}
76-
77-
@OptIn(ExperimentalSerializationApi::class)
78-
override val descriptor: SerialDescriptor =
79-
ContextualSerializer(Any::class, null, emptyArray()).descriptor
80-
}
8130
}
8231

8332
internal object Helpers {

0 commit comments

Comments
 (0)