From 6ddf82c31e65fe5eed5b134027fdec47c14e9272 Mon Sep 17 00:00:00 2001 From: uniumuniu Date: Tue, 7 Nov 2023 13:41:43 +0100 Subject: [PATCH] Add custom serializers --- build.gradle.kts | 3 +- .../kotlin/com/featurevisor/sdk/Conditions.kt | 52 ++-- .../com/featurevisor/sdk/DatafileReader.kt | 10 +- .../kotlin/com/featurevisor/sdk/Emitter.kt | 2 - .../featurevisor/sdk/Instance+Activation.kt | 6 +- .../featurevisor/sdk/Instance+Evaluation.kt | 19 +- .../com/featurevisor/sdk/Instance+Feature.kt | 7 +- .../com/featurevisor/sdk/Instance+Fetch.kt | 29 ++- .../com/featurevisor/sdk/Instance+Refresh.kt | 1 - .../com/featurevisor/sdk/Instance+Segments.kt | 7 +- .../com/featurevisor/sdk/Instance+Variable.kt | 6 +- .../featurevisor/sdk/Instance+Variation.kt | 4 - .../kotlin/com/featurevisor/sdk/Instance.kt | 17 +- .../com/featurevisor/sdk/InstanceOptions.kt | 4 - .../com/featurevisor/{types => sdk}/Types.kt | 88 ++++--- .../sdk/serializers/Serializers.kt | 233 ++++++++++++++++++ .../com/featurevisor/sdk/ConditionsTest.kt | 41 ++- .../com/featurevisor/sdk/EmitterTest.kt | 10 +- .../sdk/factory/DatafileContentFactory.kt | 18 +- 19 files changed, 374 insertions(+), 183 deletions(-) rename src/main/kotlin/com/featurevisor/{types => sdk}/Types.kt (81%) create mode 100644 src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt diff --git a/build.gradle.kts b/build.gradle.kts index 4585924..4b53335 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,8 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. - id("org.jetbrains.kotlin.jvm") version "1.8.20" + kotlin("jvm") version "1.9.20" + kotlin("plugin.serialization") version "1.9.20" // Apply the java-library plugin for API and implementation separation. `java-library` diff --git a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt index 7b12337..605514e 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt @@ -1,33 +1,29 @@ package com.featurevisor.sdk -import com.featurevisor.types.AttributeValue -import com.featurevisor.types.Condition -import com.featurevisor.types.Condition.And -import com.featurevisor.types.Condition.Not -import com.featurevisor.types.Condition.Or -import com.featurevisor.types.Condition.Plain -import com.featurevisor.types.ConditionValue -import com.featurevisor.types.Context -import com.featurevisor.types.Operator.AFTER -import com.featurevisor.types.Operator.BEFORE -import com.featurevisor.types.Operator.CONTAINS -import com.featurevisor.types.Operator.ENDS_WITH -import com.featurevisor.types.Operator.EQUALS -import com.featurevisor.types.Operator.GREATER_THAN -import com.featurevisor.types.Operator.GREATER_THAN_OR_EQUAL -import com.featurevisor.types.Operator.IN_ARRAY -import com.featurevisor.types.Operator.LESS_THAN -import com.featurevisor.types.Operator.LESS_THAN_OR_EQUAL -import com.featurevisor.types.Operator.NOT_CONTAINS -import com.featurevisor.types.Operator.NOT_EQUALS -import com.featurevisor.types.Operator.NOT_IN_ARRAY -import com.featurevisor.types.Operator.SEMVER_EQUALS -import com.featurevisor.types.Operator.SEMVER_GREATER_THAN -import com.featurevisor.types.Operator.SEMVER_GREATER_THAN_OR_EQUAL -import com.featurevisor.types.Operator.SEMVER_LESS_THAN -import com.featurevisor.types.Operator.SEMVER_LESS_THAN_OR_EQUAL -import com.featurevisor.types.Operator.SEMVER_NOT_EQUALS -import com.featurevisor.types.Operator.STARTS_WITH +import com.featurevisor.sdk.Condition.And +import com.featurevisor.sdk.Condition.Not +import com.featurevisor.sdk.Condition.Or +import com.featurevisor.sdk.Condition.Plain +import com.featurevisor.sdk.Operator.AFTER +import com.featurevisor.sdk.Operator.BEFORE +import com.featurevisor.sdk.Operator.CONTAINS +import com.featurevisor.sdk.Operator.ENDS_WITH +import com.featurevisor.sdk.Operator.EQUALS +import com.featurevisor.sdk.Operator.GREATER_THAN +import com.featurevisor.sdk.Operator.GREATER_THAN_OR_EQUAL +import com.featurevisor.sdk.Operator.IN_ARRAY +import com.featurevisor.sdk.Operator.LESS_THAN +import com.featurevisor.sdk.Operator.LESS_THAN_OR_EQUAL +import com.featurevisor.sdk.Operator.NOT_CONTAINS +import com.featurevisor.sdk.Operator.NOT_EQUALS +import com.featurevisor.sdk.Operator.NOT_IN_ARRAY +import com.featurevisor.sdk.Operator.SEMVER_EQUALS +import com.featurevisor.sdk.Operator.SEMVER_GREATER_THAN +import com.featurevisor.sdk.Operator.SEMVER_GREATER_THAN_OR_EQUAL +import com.featurevisor.sdk.Operator.SEMVER_LESS_THAN +import com.featurevisor.sdk.Operator.SEMVER_LESS_THAN_OR_EQUAL +import com.featurevisor.sdk.Operator.SEMVER_NOT_EQUALS +import com.featurevisor.sdk.Operator.STARTS_WITH import net.swiftzer.semver.SemVer object Conditions { diff --git a/src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt b/src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt index eef4c77..587148c 100644 --- a/src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt +++ b/src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt @@ -1,13 +1,5 @@ package com.featurevisor.sdk -import com.featurevisor.types.Attribute -import com.featurevisor.types.AttributeKey -import com.featurevisor.types.DatafileContent -import com.featurevisor.types.Feature -import com.featurevisor.types.FeatureKey -import com.featurevisor.types.Segment -import com.featurevisor.types.SegmentKey - class DatafileReader constructor( datafileJson: DatafileContent, ) { @@ -34,7 +26,7 @@ class DatafileReader constructor( return attributes.find { attribute -> attribute.key == attributeKey } } - fun getSegment(segmentKey: SegmentKey): Segment? { + fun getSegment(segmentKey: String): Segment? { return segments.find { segment -> segment.key == segmentKey } } diff --git a/src/main/kotlin/com/featurevisor/sdk/Emitter.kt b/src/main/kotlin/com/featurevisor/sdk/Emitter.kt index b5cafdf..e37b2ca 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Emitter.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Emitter.kt @@ -1,7 +1,5 @@ package com.featurevisor.sdk -import com.featurevisor.types.EventName - class Emitter { private val listeners = mutableMapOf) -> Unit>() diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Activation.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Activation.kt index 3454b0b..154dd70 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Activation.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Activation.kt @@ -1,10 +1,6 @@ package com.featurevisor.sdk -import com.featurevisor.types.AttributeValue -import com.featurevisor.types.Context -import com.featurevisor.types.EventName.ACTIVATION -import com.featurevisor.types.FeatureKey -import com.featurevisor.types.VariationValue +import com.featurevisor.sdk.EventName.ACTIVATION fun FeaturevisorInstance.activate(featureKey: FeatureKey, context: Context = emptyMap()): VariationValue? { val evaluation = evaluateVariation(featureKey, context) diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt index cb87154..4f80a9d 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt @@ -2,23 +2,6 @@ package com.featurevisor.sdk import com.featurevisor.sdk.Conditions.allConditionsAreMatched import com.featurevisor.sdk.EvaluationReason.* -import com.featurevisor.types.AttributeKey -import com.featurevisor.types.AttributeValue -import com.featurevisor.types.BucketBy -import com.featurevisor.types.BucketKey -import com.featurevisor.types.BucketValue -import com.featurevisor.types.Context -import com.featurevisor.types.Feature -import com.featurevisor.types.FeatureKey -import com.featurevisor.types.OverrideFeature -import com.featurevisor.types.Required -import com.featurevisor.types.RuleKey -import com.featurevisor.types.Traffic -import com.featurevisor.types.VariableKey -import com.featurevisor.types.VariableSchema -import com.featurevisor.types.VariableValue -import com.featurevisor.types.Variation -import com.featurevisor.types.VariationValue import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromJsonElement @@ -612,7 +595,7 @@ private fun FeaturevisorInstance.getBucketKey(feature: Feature, context: Context is BucketBy.Or -> { type = "or" - attributeKeys = bucketBy.bucketBy.or + attributeKeys = bucketBy.bucketBy } } diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Feature.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Feature.kt index a3f04d3..870d315 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Feature.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Feature.kt @@ -1,13 +1,8 @@ package com.featurevisor.sdk import com.featurevisor.sdk.Conditions.allConditionsAreMatched -import com.featurevisor.types.Allocation -import com.featurevisor.types.Context -import com.featurevisor.types.Feature -import com.featurevisor.types.Force -import com.featurevisor.types.Traffic -internal fun FeaturevisorInstance.getFeatureByKey(featureKey: String): Feature? { +fun FeaturevisorInstance.getFeatureByKey(featureKey: String): Feature? { return datafileReader.getFeature(featureKey) } diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt index 6a45ee4..baec776 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt @@ -1,6 +1,5 @@ package com.featurevisor.sdk -import com.featurevisor.types.DatafileContent import java.io.IOException import okhttp3.* import kotlinx.serialization.json.Json @@ -39,21 +38,35 @@ private fun fetchDatafileContentFromUrl( } } -private inline fun fetch( +const val BODY_BYTE_COUNT = 1000000L +private inline fun fetch( request: Request, - crossinline completion: (Result) -> Unit, + crossinline completion: (Result) -> Unit, ) { val client = OkHttpClient() val call = client.newCall(request) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { - val responseBody = response.body - if (response.isSuccessful && responseBody != null) { - val json = Json { ignoreUnknownKeys = true } - val content = json.decodeFromString(responseBody.string()) + val responseBody = response.peekBody(BODY_BYTE_COUNT) + if (response.isSuccessful) { + val json = Json { + ignoreUnknownKeys = true + serializersModule = this.serializersModule.apply { + + } + } + val responseBodyString = responseBody.string() + val content = json.decodeFromString(responseBodyString) completion(Result.success(content)) } else { - completion(Result.failure(FeaturevisorError.UnparsableJson(responseBody?.string(), response.message))) + completion( + Result.failure( + FeaturevisorError.UnparsableJson( + responseBody.string(), + response.message + ) + ) + ) } } diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt index f3094e6..01f7088 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt @@ -1,7 +1,6 @@ package com.featurevisor.sdk import com.featurevisor.sdk.FeaturevisorError.* -import com.featurevisor.types.EventName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt index 06a4c10..2c4a8f1 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt @@ -1,12 +1,7 @@ package com.featurevisor.sdk import com.featurevisor.sdk.Conditions.allConditionsAreMatched -import com.featurevisor.types.Context -import com.featurevisor.types.FeatureKey -import com.featurevisor.types.GroupSegment -import com.featurevisor.types.GroupSegment.* -import com.featurevisor.types.Segment -import com.featurevisor.types.VariationValue +import com.featurevisor.sdk.GroupSegment.* internal fun FeaturevisorInstance.segmentIsMatched( featureKey: FeatureKey, diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt index 50d000a..de38be2 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt @@ -1,10 +1,6 @@ package com.featurevisor.sdk -import com.featurevisor.types.Context -import com.featurevisor.types.FeatureKey -import com.featurevisor.types.VariableKey -import com.featurevisor.types.VariableValue -import com.featurevisor.types.VariableValue.* +import com.featurevisor.sdk.VariableValue.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.encodeToJsonElement diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Variation.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Variation.kt index 5a9eb2a..f506319 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Variation.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Variation.kt @@ -1,9 +1,5 @@ package com.featurevisor.sdk -import com.featurevisor.types.Context -import com.featurevisor.types.FeatureKey -import com.featurevisor.types.VariationValue - internal fun FeaturevisorInstance.getVariation(featureKey: FeatureKey, context: Context): VariationValue? { val evaluation = evaluateVariation(featureKey, context) return when { diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance.kt b/src/main/kotlin/com/featurevisor/sdk/Instance.kt index f6a0579..c0ef784 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance.kt @@ -5,17 +5,10 @@ package com.featurevisor.sdk import com.featurevisor.sdk.FeaturevisorError.FetchingDataFileFailed import com.featurevisor.sdk.FeaturevisorError.MissingDatafileOptions -import com.featurevisor.types.BucketKey -import com.featurevisor.types.BucketValue -import com.featurevisor.types.Context -import com.featurevisor.types.DatafileContent -import com.featurevisor.types.EventName -import com.featurevisor.types.EventName.ACTIVATION -import com.featurevisor.types.EventName.READY -import com.featurevisor.types.EventName.REFRESH -import com.featurevisor.types.EventName.UPDATE -import com.featurevisor.types.Feature -import com.featurevisor.types.StickyFeatures +import com.featurevisor.sdk.EventName.ACTIVATION +import com.featurevisor.sdk.EventName.READY +import com.featurevisor.sdk.EventName.REFRESH +import com.featurevisor.sdk.EventName.UPDATE import kotlinx.coroutines.Job import kotlinx.serialization.json.Json @@ -96,7 +89,7 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) { if (result.isSuccess) { datafileReader = DatafileReader(result.getOrThrow()) statuses.ready = true - emitter.emit(READY) + emitter.emit(READY, result.getOrThrow()) if (refreshInterval != null) startRefreshing() } else { logger?.error("Failed to fetch datafile: $result") diff --git a/src/main/kotlin/com/featurevisor/sdk/InstanceOptions.kt b/src/main/kotlin/com/featurevisor/sdk/InstanceOptions.kt index 86e72bd..4261ced 100644 --- a/src/main/kotlin/com/featurevisor/sdk/InstanceOptions.kt +++ b/src/main/kotlin/com/featurevisor/sdk/InstanceOptions.kt @@ -1,9 +1,5 @@ package com.featurevisor.sdk -import com.featurevisor.types.DatafileContent -import com.featurevisor.types.InitialFeatures -import com.featurevisor.types.StickyFeatures - typealias Listener = (Array) -> Unit data class InstanceOptions( diff --git a/src/main/kotlin/com/featurevisor/types/Types.kt b/src/main/kotlin/com/featurevisor/sdk/Types.kt similarity index 81% rename from src/main/kotlin/com/featurevisor/types/Types.kt rename to src/main/kotlin/com/featurevisor/sdk/Types.kt index c2150d9..b5306a6 100644 --- a/src/main/kotlin/com/featurevisor/types/Types.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Types.kt @@ -1,5 +1,10 @@ -package com.featurevisor.types +package com.featurevisor.sdk +import com.featurevisor.sdk.serializers.BucketBySerializer +import com.featurevisor.sdk.serializers.ConditionSerializer +import com.featurevisor.sdk.serializers.ConditionValueSerializer +import com.featurevisor.sdk.serializers.GroupSegmentSerializer +import kotlinx.serialization.Serializable import java.time.LocalDate typealias Context = Map @@ -18,6 +23,7 @@ enum class VariableType(val value: String) { typealias VariableObjectValue = Map +@Serializable sealed class VariableValue { data class BooleanValue(val value: Boolean) : VariableValue() data class StringValue(val value: String) : VariableValue() @@ -28,6 +34,7 @@ sealed class VariableValue { data class JsonValue(val value: String) : VariableValue() } +@Serializable data class VariableOverride( val value: VariableValue, @@ -36,12 +43,14 @@ data class VariableOverride( val segments: GroupSegment?, ) +@Serializable data class Variable( val key: VariableKey, val value: VariableValue, val overrides: List?, ) +@Serializable data class Variation( // only available in YAML val description: String?, @@ -54,6 +63,7 @@ data class Variation( val variables: List?, ) +@Serializable data class VariableSchema( val key: VariableKey, val type: VariableType, @@ -64,6 +74,7 @@ typealias FeatureKey = String typealias VariableValues = Map +@Serializable data class Force( // one of the below must be present in YAML val conditions: Condition?, @@ -165,7 +176,7 @@ data class SegmentAssertion( ) data class TestSegment( - val key: SegmentKey, + val key: String, val assertions: List, ) @@ -176,11 +187,12 @@ sealed class Test { typealias AttributeKey = String +@Serializable data class Attribute( val key: AttributeKey, val type: String, - val archived: Boolean?, - val capture: Boolean?, + val archived: Boolean? = null, + val capture: Boolean? = null, ) sealed class AttributeValue { @@ -191,6 +203,7 @@ sealed class AttributeValue { data class DateValue(val value: LocalDate) : AttributeValue() } +@Serializable(with = ConditionSerializer::class) sealed class Condition { data class Plain( val attributeKey: AttributeKey, @@ -203,6 +216,9 @@ sealed class Condition { data class Not(val not: List) : Condition() } +const val TAG = "FeaturevisorService" + +@Serializable(with = ConditionValueSerializer::class) sealed class ConditionValue { data class StringValue(val value: String) : ConditionValue() data class IntValue(val value: Int) : ConditionValue() @@ -212,16 +228,13 @@ sealed class ConditionValue { data class DateTimeValue(val value: LocalDate) : ConditionValue() } -typealias SegmentKey = String - +@Serializable data class Segment( - val archived: Boolean?, - val key: SegmentKey, + val archived: Boolean? = null, + val key: String, val conditions: Condition, ) -typealias PlainGroupSegment = SegmentKey - data class AndGroupSegment( val and: List, ) @@ -234,10 +247,10 @@ data class NotGroupSegment( val not: List, ) +@Serializable(with = GroupSegmentSerializer::class) sealed class GroupSegment { - data class Plain(val segment: PlainGroupSegment) : GroupSegment() + data class Plain(val segment: String) : GroupSegment() data class Multiple(val segments: List) : GroupSegment() - data class And(val segment: AndGroupSegment) : GroupSegment() data class Or(val segment: OrGroupSegment) : GroupSegment() data class Not(val segment: NotGroupSegment) : GroupSegment() @@ -272,8 +285,8 @@ enum class Operator(val value: String) { AFTER("after"), // array of strings - IN_ARRAY("inArray"), - NOT_IN_ARRAY("notInArray"); + IN_ARRAY("in"), + NOT_IN_ARRAY("notIn"); } enum class EventName { @@ -283,47 +296,42 @@ enum class EventName { ACTIVATION, } - /** * Datafile-only types */ // 0 to 100,000 typealias Percentage = Int +@Serializable data class Range( val start: Percentage, val end: Percentage, ) +@Serializable data class Allocation( val variation: VariationValue, val range: Range, ) +@Serializable data class Traffic( val key: RuleKey, val segments: GroupSegment, val percentage: Percentage, - val enabled: Boolean?, - val variation: VariationValue?, - val variables: VariableValues?, + val enabled: Boolean? = null, + val variation: VariationValue? = null, + val variables: VariableValues? = null, val allocation: List, ) -typealias PlainBucketBy = String - -typealias AndBucketBy = List - -data class OrBucketBy( - val or: List, -) - +@Serializable(with = BucketBySerializer::class) sealed class BucketBy { - data class Single(val bucketBy: PlainBucketBy) : BucketBy() - data class And(val bucketBy: AndBucketBy) : BucketBy() - data class Or(val bucketBy: OrBucketBy) : BucketBy() + data class Single(val bucketBy: String) : BucketBy() + data class And(val bucketBy: List) : BucketBy() + data class Or(val bucketBy: List) : BucketBy() } data class RequiredWithVariation( @@ -331,25 +339,28 @@ data class RequiredWithVariation( val variation: VariationValue, ) +@Serializable sealed class Required { - data class FeatureKey(val required: com.featurevisor.types.FeatureKey) : Required() + data class FeatureKey(val required: com.featurevisor.sdk.FeatureKey) : Required() data class WithVariation(val required: RequiredWithVariation) : Required() } +@Serializable data class Feature( val key: FeatureKey, - val deprecated: Boolean?, - val variablesSchema: List?, - val variations: List?, + val deprecated: Boolean? = null, + val variablesSchema: List? = null, + val variations: List? = null, val bucketBy: BucketBy, - val required: List?, + val required: List? = null, val traffic: List, - val force: List?, + val force: List? = null, // if in a Group (mutex), these are available slot ranges - val ranges: List?, + val ranges: List? = null, ) +@Serializable data class DatafileContent( val schemaVersion: String, val revision: String, @@ -358,8 +369,9 @@ data class DatafileContent( val features: List, ) +@Serializable data class OverrideFeature( val enabled: Boolean, - val variation: VariationValue?, - val variables: VariableValues?, + val variation: VariationValue? = null, + val variables: VariableValues? = null, ) diff --git a/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt b/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt new file mode 100644 index 0000000..c0fa2e9 --- /dev/null +++ b/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt @@ -0,0 +1,233 @@ +package com.featurevisor.sdk.serializers + +import com.featurevisor.sdk.BucketBy +import com.featurevisor.sdk.Condition +import com.featurevisor.sdk.ConditionValue +import com.featurevisor.sdk.GroupSegment +import com.featurevisor.sdk.Operator +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.serializer + +@OptIn(InternalSerializationApi::class) +@Serializer(forClass = Condition::class) +object ConditionSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildSerialDescriptor("package.Condition", PolymorphicKind.SEALED) + + override fun deserialize(decoder: Decoder): Condition { + val input = decoder as? JsonDecoder + ?: throw SerializationException("This class can be decoded only by Json format") + return when (val tree = input.decodeJsonElement()) { + is JsonArray -> { + Condition.And(tree.map { jsonElement -> + input.json.decodeFromJsonElement( + Condition::class.serializer(), + jsonElement + ) + }) + } + + is JsonObject -> { + Condition.Plain( + attributeKey = tree["attribute"]?.jsonPrimitive?.content ?: "", + operator = mapOperator(tree["operator"]?.jsonPrimitive?.content ?: ""), + value = input.json.decodeFromJsonElement( + ConditionValue::class.serializer(), + tree["value"]!! + ), + ) + } + + is JsonPrimitive -> { + val jsonElement = input.json.parseToJsonElement(tree.content) + input.json.decodeFromJsonElement( + Condition::class.serializer(), + jsonElement + ) + } + } + } + + override fun serialize(encoder: Encoder, value: Condition) { + // TODO: Later if needed + } +} + +@OptIn(InternalSerializationApi::class) +@Serializer(forClass = GroupSegment::class) +object GroupSegmentSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildSerialDescriptor("package.GroupSegment", PolymorphicKind.SEALED) + + override fun deserialize(decoder: Decoder): GroupSegment { + val input = decoder as? JsonDecoder + ?: throw SerializationException("This class can be decoded only by Json format") + return when (val tree = input.decodeJsonElement()) { + is JsonArray -> GroupSegment.Multiple(tree.map { jsonElement -> + input.json.decodeFromJsonElement( + GroupSegment::class.serializer(), + jsonElement + ) + }) + + is JsonObject -> { + // TODO: + GroupSegment.Plain("") + } + + is JsonPrimitive -> { + val isString = tree.content.none {it in setOf('{', '}', ':', '[', ']')} + if (isString) { + GroupSegment.Plain(tree.content) + } else { + val jsonElement = Json.parseToJsonElement(tree.content) + input.json.decodeFromJsonElement( + GroupSegment::class.serializer(), + jsonElement, + ) + } + } + } + } + + override fun serialize(encoder: Encoder, value: GroupSegment) { + // TODO: Later if needed + } +} + +@OptIn(InternalSerializationApi::class) +@Serializer(forClass = BucketBy::class) +object BucketBySerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildSerialDescriptor("package.BucketBy", PolymorphicKind.SEALED) + + override fun deserialize(decoder: Decoder): BucketBy { + val input = decoder as? JsonDecoder + ?: throw SerializationException("This class can be decoded only by Json format") + return when (val tree = input.decodeJsonElement()) { + is JsonArray -> { + BucketBy.And(tree.map { jsonElement -> + jsonElement.jsonPrimitive.content + }) + } + + is JsonObject -> { + when { + tree.containsKey("or") -> BucketBy.Or(tree["or"]!!.jsonArray.map { it.jsonPrimitive.content }) + tree.containsKey("and") -> BucketBy.And(tree["and"]!!.jsonArray.map { it.jsonPrimitive.content }) + else -> throw Exception("Unexpected BucketBy element content") + } + } + + is JsonPrimitive -> { + val isString = tree.content.none {it in setOf('{', '}', ':', '[', ']')} + if (isString) { + BucketBy.Single(tree.content) + } else { + val jsonElement = Json.parseToJsonElement(tree.content) + input.json.decodeFromJsonElement( + BucketBy::class.serializer(), + jsonElement + ) + } + } + } + } + + override fun serialize(encoder: Encoder, value: BucketBy) { + // TODO: Later if needed + } +} + +@OptIn(InternalSerializationApi::class) +@Serializer(forClass = ConditionValue::class) +object ConditionValueSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildSerialDescriptor("package.ConditionValue", PolymorphicKind.SEALED) + + override fun deserialize(decoder: Decoder): ConditionValue { + val input = decoder as? JsonDecoder + ?: throw SerializationException("This class can be decoded only by Json format") + return when (val tree = input.decodeJsonElement()) { + is JsonPrimitive -> { + tree.intOrNull?.let { + ConditionValue.IntValue(it) + } ?: tree.booleanOrNull?.let { + ConditionValue.BooleanValue(it) + } ?: tree.doubleOrNull?.let { + ConditionValue.DoubleValue(it) + } ?: tree.content.let { + ConditionValue.StringValue(it) + // TODO: +// ConditionValue.DateTimeValue + } + } + + is JsonArray -> { + ConditionValue.ArrayValue(tree.jsonArray.map { jsonElement -> jsonElement.jsonPrimitive.content }) + } + + is JsonObject -> { + throw NotImplementedError("ConditionValue does not support JsonObject") + } + } + } + + override fun serialize(encoder: Encoder, value: ConditionValue) { + // TODO: Later if needed + } +} + +private fun mapOperator(value: String): Operator { + return when (value.trim()) { + "equals" -> Operator.EQUALS + "notEquals" -> Operator.NOT_EQUALS + + // numeric + "greaterThan" -> Operator.GREATER_THAN + "greaterThanOrEqual" -> Operator.GREATER_THAN_OR_EQUAL + "lessThan" -> Operator.LESS_THAN + "lessThanOrEqual" -> Operator.LESS_THAN_OR_EQUAL + + // string + "contains" -> Operator.CONTAINS + "notContains" -> Operator.NOT_CONTAINS + "startsWith" -> Operator.STARTS_WITH + "endsWith" -> Operator.ENDS_WITH + + // semver (string) + "semverEquals" -> Operator.SEMVER_EQUALS + "semverNotEquals" -> Operator.SEMVER_NOT_EQUALS + "semverGreaterThan" -> Operator.SEMVER_GREATER_THAN + "semverGreaterThanOrEqual" -> Operator.SEMVER_GREATER_THAN_OR_EQUAL + "semverLessThan" -> Operator.SEMVER_LESS_THAN + "semverLessThanOrEqual" -> Operator.SEMVER_LESS_THAN_OR_EQUAL + + // date comparisons + "before" -> Operator.BEFORE + "after" -> Operator.AFTER + + // array of strings + "in" -> Operator.IN_ARRAY + "not" -> Operator.NOT_IN_ARRAY + else -> throw Exception("Unexpected value of operator: $value") + } +} diff --git a/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt b/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt index af5bf0a..5cd3771 100644 --- a/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt +++ b/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt @@ -1,27 +1,24 @@ package com.featurevisor.sdk -import com.featurevisor.types.AttributeValue -import com.featurevisor.types.Condition -import com.featurevisor.types.ConditionValue -import com.featurevisor.types.Operator.AFTER -import com.featurevisor.types.Operator.BEFORE -import com.featurevisor.types.Operator.CONTAINS -import com.featurevisor.types.Operator.ENDS_WITH -import com.featurevisor.types.Operator.EQUALS -import com.featurevisor.types.Operator.GREATER_THAN -import com.featurevisor.types.Operator.GREATER_THAN_OR_EQUAL -import com.featurevisor.types.Operator.IN_ARRAY -import com.featurevisor.types.Operator.LESS_THAN -import com.featurevisor.types.Operator.LESS_THAN_OR_EQUAL -import com.featurevisor.types.Operator.NOT_EQUALS -import com.featurevisor.types.Operator.NOT_IN_ARRAY -import com.featurevisor.types.Operator.SEMVER_EQUALS -import com.featurevisor.types.Operator.SEMVER_GREATER_THAN -import com.featurevisor.types.Operator.SEMVER_GREATER_THAN_OR_EQUAL -import com.featurevisor.types.Operator.SEMVER_LESS_THAN -import com.featurevisor.types.Operator.SEMVER_LESS_THAN_OR_EQUAL -import com.featurevisor.types.Operator.SEMVER_NOT_EQUALS -import com.featurevisor.types.Operator.STARTS_WITH +import com.featurevisor.sdk.Operator.AFTER +import com.featurevisor.sdk.Operator.BEFORE +import com.featurevisor.sdk.Operator.CONTAINS +import com.featurevisor.sdk.Operator.ENDS_WITH +import com.featurevisor.sdk.Operator.EQUALS +import com.featurevisor.sdk.Operator.GREATER_THAN +import com.featurevisor.sdk.Operator.GREATER_THAN_OR_EQUAL +import com.featurevisor.sdk.Operator.IN_ARRAY +import com.featurevisor.sdk.Operator.LESS_THAN +import com.featurevisor.sdk.Operator.LESS_THAN_OR_EQUAL +import com.featurevisor.sdk.Operator.NOT_EQUALS +import com.featurevisor.sdk.Operator.NOT_IN_ARRAY +import com.featurevisor.sdk.Operator.SEMVER_EQUALS +import com.featurevisor.sdk.Operator.SEMVER_GREATER_THAN +import com.featurevisor.sdk.Operator.SEMVER_GREATER_THAN_OR_EQUAL +import com.featurevisor.sdk.Operator.SEMVER_LESS_THAN +import com.featurevisor.sdk.Operator.SEMVER_LESS_THAN_OR_EQUAL +import com.featurevisor.sdk.Operator.SEMVER_NOT_EQUALS +import com.featurevisor.sdk.Operator.STARTS_WITH import io.kotest.matchers.shouldBe import java.time.LocalDate import kotlin.test.Test diff --git a/src/test/kotlin/com/featurevisor/sdk/EmitterTest.kt b/src/test/kotlin/com/featurevisor/sdk/EmitterTest.kt index 18dd185..517e1e5 100644 --- a/src/test/kotlin/com/featurevisor/sdk/EmitterTest.kt +++ b/src/test/kotlin/com/featurevisor/sdk/EmitterTest.kt @@ -1,10 +1,10 @@ package com.featurevisor.sdk -import com.featurevisor.types.EventName.ACTIVATION -import com.featurevisor.types.EventName.READY -import com.featurevisor.types.EventName.REFRESH -import com.featurevisor.types.EventName.UPDATE -import com.featurevisor.types.EventName.values +import com.featurevisor.sdk.EventName.ACTIVATION +import com.featurevisor.sdk.EventName.READY +import com.featurevisor.sdk.EventName.REFRESH +import com.featurevisor.sdk.EventName.UPDATE +import com.featurevisor.sdk.EventName.values import io.mockk.every import io.mockk.mockk import io.mockk.verify diff --git a/src/test/kotlin/com/featurevisor/sdk/factory/DatafileContentFactory.kt b/src/test/kotlin/com/featurevisor/sdk/factory/DatafileContentFactory.kt index 7dc13a7..66d982a 100644 --- a/src/test/kotlin/com/featurevisor/sdk/factory/DatafileContentFactory.kt +++ b/src/test/kotlin/com/featurevisor/sdk/factory/DatafileContentFactory.kt @@ -1,14 +1,14 @@ package com.featurevisor.sdk.factory -import com.featurevisor.types.Attribute -import com.featurevisor.types.BucketBy -import com.featurevisor.types.Condition -import com.featurevisor.types.ConditionValue -import com.featurevisor.types.DatafileContent -import com.featurevisor.types.Feature -import com.featurevisor.types.Operator.EQUALS -import com.featurevisor.types.Operator.NOT_EQUALS -import com.featurevisor.types.Segment +import com.featurevisor.sdk.Attribute +import com.featurevisor.sdk.BucketBy +import com.featurevisor.sdk.Condition +import com.featurevisor.sdk.ConditionValue +import com.featurevisor.sdk.DatafileContent +import com.featurevisor.sdk.Feature +import com.featurevisor.sdk.Operator.EQUALS +import com.featurevisor.sdk.Operator.NOT_EQUALS +import com.featurevisor.sdk.Segment object DatafileContentFactory {