diff --git a/build.gradle.kts b/build.gradle.kts index 8b77144..a6cfb90 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -66,7 +66,6 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.yaml:snakeyaml:2.2") - implementation("com.google.code.gson:gson:2.10.1") } // Apply a specific Java toolchain to ease working on different environments. diff --git a/gradle.properties b/gradle.properties index 185c1b3..0dfcc03 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,4 @@ group=com.featurevisor version=0.0.1-SNAPSHOT +org.gradle.daemon=true +org.gradle.parallel=true diff --git a/src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt b/src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt index b39d6bd..7a51df0 100644 --- a/src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt +++ b/src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt @@ -8,15 +8,15 @@ import com.featurevisor.types.FeatureKey import com.featurevisor.types.Segment import com.featurevisor.types.SegmentKey -class DatafileReader constructor( +class DatafileReader ( datafileContent: DatafileContent, ) { private val schemaVersion: String = datafileContent.schemaVersion private val revision: String = datafileContent.revision - private val attributes: Map = datafileContent.attributes.associateBy { it.key } - private val segments: Map = datafileContent.segments.associateBy { it.key } - private val features: Map = datafileContent.features.associateBy { it.key } + private val attributes: Map = datafileContent.getAttributes().associateBy { it.key } + private val segments: Map = datafileContent.getSegment().associateBy { it.key } + private val features: Map = datafileContent.getFeature().associateBy { it.key } fun getRevision(): String { return revision diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt index 9e3cef9..7aa339b 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt @@ -73,7 +73,6 @@ fun FeaturevisorInstance.isEnabled(featureKey: FeatureKey, context: Context = em return evaluation.enabled == true } -@Suppress("UNREACHABLE_CODE") fun FeaturevisorInstance.evaluateVariation(featureKey: FeatureKey, context: Context = emptyMap()): Evaluation { var evaluation: Evaluation try { @@ -125,7 +124,7 @@ fun FeaturevisorInstance.evaluateVariation(featureKey: FeatureKey, context: Cont return evaluation } - if (feature.variations.isNullOrEmpty()) { + if (feature.getVariations().isEmpty()) { // no variations evaluation = Evaluation( featureKey = featureKey, @@ -141,7 +140,7 @@ fun FeaturevisorInstance.evaluateVariation(featureKey: FeatureKey, context: Cont // forced val force = findForceFromFeature(feature, context, datafileReader) if (force != null) { - val variation = feature.variations.firstOrNull { it.value == force.variation } + val variation = feature.getVariations().firstOrNull { it.value == force.variation } if (variation != null) { evaluation = Evaluation( @@ -160,18 +159,17 @@ fun FeaturevisorInstance.evaluateVariation(featureKey: FeatureKey, context: Cont val bucketValue = getBucketValue(feature, finalContext) val matchedTrafficAndAllocation = getMatchedTrafficAndAllocation( - feature.traffic, + feature.getTraffic(), finalContext, bucketValue, datafileReader, - logger ) val matchedTraffic = matchedTrafficAndAllocation.matchedTraffic // override from rule if (matchedTraffic?.variation != null) { - val variation = feature.variations.firstOrNull { it.value == matchedTraffic.variation } + val variation = feature.getVariations().firstOrNull { it.value == matchedTraffic.variation } if (variation != null) { evaluation = Evaluation( featureKey = feature.key, @@ -191,7 +189,7 @@ fun FeaturevisorInstance.evaluateVariation(featureKey: FeatureKey, context: Cont // regular allocation if (matchedAllocation != null) { - val variation = feature.variations?.firstOrNull { it.value == matchedAllocation.variation } + val variation = feature.getVariations().firstOrNull { it.value == matchedAllocation.variation } if (variation != null) { evaluation = Evaluation( featureKey = feature.key, @@ -229,7 +227,6 @@ fun FeaturevisorInstance.evaluateVariation(featureKey: FeatureKey, context: Cont } -@Suppress("UNREACHABLE_CODE") fun FeaturevisorInstance.evaluateFlag(featureKey: FeatureKey, context: Context = emptyMap()): Evaluation { var evaluation: Evaluation @@ -301,8 +298,8 @@ fun FeaturevisorInstance.evaluateFlag(featureKey: FeatureKey, context: Context = } // required - if (feature.required.isNullOrEmpty().not()) { - val requiredFeaturesAreEnabled = feature.required?.all { item -> + if (feature.getRequired().isNullOrEmpty().not()) { + val requiredFeaturesAreEnabled = feature.getRequired()?.all { item -> var requiredKey: FeatureKey? = null var requiredVariation: VariationValue? = null when (item) { @@ -347,7 +344,7 @@ fun FeaturevisorInstance.evaluateFlag(featureKey: FeatureKey, context: Context = val bucketValue = getBucketValue(feature = feature, context = finalContext) val matchedTraffic = getMatchedTraffic( - traffic = feature.traffic, + traffic = feature.getTraffic(), context = finalContext, datafileReader = datafileReader, ) @@ -440,7 +437,6 @@ fun FeaturevisorInstance.evaluateFlag(featureKey: FeatureKey, context: Context = } } -@Suppress("UNREACHABLE_CODE") fun FeaturevisorInstance.evaluateVariable( featureKey: FeatureKey, variableKey: VariableKey, @@ -497,7 +493,7 @@ fun FeaturevisorInstance.evaluateVariable( return evaluation } - val variableSchema = feature.variablesSchema?.firstOrNull { variableSchema -> + val variableSchema = feature.getVariablesSchema().firstOrNull { variableSchema -> variableSchema.key == variableKey } @@ -537,11 +533,10 @@ fun FeaturevisorInstance.evaluateVariable( val bucketValue = getBucketValue(feature, finalContext) val matchedTrafficAndAllocation = getMatchedTrafficAndAllocation( - traffic = feature.traffic, + traffic = feature.getTraffic(), context = finalContext, bucketValue = bucketValue, datafileReader = datafileReader, - logger = logger ) matchedTrafficAndAllocation.matchedTraffic?.let { matchedTraffic -> @@ -571,7 +566,7 @@ fun FeaturevisorInstance.evaluateVariable( matchedAllocation.variation } - val variation = feature.variations?.firstOrNull { variation -> + val variation = feature.getVariations().firstOrNull { variation -> variation.value == variationValue } @@ -654,10 +649,10 @@ fun FeaturevisorInstance.evaluateVariable( private fun FeaturevisorInstance.getBucketKey(feature: Feature, context: Context): BucketKey { val featureKey = feature.key - var type: String - var attributeKeys: List + val type: String + val attributeKeys: List - when (val bucketBy = feature.bucketBy) { + when (val bucketBy = feature.getBucketBy()) { is BucketBy.Single -> { type = "plain" attributeKeys = listOf(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 46cd705..46d27db 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Feature.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Feature.kt @@ -21,7 +21,7 @@ internal fun FeaturevisorInstance.findForceFromFeature( datafileReader: DatafileReader, ): Force? { - return feature.force?.firstOrNull { force -> + return feature.getForce().firstOrNull { force -> when { force.conditions != null -> allConditionsAreMatched(force.conditions, context) force.segments != null -> allGroupSegmentsAreMatched( @@ -46,7 +46,7 @@ internal fun FeaturevisorInstance.getMatchedTraffic( } } -internal fun FeaturevisorInstance.getMatchedAllocation( +internal fun getMatchedAllocation( traffic: Traffic, bucketValue: Int, ): Allocation? { @@ -68,7 +68,6 @@ internal fun FeaturevisorInstance.getMatchedTrafficAndAllocation( context: Context, bucketValue: Int, datafileReader: DatafileReader, - logger: Logger?, ): MatchedTrafficAndAllocation { var matchedAllocation: Allocation? = null diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt index 1c237d6..545a00d 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt @@ -2,25 +2,23 @@ package com.featurevisor.sdk import com.featurevisor.types.DatafileContent import kotlinx.serialization.decodeFromString -import java.io.IOException import okhttp3.* -import kotlinx.serialization.json.Json import okhttp3.HttpUrl.Companion.toHttpUrl -import java.lang.IllegalArgumentException +import java.io.IOException const val BODY_BYTE_COUNT = 1000000L val client = OkHttpClient() // MARK: - Fetch datafile content @Throws(IOException::class) -suspend fun FeaturevisorInstance.fetchDatafileContent( +fun FeaturevisorInstance.fetchDatafileContent( url: String, handleDatafileFetch: DatafileFetchHandler? = null, - completion: (Result) -> Unit, + completion: (Result>) -> Unit, ) { handleDatafileFetch?.let { handleFetch -> - val result = handleFetch(url) - completion(result) + val result = handleFetch(url).getOrNull()!! + completion(Result.success(Pair(result, ""))) } ?: run { fetchDatafileContentFromUrl(url, completion) } @@ -28,7 +26,7 @@ suspend fun FeaturevisorInstance.fetchDatafileContent( private fun fetchDatafileContentFromUrl( url: String, - completion: (Result) -> Unit, + completion: (Result>) -> Unit, ) { try { val httpUrl = url.toHttpUrl() @@ -45,21 +43,18 @@ private fun fetchDatafileContentFromUrl( private inline fun fetch( request: Request, - crossinline completion: (Result) -> Unit, + crossinline completion: (Result>) -> Unit, ) { val call = client.newCall(request) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val responseBody = response.peekBody(BODY_BYTE_COUNT) if (response.isSuccessful) { - val json = Json { - ignoreUnknownKeys = true - } val responseBodyString = responseBody.string() FeaturevisorInstance.companionLogger?.debug(responseBodyString) try { - val content = json.decodeFromString(responseBodyString) - completion(Result.success(content)) + val content = JsonConfigFeatureVisor.json.decodeFromString(responseBodyString) + completion(Result.success(Pair(content, responseBodyString))) } catch (throwable: Throwable) { completion( Result.failure( diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt index a8e09f1..936aedc 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt @@ -1,9 +1,7 @@ package com.featurevisor.sdk -import com.featurevisor.sdk.FeaturevisorError.* +import com.featurevisor.sdk.FeaturevisorError.MissingDatafileUrlWhileRefreshing import com.featurevisor.types.EventName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -45,10 +43,10 @@ private suspend fun FeaturevisorInstance.refresh() { ) { result -> result.onSuccess { datafileContent -> val currentRevision = getRevision() - val newRevision = datafileContent.revision + val newRevision = datafileContent.first.revision val isNotSameRevision = currentRevision != newRevision - datafileReader = DatafileReader(datafileContent) + datafileReader = DatafileReader(datafileContent.first) logger?.info("refreshed datafile") emitter.emit(EventName.REFRESH) diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt index b71d893..097216a 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt @@ -2,31 +2,12 @@ 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 - -internal fun FeaturevisorInstance.segmentIsMatched( - featureKey: FeatureKey, - context: Context, -): VariationValue? { - val evaluation = evaluateVariation(featureKey, context) - - if (evaluation.variationValue != null) { - return evaluation.variationValue - } - - if (evaluation.variation != null) { - return evaluation.variation.value - } - - return null -} internal fun segmentIsMatched(segment: Segment, context: Context): Boolean { - return allConditionsAreMatched(segment.conditions, context) + return allConditionsAreMatched(segment.getCondition(), context) } internal fun FeaturevisorInstance.allGroupSegmentsAreMatched( diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt index 6d2e9b9..7dfa3ef 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt @@ -85,7 +85,7 @@ inline fun FeaturevisorInstance.getVariableObject( } } -inline fun FeaturevisorInstance.getVariableJSON( +inline fun FeaturevisorInstance.getVariableJSON( featureKey: FeatureKey, variableKey: VariableKey, context: Context, @@ -97,4 +97,3 @@ inline fun FeaturevisorInstance.getVariableJSON( null } } - diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance.kt b/src/main/kotlin/com/featurevisor/sdk/Instance.kt index 4b357cf..d60d167 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance.kt @@ -8,7 +8,6 @@ import com.featurevisor.types.* import com.featurevisor.types.EventName.* import kotlinx.coroutines.* import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json import kotlin.coroutines.resume typealias ConfigureBucketKey = (Feature, Context, BucketKey) -> BucketKey @@ -111,9 +110,9 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) { handleDatafileFetch = handleDatafileFetch, ) { result -> result.onSuccess { datafileContent -> - datafileReader = DatafileReader(datafileContent) + datafileReader = DatafileReader(datafileContent.first) statuses.ready = true - emitter.emit(READY, datafileContent) + emitter.emit(READY, datafileContent.first, datafileContent.second) if (refreshInterval != null) startRefreshing() }.onFailure { error -> logger?.error("Failed to fetch datafile: $error") @@ -145,19 +144,19 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) { continuation.resume(this) } - val cb :(result:Array) -> Unit = { + val cb: (result: Array) -> Unit = { this.emitter.removeListener(READY) continuation.resume(this) } - this.emitter.addListener(READY,cb) + this.emitter.addListener(READY, cb) } } fun setDatafile(datafileJSON: String) { val data = datafileJSON.toByteArray(Charsets.UTF_8) try { - val datafileContent = Json.decodeFromString(String(data)) + val datafileContent = JsonConfigFeatureVisor.json.decodeFromString(String(data)) datafileReader = DatafileReader(datafileContent = datafileContent) } catch (e: Exception) { logger?.error("could not parse datafile", mapOf("error" to e)) diff --git a/src/main/kotlin/com/featurevisor/sdk/Utils.kt b/src/main/kotlin/com/featurevisor/sdk/Utils.kt new file mode 100644 index 0000000..0a95de6 --- /dev/null +++ b/src/main/kotlin/com/featurevisor/sdk/Utils.kt @@ -0,0 +1,93 @@ +package com.featurevisor.sdk + +import com.featurevisor.sdk.serializers.* +import com.featurevisor.types.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.modules.SerializersModule + +object JsonConfigFeatureVisor { + val json: Json by lazy { + Json { + serializersModule = SerializersModule { + contextual(BucketBy::class, BucketBySerializer) + contextual(Condition::class, ConditionSerializer) + contextual(ConditionValue::class, ConditionValueSerializer) + contextual(GroupSegment::class, GroupSegmentSerializer) + contextual(Required::class, RequiredSerializer) + contextual(VariableValue::class, VariableValueSerializer) + } + ignoreUnknownKeys = true + isLenient = true + allowStructuredMapKeys = true + } + } +} + +fun Segment.getCondition(): Condition { + return synchronized(this) { + if (conditions == null) { + conditions = JsonConfigFeatureVisor.json.decodeFromString(ConditionSerializer, conditionStrings) + } + conditions!! + } +} + +fun Feature.getVariablesSchema(): List { + return synchronized(this) { + if (variablesSchema == null) { + variablesSchema = variablesSchemaString?.let { + JsonConfigFeatureVisor.json.decodeFromJsonElement>(it) + } + } + variablesSchema.orEmpty() + } +} + +fun Feature.getVariations(): List { + return synchronized(this) { + if (variations == null) { + variations = variationStrings?.let { + JsonConfigFeatureVisor.json.decodeFromJsonElement>(it) + } + } + variations.orEmpty() + } +} + +fun Feature.getBucketBy(): BucketBy { + return synchronized(this) { + if (bucketBy == null) { + bucketBy = JsonConfigFeatureVisor.json.decodeFromJsonElement(BucketBy.serializer(), bucketByString) + } + bucketBy!! + } +} + +fun Feature.getTraffic(): List { + return synchronized(this) { + if (traffic == null) { + traffic = JsonConfigFeatureVisor.json.decodeFromJsonElement>(trafficString) + } + traffic.orEmpty() + } +} + +fun Feature.getForce(): List { + return synchronized(this) { + if (force == null) { + force = forceString?.let { + JsonConfigFeatureVisor.json.decodeFromJsonElement>(it) + } + } + force.orEmpty() + } +} + +fun Feature.getRequired() = required + +fun DatafileContent.getAttributes() = attributes + +fun DatafileContent.getFeature() = features + +fun DatafileContent.getSegment() = segments diff --git a/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt b/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt index 43456dc..c95803b 100644 --- a/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt +++ b/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt @@ -9,326 +9,315 @@ 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.JsonElement -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.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.* import java.text.SimpleDateFormat @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) @Serializer(forClass = Required::class) -object RequiredSerializer: KSerializer{ +object RequiredSerializer : KSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("package.Required", PolymorphicKind.SEALED) override fun deserialize(decoder: Decoder): Required { val input = decoder as? JsonDecoder - ?: throw SerializationException("This class can be decoded only by Json format") - return when (val tree = input.decodeJsonElement()) { - is JsonPrimitive ->{ - Required.FeatureKey(tree.content) - } - is JsonArray -> { - // Never lies in JsonArray block - Required.FeatureKey(tree.toString()) - } - is JsonObject ->{ - val requiredWithVariation = RequiredWithVariation(tree["key"]?.jsonPrimitive?.content.orEmpty(),tree["variation"]?.jsonPrimitive?.content.orEmpty()) - Required.WithVariation(requiredWithVariation) + ?: throw SerializationException("This class can only be decoded using the Json format") + + return when (val element = input.decodeJsonElement()) { + is JsonPrimitive -> Required.FeatureKey(element.content) + is JsonObject -> { + val key = element["key"]?.jsonPrimitive?.content.orEmpty() + val variation = element["variation"]?.jsonPrimitive?.content.orEmpty() + Required.WithVariation(RequiredWithVariation(key, variation)) } + + else -> throw SerializationException("Unexpected JSON element: ${element::class.simpleName}") } } } -@OptIn(InternalSerializationApi::class) -@Serializer(forClass = Condition::class) +@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::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()) { + ?: throw SerializationException("This class can only be decoded using the Json format") + + return when (val element = input.decodeJsonElement()) { is JsonArray -> { - Condition.And(tree.map { jsonElement -> - input.json.decodeFromJsonElement( - Condition::class.serializer(), - jsonElement - ) - }) + val conditions = element.map { + input.json.decodeFromJsonElement(Condition.serializer(), it) + } + Condition.And(conditions) } is JsonObject -> { - FeaturevisorInstance.companionLogger?.debug("Segment deserializing: ${tree["attribute"]?.jsonPrimitive?.content}, tree: $tree") + FeaturevisorInstance.companionLogger?.debug( + "Segment deserializing: ${element["attribute"]?.jsonPrimitive?.content}, tree: $element" + ) + when { - tree.containsKey("and") -> Condition.And( - tree["and"]!!.jsonArray.map { - input.json.decodeFromJsonElement( - Condition::class.serializer(), - it - ) + "and" in element -> Condition.And( + element["and"]!!.jsonArray.map { + input.json.decodeFromJsonElement(Condition.serializer(), it) } ) - tree.containsKey("or") -> Condition.Or( - tree["or"]!!.jsonArray.map { - input.json.decodeFromJsonElement( - Condition::class.serializer(), - it - ) + "or" in element -> Condition.Or( + element["or"]!!.jsonArray.map { + input.json.decodeFromJsonElement(Condition.serializer(), it) } ) - tree.containsKey("not") -> Condition.Not( - tree["not"]!!.jsonArray.map { - input.json.decodeFromJsonElement( - Condition::class.serializer(), - it - ) + "not" in element -> Condition.Not( + element["not"]!!.jsonArray.map { + input.json.decodeFromJsonElement(Condition.serializer(), it) } ) else -> Condition.Plain( - attributeKey = tree["attribute"]?.jsonPrimitive?.content ?: "", - operator = mapOperator(tree["operator"]?.jsonPrimitive?.content ?: ""), + attributeKey = element["attribute"]?.jsonPrimitive?.content.orEmpty(), + operator = mapOperator(element["operator"]?.jsonPrimitive?.content.orEmpty()), value = input.json.decodeFromJsonElement( - ConditionValue::class.serializer(), - tree["value"]!! - ), + ConditionValue.serializer(), + element["value"]!! + ) ) } } is JsonPrimitive -> { - val jsonElement = input.json.parseToJsonElement(tree.content) - input.json.decodeFromJsonElement( - Condition::class.serializer(), - jsonElement - ) + val parsedElement = input.json.parseToJsonElement(element.content) + input.json.decodeFromJsonElement(Condition.serializer(), parsedElement) } + + else -> throw SerializationException("Unexpected JSON element: ${element::class.simpleName}") } } override fun serialize(encoder: Encoder, value: Condition) { - // TODO: Later if needed + // TODO: Implement if serialization is required in the future } } @OptIn(InternalSerializationApi::class) -@Serializer(forClass = GroupSegment::class) object GroupSegmentSerializer : KSerializer { + + @OptIn(ExperimentalSerializationApi::class) 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 - ) - }) + ?: throw SerializationException("This class can only be decoded by Json format") - is JsonObject -> { - when { - tree.containsKey("and") -> GroupSegment.And( - AndGroupSegment( - tree["and"]!!.jsonArray.map { - input.json.decodeFromJsonElement( - GroupSegment::class.serializer(), - it - ) - } - ) - ) + return when (val jsonElement = input.decodeJsonElement()) { + is JsonArray -> parseJsonArray(input, jsonElement) + is JsonObject -> parseJsonObject(input, jsonElement) + is JsonPrimitive -> parseJsonPrimitive(input, jsonElement) + else -> throw SerializationException("Unexpected GroupSegment element type") + } + } - tree.containsKey("or") -> GroupSegment.Or( - OrGroupSegment( - tree["or"]!!.jsonArray.map { - input.json.decodeFromJsonElement( - GroupSegment::class.serializer(), - it - ) - } - ) - ) + private fun parseJsonArray(input: JsonDecoder, jsonArray: JsonArray): GroupSegment.Multiple { + val elements = jsonArray.map { + input.json.decodeFromJsonElement(GroupSegment::class.serializer(), it) + } + return GroupSegment.Multiple(elements) + } - tree.containsKey("not") -> GroupSegment.Not( - NotGroupSegment( - tree["not"]!!.jsonArray.map { - input.json.decodeFromJsonElement( - GroupSegment::class.serializer(), - it - ) - } - ) - ) + private fun parseJsonObject(input: JsonDecoder, jsonObject: JsonObject): GroupSegment { + val keys = jsonObject.keys + return when { + "and" in keys -> GroupSegment.And( + AndGroupSegment(parseNestedArray(input, jsonObject["and"]!!.jsonArray)) + ) - else -> throw Exception("Unexpected GroupSegment element content") - } - } + "or" in keys -> GroupSegment.Or( + OrGroupSegment(parseNestedArray(input, jsonObject["or"]!!.jsonArray)) + ) - 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, - ) - } - } + "not" in keys -> GroupSegment.Not( + NotGroupSegment(parseNestedArray(input, jsonObject["not"]!!.jsonArray)) + ) + + else -> throw SerializationException("Unexpected GroupSegment object keys: $keys") + } + } + + private fun parseJsonPrimitive(input: JsonDecoder, jsonPrimitive: JsonPrimitive): GroupSegment { + val content = jsonPrimitive.content + return if (content.none { it in setOf('{', '}', ':', '[', ']') }) { + GroupSegment.Plain(content) + } else { + val parsedElement = Json.parseToJsonElement(content) + input.json.decodeFromJsonElement(GroupSegment::class.serializer(), parsedElement) + } + } + + private fun parseNestedArray(input: JsonDecoder, jsonArray: JsonArray): List { + return jsonArray.map { + input.json.decodeFromJsonElement(GroupSegment::class.serializer(), it) } } override fun serialize(encoder: Encoder, value: GroupSegment) { - // TODO: Later if needed + // TODO: Implement serialization logic if needed } } @OptIn(InternalSerializationApi::class) -@Serializer(forClass = BucketBy::class) object BucketBySerializer : KSerializer { + + @OptIn(ExperimentalSerializationApi::class) 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 - }) - } + ?: throw SerializationException("This class can only be decoded by Json format") - 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") - } - } + return when (val jsonElement = input.decodeJsonElement()) { + is JsonArray -> parseJsonArray(jsonElement) + is JsonObject -> parseJsonObject(jsonElement) + is JsonPrimitive -> parseJsonPrimitive(input, jsonElement) + else -> throw SerializationException("Unexpected BucketBy element type") + } + } - 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 - ) - } - } + private fun parseJsonArray(jsonArray: JsonArray): BucketBy.And { + val elements = jsonArray.map { it.jsonPrimitive.content } + return BucketBy.And(elements) + } + + private fun parseJsonObject(jsonObject: JsonObject): BucketBy { + return when { + "or" in jsonObject -> BucketBy.Or( + parseJsonArrayContent(jsonObject["or"]!!.jsonArray) + ) + + "and" in jsonObject -> BucketBy.And( + parseJsonArrayContent(jsonObject["and"]!!.jsonArray) + ) + + else -> throw SerializationException("Unexpected BucketBy object keys: ${jsonObject.keys}") + } + } + + private fun parseJsonPrimitive(input: JsonDecoder, jsonPrimitive: JsonPrimitive): BucketBy { + val content = jsonPrimitive.content + return if (content.none { it in setOf('{', '}', ':', '[', ']') }) { + BucketBy.Single(content) + } else { + val parsedElement = Json.parseToJsonElement(content) + input.json.decodeFromJsonElement(BucketBy::class.serializer(), parsedElement) } } + private fun parseJsonArrayContent(jsonArray: JsonArray): List { + return jsonArray.map { it.jsonPrimitive.content } + } + override fun serialize(encoder: Encoder, value: BucketBy) { - // TODO: Later if needed + // TODO: Implement serialization logic if needed } } @OptIn(InternalSerializationApi::class) -@Serializer(forClass = ConditionValue::class) object ConditionValueSerializer : KSerializer { + + @OptIn(ExperimentalSerializationApi::class) 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 { - try { - val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - val date = dateFormat.parse(it) - ConditionValue.DateTimeValue(date) - }catch (e:Exception){ - ConditionValue.StringValue(it) - } - } - } + ?: throw SerializationException("This class can only be decoded by Json format") - is JsonArray -> { - ConditionValue.ArrayValue(tree.jsonArray.map { jsonElement -> jsonElement.jsonPrimitive.content }) - } + return when (val jsonElement = input.decodeJsonElement()) { + is JsonPrimitive -> parseJsonPrimitive(jsonElement) + is JsonArray -> parseJsonArray(jsonElement) + is JsonObject -> throw NotImplementedError("ConditionValue does not support JsonObject") + else -> throw SerializationException("Unexpected ConditionValue element type") + } + } - is JsonObject -> { - throw NotImplementedError("ConditionValue does not support JsonObject") - } + private fun parseJsonPrimitive(jsonPrimitive: JsonPrimitive): ConditionValue { + return jsonPrimitive.intOrNull?.let { ConditionValue.IntValue(it) } + ?: jsonPrimitive.booleanOrNull?.let { ConditionValue.BooleanValue(it) } + ?: jsonPrimitive.doubleOrNull?.let { ConditionValue.DoubleValue(it) } + ?: parseStringValue(jsonPrimitive.content) + } + + private fun parseStringValue(content: String): ConditionValue { + return try { + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + val date = dateFormat.parse(content) + ConditionValue.DateTimeValue(date) + } catch (e: Exception) { + ConditionValue.StringValue(content) } } + private fun parseJsonArray(jsonArray: JsonArray): ConditionValue.ArrayValue { + val elements = jsonArray.map { it.jsonPrimitive.content } + return ConditionValue.ArrayValue(elements) + } + override fun serialize(encoder: Encoder, value: ConditionValue) { - // TODO: Later if needed + // TODO: Implement serialization logic if needed } } @OptIn(InternalSerializationApi::class) -@Serializer(forClass = VariableValue::class) object VariableValueSerializer : KSerializer { + + @OptIn(ExperimentalSerializationApi::class) override val descriptor: SerialDescriptor = buildSerialDescriptor("package.VariableValue", PolymorphicKind.SEALED) override fun deserialize(decoder: Decoder): VariableValue { val input = decoder as? JsonDecoder - ?: throw SerializationException("This class can be decoded only by Json format") - return when (val tree = input.decodeJsonElement()) { - is JsonPrimitive -> { - if (tree.isString) { - if (isValidJson(tree.content)) { - VariableValue.JsonValue(tree.content) - } else { - VariableValue.StringValue(tree.content) - } + ?: throw SerializationException("This class can only be decoded by Json format") + + return when (val jsonElement = input.decodeJsonElement()) { + is JsonPrimitive -> parseJsonPrimitive(jsonElement) + is JsonArray -> parseJsonArray(jsonElement) + is JsonObject -> parseJsonObject(jsonElement) + else -> throw SerializationException("Unexpected VariableValue element type") + } + } + + private fun parseJsonPrimitive(jsonPrimitive: JsonPrimitive): VariableValue { + return when { + jsonPrimitive.isString -> { + if (isValidJson(jsonPrimitive.content)) { + VariableValue.JsonValue(jsonPrimitive.content) } else { - tree.intOrNull?.let { - VariableValue.IntValue(it) - } ?: tree.booleanOrNull?.let { - VariableValue.BooleanValue(it) - } ?: tree.doubleOrNull?.let { - VariableValue.DoubleValue(it) - } ?: tree.content.let { - VariableValue.StringValue(it) - } + VariableValue.StringValue(jsonPrimitive.content) } } - is JsonArray -> { - VariableValue.ArrayValue(tree.jsonArray.map { jsonElement -> jsonElement.jsonPrimitive.content }) - } - - is JsonObject -> { - FeaturevisorInstance.companionLogger?.debug("VariableValueSerializer, JsonObject, tree.jsonObject: ${tree.jsonObject}, tree: $tree") - VariableValue.JsonValue(tree.jsonObject.toString()) - } + jsonPrimitive.intOrNull != null -> VariableValue.IntValue(jsonPrimitive.int) + jsonPrimitive.booleanOrNull != null -> VariableValue.BooleanValue(jsonPrimitive.boolean) + jsonPrimitive.doubleOrNull != null -> VariableValue.DoubleValue(jsonPrimitive.double) + else -> VariableValue.StringValue(jsonPrimitive.content) } } + private fun parseJsonArray(jsonArray: JsonArray): VariableValue.ArrayValue { + val elements = jsonArray.map { it.jsonPrimitive.content } + return VariableValue.ArrayValue(elements) + } + + private fun parseJsonObject(jsonObject: JsonObject): VariableValue.JsonValue { + FeaturevisorInstance.companionLogger?.debug("VariableValueSerializer, JsonObject: $jsonObject") + return VariableValue.JsonValue(jsonObject.toString()) + } + override fun serialize(encoder: Encoder, value: VariableValue) { - // TODO: Later if needed + // TODO: Implement serialization logic if needed } } diff --git a/src/main/kotlin/com/featurevisor/testRunner/Parser.kt b/src/main/kotlin/com/featurevisor/testRunner/Parser.kt index b3f140d..69c9de5 100644 --- a/src/main/kotlin/com/featurevisor/testRunner/Parser.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/Parser.kt @@ -1,9 +1,11 @@ package com.featurevisor.testRunner +import com.featurevisor.sdk.JsonConfigFeatureVisor import com.featurevisor.sdk.serializers.isValidJson import com.featurevisor.sdk.serializers.mapOperator import com.featurevisor.types.* -import com.google.gson.Gson +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.yaml.snakeyaml.Yaml import java.io.File import java.util.* @@ -66,28 +68,34 @@ internal fun parseTestFeatureAssertions(yamlFilePath: String) = } private fun mapMatrixValues(value: Any) = - when(value){ + when (value) { is Boolean -> { - if (value){ + if (value) { AttributeValue.StringValue("yes") - }else{ + } else { AttributeValue.StringValue("no") } } + is Int -> { AttributeValue.IntValue(value) } + is Double -> { AttributeValue.DoubleValue(value) } + is String -> { AttributeValue.StringValue(value) } + is Date -> { AttributeValue.DateValue(value) } - else -> { AttributeValue.StringValue("")} + else -> { + AttributeValue.StringValue("") + } } private fun parseWeightValue(value: Any): WeightType { @@ -118,7 +126,7 @@ private fun parseVariableValue(value: Any?): VariableValue { } is Map<*, *> -> { - val json = Gson().toJson(value) + val json = Json.encodeToString(value) VariableValue.JsonValue(json) } @@ -156,7 +164,7 @@ private fun parseAttributeValue(value: Any?): AttributeValue { } is Map<*, *> -> { - val json = Gson().toJson(value) + val json = Json.encodeToString(value) AttributeValue.StringValue(json) } @@ -174,14 +182,14 @@ internal fun parseYamlSegment(segmentFilePath: String) = val data = yaml.load>(yamlContent) val archived = data["archived"] as? Boolean - val description = data["description"] as? String val conditionsData = data["conditions"] Segment( archived = archived, key = "", - conditions = parseCondition(conditionsData) + conditions = parseCondition(conditionsData), + conditionStrings = "" ) } catch (e: Exception) { @@ -245,6 +253,7 @@ private fun parseConditionValue(value: Any?): ConditionValue { ConditionValue.DoubleValue(value.toDouble()) } ?: ConditionValue.StringValue(value) } + is Int -> ConditionValue.IntValue(value) is Double -> ConditionValue.DoubleValue(value) is Boolean -> ConditionValue.BooleanValue(value) @@ -258,6 +267,6 @@ private fun parseConditionValue(value: Any?): ConditionValue { } fun parseConfiguration(projectRootPath: String) = - json.decodeFromString(Configuration.serializer(),getConfigurationJson(projectRootPath)!!) + JsonConfigFeatureVisor.json.decodeFromString(Configuration.serializer(), getConfigurationJson(projectRootPath)!!) diff --git a/src/main/kotlin/com/featurevisor/testRunner/TestFeature.kt b/src/main/kotlin/com/featurevisor/testRunner/TestFeature.kt index 379647d..fb9a465 100644 --- a/src/main/kotlin/com/featurevisor/testRunner/TestFeature.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/TestFeature.kt @@ -1,9 +1,6 @@ package com.featurevisor.testRunner -import com.featurevisor.sdk.Logger -import com.featurevisor.sdk.getVariable -import com.featurevisor.sdk.getVariation -import com.featurevisor.sdk.isEnabled +import com.featurevisor.sdk.* import com.featurevisor.types.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json @@ -11,7 +8,7 @@ import kotlinx.serialization.json.JsonElement fun testFeature( testFeature: TestFeature, - datafileContentByEnvironment:MutableMap, + datafileContentByEnvironment: MutableMap, option: TestProjectOption ): TestResult { val testStartTime = System.currentTimeMillis() @@ -121,9 +118,9 @@ fun testFeature( val actualValue = sdk.getVariable(featureKey, variableKey, it.context) val passed: Boolean - val variableSchema = datafileContent.features.find { feature -> + val variableSchema = datafileContent.getFeature().find { feature -> feature.key == testFeature.key - }?.variablesSchema?.find { variableSchema -> + }?.getVariablesSchema()?.find { variableSchema -> variableSchema.key.equals(variableKey, ignoreCase = true) } diff --git a/src/main/kotlin/com/featurevisor/testRunner/Utils.kt b/src/main/kotlin/com/featurevisor/testRunner/Utils.kt index e16d980..bf73d5d 100644 --- a/src/main/kotlin/com/featurevisor/testRunner/Utils.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/Utils.kt @@ -2,6 +2,7 @@ package com.featurevisor.testRunner import com.featurevisor.sdk.FeaturevisorInstance import com.featurevisor.sdk.InstanceOptions +import com.featurevisor.sdk.JsonConfigFeatureVisor import com.featurevisor.sdk.emptyDatafile import com.featurevisor.types.* import com.featurevisor.types.VariableValue.* @@ -22,11 +23,6 @@ internal const val ANSI_GREEN = "\u001B[32m" internal const val MAX_BUCKETED_NUMBER = 100000 -internal val json = Json { - ignoreUnknownKeys = true - isLenient = true -} - internal fun printMessageInGreenColor(message: String) = println("$ANSI_GREEN$message$ANSI_RESET") @@ -66,7 +62,7 @@ internal fun initializeSdkWithDataFileContent(datafileContent: DatafileContent?) internal fun getFileForSpecificPath(path: String) = File(path) -internal inline fun String.convertToDataClass() = json.decodeFromString(this) +internal inline fun String.convertToDataClass() = JsonConfigFeatureVisor.json.decodeFromString(this) internal fun getRootProjectDir(): String { var currentDir = File("../").absoluteFile @@ -251,13 +247,15 @@ fun checkJsonIsEquals(a: String, b: String): Boolean { return map1 == map2 } - -fun buildDataFileAsPerEnvironment(projectRootPath: String,environment: String) = try { +fun buildDataFileAsPerEnvironment(projectRootPath: String, environment: String) = try { getJsonForDataFile(environment = environment, projectRootPath = projectRootPath)?.run { - convertToDataClass() + printMessageInRedColor("Start reading ${System.currentTimeMillis()}") + val abc = convertToDataClass() + printMessageInRedColor("End reading ${System.currentTimeMillis()}") + return@run abc } ?: emptyDatafile } catch (e: Exception) { - printMessageInRedColor("Unable to parse data file") + printMessageInRedColor("Unable to parse data file $e") emptyDatafile } @@ -275,11 +273,11 @@ fun getDataFileContent(featureName: String, environment: String, projectRootPath null } -fun convertNanoSecondToMilliSecond(timeInNanoSecond:Double):String { - val timeInMilliSecond = timeInNanoSecond/1000000 - return if (timeInMilliSecond > 1000){ +fun convertNanoSecondToMilliSecond(timeInNanoSecond: Double): String { + val timeInMilliSecond = timeInNanoSecond / 1000000 + return if (timeInMilliSecond > 1000) { "${timeInMilliSecond / 1000} s" - }else{ + } else { "$timeInMilliSecond ms" } } diff --git a/src/main/kotlin/com/featurevisor/types/Types.kt b/src/main/kotlin/com/featurevisor/types/Types.kt index dff5b19..e3b2365 100644 --- a/src/main/kotlin/com/featurevisor/types/Types.kt +++ b/src/main/kotlin/com/featurevisor/types/Types.kt @@ -3,6 +3,8 @@ package com.featurevisor.types import com.featurevisor.sdk.serializers.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.JsonElement import java.util.* typealias Context = Map @@ -13,16 +15,22 @@ typealias VariableKey = String enum class VariableType { @SerialName("boolean") BOOLEAN, + @SerialName("string") STRING, + @SerialName("integer") INTEGER, + @SerialName("double") DOUBLE, + @SerialName("array") ARRAY, + @SerialName("object") OBJECT, + @SerialName("json") JSON } @@ -46,7 +54,7 @@ data class VariableOverride( // one of the below must be present in YAML val conditions: Condition? = null, - val segments: GroupSegment?=null, + val segments: GroupSegment? = null, ) @Serializable @@ -91,20 +99,6 @@ data class Force( val variables: VariableValues? = null, ) -data class Slot( - // @TODO: allow false? - val feature: FeatureKey? = null, - - // 0 to 100 - val percentage: Weight, -) - -data class Group( - val key: String, - val description: String, - val slots: List, -) - typealias BucketKey = String // 0 to 100,000 typealias BucketValue = Int @@ -119,43 +113,6 @@ typealias Weight = Double typealias EnvironmentKey = String typealias RuleKey = String -data class Rule( - val key: RuleKey, - val segments: GroupSegment, - val percentage: Weight, - - val enabled: Boolean? = null, - val variation: VariationValue? = null, - val variables: VariableValues? = null, -) - -data class Environment( - val expose: Boolean? = null, - val rules: List, - val force: List? = null, -) - -typealias Environments = Map - -data class ParsedFeature( - val key: FeatureKey, - - val archived: Boolean?, - val deprecated: Boolean?, - - val description: String, - val tags: List, - - val bucketBy: BucketBy, - - val required: List?, - - val variablesSchema: List?, - val variations: List?, - - val environments: Environments, -) - typealias AttributeKey = String @Serializable @@ -187,8 +144,6 @@ 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() @@ -205,7 +160,12 @@ typealias SegmentKey = String data class Segment( val archived: Boolean? = null, val key: SegmentKey, - val conditions: Condition, + + @SerialName("conditions") + val conditionStrings: String, + + @Transient + var conditions: Condition? = null, ) data class AndGroupSegment( @@ -319,15 +279,44 @@ sealed class Required { data class Feature( val key: FeatureKey, val deprecated: Boolean? = null, - val variablesSchema: List? = null, - val variations: List? = null, - val bucketBy: BucketBy, - val required: List? = null, - val traffic: List, - val force: List? = null, - // if in a Group (mutex), these are available slot ranges + @SerialName("variablesSchema") + val variablesSchemaString: JsonElement? = null, + + @SerialName("variations") + val variationStrings: JsonElement? = null, + + @SerialName("bucketBy") + val bucketByString: JsonElement, + + @SerialName("traffic") + val trafficString: JsonElement, + + @SerialName("force") + val forceString: JsonElement? = null, + + val required: List? = null, val ranges: List? = null, + + @Transient + @SerialName("variablesSchemaObject") + var variablesSchema: List? = null, + + @Transient + @SerialName("variationsObject") + var variations: List? = null, + + @Transient + @SerialName("bucketByObject") + var bucketBy: BucketBy? = null, + + @Transient + @SerialName("trafficObject") + var traffic: List? = null, + + @Transient + @SerialName("forceObject") + var force: List? = null ) @Serializable @@ -354,14 +343,14 @@ typealias AssertionMatrix = Map> data class FeatureAssertion( - var description: String?=null, - var environment: EnvironmentKey="staging", + var description: String? = null, + var environment: EnvironmentKey = "staging", // bucket weight: 0 to 100 var at: WeightType = WeightType.IntType(40), var context: Context = mapOf("devMode" to AttributeValue.BooleanValue(false)), - val expectedToBeEnabled: Boolean?=null, - val expectedVariation: VariationValue?=null, - val expectedVariables: VariableValues?=null, + val expectedToBeEnabled: Boolean? = null, + val expectedVariation: VariationValue? = null, + val expectedVariables: VariableValues? = null, val matrix: AssertionMatrix? = null ) @@ -371,7 +360,7 @@ data class TestFeature( ) data class SegmentAssertion( - var description: String?=null, + var description: String? = null, var context: Context, val expectedToMatch: Boolean, val matrix: AssertionMatrix? = null @@ -387,20 +376,20 @@ sealed class Test { data class Segment(val value: TestSegment) : Test() } -sealed class WeightType{ - data class IntType(val value: Int):WeightType() +sealed class WeightType { + data class IntType(val value: Int) : WeightType() - data class DoubleType(val value: Double):WeightType() + data class DoubleType(val value: Double) : WeightType() - data class StringType(val value: String):WeightType() + data class StringType(val value: String) : WeightType() } data class TestResultAssertionError( val type: String, - val expected: Any?=null, - val actual: Any?=null, - val message: String?=null, - val details: Map?=null + val expected: Any? = null, + val actual: Any? = null, + val message: String? = null, + val details: Map? = null ) data class TestResultAssertion( @@ -414,7 +403,7 @@ data class TestResultAssertion( data class TestResult( val type: String, val key: FeatureKey, - var notFound: Boolean?=null, + var notFound: Boolean? = null, var passed: Boolean, var duration: Long, val assertions: List @@ -426,29 +415,24 @@ data class ExecutionResult( ) data class AssertionsCount( - var passed: Int=0, - var failed: Int=0 -) - -data class DataFile( - val stagingDataFiles: DatafileContent? = null, - val productionDataFiles: DatafileContent? = null + var passed: Int = 0, + var failed: Int = 0 ) @Serializable data class Configuration( - val environments:List, + val environments: List, val tags: List, - val defaultBucketBy:String, - val prettyState:Boolean, - val prettyDatafile:Boolean, - val stringify:Boolean, - val featuresDirectoryPath:String, - val segmentsDirectoryPath:String, - val attributesDirectoryPath:String, - val groupsDirectoryPath:String, - val testsDirectoryPath:String, - val stateDirectoryPath:String, - val outputDirectoryPath:String, - val siteExportDirectoryPath:String + val defaultBucketBy: String, + val prettyState: Boolean, + val prettyDatafile: Boolean, + val stringify: Boolean, + val featuresDirectoryPath: String, + val segmentsDirectoryPath: String, + val attributesDirectoryPath: String, + val groupsDirectoryPath: String, + val testsDirectoryPath: String, + val stateDirectoryPath: String, + val outputDirectoryPath: String, + val siteExportDirectoryPath: String ) diff --git a/src/test/kotlin/com/featurevisor/sdk/InstanceTest.kt b/src/test/kotlin/com/featurevisor/sdk/InstanceTest.kt index 2e091c9..ab281c3 100644 --- a/src/test/kotlin/com/featurevisor/sdk/InstanceTest.kt +++ b/src/test/kotlin/com/featurevisor/sdk/InstanceTest.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain +import kotlinx.serialization.json.JsonObject import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach @@ -192,6 +193,9 @@ class InstanceTest { features = listOf( Feature( key = "test", + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), bucketBy = BucketBy.Single("userId"), variations = listOf( Variation(value = "control"), @@ -251,7 +255,10 @@ class InstanceTest { Allocation(variation = "treatment", range = listOf(0, 0)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ), @@ -302,7 +309,10 @@ class InstanceTest { Allocation(variation = "treatment", range = listOf(0, 0)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ), @@ -355,7 +365,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 0)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ), @@ -400,7 +413,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 0)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ), @@ -494,7 +510,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 100000)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ), attributes = emptyList(), @@ -556,7 +575,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 100000)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ) @@ -591,7 +613,10 @@ class InstanceTest { percentage = 0, // disabled allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ), Feature( key = "myKey", @@ -606,7 +631,10 @@ class InstanceTest { percentage = 100000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ) @@ -630,7 +658,10 @@ class InstanceTest { percentage = 100000, // enabled allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ), Feature( key = "myKey", @@ -643,7 +674,10 @@ class InstanceTest { percentage = 100000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ) @@ -676,7 +710,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 100000)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ), Feature( key = "myKey", @@ -696,7 +733,10 @@ class InstanceTest { percentage = 100000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ) @@ -727,7 +767,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 100000)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ), Feature( key = "myKey", @@ -747,7 +790,10 @@ class InstanceTest { percentage = 100000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ) @@ -784,7 +830,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 0)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ), Feature( key = "deprecatedTest", @@ -804,7 +853,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 0)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ), @@ -857,7 +909,10 @@ class InstanceTest { percentage = 100000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ), segments = listOf( @@ -865,7 +920,8 @@ class InstanceTest { key = "netherlands", conditions = Condition.Plain( "country", Operator.EQUALS, ConditionValue.StringValue("nl") - ) + ), + conditionStrings = "" ) ) ) @@ -913,7 +969,10 @@ class InstanceTest { percentage = 50000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ) @@ -972,7 +1031,10 @@ class InstanceTest { Allocation("treatment", listOf(0, 100000)) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ), Feature( key = "testWithNoVariation", @@ -984,7 +1046,10 @@ class InstanceTest { percentage = 100000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ), segments = listOf( @@ -998,7 +1063,8 @@ class InstanceTest { ConditionValue.StringValue("nl") ) ) - ) + ), + conditionStrings = "", ) ) ) @@ -1223,7 +1289,10 @@ class InstanceTest { ) ) ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ), attributes = listOf( @@ -1237,7 +1306,8 @@ class InstanceTest { attributeKey = "country", operator = Operator.EQUALS, value = ConditionValue.StringValue("nl") - ) + ), + conditionStrings = "", ), Segment( key = "belgium", @@ -1245,7 +1315,8 @@ class InstanceTest { attributeKey = "country", operator = Operator.EQUALS, value = ConditionValue.StringValue("be") - ) + ), + conditionStrings = "", ) ) ) @@ -1362,8 +1433,8 @@ class InstanceTest { operator = Operator.EQUALS, value = ConditionValue.StringValue("nl") ), - - ) + conditionStrings = "", + ) ), features = listOf( Feature( @@ -1390,7 +1461,10 @@ class InstanceTest { percentage = 100000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) ) @@ -1439,7 +1513,10 @@ class InstanceTest { percentage = 100000, allocation = emptyList() ) - ) + ), + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ), attributes = emptyList(), @@ -1450,7 +1527,8 @@ class InstanceTest { attributeKey = "country", operator = Operator.EQUALS, value = ConditionValue.StringValue("nl") - ) + ), + conditionStrings = "" ), Segment( key = "iphone", @@ -1458,8 +1536,8 @@ class InstanceTest { attributeKey = "device", operator = Operator.EQUALS, value = ConditionValue.StringValue("iphone") - ) - + ), + conditionStrings = "", ), Segment( key = "unitedStates", @@ -1467,7 +1545,8 @@ class InstanceTest { attributeKey = "country", operator = Operator.EQUALS, value = ConditionValue.StringValue("us") - ) + ), + conditionStrings = "", ) ) ) diff --git a/src/test/kotlin/com/featurevisor/sdk/factory/DatafileContentFactory.kt b/src/test/kotlin/com/featurevisor/sdk/factory/DatafileContentFactory.kt index 7dc13a7..b083a05 100644 --- a/src/test/kotlin/com/featurevisor/sdk/factory/DatafileContentFactory.kt +++ b/src/test/kotlin/com/featurevisor/sdk/factory/DatafileContentFactory.kt @@ -9,6 +9,7 @@ import com.featurevisor.types.Feature import com.featurevisor.types.Operator.EQUALS import com.featurevisor.types.Operator.NOT_EQUALS import com.featurevisor.types.Segment +import kotlinx.serialization.json.JsonObject object DatafileContentFactory { @@ -56,6 +57,7 @@ object DatafileContentFactory { ) ), ), + conditionStrings = "" ), ) @@ -70,6 +72,9 @@ object DatafileContentFactory { traffic = emptyList(), force = null, ranges = null, + bucketByString = JsonObject(emptyMap()), + variationStrings = JsonObject(emptyMap()), + trafficString = JsonObject(emptyMap()), ) ) }