diff --git a/eppo/build.gradle b/eppo/build.gradle index 2a668cf..6e7c87c 100644 --- a/eppo/build.gradle +++ b/eppo/build.gradle @@ -71,6 +71,7 @@ dependencies { androidTestImplementation "commons-io:commons-io:${versions.commonsio}" implementation("com.google.code.gson:gson:${versions.gson}") implementation("com.squareup.okhttp3:okhttp:${versions.okhttp}") + implementation("com.github.zafarkhaja:java-semver:0.10.2") } publishing { diff --git a/eppo/src/androidTest/java/cloud/eppo/android/EppoClientTest.java b/eppo/src/androidTest/java/cloud/eppo/android/EppoClientTest.java index eaa7437..1ade4f7 100644 --- a/eppo/src/androidTest/java/cloud/eppo/android/EppoClientTest.java +++ b/eppo/src/androidTest/java/cloud/eppo/android/EppoClientTest.java @@ -262,7 +262,7 @@ public void testCachedAssignments() { // wait for a bit since cache file is loaded asynchronously System.out.println("Sleeping for a bit to wait for cache population to complete"); - Thread.sleep(5000); + Thread.sleep(10000); // Then reinitialize with a bad host so we know it's using the cached RAC built from the first initialization initClient(INVALID_HOST, false, false, false); // invalid port to force to use cache diff --git a/eppo/src/main/java/cloud/eppo/android/RuleEvaluator.java b/eppo/src/main/java/cloud/eppo/android/RuleEvaluator.java index 780f540..4075cd1 100644 --- a/eppo/src/main/java/cloud/eppo/android/RuleEvaluator.java +++ b/eppo/src/main/java/cloud/eppo/android/RuleEvaluator.java @@ -1,24 +1,17 @@ package cloud.eppo.android; +import com.github.zafarkhaja.semver.Version; + import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; -import java.util.stream.Collectors; import cloud.eppo.android.dto.EppoValue; import cloud.eppo.android.dto.SubjectAttributes; import cloud.eppo.android.dto.TargetingCondition; import cloud.eppo.android.dto.TargetingRule; -interface IConditionFunc { - boolean check(T a, T b); -} - class Compare { - public static boolean compareNumber(double a, double b, IConditionFunc conditionFunc) { - return conditionFunc.check(a, b); - } - public static boolean compareRegex(String a, Pattern pattern) { return pattern.matcher(a).matches(); } @@ -52,17 +45,72 @@ private static boolean evaluateCondition(SubjectAttributes subjectAttributes, Ta ) { if (subjectAttributes.containsKey(condition.getAttribute())) { EppoValue value = subjectAttributes.get(condition.getAttribute()); + if (value == null) { + return false; + } + + boolean numericComparison = value.isNumeric() && condition.getValue().isNumeric(); + + // Android API version 21 does not have access to the java.util.Optional class. + // Version.tryParse returns a Optional would be ideal. + // Instead use Version.parse which throws an exception if the string is not a valid SemVer. + // We front-load the parsing here so many evaluation of gte, gt, lte, lt operations + // more straight-forward. + Version valueSemVer = null; + Version conditionSemVer = null; + try { + valueSemVer = Version.parse(value.stringValue()); + conditionSemVer = Version.parse(condition.getValue().stringValue()); + } catch (Exception e) { + // no-op + } + + // Performing this check satisfies the compiler that the possibly + // null value can be safely accessed later. + boolean semVerComparison = valueSemVer != null && conditionSemVer != null; + try { switch (condition.getOperator()) { case GreaterThanEqualTo: - return Compare.compareNumber(value.doubleValue(), condition.getValue().doubleValue() - , (a, b) -> a >= b); + if (numericComparison) { + return value.doubleValue() >= condition.getValue().doubleValue(); + } + + if (semVerComparison) { + return valueSemVer.isHigherThanOrEquivalentTo(conditionSemVer); + } + + return false; case GreaterThan: - return Compare.compareNumber(value.doubleValue(), condition.getValue().doubleValue(), (a, b) -> a > b); + if (numericComparison) { + return value.doubleValue() > condition.getValue().doubleValue(); + } + + if (semVerComparison) { + return valueSemVer.isHigherThan(conditionSemVer); + } + + return false; case LessThanEqualTo: - return Compare.compareNumber(value.doubleValue(), condition.getValue().doubleValue(), (a, b) -> a <= b); + if (numericComparison) { + return value.doubleValue() <= condition.getValue().doubleValue(); + } + + if (semVerComparison) { + return valueSemVer.isLowerThanOrEquivalentTo(conditionSemVer); + } + + return false; case LessThan: - return Compare.compareNumber(value.doubleValue(), condition.getValue().doubleValue(), (a, b) -> a < b); + if (numericComparison) { + return value.doubleValue() < condition.getValue().doubleValue(); + } + + if (semVerComparison) { + return valueSemVer.isLowerThan(conditionSemVer); + } + + return false; case Matches: return Compare.compareRegex(value.stringValue(), Pattern.compile(condition.getValue().stringValue())); case OneOf: diff --git a/eppo/src/main/java/cloud/eppo/android/dto/EppoValue.java b/eppo/src/main/java/cloud/eppo/android/dto/EppoValue.java index b244376..564199f 100644 --- a/eppo/src/main/java/cloud/eppo/android/dto/EppoValue.java +++ b/eppo/src/main/java/cloud/eppo/android/dto/EppoValue.java @@ -57,18 +57,10 @@ public static EppoValue valueOf() { return new EppoValue(EppoValueType.Null); } - public int intValue() { - return Integer.parseInt(value, 10); - } - public double doubleValue() { return Double.parseDouble(value); } - public long longValue() { - return Long.parseLong(value, 10); - } - public String stringValue() { return value; } @@ -87,7 +79,7 @@ public JsonElement jsonValue() { public boolean isNumeric() { try { - Long.parseLong(value, 10); + Double.parseDouble(value); return true; } catch (Exception e) { return false; diff --git a/eppo/src/test/java/cloud/eppo/android/RuleEvaluatorTest.java b/eppo/src/test/java/cloud/eppo/android/RuleEvaluatorTest.java index 387651f..ce157ce 100644 --- a/eppo/src/test/java/cloud/eppo/android/RuleEvaluatorTest.java +++ b/eppo/src/test/java/cloud/eppo/android/RuleEvaluatorTest.java @@ -43,6 +43,21 @@ public void addNumericConditionToRule(TargetingRule TargetingRule) { addConditionToRule(TargetingRule, condition2); } + public void addSemVerConditionToRule(TargetingRule TargetingRule) { + TargetingCondition condition1 = new TargetingCondition(); + condition1.setValue(EppoValue.valueOf("1.5.0")); + condition1.setAttribute("appVersion"); + condition1.setOperator(OperatorType.GreaterThanEqualTo); + + TargetingCondition condition2 = new TargetingCondition(); + condition2.setValue(EppoValue.valueOf("2.2.0")); + condition2.setAttribute("appVersion"); + condition2.setOperator(OperatorType.LessThan); + + addConditionToRule(TargetingRule, condition1); + addConditionToRule(TargetingRule, condition2); + } + public void addRegexConditionToRule(TargetingRule TargetingRule) { TargetingCondition condition = new TargetingCondition(); condition.setValue(EppoValue.valueOf("[a-z]+")); @@ -131,6 +146,18 @@ public void testMatchesAnyRuleWhenRuleMatches() { assertEquals(targetingRule, RuleEvaluator.findMatchingRule(subjectAttributes, targetingRules)); } + @Test + public void testMatchesAnyRuleWhenRuleMatchesWithSemVer() { + List targetingRules = new ArrayList<>(); + TargetingRule targetingRule = createRule(new ArrayList<>()); + addSemVerConditionToRule(targetingRule); + targetingRules.add(targetingRule); + + SubjectAttributes subjectAttributes = new SubjectAttributes(); + subjectAttributes.put("appVersion", "1.15.5"); + + assertEquals(targetingRule, RuleEvaluator.findMatchingRule(subjectAttributes, targetingRules)); + } @Test public void testMatchesAnyRuleWhenThrowInvalidSubjectAttribute() {