diff --git a/build.gradle.kts b/build.gradle.kts index a9bb9493..904753ab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 @@ -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 { @@ -143,7 +124,7 @@ kotlin { val nativeRestTest by creating { dependsOn(commonTest.get()) dependencies { - implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation(libs.ktor.cio) } } diff --git a/gradle.properties b/gradle.properties index 63722bce..d405897a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..ab4e9181 --- /dev/null +++ b/gradle/libs.versions.toml @@ -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" } \ No newline at end of file diff --git a/src/commonMain/kotlin/com/configcat/ConfigCatClient.kt b/src/commonMain/kotlin/com/configcat/ConfigCatClient.kt index 05ec6df3..b381dfe6 100644 --- a/src/commonMain/kotlin/com/configcat/ConfigCatClient.kt +++ b/src/commonMain/kotlin/com/configcat/ConfigCatClient.kt @@ -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 @@ -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 = atomic(null) private val isClosed = atomic(false) override val hooks: Hooks @@ -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) { @@ -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) { @@ -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 @@ -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 = @@ -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) { @@ -577,7 +584,7 @@ internal class Client private constructor( ) return } - defaultUser = user + defaultUser.value = user } override fun clearDefaultUser() { @@ -588,7 +595,7 @@ internal class Client private constructor( ) return } - defaultUser = null + defaultUser.value = null } override fun close() { diff --git a/src/commonMain/kotlin/com/configcat/ConfigCatUser.kt b/src/commonMain/kotlin/com/configcat/ConfigCatUser.kt index e4a590e3..3d43ffba 100644 --- a/src/commonMain/kotlin/com/configcat/ConfigCatUser.kt +++ b/src/commonMain/kotlin/com/configcat/ConfigCatUser.kt @@ -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. @@ -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()) + } } diff --git a/src/commonMain/kotlin/com/configcat/Constants.kt b/src/commonMain/kotlin/com/configcat/Constants.kt index d060d3e8..c815dcf2 100644 --- a/src/commonMain/kotlin/com/configcat/Constants.kt +++ b/src/commonMain/kotlin/com/configcat/Constants.kt @@ -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() @@ -37,47 +26,7 @@ internal object Constants { val json = Json { ignoreUnknownKeys = true - serializersModule = - SerializersModule { - contextual(Any::class, FlagValueSerializer) - } } - - internal object FlagValueSerializer : KSerializer { - 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 { diff --git a/src/commonTest/kotlin/com/configcat/CommonUtilsTests.kt b/src/commonTest/kotlin/com/configcat/CommonUtilsTests.kt index 57d085d6..10863820 100644 --- a/src/commonTest/kotlin/com/configcat/CommonUtilsTests.kt +++ b/src/commonTest/kotlin/com/configcat/CommonUtilsTests.kt @@ -93,60 +93,4 @@ class CommonUtilsTests { assertEquals(1, configResult.settings?.size) assertEquals("fake", configResult.settings?.get("fakeKey")?.settingValue?.stringValue) } - - @Test - fun testFlagValueSerializer() = - runTest { - var obj = SerializeTestClass("testWithString") - var encoded = Json.encodeToString(obj) - assertEquals("{\"testData\":\"testWithString\"}", encoded) - var decoded = Json.decodeFromString(encoded) - assertEquals(obj, decoded) - - obj = SerializeTestClass(1) - encoded = Json.encodeToString(obj) - assertEquals("{\"testData\":1}", encoded) - decoded = Json.decodeFromString(encoded) - assertEquals(obj, decoded) - - obj = SerializeTestClass(true) - encoded = Json.encodeToString(obj) - assertEquals("{\"testData\":true}", encoded) - decoded = Json.decodeFromString(encoded) - assertEquals(obj, decoded) - - obj = SerializeTestClass(JsonPrimitive("testJsonElement")) - encoded = Json.encodeToString(obj) - assertEquals("{\"testData\":\"testJsonElement\"}", encoded) - decoded = Json.decodeFromString(encoded) - assertEquals("testJsonElement", decoded.testData) - - // test fails - val failObject = ConfigCatUser("test") - val serializeException = - assertFailsWith( - exceptionClass = IllegalArgumentException::class, - block = { - obj = SerializeTestClass(failObject) - encoded = Json.encodeToString(obj) - }, - ) - assertEquals("Unable to encode $failObject", serializeException.message) - - val failDecodeString = "{\"testData\":{\"testData2\":\"testJsonElement\"}}" - val decodeException = - assertFailsWith( - exceptionClass = IllegalStateException::class, - block = { - decoded = Json.decodeFromString(failDecodeString) - }, - ) - assertEquals("Unable to decode {\"testData2\":\"testJsonElement\"}", decodeException.message) - } - - @Serializable - data class SerializeTestClass( - @Serializable(with = Constants.FlagValueSerializer::class) - val testData: Any, - ) } diff --git a/src/commonTest/kotlin/com/configcat/ConfigCatUserTests.kt b/src/commonTest/kotlin/com/configcat/ConfigCatUserTests.kt new file mode 100644 index 00000000..da02cdf1 --- /dev/null +++ b/src/commonTest/kotlin/com/configcat/ConfigCatUserTests.kt @@ -0,0 +1,35 @@ +package com.configcat + +import korlibs.time.DateTime +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.assertEquals +import kotlin.test.Test +import kotlin.test.assertNull + +class ConfigCatUserTests { + @Test + fun testGetAttribute() { + val user = ConfigCatUser(identifier = "test") + assertEquals("test", user.attributeFor("Identifier")) + assertNull(user.attributeFor("Email")) + } + + @Test + fun testToString() { + val user = ConfigCatUser(identifier = "test", custom = mapOf( + "a" to 1, + "b" to 1.2, + "c" to true, + "d" to TestEnum.A, + "e" to arrayOf("A", "B"), + "f" to listOf("C", "D"), + "g" to DateTime.EPOCH, + "h" to JsonPrimitive("json"), + )) + assertEquals("{\"Identifier\":\"test\",\"a\":1,\"b\":1.2,\"c\":true,\"d\":\"A\",\"e\":[\"A\",\"B\"],\"f\":[\"C\",\"D\"],\"g\":\"DateTime(0)\",\"h\":\"json\"}", user.toString()) + } + + enum class TestEnum { + A, B + } +} \ No newline at end of file