-
Notifications
You must be signed in to change notification settings - Fork 1
/
SplitProvider.java
206 lines (185 loc) · 7.41 KB
/
SplitProvider.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package io.split.openfeature;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.exceptions.ParseError;
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
import io.split.client.SplitClient;
import io.split.openfeature.utils.Serialization;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class SplitProvider implements FeatureProvider {
private static final String NAME = "Split";
private final SplitClient client;
public SplitProvider(SplitClient splitClient) {
client = splitClient;
}
public SplitProvider(String apiKey) {
SplitModule splitModule = SplitModule.getInstance();
if (splitModule.getClient() == null) {
splitModule.init(apiKey);
}
client = splitModule.getClient();
}
@Override
public Metadata getMetadata() {
return () -> NAME;
}
@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultTreatment, EvaluationContext evaluationContext) {
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
// if treatment is "on" or "true" we treat that as true
// if it is "off" or "false" we treat it as false
// if it is some other value we throw an error (sdk will catch it and throw default treatment)
boolean value;
if (Boolean.parseBoolean(evaluated) || evaluated.equals("on")) {
value = true;
} else if (evaluated.equalsIgnoreCase("false") || evaluated.equals("off")) {
value = false;
} else {
throw new ParseError();
}
return constructProviderEvaluation(value, evaluated);
} catch (OpenFeatureError e) {
throw e;
} catch (Exception e) {
throw new GeneralError("Error getting boolean evaluation", e);
}
}
@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultTreatment, EvaluationContext evaluationContext) {
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
return constructProviderEvaluation(evaluated, evaluated);
} catch (OpenFeatureError e) {
throw e;
} catch (Exception e) {
throw new GeneralError("Error getting String evaluation", e);
}
}
@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultTreatment, EvaluationContext evaluationContext) {
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
Integer value = Integer.valueOf(evaluated);
return constructProviderEvaluation(value, evaluated);
} catch (OpenFeatureError e) {
throw e;
} catch (NumberFormatException e) {
throw new ParseError();
} catch (Exception e) {
throw new GeneralError("Error getting Integer evaluation", e);
}
}
@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultTreatment, EvaluationContext evaluationContext) {
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
Double value = Double.valueOf(evaluated);
return constructProviderEvaluation(value, evaluated);
} catch (OpenFeatureError e) {
throw e;
} catch (NumberFormatException e) {
throw new ParseError();
} catch (Exception e) {
throw new GeneralError("Error getting Double evaluation", e);
}
}
@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultTreatment, EvaluationContext evaluationContext) {
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
Map<String, Object> rawMap = Serialization.stringToMap(evaluated);
Value value = mapToValue(rawMap);
return constructProviderEvaluation(value, evaluated);
} catch (OpenFeatureError e) {
throw e;
} catch (Exception e) {
throw new GeneralError("Error getting Object evaluation", e);
}
}
public Map<String, Object> transformContext(EvaluationContext context) {
return context.asObjectMap();
}
private String evaluateTreatment(String key, EvaluationContext evaluationContext) {
String id = evaluationContext.getTargetingKey();
if (id == null || id.isEmpty()) {
// targeting key is always required
throw new TargetingKeyMissingError();
}
Map<String, Object> attributes = transformContext(evaluationContext);
return client.getTreatment(id, key, attributes);
}
private boolean noTreatment(String treatment) {
return treatment == null || treatment.isEmpty() || treatment.equals("control");
}
private <T> ProviderEvaluation<T> constructProviderEvaluation(T value, String variant) {
return constructProviderEvaluation(value, variant, Reason.TARGETING_MATCH, null);
}
private <T> ProviderEvaluation<T> constructProviderEvaluation(T value, String variant, Reason reason, ErrorCode errorCode) {
ProviderEvaluation.ProviderEvaluationBuilder<T> builder = ProviderEvaluation.builder();
return builder
.value(value)
.reason(reason.name())
.variant(variant)
.errorCode(errorCode)
.build();
}
/**
* Turn map String->Object into a Value.
* @param map a Map String->Object, where object is NOT Value or Structure
* @return Value representing the map passed in
*/
private Value mapToValue(Map<String, Object> map) {
return new Value(
new MutableStructure(
map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))));
}
private Value objectToValue(Object object) {
if (object instanceof String) {
// try to parse as instant, otherwise use as string
try {
return new Value(Instant.parse((String) object));
} catch (DateTimeParseException e) {
return new Value((String) object);
}
} else if (object instanceof List) {
// need to translate each elem in list to a value
return new Value(((List<Object>) object).stream().map(this::objectToValue).collect(Collectors.toList()));
} else if (object instanceof Map) {
return mapToValue((Map<String, Object>) object);
} else {
try {
return new Value(object);
} catch (InstantiationException e) {
throw new ClassCastException("Could not cast Object to Value");
}
}
}
}