diff --git a/pom.xml b/pom.xml index 37d0d829..39064bce 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,11 @@ slf4j-api ${slf4j.version} + + org.apache.maven + maven-artifact + 3.6.3 + diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index a7834262..18021791 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -154,9 +154,11 @@ private static Boolean traitsMatchSegmentCondition(List identityTrai * @param value Trait value to compare with. * @return */ - private static Boolean traitsMatchValue(SegmentConditionModel condition, Object value) { + public static Boolean traitsMatchValue(SegmentConditionModel condition, Object value) { SegmentConditions operator = condition.getOperator(); if (operator.equals(SegmentConditions.NOT_CONTAINS)) { + return ((String) value).indexOf(condition.getValue()) == -1; + } else if (operator.equals(SegmentConditions.CONTAINS)) { return ((String) value).indexOf(condition.getValue()) > -1; } else if (operator.equals(SegmentConditions.REGEX)) { Pattern pattern = Pattern.compile(condition.getValue()); diff --git a/src/main/java/com/flagsmith/flagengine/utils/SemanticVersioning.java b/src/main/java/com/flagsmith/flagengine/utils/SemanticVersioning.java new file mode 100644 index 00000000..5fa6767a --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/utils/SemanticVersioning.java @@ -0,0 +1,30 @@ +package com.flagsmith.flagengine.utils; + +public class SemanticVersioning { + + /** + * Checks if the given string have `:semver` suffix or not + * >>> is_semver("2.1.41-beta:semver") + * True + * >>> is_semver("2.1.41-beta") + * False + * @param version The version string. + * @return + */ + public static Boolean isSemver(String version) { + return version.endsWith(":semver"); + } + + /** + * Remove the semver suffix(i.e: last 7 characters) from the given value + * >>> remove_semver_suffix("2.1.41-beta:semver") + * '2.1.41-beta' + * >>> remove_semver_suffix("2.1.41:semver") + * '2.1.41' + * @param version the version string to strip version from. + * @return + */ + public static String removeSemver(String version) { + return version.substring(0, version.length() - 7); + } +} diff --git a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java index 0645b607..32882f6d 100644 --- a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java +++ b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java @@ -1,6 +1,9 @@ package com.flagsmith.flagengine.utils.types; import com.flagsmith.flagengine.segments.constants.SegmentConditions; +import com.flagsmith.flagengine.utils.SemanticVersioning; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.maven.artifact.versioning.ComparableVersion; public class TypeCasting { @@ -13,15 +16,19 @@ public class TypeCasting { */ public static Boolean compare(SegmentConditions condition, Object value1, Object value2) { - if (TypeCasting.isInteger(value1) && TypeCasting.isInteger(value2)) { - return compare(condition, TypeCasting.toInteger(value1), TypeCasting.toInteger(value2)); - } else if (TypeCasting.isFloat(value1) && TypeCasting.isFloat(value2)) { - return compare(condition, TypeCasting.toFloat(value1), TypeCasting.toFloat(value2)); - } else if (TypeCasting.isBoolean(value1) && TypeCasting.isBoolean(value2)) { - return compare(condition, TypeCasting.toBoolean(value1), TypeCasting.toBoolean(value2)); + if (isInteger(value1) && isInteger(value2)) { + return compare(condition, toInteger(value1), toInteger(value2)); + } else if (isFloat(value1) && isFloat(value2)) { + return compare(condition, toFloat(value1), toFloat(value2)); + } else if (isDouble(value1) && isDouble(value2)) { + return compare(condition, toDouble(value1), toDouble(value2)); + } else if (isBoolean(value1) && isBoolean(value2)) { + return compare(condition, toBoolean(value1), toBoolean(value2)); + } else if (isSemver(value2)) { + return compare(condition, toSemver(value1), toSemver(value2)); } - return value1.equals(value2); + return compare(condition, (String) value1, (String) value2); } /** @@ -51,6 +58,28 @@ public static Boolean compare(SegmentConditions condition, Comparable value1, Co return value1.compareTo(value2) == 0; } + /** + * Convert the object to Double. + * @param number Object to convert to Double. + * @return + */ + public static Double toDouble(Object number) { + try { + return number instanceof Double ? ((Double) number) : Double.parseDouble((String) number); + } catch (Exception nfe) { + return null; + } + } + + /** + * Is the object of type Double?. + * @param number Object to type check. + * @return + */ + public static Boolean isDouble(Object number) { + return number instanceof Float || toDouble(number) != null; + } + /** * Convert the object to float. * @param number Object to convert to Float. @@ -59,7 +88,7 @@ public static Boolean compare(SegmentConditions condition, Comparable value1, Co public static Float toFloat(Object number) { try { return number instanceof Float ? ((Float) number) : Float.parseFloat((String) number); - } catch (NumberFormatException nfe) { + } catch (Exception nfe) { return null; } } @@ -81,7 +110,7 @@ public static Boolean isFloat(Object number) { public static Integer toInteger(Object number) { try { return number instanceof Integer ? ((Integer) number) : Integer.valueOf((String) number); - } catch (NumberFormatException nfe) { + } catch (Exception nfe) { return null; } } @@ -102,9 +131,9 @@ public static Boolean isInteger(Object number) { */ public static Boolean toBoolean(Object str) { try { - String value = ((String) str).toLowerCase(); - return Boolean.parseBoolean(value); - } catch (NumberFormatException nfe) { + return str instanceof Boolean ? ((Boolean) str) + : BooleanUtils.toBoolean((String) str); + } catch (Exception nfe) { return null; } } @@ -115,8 +144,32 @@ public static Boolean toBoolean(Object str) { * @return */ public static Boolean isBoolean(Object str) { - String value = ((String) str).toLowerCase(); - return Boolean.TRUE.toString().toLowerCase().equals(value) - || Boolean.FALSE.toString().toLowerCase().equals(value); + return str instanceof Boolean + || Boolean.TRUE.toString().equalsIgnoreCase(((String) str)) + || Boolean.FALSE.toString().equalsIgnoreCase(((String) str)); + } + + /** + * Convert the object to Semver. + * @param str Object to convert to Semver. + * @return + */ + public static ComparableVersion toSemver(Object str) { + try { + String value = SemanticVersioning.isSemver((String) str) + ? SemanticVersioning.removeSemver((String) str) : ((String) str); + return new ComparableVersion(value); + } catch (Exception nfe) { + return null; + } + } + + /** + * Is the object of type Semver?. + * @param str Object to type check. + * @return + */ + public static Boolean isSemver(Object str) { + return SemanticVersioning.isSemver((String) str); } } diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java new file mode 100644 index 00000000..d5fb03d2 --- /dev/null +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java @@ -0,0 +1,128 @@ +package com.flagsmith.flagengine.unit.segments; + +import com.flagsmith.flagengine.segments.SegmentConditionModel; +import com.flagsmith.flagengine.segments.SegmentEvaluator; +import com.flagsmith.flagengine.segments.constants.SegmentConditions; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class SegmentModelTest { + + @DataProvider(name = "conditionTestData") + public Object[][] conditionTestData() { + return new Object[][] { + new Object[] {SegmentConditions.EQUAL, "bar", "bar", true}, + new Object[] {SegmentConditions.EQUAL, "bar", "baz", false}, + new Object[] {SegmentConditions.EQUAL, 1, "1", true}, + new Object[] {SegmentConditions.EQUAL, 1, "2", false}, + new Object[] {SegmentConditions.EQUAL, true, "true", true}, + new Object[] {SegmentConditions.EQUAL, false, "false", true}, + new Object[] {SegmentConditions.EQUAL, false, "true", false}, + new Object[] {SegmentConditions.EQUAL, true, "false", false}, + new Object[] {SegmentConditions.EQUAL, 1.23, "1.23", true}, + new Object[] {SegmentConditions.EQUAL, 1.23, "4.56", false}, + new Object[] {SegmentConditions.GREATER_THAN, 2, "1", true}, + new Object[] {SegmentConditions.GREATER_THAN, 1, "1", false}, + new Object[] {SegmentConditions.GREATER_THAN, 0, "1", false}, + new Object[] {SegmentConditions.GREATER_THAN, 2.1, "2.0", true}, + new Object[] {SegmentConditions.GREATER_THAN, 2.1, "2.1", false}, + new Object[] {SegmentConditions.GREATER_THAN, 2.0, "2.1", false}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 2, "1", true}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 1, "1", true}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 0, "1", false}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 2.1, "2.0", true}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 2.1, "2.1", true}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 2.0, "2.1", false}, + new Object[] {SegmentConditions.LESS_THAN, 1, "2", true}, + new Object[] {SegmentConditions.LESS_THAN, 1, "1", false}, + new Object[] {SegmentConditions.LESS_THAN, 1, "0", false}, + new Object[] {SegmentConditions.LESS_THAN, 2.0, "2.1", true}, + new Object[] {SegmentConditions.LESS_THAN, 2.1, "2.1", false}, + new Object[] {SegmentConditions.LESS_THAN, 2.1, "2.0", false}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 1, "2", true}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 1, "1", true}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 1, "0", false}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 2.0, "2.1", true}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 2.1, "2.1", true}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 2.1, "2.0", false}, + new Object[] {SegmentConditions.NOT_EQUAL, "bar", "baz", true}, + new Object[] {SegmentConditions.NOT_EQUAL, "bar", "bar", false}, + new Object[] {SegmentConditions.NOT_EQUAL, 1, "2", true}, + new Object[] {SegmentConditions.NOT_EQUAL, 1, "1", false}, + new Object[] {SegmentConditions.NOT_EQUAL, true, "false", true}, + new Object[] {SegmentConditions.NOT_EQUAL, false, "true", true}, + new Object[] {SegmentConditions.NOT_EQUAL, false, "false", false}, + new Object[] {SegmentConditions.NOT_EQUAL, true, "true", false}, + new Object[] {SegmentConditions.CONTAINS, "bar", "b", true}, + new Object[] {SegmentConditions.CONTAINS, "bar", "bar", true}, + new Object[] {SegmentConditions.CONTAINS, "bar", "baz", false}, + new Object[] {SegmentConditions.NOT_CONTAINS, "bar", "b", false}, + new Object[] {SegmentConditions.NOT_CONTAINS, "bar", "bar", false}, + new Object[] {SegmentConditions.NOT_CONTAINS, "bar", "baz", true}, + new Object[] {SegmentConditions.REGEX, "foo", "[a-z]+", true}, + new Object[] {SegmentConditions.REGEX, "FOO", "[a-z]+", false}, + }; + } + + @Test(dataProvider = "conditionTestData") + public void testSegmentConditionMatchesTraitValue( + SegmentConditions condition, + Object traitValue, + String conditionValue, + Boolean expectedResponse) { + + SegmentConditionModel conditionModel = new SegmentConditionModel(); + conditionModel.setValue(conditionValue); + conditionModel.setOperator(condition); + conditionModel.setProperty_("foo"); + + Boolean actualResult = SegmentEvaluator.traitsMatchValue(conditionModel, traitValue); + + Assert.assertTrue(actualResult.equals(expectedResponse)); + } + + @DataProvider(name = "semverTestData") + public Object[][] semverTestData() { + return new Object[][] { + new Object[] {SegmentConditions.EQUAL, "1.0.0", "1.0.0:semver", true}, + new Object[] {SegmentConditions.EQUAL, "1.0.0", "1.0.1:semver", false}, + new Object[] {SegmentConditions.NOT_EQUAL, "1.0.0", "1.0.0:semver", false}, + new Object[] {SegmentConditions.NOT_EQUAL, "1.0.0", "1.0.1:semver", true}, + new Object[] {SegmentConditions.GREATER_THAN, "1.0.1", "1.0.0:semver", true}, + new Object[] {SegmentConditions.GREATER_THAN, "1.0.0", "1.0.0-beta:semver", true}, + new Object[] {SegmentConditions.GREATER_THAN, "1.0.1", "1.2.0:semver", false}, + new Object[] {SegmentConditions.GREATER_THAN, "1.0.1", "1.0.1:semver", false}, + new Object[] {SegmentConditions.GREATER_THAN, "1.2.4", "1.2.3-pre.2+build.4:semver", true}, + new Object[] {SegmentConditions.LESS_THAN, "1.0.0", "1.0.1:semver", true}, + new Object[] {SegmentConditions.LESS_THAN, "1.0.0", "1.0.0:semver", false}, + new Object[] {SegmentConditions.LESS_THAN, "1.0.1", "1.0.0:semver", false}, + new Object[] {SegmentConditions.LESS_THAN, "1.0.0-rc.2", "1.0.0-rc.3:semver", true}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, "1.0.1", "1.0.0:semver", true}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, "1.0.1", "1.2.0:semver", false}, + new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, "1.0.1", "1.0.1:semver", true}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, "1.0.0", "1.0.1:semver", true}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, "1.0.0", "1.0.0:semver", true}, + new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, "1.0.1", "1.0.0:semver", false}, + }; + } + + @Test(dataProvider = "semverTestData") + public void testSemverMatchesTraitValue( + SegmentConditions condition, + Object traitValue, + String conditionValue, + Boolean expectedResponse) { + + SegmentConditionModel conditionModel = new SegmentConditionModel(); + conditionModel.setValue(conditionValue); + conditionModel.setOperator(condition); + conditionModel.setProperty_("foo"); + + Boolean actualResult = SegmentEvaluator.traitsMatchValue(conditionModel, traitValue); + + Assert.assertTrue(actualResult.equals(expectedResponse)); + } + + +}