diff --git a/src/main/java/com/eppo/sdk/dto/EppoValue.java b/src/main/java/com/eppo/sdk/dto/EppoValue.java index 7d798b5..36522b3 100644 --- a/src/main/java/com/eppo/sdk/dto/EppoValue.java +++ b/src/main/java/com/eppo/sdk/dto/EppoValue.java @@ -112,13 +112,32 @@ public boolean isNull() { return type == EppoValueType.NULL; } + /** + * Converts the EppoValue into a string representation. + * NOTE: Take care when updating this method as it's currently used + * by the IN and NOT IN target rule evaluations. + * + * @return String the string representation of the EppoValue + */ @Override public String toString() { switch(this.type) { case STRING: return this.stringValue; case NUMBER: - return this.doubleValue.toString(); + // By default, `String.valueOf()` will include at least one decimal place. + // Though numeric flags can either be integers or floating-point types. And target + // rule logic will cast a number type to a String before evaluating `oneOf` or `notOneOf` + // rules. + // The logic below ensures the cast to string better represents the intended numeric + // field type. + // + // @see https://docs.geteppo.com/feature-flagging/flag-variations#numeric-flags + // @see https://docs.geteppo.com/feature-flagging/targeting#supported-rule-operators + if (this.doubleValue.intValue() == this.doubleValue) { + return String.valueOf(this.doubleValue.intValue()); + } + return String.valueOf(this.doubleValue); case BOOLEAN: return this.boolValue.toString(); case ARRAY_OF_STRING: diff --git a/src/main/java/com/eppo/sdk/helpers/RuleValidator.java b/src/main/java/com/eppo/sdk/helpers/RuleValidator.java index 196af46..97a31a1 100644 --- a/src/main/java/com/eppo/sdk/helpers/RuleValidator.java +++ b/src/main/java/com/eppo/sdk/helpers/RuleValidator.java @@ -153,9 +153,9 @@ private static boolean evaluateCondition( return Compare.compareRegex(value.stringValue(), Pattern.compile(condition.value.stringValue())); case ONE_OF: - return Compare.isOneOf(value.stringValue(), condition.value.arrayValue()); + return Compare.isOneOf(value.toString(), condition.value.arrayValue()); case NOT_ONE_OF: - return !Compare.isOneOf(value.stringValue(), condition.value.arrayValue()); + return !Compare.isOneOf(value.toString(), condition.value.arrayValue()); } } catch (Exception e) { return false; diff --git a/src/test/java/com/eppo/sdk/helpers/RuleValidatorTest.java b/src/test/java/com/eppo/sdk/helpers/RuleValidatorTest.java index ea4dff0..951423a 100644 --- a/src/test/java/com/eppo/sdk/helpers/RuleValidatorTest.java +++ b/src/test/java/com/eppo/sdk/helpers/RuleValidatorTest.java @@ -71,6 +71,44 @@ public void addOneOfCondition(Rule rule) { addConditionToRule(rule, condition); } + public void addOneOfConditionWithIntegers(Rule rule) { + Condition condition = new Condition(); + List values = new ArrayList<>(); + values.add("1"); + values.add("2"); + + condition.value = EppoValue.valueOf(values); + condition.attribute = "oneOf"; + condition.operator = OperatorType.ONE_OF; + + addConditionToRule(rule, condition); + } + + public void addOneOfConditionWithDoubles(Rule rule) { + Condition condition = new Condition(); + List values = new ArrayList<>(); + values.add("1.5"); + values.add("2.7"); + + condition.value = EppoValue.valueOf(values); + condition.attribute = "oneOf"; + condition.operator = OperatorType.ONE_OF; + + addConditionToRule(rule, condition); + } + + public void addOneOfConditionWithBoolean(Rule rule) { + Condition condition = new Condition(); + List values = new ArrayList<>(); + values.add("true"); + + condition.value = EppoValue.valueOf(values); + condition.attribute = "oneOf"; + condition.operator = OperatorType.ONE_OF; + + addConditionToRule(rule, condition); + } + public void addNotOneOfCondition(Rule rule) { Condition condition = new Condition(); List values = new ArrayList<>(); @@ -226,4 +264,60 @@ void testMatchesAnyRuleWithNotOneOfRuleNotPassed() { Assertions.assertFalse(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); } + @DisplayName("findMatchingRule() with oneOf rule on a string") + @Test + void testMatchesAnyRuleWithOneOfRuleOnString() { + List rules = new ArrayList<>(); + Rule rule = createRule(new ArrayList<>()); + addOneOfCondition(rule); + rules.add(rule); + + EppoAttributes subjectAttributes = new EppoAttributes(); + subjectAttributes.put("oneOf", EppoValue.valueOf("value1")); + + Assertions.assertTrue(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); + } + + @DisplayName("findMatchingRule() with oneOf rule on an integer") + @Test + void testMatchesAnyRuleWithOneOfRuleOnInteger() { + List rules = new ArrayList<>(); + Rule rule = createRule(new ArrayList<>()); + addOneOfConditionWithIntegers(rule); + rules.add(rule); + + EppoAttributes subjectAttributes = new EppoAttributes(); + subjectAttributes.put("oneOf", EppoValue.valueOf(2)); + + Assertions.assertTrue(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); + } + + @DisplayName("findMatchingRule() with oneOf rule on a double") + @Test + void testMatchesAnyRuleWithOneOfRuleOnDouble() { + List rules = new ArrayList<>(); + Rule rule = createRule(new ArrayList<>()); + addOneOfConditionWithDoubles(rule); + rules.add(rule); + + EppoAttributes subjectAttributes = new EppoAttributes(); + subjectAttributes.put("oneOf", EppoValue.valueOf(1.5)); + + Assertions.assertTrue(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); + } + + @DisplayName("findMatchingRule() with oneOf rule on a boolean") + @Test + void testMatchesAnyRuleWithOneOfRuleOnBoolean() { + List rules = new ArrayList<>(); + Rule rule = createRule(new ArrayList<>()); + addOneOfConditionWithBoolean(rule); + rules.add(rule); + + EppoAttributes subjectAttributes = new EppoAttributes(); + subjectAttributes.put("oneOf", EppoValue.valueOf(true)); + + Assertions.assertTrue(RuleValidator.findMatchingRule(subjectAttributes, rules).isPresent()); + } + }