Skip to content

Commit

Permalink
Make conditions work properly for every case (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
uniumuniu authored Oct 6, 2023
1 parent 366abf2 commit c5ba000
Show file tree
Hide file tree
Showing 9 changed files with 712 additions and 413 deletions.
8 changes: 6 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ publishing {
dependencies {
// Use the Kotlin JUnit 5 integration.
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")

// Use the JUnit 5 integration.
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.2")
// Uncomment when needed
// testImplementation("io.mockk:mockk:1.13.8")
testImplementation("io.kotest:kotest-assertions-core:5.7.2")

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

Expand All @@ -57,12 +59,14 @@ dependencies {

// This dependency is used internally, and not exposed to consumers on their own compile classpath.
implementation("com.google.guava:guava:31.1-jre")
implementation("net.swiftzer.semver:semver:1.3.0")
implementation("com.goncalossilva:murmurhash:0.4.0")
}

// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
languageVersion.set(JavaLanguageVersion.of(11))
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/main/kotlin/com/featurevisor/sdk/Bucket.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.featurevisor.sdk

import com.goncalossilva.murmurhash.MurmurHash3

object Bucket {
private const val HASH_SEED = 1
private const val HASH_SEED = 1u
private const val MAX_HASH_VALUE = 4294967296 // 2^32

// 100% * 1000 to include three decimal places in the same integer value
private const val MAX_BUCKETED_NUMBER = 100000

fun getBucketedNumber(bucketKey: String): Int {
val hashValue = MurmurHash3().hash32x86(bucketKey.toByteArray())
val hashValue = MurmurHash3(HASH_SEED).hash32x86(bucketKey.toByteArray())
val ratio = hashValue.toDouble() / MAX_HASH_VALUE

return kotlin.math.floor(ratio * MAX_BUCKETED_NUMBER).toInt()
Expand Down
161 changes: 103 additions & 58 deletions src/main/kotlin/com/featurevisor/sdk/Conditions.kt
Original file line number Diff line number Diff line change
@@ -1,84 +1,129 @@
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
import com.featurevisor.types.PlainCondition
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 net.swiftzer.semver.SemVer

object Conditions {
fun conditionIsMatched(condition: PlainCondition, context: Context): Boolean {
val (attribute, operator, value) = condition
fun conditionIsMatched(condition: Plain, context: Context): Boolean {
val (attributeKey, operator, conditionValue) = condition
val attributeValue = context.getOrDefault(attributeKey, null) ?: return false

val contextValue = context[attribute]
val conditionValue = value
return when {
attributeValue is AttributeValue.StringValue && conditionValue is ConditionValue.StringValue -> {
when (operator) {
EQUALS -> attributeValue.value == conditionValue.value
NOT_EQUALS -> attributeValue.value != conditionValue.value
CONTAINS -> attributeValue.value.contains(conditionValue.value)
NOT_CONTAINS -> attributeValue.value.contains(conditionValue.value).not()
STARTS_WITH -> attributeValue.value.startsWith(conditionValue.value)
ENDS_WITH -> attributeValue.value.endsWith(conditionValue.value)
SEMVER_EQUALS -> compareVersions(attributeValue.value, conditionValue.value) == 0
SEMVER_NOT_EQUALS -> compareVersions(attributeValue.value, conditionValue.value) != 0
SEMVER_GREATER_THAN -> compareVersions(attributeValue.value, conditionValue.value) == 1
SEMVER_GREATER_THAN_OR_EQUAL -> compareVersions(attributeValue.value, conditionValue.value) >= 0
SEMVER_LESS_THAN -> compareVersions(attributeValue.value, conditionValue.value) == -1
SEMVER_LESS_THAN_OR_EQUAL -> compareVersions(attributeValue.value, conditionValue.value) <= 0
else -> false
}
}

// string
if (contextValue is AttributeValue.StringValue) {
// string / string
if (conditionValue is ConditionValue.StringValue) {
attributeValue is AttributeValue.IntValue && conditionValue is ConditionValue.IntValue -> {
when (operator) {
Operator.equals -> return contextValue.value == conditionValue.value
Operator.notEquals -> return contextValue.value != conditionValue.value
Operator.contains -> return contextValue.value.contains(conditionValue.value)
Operator.notContains ->
return !contextValue.value.contains(conditionValue.value)
EQUALS -> attributeValue.value == conditionValue.value
NOT_EQUALS -> attributeValue.value != conditionValue.value
GREATER_THAN -> attributeValue.value > conditionValue.value
GREATER_THAN_OR_EQUAL -> attributeValue.value >= conditionValue.value
LESS_THAN -> attributeValue.value < conditionValue.value
LESS_THAN_OR_EQUAL -> attributeValue.value <= conditionValue.value
else -> false
}
}

Operator.startsWith ->
return contextValue.value.startsWith(conditionValue.value)
attributeValue is AttributeValue.DoubleValue && conditionValue is ConditionValue.DoubleValue -> {
when (operator) {
EQUALS -> attributeValue.value == conditionValue.value
NOT_EQUALS -> attributeValue.value != conditionValue.value
GREATER_THAN -> attributeValue.value > conditionValue.value
GREATER_THAN_OR_EQUAL -> attributeValue.value >= conditionValue.value
LESS_THAN -> attributeValue.value < conditionValue.value
LESS_THAN_OR_EQUAL -> attributeValue.value <= conditionValue.value
else -> false
}
}

Operator.endsWith -> return contextValue.value.endsWith(conditionValue.value)
else -> return false
attributeValue is AttributeValue.BooleanValue && conditionValue is ConditionValue.BooleanValue -> {
when (operator) {
EQUALS -> attributeValue.value == conditionValue.value
NOT_EQUALS -> attributeValue.value != conditionValue.value
else -> false
}
}

// @TODO: string / array of strings for in/notIn operators
attributeValue is AttributeValue.StringValue && conditionValue is ConditionValue.ArrayValue -> {
when (operator) {
IN_ARRAY -> attributeValue.value in conditionValue.values
NOT_IN_ARRAY -> (attributeValue.value in conditionValue.values).not()
else -> false
}
}

// @TODO: handle semvers
attributeValue is AttributeValue.DateValue && conditionValue is ConditionValue.DateTimeValue -> {
when (operator) {
EQUALS -> attributeValue.value == conditionValue.value
BEFORE -> attributeValue.value < conditionValue.value
AFTER -> attributeValue.value > conditionValue.value
else -> false
}
}

return false
else -> false
}
}

// int
if (contextValue is AttributeValue.IntValue && value is ConditionValue.IntValue) {
// int / int
when (operator) {
Operator.equals -> return contextValue.value == value.value
Operator.notEquals -> return contextValue.value != value.value
Operator.greaterThan -> return contextValue.value > value.value
Operator.greaterThanOrEquals -> return contextValue.value >= value.value
Operator.lessThan -> return contextValue.value < value.value
Operator.lessThanOrEquals -> return contextValue.value <= value.value
else -> return false
}
}
fun allConditionsAreMatched(condition: Condition, context: Context): Boolean =
when (condition) {
is Plain -> conditionIsMatched(condition, context)

// double
if (contextValue is AttributeValue.DoubleValue && value is ConditionValue.DoubleValue) {
// double / double
when (operator) {
Operator.equals -> return contextValue.value == value.value
Operator.notEquals -> return contextValue.value != value.value
Operator.greaterThan -> return contextValue.value > value.value
Operator.greaterThanOrEquals -> return contextValue.value >= value.value
Operator.lessThan -> return contextValue.value < value.value
Operator.lessThanOrEquals -> return contextValue.value <= value.value
else -> return false
is And -> condition.and.all {
allConditionsAreMatched(it, context)
}
}

// boolean
if (contextValue is AttributeValue.BooleanValue && value is ConditionValue.BooleanValue) {
// boolean / boolean
when (operator) {
Operator.equals -> return contextValue.value == value.value
Operator.notEquals -> return contextValue.value != value.value
else -> return false
is Or -> condition.or.any {
allConditionsAreMatched(it, context)
}
}

// @TODO: handle dates
is Not -> condition.not.all {
allConditionsAreMatched(it, context)
}.not()
}

return false
}
private fun compareVersions(actual: String, condition: String): Int =
SemVer.parse(actual).compareTo(SemVer.parse(condition))
}
161 changes: 0 additions & 161 deletions src/main/kotlin/com/featurevisor/sdk/MurmurHash3.kt

This file was deleted.

Loading

0 comments on commit c5ba000

Please sign in to comment.