Skip to content

Commit cc95c81

Browse files
authored
Semver Support - Added tests and support for Semantic Versions (#65)
1 parent ed1dbdb commit cc95c81

File tree

5 files changed

+234
-16
lines changed

5 files changed

+234
-16
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@
9090
<artifactId>slf4j-api</artifactId>
9191
<version>${slf4j.version}</version>
9292
</dependency>
93+
<dependency>
94+
<groupId>org.apache.maven</groupId>
95+
<artifactId>maven-artifact</artifactId>
96+
<version>3.6.3</version>
97+
</dependency>
9398

9499
<!-- Test Dependencies-->
95100
<dependency>

src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,11 @@ private static Boolean traitsMatchSegmentCondition(List<TraitModel> identityTrai
154154
* @param value Trait value to compare with.
155155
* @return
156156
*/
157-
private static Boolean traitsMatchValue(SegmentConditionModel condition, Object value) {
157+
public static Boolean traitsMatchValue(SegmentConditionModel condition, Object value) {
158158
SegmentConditions operator = condition.getOperator();
159159
if (operator.equals(SegmentConditions.NOT_CONTAINS)) {
160+
return ((String) value).indexOf(condition.getValue()) == -1;
161+
} else if (operator.equals(SegmentConditions.CONTAINS)) {
160162
return ((String) value).indexOf(condition.getValue()) > -1;
161163
} else if (operator.equals(SegmentConditions.REGEX)) {
162164
Pattern pattern = Pattern.compile(condition.getValue());
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.flagsmith.flagengine.utils;
2+
3+
public class SemanticVersioning {
4+
5+
/**
6+
* Checks if the given string have `:semver` suffix or not
7+
* >>> is_semver("2.1.41-beta:semver")
8+
* True
9+
* >>> is_semver("2.1.41-beta")
10+
* False
11+
* @param version The version string.
12+
* @return
13+
*/
14+
public static Boolean isSemver(String version) {
15+
return version.endsWith(":semver");
16+
}
17+
18+
/**
19+
* Remove the semver suffix(i.e: last 7 characters) from the given value
20+
* >>> remove_semver_suffix("2.1.41-beta:semver")
21+
* '2.1.41-beta'
22+
* >>> remove_semver_suffix("2.1.41:semver")
23+
* '2.1.41'
24+
* @param version the version string to strip version from.
25+
* @return
26+
*/
27+
public static String removeSemver(String version) {
28+
return version.substring(0, version.length() - 7);
29+
}
30+
}

src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.flagsmith.flagengine.utils.types;
22

33
import com.flagsmith.flagengine.segments.constants.SegmentConditions;
4+
import com.flagsmith.flagengine.utils.SemanticVersioning;
5+
import org.apache.commons.lang3.BooleanUtils;
6+
import org.apache.maven.artifact.versioning.ComparableVersion;
47

58
public class TypeCasting {
69

@@ -13,15 +16,19 @@ public class TypeCasting {
1316
*/
1417
public static Boolean compare(SegmentConditions condition, Object value1, Object value2) {
1518

16-
if (TypeCasting.isInteger(value1) && TypeCasting.isInteger(value2)) {
17-
return compare(condition, TypeCasting.toInteger(value1), TypeCasting.toInteger(value2));
18-
} else if (TypeCasting.isFloat(value1) && TypeCasting.isFloat(value2)) {
19-
return compare(condition, TypeCasting.toFloat(value1), TypeCasting.toFloat(value2));
20-
} else if (TypeCasting.isBoolean(value1) && TypeCasting.isBoolean(value2)) {
21-
return compare(condition, TypeCasting.toBoolean(value1), TypeCasting.toBoolean(value2));
19+
if (isInteger(value1) && isInteger(value2)) {
20+
return compare(condition, toInteger(value1), toInteger(value2));
21+
} else if (isFloat(value1) && isFloat(value2)) {
22+
return compare(condition, toFloat(value1), toFloat(value2));
23+
} else if (isDouble(value1) && isDouble(value2)) {
24+
return compare(condition, toDouble(value1), toDouble(value2));
25+
} else if (isBoolean(value1) && isBoolean(value2)) {
26+
return compare(condition, toBoolean(value1), toBoolean(value2));
27+
} else if (isSemver(value2)) {
28+
return compare(condition, toSemver(value1), toSemver(value2));
2229
}
2330

24-
return value1.equals(value2);
31+
return compare(condition, (String) value1, (String) value2);
2532
}
2633

2734
/**
@@ -51,6 +58,28 @@ public static Boolean compare(SegmentConditions condition, Comparable value1, Co
5158
return value1.compareTo(value2) == 0;
5259
}
5360

61+
/**
62+
* Convert the object to Double.
63+
* @param number Object to convert to Double.
64+
* @return
65+
*/
66+
public static Double toDouble(Object number) {
67+
try {
68+
return number instanceof Double ? ((Double) number) : Double.parseDouble((String) number);
69+
} catch (Exception nfe) {
70+
return null;
71+
}
72+
}
73+
74+
/**
75+
* Is the object of type Double?.
76+
* @param number Object to type check.
77+
* @return
78+
*/
79+
public static Boolean isDouble(Object number) {
80+
return number instanceof Float || toDouble(number) != null;
81+
}
82+
5483
/**
5584
* Convert the object to float.
5685
* @param number Object to convert to Float.
@@ -59,7 +88,7 @@ public static Boolean compare(SegmentConditions condition, Comparable value1, Co
5988
public static Float toFloat(Object number) {
6089
try {
6190
return number instanceof Float ? ((Float) number) : Float.parseFloat((String) number);
62-
} catch (NumberFormatException nfe) {
91+
} catch (Exception nfe) {
6392
return null;
6493
}
6594
}
@@ -81,7 +110,7 @@ public static Boolean isFloat(Object number) {
81110
public static Integer toInteger(Object number) {
82111
try {
83112
return number instanceof Integer ? ((Integer) number) : Integer.valueOf((String) number);
84-
} catch (NumberFormatException nfe) {
113+
} catch (Exception nfe) {
85114
return null;
86115
}
87116
}
@@ -102,9 +131,9 @@ public static Boolean isInteger(Object number) {
102131
*/
103132
public static Boolean toBoolean(Object str) {
104133
try {
105-
String value = ((String) str).toLowerCase();
106-
return Boolean.parseBoolean(value);
107-
} catch (NumberFormatException nfe) {
134+
return str instanceof Boolean ? ((Boolean) str)
135+
: BooleanUtils.toBoolean((String) str);
136+
} catch (Exception nfe) {
108137
return null;
109138
}
110139
}
@@ -115,8 +144,32 @@ public static Boolean toBoolean(Object str) {
115144
* @return
116145
*/
117146
public static Boolean isBoolean(Object str) {
118-
String value = ((String) str).toLowerCase();
119-
return Boolean.TRUE.toString().toLowerCase().equals(value)
120-
|| Boolean.FALSE.toString().toLowerCase().equals(value);
147+
return str instanceof Boolean
148+
|| Boolean.TRUE.toString().equalsIgnoreCase(((String) str))
149+
|| Boolean.FALSE.toString().equalsIgnoreCase(((String) str));
150+
}
151+
152+
/**
153+
* Convert the object to Semver.
154+
* @param str Object to convert to Semver.
155+
* @return
156+
*/
157+
public static ComparableVersion toSemver(Object str) {
158+
try {
159+
String value = SemanticVersioning.isSemver((String) str)
160+
? SemanticVersioning.removeSemver((String) str) : ((String) str);
161+
return new ComparableVersion(value);
162+
} catch (Exception nfe) {
163+
return null;
164+
}
165+
}
166+
167+
/**
168+
* Is the object of type Semver?.
169+
* @param str Object to type check.
170+
* @return
171+
*/
172+
public static Boolean isSemver(Object str) {
173+
return SemanticVersioning.isSemver((String) str);
121174
}
122175
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.flagsmith.flagengine.unit.segments;
2+
3+
import com.flagsmith.flagengine.segments.SegmentConditionModel;
4+
import com.flagsmith.flagengine.segments.SegmentEvaluator;
5+
import com.flagsmith.flagengine.segments.constants.SegmentConditions;
6+
import org.testng.Assert;
7+
import org.testng.annotations.DataProvider;
8+
import org.testng.annotations.Test;
9+
10+
public class SegmentModelTest {
11+
12+
@DataProvider(name = "conditionTestData")
13+
public Object[][] conditionTestData() {
14+
return new Object[][] {
15+
new Object[] {SegmentConditions.EQUAL, "bar", "bar", true},
16+
new Object[] {SegmentConditions.EQUAL, "bar", "baz", false},
17+
new Object[] {SegmentConditions.EQUAL, 1, "1", true},
18+
new Object[] {SegmentConditions.EQUAL, 1, "2", false},
19+
new Object[] {SegmentConditions.EQUAL, true, "true", true},
20+
new Object[] {SegmentConditions.EQUAL, false, "false", true},
21+
new Object[] {SegmentConditions.EQUAL, false, "true", false},
22+
new Object[] {SegmentConditions.EQUAL, true, "false", false},
23+
new Object[] {SegmentConditions.EQUAL, 1.23, "1.23", true},
24+
new Object[] {SegmentConditions.EQUAL, 1.23, "4.56", false},
25+
new Object[] {SegmentConditions.GREATER_THAN, 2, "1", true},
26+
new Object[] {SegmentConditions.GREATER_THAN, 1, "1", false},
27+
new Object[] {SegmentConditions.GREATER_THAN, 0, "1", false},
28+
new Object[] {SegmentConditions.GREATER_THAN, 2.1, "2.0", true},
29+
new Object[] {SegmentConditions.GREATER_THAN, 2.1, "2.1", false},
30+
new Object[] {SegmentConditions.GREATER_THAN, 2.0, "2.1", false},
31+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 2, "1", true},
32+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 1, "1", true},
33+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 0, "1", false},
34+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 2.1, "2.0", true},
35+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 2.1, "2.1", true},
36+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, 2.0, "2.1", false},
37+
new Object[] {SegmentConditions.LESS_THAN, 1, "2", true},
38+
new Object[] {SegmentConditions.LESS_THAN, 1, "1", false},
39+
new Object[] {SegmentConditions.LESS_THAN, 1, "0", false},
40+
new Object[] {SegmentConditions.LESS_THAN, 2.0, "2.1", true},
41+
new Object[] {SegmentConditions.LESS_THAN, 2.1, "2.1", false},
42+
new Object[] {SegmentConditions.LESS_THAN, 2.1, "2.0", false},
43+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 1, "2", true},
44+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 1, "1", true},
45+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 1, "0", false},
46+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 2.0, "2.1", true},
47+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 2.1, "2.1", true},
48+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, 2.1, "2.0", false},
49+
new Object[] {SegmentConditions.NOT_EQUAL, "bar", "baz", true},
50+
new Object[] {SegmentConditions.NOT_EQUAL, "bar", "bar", false},
51+
new Object[] {SegmentConditions.NOT_EQUAL, 1, "2", true},
52+
new Object[] {SegmentConditions.NOT_EQUAL, 1, "1", false},
53+
new Object[] {SegmentConditions.NOT_EQUAL, true, "false", true},
54+
new Object[] {SegmentConditions.NOT_EQUAL, false, "true", true},
55+
new Object[] {SegmentConditions.NOT_EQUAL, false, "false", false},
56+
new Object[] {SegmentConditions.NOT_EQUAL, true, "true", false},
57+
new Object[] {SegmentConditions.CONTAINS, "bar", "b", true},
58+
new Object[] {SegmentConditions.CONTAINS, "bar", "bar", true},
59+
new Object[] {SegmentConditions.CONTAINS, "bar", "baz", false},
60+
new Object[] {SegmentConditions.NOT_CONTAINS, "bar", "b", false},
61+
new Object[] {SegmentConditions.NOT_CONTAINS, "bar", "bar", false},
62+
new Object[] {SegmentConditions.NOT_CONTAINS, "bar", "baz", true},
63+
new Object[] {SegmentConditions.REGEX, "foo", "[a-z]+", true},
64+
new Object[] {SegmentConditions.REGEX, "FOO", "[a-z]+", false},
65+
};
66+
}
67+
68+
@Test(dataProvider = "conditionTestData")
69+
public void testSegmentConditionMatchesTraitValue(
70+
SegmentConditions condition,
71+
Object traitValue,
72+
String conditionValue,
73+
Boolean expectedResponse) {
74+
75+
SegmentConditionModel conditionModel = new SegmentConditionModel();
76+
conditionModel.setValue(conditionValue);
77+
conditionModel.setOperator(condition);
78+
conditionModel.setProperty_("foo");
79+
80+
Boolean actualResult = SegmentEvaluator.traitsMatchValue(conditionModel, traitValue);
81+
82+
Assert.assertTrue(actualResult.equals(expectedResponse));
83+
}
84+
85+
@DataProvider(name = "semverTestData")
86+
public Object[][] semverTestData() {
87+
return new Object[][] {
88+
new Object[] {SegmentConditions.EQUAL, "1.0.0", "1.0.0:semver", true},
89+
new Object[] {SegmentConditions.EQUAL, "1.0.0", "1.0.1:semver", false},
90+
new Object[] {SegmentConditions.NOT_EQUAL, "1.0.0", "1.0.0:semver", false},
91+
new Object[] {SegmentConditions.NOT_EQUAL, "1.0.0", "1.0.1:semver", true},
92+
new Object[] {SegmentConditions.GREATER_THAN, "1.0.1", "1.0.0:semver", true},
93+
new Object[] {SegmentConditions.GREATER_THAN, "1.0.0", "1.0.0-beta:semver", true},
94+
new Object[] {SegmentConditions.GREATER_THAN, "1.0.1", "1.2.0:semver", false},
95+
new Object[] {SegmentConditions.GREATER_THAN, "1.0.1", "1.0.1:semver", false},
96+
new Object[] {SegmentConditions.GREATER_THAN, "1.2.4", "1.2.3-pre.2+build.4:semver", true},
97+
new Object[] {SegmentConditions.LESS_THAN, "1.0.0", "1.0.1:semver", true},
98+
new Object[] {SegmentConditions.LESS_THAN, "1.0.0", "1.0.0:semver", false},
99+
new Object[] {SegmentConditions.LESS_THAN, "1.0.1", "1.0.0:semver", false},
100+
new Object[] {SegmentConditions.LESS_THAN, "1.0.0-rc.2", "1.0.0-rc.3:semver", true},
101+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, "1.0.1", "1.0.0:semver", true},
102+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, "1.0.1", "1.2.0:semver", false},
103+
new Object[] {SegmentConditions.GREATER_THAN_INCLUSIVE, "1.0.1", "1.0.1:semver", true},
104+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, "1.0.0", "1.0.1:semver", true},
105+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, "1.0.0", "1.0.0:semver", true},
106+
new Object[] {SegmentConditions.LESS_THAN_INCLUSIVE, "1.0.1", "1.0.0:semver", false},
107+
};
108+
}
109+
110+
@Test(dataProvider = "semverTestData")
111+
public void testSemverMatchesTraitValue(
112+
SegmentConditions condition,
113+
Object traitValue,
114+
String conditionValue,
115+
Boolean expectedResponse) {
116+
117+
SegmentConditionModel conditionModel = new SegmentConditionModel();
118+
conditionModel.setValue(conditionValue);
119+
conditionModel.setOperator(condition);
120+
conditionModel.setProperty_("foo");
121+
122+
Boolean actualResult = SegmentEvaluator.traitsMatchValue(conditionModel, traitValue);
123+
124+
Assert.assertTrue(actualResult.equals(expectedResponse));
125+
}
126+
127+
128+
}

0 commit comments

Comments
 (0)