Skip to content

Runtime deserialisation #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ dependencies {
// Uncomment when needed
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("io.kotest:kotest-assertions-core:5.7.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0")

testRuntimeOnly("org.junit.platform:junit-platform-launcher")

Expand All @@ -65,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
30 changes: 13 additions & 17 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Fetch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +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)
internal 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 @@ -40,26 +41,21 @@ private fun fetchDatafileContentFromUrl(
}
}

const val BODY_BYTE_COUNT = 1000000L
private inline fun fetch(
request: Request,
crossinline completion: (Result<DatafileContent>) -> Unit,
crossinline completion: (Result<Pair<DatafileContent, String>>) -> Unit,
) {
val client = OkHttpClient()
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))
} catch(throwable: Throwable) {
val content = JsonConfigFeatureVisor.json.decodeFromString<DatafileContent>(responseBodyString)
completion(Result.success(Pair(content, responseBodyString)))
} catch (throwable: Throwable) {
completion(
Result.failure(
FeaturevisorError.UnparsableJson(
Expand Down
22 changes: 9 additions & 13 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 All @@ -17,7 +15,7 @@ fun FeaturevisorInstance.startRefreshing() = when {
refreshJob != null -> logger?.warn("refreshing has already started")
refreshInterval == null -> logger?.warn("no `refreshInterval` option provided")
else -> {
refreshJob = CoroutineScope(Dispatchers.Unconfined).launch {
refreshJob = coroutineScope.launch {
while (isActive) {
refresh()
delay(refreshInterval)
Expand All @@ -32,25 +30,23 @@ fun FeaturevisorInstance.stopRefreshing() {
logger?.warn("refreshing has stopped")
}

private fun FeaturevisorInstance.refresh() {
private suspend fun FeaturevisorInstance.refresh() {
logger?.debug("refreshing datafile")
when {
statuses.refreshInProgress -> logger?.warn("refresh in progress, skipping")
datafileUrl.isNullOrBlank() -> logger?.error("cannot refresh since `datafileUrl` is not provided")
else -> {
statuses.refreshInProgress = true
fetchDatafileContent(
datafileUrl,
handleDatafileFetch,
url = datafileUrl,
handleDatafileFetch = handleDatafileFetch,
) { result ->

if (result.isSuccess) {
val datafileContent = result.getOrThrow()
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 All @@ -59,7 +55,7 @@ private fun FeaturevisorInstance.refresh() {
}

statuses.refreshInProgress = false
} else {
}.onFailure {
logger?.error(
"failed to refresh datafile",
mapOf("error" to result)
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
2 changes: 1 addition & 1 deletion src/main/kotlin/com/featurevisor/sdk/Instance+Status.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package com.featurevisor.sdk

data class Statuses(var ready: Boolean, var refreshInProgress: Boolean)

internal fun FeaturevisorInstance.isReady(): Boolean {
fun FeaturevisorInstance.isReady(): Boolean {
return statuses.ready
}
Loading
Loading