Skip to content

Commit

Permalink
Runtime Deserialization - POC1
Browse files Browse the repository at this point in the history
  • Loading branch information
GitHub-Ram committed Dec 19, 2024
1 parent e703d04 commit 581831c
Show file tree
Hide file tree
Showing 18 changed files with 561 additions and 440 deletions.
1 change: 0 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
group=com.featurevisor
version=0.0.1-SNAPSHOT
org.gradle.daemon=true
org.gradle.parallel=true
8 changes: 4 additions & 4 deletions src/main/kotlin/com/featurevisor/sdk/DatafileReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttributeKey, Attribute> = datafileContent.attributes.associateBy { it.key }
private val segments: Map<SegmentKey, Segment> = datafileContent.segments.associateBy { it.key }
private val features: Map<FeatureKey, Feature> = datafileContent.features.associateBy { it.key }
private val attributes: Map<AttributeKey, Attribute> = datafileContent.getAttributes().associateBy { it.key }
private val segments: Map<SegmentKey, Segment> = datafileContent.getSegment().associateBy { it.key }
private val features: Map<FeatureKey, Feature> = datafileContent.getFeature().associateBy { it.key }

fun getRevision(): String {
return revision
Expand Down
33 changes: 14 additions & 19 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -440,7 +437,6 @@ fun FeaturevisorInstance.evaluateFlag(featureKey: FeatureKey, context: Context =
}
}

@Suppress("UNREACHABLE_CODE")
fun FeaturevisorInstance.evaluateVariable(
featureKey: FeatureKey,
variableKey: VariableKey,
Expand Down Expand Up @@ -497,7 +493,7 @@ fun FeaturevisorInstance.evaluateVariable(
return evaluation
}

val variableSchema = feature.variablesSchema?.firstOrNull { variableSchema ->
val variableSchema = feature.getVariablesSchema().firstOrNull { variableSchema ->
variableSchema.key == variableKey
}

Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -571,7 +566,7 @@ fun FeaturevisorInstance.evaluateVariable(
matchedAllocation.variation
}

val variation = feature.variations?.firstOrNull { variation ->
val variation = feature.getVariations().firstOrNull { variation ->
variation.value == variationValue
}

Expand Down Expand Up @@ -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<AttributeKey>
val type: String
val attributeKeys: List<AttributeKey>

when (val bucketBy = feature.bucketBy) {
when (val bucketBy = feature.getBucketBy()) {
is BucketBy.Single -> {
type = "plain"
attributeKeys = listOf(bucketBy.bucketBy)
Expand Down
5 changes: 2 additions & 3 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Feature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -46,7 +46,7 @@ internal fun FeaturevisorInstance.getMatchedTraffic(
}
}

internal fun FeaturevisorInstance.getMatchedAllocation(
internal fun getMatchedAllocation(
traffic: Traffic,
bucketValue: Int,
): Allocation? {
Expand All @@ -68,7 +68,6 @@ internal fun FeaturevisorInstance.getMatchedTrafficAndAllocation(
context: Context,
bucketValue: Int,
datafileReader: DatafileReader,
logger: Logger?,
): MatchedTrafficAndAllocation {

var matchedAllocation: Allocation? = null
Expand Down
23 changes: 9 additions & 14 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,31 @@ 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<DatafileContent>) -> Unit,
completion: (Result<Pair<DatafileContent, String>>) -> Unit,
) {
handleDatafileFetch?.let { handleFetch ->
val result = handleFetch(url)
completion(result)
val result = handleFetch(url).getOrNull()!!
completion(Result.success(Pair(result, "")))
} ?: run {
fetchDatafileContentFromUrl(url, completion)
}
}

private fun fetchDatafileContentFromUrl(
url: String,
completion: (Result<DatafileContent>) -> Unit,
completion: (Result<Pair<DatafileContent, String>>) -> Unit,
) {
try {
val httpUrl = url.toHttpUrl()
Expand All @@ -45,21 +43,18 @@ private fun fetchDatafileContentFromUrl(

private inline fun fetch(
request: Request,
crossinline completion: (Result<DatafileContent>) -> Unit,
crossinline completion: (Result<Pair<DatafileContent, String>>) -> 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<DatafileContent>(responseBodyString)
completion(Result.success(content))
val content = JsonConfigFeatureVisor.json.decodeFromString<DatafileContent>(responseBodyString)
completion(Result.success(Pair(content, responseBodyString)))
} catch (throwable: Throwable) {
completion(
Result.failure(
Expand Down
8 changes: 3 additions & 5 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Refresh.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 1 addition & 20 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 1 addition & 2 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Variable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ inline fun <reified T : Any> FeaturevisorInstance.getVariableObject(
}
}

inline fun <reified T: Any> FeaturevisorInstance.getVariableJSON(
inline fun <reified T : Any> FeaturevisorInstance.getVariableJSON(
featureKey: FeatureKey,
variableKey: VariableKey,
context: Context,
Expand All @@ -97,4 +97,3 @@ inline fun <reified T: Any> FeaturevisorInstance.getVariableJSON(
null
}
}

11 changes: 5 additions & 6 deletions src/main/kotlin/com/featurevisor/sdk/Instance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -145,19 +144,19 @@ class FeaturevisorInstance private constructor(options: InstanceOptions) {
continuation.resume(this)
}

val cb :(result:Array<out Any>) -> Unit = {
val cb: (result: Array<out Any>) -> 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<DatafileContent>(String(data))
val datafileContent = JsonConfigFeatureVisor.json.decodeFromString<DatafileContent>(String(data))
datafileReader = DatafileReader(datafileContent = datafileContent)
} catch (e: Exception) {
logger?.error("could not parse datafile", mapOf("error" to e))
Expand Down
Loading

0 comments on commit 581831c

Please sign in to comment.