Skip to content

Commit

Permalink
Fixup user serialization issue (#52)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
z4kn4fein authored Dec 25, 2024
1 parent 4a0720e commit ab963b2
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 171 deletions.
71 changes: 26 additions & 45 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,20 @@ import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.dokka.gradle.DokkaTask
import java.net.URL

buildscript {
val kotlinVersion by extra("2.0.21")
val atomicfuVersion: String by project
dependencies {
classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfuVersion")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("org.jetbrains.kotlin.android:org.jetbrains.kotlin.android.gradle.plugin:$kotlinVersion")
}
}

apply(plugin = "kotlinx-atomicfu")

plugins {
kotlin("multiplatform") version "2.0.21"
kotlin("plugin.serialization") version "2.0.21"
id("com.android.library")
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.serialization)
alias(libs.plugins.atomicfu)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.dokka)
alias(libs.plugins.sonarqube)
alias(libs.plugins.kover)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
id("maven-publish")
id("signing")
id("org.jetbrains.dokka") version "1.9.20"
id("org.sonarqube") version "5.0.0.4638"
id("org.jetbrains.kotlinx.kover") version "0.7.6"
id("io.gitlab.arturbosch.detekt") version "1.23.6"
id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
}

val atomicfuVersion: String by project
val ktorVersion: String by project
val kotlinxSerializationVersion: String by project
val kotlinxCoroutinesVersion: String by project
val klockVersion: String by project
val kryptoVersion: String by project
val semverVersion: String by project

val buildNumber: String get() = System.getenv("BUILD_NUMBER") ?: ""
val isSnapshot: Boolean get() = System.getProperty("snapshot") != null

Expand Down Expand Up @@ -102,39 +83,39 @@ kotlin {

sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinxSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
implementation("com.soywiz.korlibs.klock:klock:$klockVersion")
implementation("com.soywiz.korlibs.krypto:krypto:$kryptoVersion")
implementation("io.github.z4kn4fein:semver:$semverVersion")
implementation(libs.ktor)
implementation(libs.serialization.core)
implementation(libs.serialization.json)
implementation(libs.coroutines.core)
implementation(libs.klock)
implementation(libs.krypto)
implementation(libs.semver)
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation("io.ktor:ktor-client-mock:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
implementation(libs.kotlin.test)
implementation(libs.ktor.mock)
implementation(libs.coroutines.test)
}

jvmMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation(libs.ktor.okhttp)
}

jsMain.dependencies {
implementation("io.ktor:ktor-client-js:$ktorVersion")
implementation(libs.ktor.js)
}

androidMain.dependencies {
implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("org.jetbrains.kotlinx:atomicfu:$atomicfuVersion")
implementation(libs.ktor.android)
implementation(libs.atomicfu)
}

appleMain.dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
implementation(libs.ktor.darwin)
}

appleTest.dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
implementation(libs.ktor.darwin)
}

val nativeRestMain by creating {
Expand All @@ -143,7 +124,7 @@ kotlin {
val nativeRestTest by creating {
dependsOn(commonTest.get())
dependencies {
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation(libs.ktor.cio)
}
}

Expand Down
10 changes: 0 additions & 10 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
group=com.configcat
version=4.1.1

ktorVersion=3.0.0
kotlinxSerializationVersion=1.7.3
kotlinxCoroutinesVersion=1.9.0
klockVersion=4.0.10
kryptoVersion=4.0.10
atomicfuVersion=0.23.1
android_gradle_plugin=8.7.0

semverVersion=2.0.0

kotlin.code.style=official

kotlin.native.ignoreIncorrectDependencies=true
Expand Down
44 changes: 44 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[versions]
kotlin = "2.0.21"
android-gradle-plugin = "8.7.3"
ktor = "3.0.0"
kotlinx-serialization = "1.7.3"
kotlinx-coroutines= "1.9.0"
klock = "4.0.10"
krypto = "4.0.10"
atomicfu = "0.26.1"
semver = "2.0.0"
dokka = "1.9.20"
sonarqube = "5.0.0.4638"
kover = "0.7.6"
detekt = "1.23.6"
ktlint = "12.1.0"

[libraries]
ktor = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
ktor-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
ktor-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
ktor-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
krypto = { module = "com.soywiz.korlibs.krypto:krypto", version.ref = "krypto" }
semver = { module = "io.github.z4kn4fein:semver", version.ref = "semver" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" }

[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
androidLibrary = { id = "com.android.library", version.ref = "android-gradle-plugin" }
atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
23 changes: 15 additions & 8 deletions src/commonMain/kotlin/com/configcat/ConfigCatClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.configcat.override.OverrideBehavior
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.ProxyConfig
import korlibs.time.DateTime
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock
Expand Down Expand Up @@ -328,7 +329,7 @@ internal class Client private constructor(
private val evaluator: Evaluator
private val logLevel: LogLevel
private val logger: InternalLogger
private var defaultUser: ConfigCatUser?
private val defaultUser: AtomicRef<ConfigCatUser?> = atomic(null)
private val isClosed = atomic(false)

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

val settingResult = getSettings()
val evalUser = user ?: defaultUser
val evalUser = user ?: defaultUser.value
val checkSettingAvailable = checkSettingAvailable(settingResult, key, defaultValue)
val setting = checkSettingAvailable.second
if (setting == null) {
Expand Down Expand Up @@ -403,7 +404,7 @@ internal class Client private constructor(
require(key.isNotEmpty()) { "'key' cannot be empty." }

val settingResult = getSettings()
val evalUser = user ?: defaultUser
val evalUser = user ?: defaultUser.value

val checkSettingAvailable = checkSettingAvailable(settingResult, key, defaultValue)
val setting = checkSettingAvailable.second
Expand Down Expand Up @@ -447,7 +448,7 @@ internal class Client private constructor(
}
return try {
settingResult.settings.map {
evaluate(it.value, it.key, user ?: defaultUser, settingResult.fetchTime, settingResult.settings)
evaluate(it.value, it.key, user ?: defaultUser.value, settingResult.fetchTime, settingResult.settings)
}
} catch (exception: Exception) {
val errorMessage =
Expand Down Expand Up @@ -528,7 +529,13 @@ internal class Client private constructor(
return try {
return settingResult.settings.map {
val evaluated =
evaluate(it.value, it.key, user ?: defaultUser, settingResult.fetchTime, settingResult.settings)
evaluate(
it.value,
it.key,
user ?: defaultUser.value,
settingResult.fetchTime,
settingResult.settings,
)
it.key to evaluated.value
}.toMap()
} catch (exception: Exception) {
Expand Down Expand Up @@ -577,7 +584,7 @@ internal class Client private constructor(
)
return
}
defaultUser = user
defaultUser.value = user
}

override fun clearDefaultUser() {
Expand All @@ -588,7 +595,7 @@ internal class Client private constructor(
)
return
}
defaultUser = null
defaultUser.value = null
}

override fun close() {
Expand Down
25 changes: 24 additions & 1 deletion src/commonMain/kotlin/com/configcat/ConfigCatUser.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.configcat

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

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

override fun toString(): String {
return Constants.json.encodeToString(attributes)
return Constants.json.encodeToString(toJsonElement(attributes))
}

private fun toJsonElement(value: Any): JsonElement =
when (value) {
is JsonElement -> value
is Number -> JsonPrimitive(value)
is String -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is Enum<*> -> JsonPrimitive(value.toString())
is Array<*> -> JsonArray(value.map { toJsonElement(it ?: "") })
is Iterable<*> -> JsonArray(value.map { toJsonElement(it ?: "") })
is Map<*, *> ->
JsonObject(
value.map {
(key, value) ->
key as String to toJsonElement(value ?: "")
}.toMap(),
)
else -> JsonPrimitive(value.toString())
}
}
51 changes: 0 additions & 51 deletions src/commonMain/kotlin/com/configcat/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,7 @@ import com.configcat.model.Config
import com.configcat.model.SettingType
import com.configcat.model.SettingValue
import korlibs.time.DateTime
import kotlinx.serialization.ContextualSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.modules.SerializersModule

internal interface Closeable {
fun close()
Expand All @@ -37,47 +26,7 @@ internal object Constants {
val json =
Json {
ignoreUnknownKeys = true
serializersModule =
SerializersModule {
contextual(Any::class, FlagValueSerializer)
}
}

internal object FlagValueSerializer : KSerializer<Any> {
override fun deserialize(decoder: Decoder): Any {
val json =
decoder as? JsonDecoder
?: error("Only JsonDecoder is supported.")
val element = json.decodeJsonElement()
val primitive = element as? JsonPrimitive ?: error("Unable to decode $element")
return when (primitive.content) {
"true", "false" -> primitive.content == "true"
else -> primitive.content.toIntOrNull() ?: primitive.content.toDoubleOrNull() ?: primitive.content
}
}

override fun serialize(
encoder: Encoder,
value: Any,
) {
val json =
encoder as? JsonEncoder
?: error("Only JsonEncoder is supported.")
val element: JsonElement =
when (value) {
is String -> JsonPrimitive(value)
is Number -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is JsonElement -> value
else -> throw IllegalArgumentException("Unable to encode $value")
}
json.encodeJsonElement(element)
}

@OptIn(ExperimentalSerializationApi::class)
override val descriptor: SerialDescriptor =
ContextualSerializer(Any::class, null, emptyArray()).descriptor
}
}

internal object Helpers {
Expand Down
Loading

0 comments on commit ab963b2

Please sign in to comment.