diff --git a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt index 41c15dc..bef5b03 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt @@ -42,7 +42,7 @@ object Conditions { EQUALS -> attributeValue.value == conditionValue.value NOT_EQUALS -> attributeValue.value != conditionValue.value CONTAINS -> attributeValue.value?.contains(conditionValue.value.orEmpty()) ?: false - NOT_CONTAINS -> attributeValue.value?.contains(conditionValue.value.orEmpty())?.not() ?: false + NOT_CONTAINS -> attributeValue.value?.contains(conditionValue.value.orEmpty())?.not() ?: false STARTS_WITH -> attributeValue.value?.startsWith(conditionValue.value.orEmpty()) ?: false ENDS_WITH -> attributeValue.value?.endsWith(conditionValue.value.orEmpty()) ?: false SEMVER_EQUALS -> compareVersions( @@ -119,6 +119,65 @@ object Conditions { } } + attributeValue is AttributeValue.IntValue && conditionValue is ConditionValue.ArrayValue -> { + when (operator) { + IN_ARRAY -> attributeValue.value.toString() in conditionValue.values + NOT_IN_ARRAY -> (attributeValue.value.toString() !in conditionValue.values) + else -> false + } + } + + attributeValue is AttributeValue.DoubleValue && conditionValue is ConditionValue.StringValue -> { + when (operator) { + EQUALS -> attributeValue.value.toString() == conditionValue.value + NOT_EQUALS -> attributeValue.value.toString() != conditionValue.value + + SEMVER_EQUALS -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty(), + ) == 0 + + SEMVER_NOT_EQUALS -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty(), + ) != 0 + + SEMVER_GREATER_THAN -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty() + ) == 1 + + SEMVER_GREATER_THAN_OR_EQUALS -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty() + ) >= 0 + + SEMVER_LESS_THAN -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty() + ) == -1 + + SEMVER_LESS_THAN_OR_EQUALS -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty() + ) <= 0 + + else -> false + } + } + + attributeValue is AttributeValue.StringValue && conditionValue is ConditionValue.IntValue -> { + when (operator) { + EQUALS -> attributeValue.value == conditionValue.value.toString() + NOT_EQUALS -> attributeValue.value != conditionValue.value.toString() + CONTAINS -> attributeValue.value?.contains(conditionValue.value.toString()) ?: false + NOT_CONTAINS -> attributeValue.value?.contains(conditionValue.value.toString())?.not() ?: false + STARTS_WITH -> attributeValue.value?.startsWith(conditionValue.value.toString()) ?: false + ENDS_WITH -> attributeValue.value?.endsWith(conditionValue.value.toString()) ?: false + else -> false + } + } + attributeValue is AttributeValue.DateValue && conditionValue is ConditionValue.DateTimeValue -> { when (operator) { EQUALS -> attributeValue.value == conditionValue.value @@ -143,9 +202,31 @@ object Conditions { private fun compareVersions(actual: String, condition: String): Int { return try { - SemVer.parse(actual).compareTo(SemVer.parse(condition)) + SemVer.parse(normalizeSemver(actual)).compareTo(SemVer.parse(normalizeSemver(condition))) } catch (e: Exception) { 0 } } + + private fun normalizeSemver(version: String): String { + val parts = version.split("-", "+") + val mainParts = parts[0].split(".").map { it.toInt().toString() } + var normalizedVersion = mainParts.joinToString(".") + + if (version.contains("-")) { + val preRelease = parts[1].split(".").joinToString(".") { + if (it.all { char -> char.isDigit() }) it.toInt().toString() + else it + } + normalizedVersion += "-$preRelease" + } + + if (version.contains("+")) { + val buildMetadata = version.split("+")[1] + normalizedVersion += "+$buildMetadata" + } + + return normalizedVersion + } + } diff --git a/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt b/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt index cc367f4..02feeed 100644 --- a/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt +++ b/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt @@ -3,25 +3,8 @@ package com.featurevisor.sdk import com.featurevisor.types.AttributeValue import com.featurevisor.types.Condition import com.featurevisor.types.ConditionValue -import com.featurevisor.types.Operator.AFTER -import com.featurevisor.types.Operator.BEFORE -import com.featurevisor.types.Operator.CONTAINS -import com.featurevisor.types.Operator.ENDS_WITH -import com.featurevisor.types.Operator.EQUALS -import com.featurevisor.types.Operator.GREATER_THAN -import com.featurevisor.types.Operator.GREATER_THAN_OR_EQUALS -import com.featurevisor.types.Operator.IN_ARRAY -import com.featurevisor.types.Operator.LESS_THAN -import com.featurevisor.types.Operator.LESS_THAN_OR_EQUALS -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_EQUALS -import com.featurevisor.types.Operator.SEMVER_LESS_THAN -import com.featurevisor.types.Operator.SEMVER_LESS_THAN_OR_EQUALS -import com.featurevisor.types.Operator.SEMVER_NOT_EQUALS -import com.featurevisor.types.Operator.STARTS_WITH +import com.featurevisor.types.Operator +import com.featurevisor.types.Operator.* import io.kotest.matchers.shouldBe import java.sql.Date import java.time.LocalDate @@ -459,6 +442,282 @@ class ConditionsTest { ) shouldBe true } + @Test + fun `SEMVER_EQUALS operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_EQUALS, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe false + } + + @Test + fun `SEMVER_NOT_EQUALS operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_NOT_EQUALS, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe true + } + + @Test + fun `SEMVER_GREATER_THAN operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_GREATER_THAN, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe true + } + + @Test + fun `SEMVER_GREATER_THAN_OR_EQUALS operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_GREATER_THAN_OR_EQUALS, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.1)) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe true + } + + @Test + fun `SEMVER_LESS_THAN operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_LESS_THAN, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.1)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe false + } + + @Test + fun `SEMVER_LESS_THAN_OR_EQUALS operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_LESS_THAN_OR_EQUALS, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.1)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe false + } + + @Test + fun `EQUALS operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = EQUALS, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("1")) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("2")) + ) shouldBe false + } + + @Test + fun `NOT_EQUALS operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = NOT_EQUALS, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("1")) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("2")) + ) shouldBe true + } + + @Test + fun `CONTAINS operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = CONTAINS, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("123")) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("23")) + ) shouldBe false + } + + @Test + fun `NOT_CONTAINS operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = NOT_CONTAINS, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("123")) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("23")) + ) shouldBe true + } + + @Test + fun `STARTS_WITH operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = STARTS_WITH, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("123")) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("23")) + ) shouldBe false + } + + @Test + fun `ENDS_WITH operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = ENDS_WITH, + value = ConditionValue.IntValue(3) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("123")) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("25")) + ) shouldBe false + } + + @Test + fun `IN_ARRAY operator works when condition value is Array and attribute value is Int`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = IN_ARRAY, + value = ConditionValue.ArrayValue(listOf("1","2","3","4")) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.IntValue(1)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.IntValue(5)) + ) shouldBe false + } + + @Test + fun `NOT_IN_ARRAY operator works when condition value is Array and attribute value is Int`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = NOT_IN_ARRAY, + value = ConditionValue.ArrayValue(listOf("1","2","3","4")) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.IntValue(1)) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.IntValue(5)) + ) shouldBe true + } + @Test fun `multiple conditions work`() { val startsWithCondition = Condition.Plain(