Skip to content

Commit f823299

Browse files
authored
feat: support Number, SemVer, DateTime condition (#7)
* feat: support Number, SemVer, DateTime condition
1 parent 0c26231 commit f823299

File tree

12 files changed

+550
-104
lines changed

12 files changed

+550
-104
lines changed

pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.featureprobe</groupId>
88
<artifactId>server-sdk-java</artifactId>
9-
<version>1.1.1</version>
9+
<version>1.2.0</version>
1010
<name>server-sdk-java</name>
1111
<url>https://github.com/FeatureProbe/server-sdk-java</url>
1212
<description>FeatureProbe Server Side SDK for Java</description>
@@ -45,6 +45,7 @@
4545
<version.logback>1.2.11</version.logback>
4646
<version.jackson>2.13.3</version.jackson>
4747
<version.commons>3.11</version.commons>
48+
<version.maven-artifact>3.8.5</version.maven-artifact>
4849
<groovy.version>3.0.9</groovy.version>
4950
</properties>
5051

@@ -91,6 +92,11 @@
9192
<artifactId>jackson-databind</artifactId>
9293
<version>${version.jackson}</version>
9394
</dependency>
95+
<dependency>
96+
<groupId>org.apache.maven</groupId>
97+
<artifactId>maven-artifact</artifactId>
98+
<version>${version.maven-artifact}</version>
99+
</dependency>
94100

95101
<dependency>
96102
<groupId>org.spockframework</groupId>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.featureprobe.sdk.server;
2+
3+
import java.util.List;
4+
5+
@FunctionalInterface
6+
public interface DatetimeMatcher {
7+
8+
/**
9+
* @throws NumberFormatException if any string in {@code objects} could not been parsed before the first match
10+
*/
11+
boolean match(long target, List<String> objects);
12+
13+
}

src/main/java/com/featureprobe/sdk/server/FPUser.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package com.featureprobe.sdk.server;
22

33
import java.util.HashMap;
4+
import java.util.Map;
45

56
public class FPUser {
67

78
private String key;
89

9-
private HashMap<String, String> attrs = new HashMap<>();
10+
private Map<String, String> attrs = new HashMap<>();
1011

1112

1213
public void with(String key, String value) {
@@ -26,11 +27,16 @@ public String getKey() {
2627
return key;
2728
}
2829

29-
public HashMap<String, String> getAttrs() {
30+
public Map<String, String> getAttrs() {
3031
return attrs;
3132
}
3233

33-
public void setAttrs(HashMap<String, String> attrs) {
34+
public void setAttrs(Map<String, String> attrs) {
3435
this.attrs = attrs;
3536
}
37+
38+
public String getAttr(String key) {
39+
return attrs.get(key);
40+
}
41+
3642
}

src/main/java/com/featureprobe/sdk/server/Loggers.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
55

6-
abstract class Loggers {
6+
public abstract class Loggers {
77

88
private Loggers() {
99
}
1010

11-
static final String BASE_LOGGER_NAME = FeatureProbe.class.getName();
12-
static final String SYNCHRONIZER_LOGGER_NAME = BASE_LOGGER_NAME + "-Synchronizer";
13-
static final String EVENT_LOGGER_NAME = BASE_LOGGER_NAME + "-Event";
11+
private static final String BASE_LOGGER_NAME = FeatureProbe.class.getName();
12+
private static final String SYNCHRONIZER_LOGGER_NAME = BASE_LOGGER_NAME + "-Synchronizer";
13+
private static final String EVENT_LOGGER_NAME = BASE_LOGGER_NAME + "-Event";
14+
private static final String EVALUATOR_LOGGER_NAME = BASE_LOGGER_NAME + "-Evaluator";
1415

15-
static final Logger MAIN = LoggerFactory.getLogger(BASE_LOGGER_NAME);
16-
static final Logger SYNCHRONIZER = LoggerFactory.getLogger(SYNCHRONIZER_LOGGER_NAME);
17-
static final Logger EVENT = LoggerFactory.getLogger(EVENT_LOGGER_NAME);
16+
public static final Logger MAIN = LoggerFactory.getLogger(BASE_LOGGER_NAME);
17+
public static final Logger SYNCHRONIZER = LoggerFactory.getLogger(SYNCHRONIZER_LOGGER_NAME);
18+
public static final Logger EVENT = LoggerFactory.getLogger(EVENT_LOGGER_NAME);
19+
public static final Logger EVALUATOR = LoggerFactory.getLogger(EVALUATOR_LOGGER_NAME);
1820

1921
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.featureprobe.sdk.server;
2+
3+
import java.util.List;
4+
5+
@FunctionalInterface
6+
public interface NumberMatcher {
7+
8+
/**
9+
* @throws NumberFormatException if any string in {@code objects} could not been parsed before the first match
10+
*/
11+
boolean match(double target, List<String> objects);
12+
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.featureprobe.sdk.server;
2+
3+
import org.apache.maven.artifact.versioning.ComparableVersion;
4+
5+
import java.util.List;
6+
7+
@FunctionalInterface
8+
public interface SemverMatcher {
9+
10+
boolean match(ComparableVersion target, List<String> objects);
11+
12+
}

src/main/java/com/featureprobe/sdk/server/model/Condition.java

Lines changed: 138 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
package com.featureprobe.sdk.server.model;
22

3-
import com.featureprobe.sdk.server.FPUser;
4-
import com.featureprobe.sdk.server.StringMatcher;
5-
import com.featureprobe.sdk.server.SegmentMatcher;
3+
import com.featureprobe.sdk.server.*;
64
import org.apache.commons.lang3.StringUtils;
5+
import org.apache.maven.artifact.versioning.ComparableVersion;
6+
import org.slf4j.Logger;
77

8-
import java.util.HashMap;
9-
import java.util.List;
10-
import java.util.Map;
11-
import java.util.Objects;
8+
import java.util.*;
129
import java.util.regex.Pattern;
1310

1411
public final class Condition {
1512

13+
private static final Logger logger = Loggers.EVALUATOR;
14+
1615
private ConditionType type;
1716

1817
private String subject;
@@ -21,13 +20,20 @@ public final class Condition {
2120

2221
private List<String> objects;
2322

24-
private static final Map<PredicateType, StringMatcher> stringMatchers =
25-
new HashMap<>(PredicateType.values().length);
23+
private static final long MILLISECONDS_IN_ONE_SEC = 1000;
24+
25+
private static final Map<PredicateType, StringMatcher> stringMatchers = new EnumMap<>(PredicateType.class);
26+
27+
private static final Map<PredicateType, SegmentMatcher> segmentMatchers = new EnumMap<>(PredicateType.class);
28+
29+
private static final Map<PredicateType, DatetimeMatcher> datetimeMatchers = new EnumMap<>(PredicateType.class);
30+
31+
private static final Map<PredicateType, NumberMatcher> numberMatchers = new EnumMap<>(PredicateType.class);
2632

27-
private static final Map<PredicateType, SegmentMatcher> segmentMatchers =
28-
new HashMap<>(PredicateType.values().length);
33+
private static final Map<PredicateType, SemverMatcher> semverMatchers = new EnumMap<>(PredicateType.class);
2934

3035
static {
36+
3137
stringMatchers.put(PredicateType.IS_ONE_OF, (target, objects) ->
3238
objects.contains(target));
3339
stringMatchers.put(PredicateType.ENDS_WITH, (target, objects) ->
@@ -50,43 +56,149 @@ public final class Condition {
5056
objects.stream().noneMatch(s -> Pattern.compile(s).matcher(target).find()));
5157

5258
segmentMatchers.put(PredicateType.IS_IN, (user, segments, objects) ->
53-
objects.stream().anyMatch(s -> segments.get(s).contains(user, segments)));
59+
objects.stream().anyMatch(s -> segments.get(s).contains(user, segments)));
5460
segmentMatchers.put(PredicateType.IS_NOT_IN, (user, segments, objects) ->
5561
objects.stream().noneMatch(s -> segments.get(s).contains(user, segments)));
5662

63+
datetimeMatchers.put(PredicateType.AFTER, ((target, objects) ->
64+
objects.stream().map(Long::parseLong).anyMatch(o -> target >= o)));
65+
datetimeMatchers.put(PredicateType.BEFORE, ((target, objects) ->
66+
objects.stream().map(Long::parseLong).anyMatch(o -> target < o)));
67+
68+
numberMatchers.put(PredicateType.EQUAL_TO, ((target, objects) ->
69+
objects.stream().map(Double::parseDouble).anyMatch(o -> target == o)));
70+
numberMatchers.put(PredicateType.NOT_EQUAL_TO, ((target, objects) ->
71+
objects.stream().map(Double::parseDouble).noneMatch(o -> target == o)));
72+
numberMatchers.put(PredicateType.GREATER_THAN, ((target, objects) ->
73+
objects.stream().map(Double::parseDouble).anyMatch(o -> target > o)));
74+
numberMatchers.put(PredicateType.GREATER_OR_EQUAL, ((target, objects) ->
75+
objects.stream().map(Double::parseDouble).anyMatch(o -> target >= o)));
76+
numberMatchers.put(PredicateType.LESS_THAN, ((target, objects) ->
77+
objects.stream().map(Double::parseDouble).anyMatch(o -> target < o)));
78+
numberMatchers.put(PredicateType.LESS_OR_EQUAL, ((target, objects) ->
79+
objects.stream().map(Double::parseDouble).anyMatch(o -> target <= o)));
80+
81+
semverMatchers.put(PredicateType.EQUAL_TO, ((target, objects) ->
82+
objects.stream().filter(Objects::nonNull).map(ComparableVersion::new).anyMatch(t -> target.compareTo(t) == 0)));
83+
semverMatchers.put(PredicateType.NOT_EQUAL_TO, ((target, objects) ->
84+
objects.stream().filter(Objects::nonNull).map(ComparableVersion::new).noneMatch(t -> target.compareTo(t) == 0)));
85+
semverMatchers.put(PredicateType.GREATER_THAN, ((target, objects) ->
86+
objects.stream().filter(Objects::nonNull).map(ComparableVersion::new).anyMatch(t -> target.compareTo(t) > 0)));
87+
semverMatchers.put(PredicateType.GREATER_OR_EQUAL, ((target, objects) ->
88+
objects.stream().filter(Objects::nonNull).map(ComparableVersion::new).anyMatch(t -> target.compareTo(t) >= 0)));
89+
semverMatchers.put(PredicateType.LESS_THAN, ((target, objects) ->
90+
objects.stream().filter(Objects::nonNull).map(ComparableVersion::new).anyMatch(t -> target.compareTo(t) < 0)));
91+
semverMatchers.put(PredicateType.LESS_OR_EQUAL, ((target, objects) ->
92+
objects.stream().filter(Objects::nonNull).map(ComparableVersion::new).anyMatch(t -> target.compareTo(t) <= 0)));
93+
5794
}
5895

5996
public boolean matchObjects(FPUser user, Map<String, Segment> segments) {
6097
switch (type) {
6198
case STRING:
62-
String subjectValue = user.getAttrs().get(subject);
63-
if (StringUtils.isBlank(subjectValue)) {
64-
return false;
65-
}
66-
return matchStringCondition(subjectValue);
99+
return matchStringCondition(user);
100+
67101
case SEGMENT:
68102
return matchSegmentCondition(user, segments);
69-
case DATE:
70-
// TODO
103+
104+
case DATETIME:
105+
return matchDatetimeCondition(user);
106+
107+
case NUMBER:
108+
return matchNumberCondition(user);
109+
110+
case SEMVER:
111+
return matchSemverCondition(user);
112+
71113
default:
72114
return false;
73115
}
74116
}
75117

76-
private boolean matchStringCondition(String subjectValue) {
118+
private boolean matchStringCondition(FPUser user) {
119+
String subjectValue = user.getAttr(subject);
120+
if (StringUtils.isBlank(subjectValue)) {
121+
return false;
122+
}
123+
77124
StringMatcher stringMatcher = stringMatchers.get(this.predicate);
78-
if (Objects.nonNull(stringMatcher)) {
79-
return stringMatcher.match(subjectValue, this.objects);
125+
if (Objects.isNull(stringMatcher)) {
126+
return false;
80127
}
81-
return false;
128+
129+
return stringMatcher.match(subjectValue, this.objects);
82130
}
83131

84132
private boolean matchSegmentCondition(FPUser user, Map<String, Segment> segments) {
85133
SegmentMatcher segmentMatcher = segmentMatchers.get(this.predicate);
86-
if (Objects.nonNull(segmentMatcher)) {
87-
return segmentMatcher.match(user, segments, this.objects);
134+
if (Objects.isNull(segmentMatcher)) {
135+
return false;
136+
}
137+
138+
return segmentMatcher.match(user, segments, this.objects);
139+
}
140+
141+
private boolean matchDatetimeCondition(FPUser user) {
142+
DatetimeMatcher datetimeMatcher = datetimeMatchers.get(this.predicate);
143+
if (Objects.isNull(datetimeMatcher)) {
144+
return false;
145+
}
146+
147+
String customValue = user.getAttr(this.subject);
148+
long cv;
149+
try {
150+
cv = StringUtils.isBlank(customValue)
151+
? System.currentTimeMillis() / MILLISECONDS_IN_ONE_SEC
152+
: Long.parseLong(customValue);
153+
} catch (NumberFormatException e) {
154+
logger.error("User attribute type mismatch. attribute value: {}, target type long", customValue);
155+
return false;
156+
}
157+
try {
158+
return datetimeMatcher.match(cv, objects);
159+
} catch (NumberFormatException e) {
160+
logger.error("Met a string that cannot be parsed to long in Condition.objects: {}", e.getMessage());
161+
return false;
162+
}
163+
}
164+
165+
private boolean matchNumberCondition(FPUser user) {
166+
NumberMatcher numberMatcher = numberMatchers.get(this.predicate);
167+
if (Objects.isNull(numberMatcher)) {
168+
return false;
169+
}
170+
171+
String customValue = user.getAttr(this.subject);
172+
if (StringUtils.isBlank(customValue)) {
173+
return false;
174+
}
175+
double cv;
176+
try {
177+
cv = Double.parseDouble(customValue);
178+
} catch (NumberFormatException e) {
179+
logger.error("User attribute type mismatch. attribute value : {}, target type double", customValue);
180+
return false;
181+
}
182+
try {
183+
return numberMatcher.match(cv, this.objects);
184+
} catch (NumberFormatException e) {
185+
logger.error("Met a string that cannot be parsed to double in Condition.objects: {}", e.getMessage());
186+
return false;
187+
}
188+
}
189+
190+
private boolean matchSemverCondition(FPUser user) {
191+
SemverMatcher semverMatcher = semverMatchers.get(this.predicate);
192+
if (Objects.isNull(semverMatcher)) {
193+
return false;
194+
}
195+
196+
String customValue = user.getAttr(this.subject);
197+
if (StringUtils.isBlank(customValue)) {
198+
return false;
88199
}
89-
return false;
200+
ComparableVersion cv = new ComparableVersion(customValue);
201+
return semverMatcher.match(cv, this.objects);
90202
}
91203

92204
public ConditionType getType() {

src/main/java/com/featureprobe/sdk/server/model/ConditionType.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import com.fasterxml.jackson.annotation.JsonValue;
55

66
import java.util.Arrays;
7-
import java.util.HashMap;
87
import java.util.Map;
98
import java.util.stream.Collectors;
109

1110
public enum ConditionType {
1211
STRING("string"),
1312
SEGMENT("segment"),
14-
DATE("date");
13+
DATETIME("datetime"),
14+
NUMBER("number"),
15+
SEMVER("semver");
1516

1617
private final String value;
1718

0 commit comments

Comments
 (0)