Skip to content

Commit 4d04281

Browse files
Support serializing FlagConfigResponse
Previous implementation didn't have a serializer `EppoValueSerializer` should only write one value: Previous implemenation could write multiple values (would always write a `null`) for anything that isn't an `ARRAY_OF_STRING` type
1 parent 213d5c8 commit 4d04281

File tree

6 files changed

+317
-16
lines changed

6 files changed

+317
-16
lines changed

src/main/java/cloud/eppo/api/EppoValue.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import java.util.Objects;
77

88
public class EppoValue {
9-
protected final EppoValueType type;
9+
public final EppoValueType type;
1010
protected Boolean boolValue;
1111
protected Double doubleValue;
1212
protected String stringValue;

src/main/java/cloud/eppo/ufc/dto/adapters/EppoModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class EppoModule {
1010
public static SimpleModule eppoModule() {
1111
SimpleModule module = new SimpleModule();
1212
module.addDeserializer(FlagConfigResponse.class, new FlagConfigResponseDeserializer());
13+
module.addSerializer(FlagConfigResponse.class, new FlagConfigResponseSerializer());
1314
module.addDeserializer(
1415
BanditParametersResponse.class, new BanditParametersResponseDeserializer());
1516
module.addDeserializer(EppoValue.class, new EppoValueDeserializer());

src/main/java/cloud/eppo/ufc/dto/adapters/EppoValueSerializer.java

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,23 @@ public EppoValueSerializer() {
1818
@Override
1919
public void serialize(EppoValue src, JsonGenerator jgen, SerializerProvider provider)
2020
throws IOException {
21-
if (src.isBoolean()) {
22-
jgen.writeBoolean(src.booleanValue());
23-
}
24-
if (src.isNumeric()) {
25-
jgen.writeNumber(src.doubleValue());
26-
}
27-
if (src.isString()) {
28-
jgen.writeString(src.stringValue());
29-
}
30-
if (src.isStringArray()) {
31-
String[] arr = src.stringArrayValue().toArray(new String[0]);
32-
jgen.writeArray(arr, 0, arr.length);
33-
} else {
34-
jgen.writeNull();
21+
switch (src.type) {
22+
case NULL:
23+
jgen.writeNull();
24+
break;
25+
case BOOLEAN:
26+
jgen.writeBoolean(src.booleanValue());
27+
break;
28+
case NUMBER:
29+
jgen.writeNumber(src.doubleValue());
30+
break;
31+
case STRING:
32+
jgen.writeString(src.stringValue());
33+
break;
34+
case ARRAY_OF_STRING:
35+
String[] arr = src.stringArrayValue().toArray(new String[0]);
36+
jgen.writeArray(arr, 0, arr.length);
37+
break;
3538
}
3639
}
3740
}

src/main/java/cloud/eppo/ufc/dto/adapters/FlagConfigResponseDeserializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private Set<TargetingRule> deserializeTargetingRules(JsonNode jsonNode) {
135135
Set<TargetingCondition> conditions = new HashSet<>();
136136
for (JsonNode conditionNode : ruleNode.get("conditions")) {
137137
String attribute = conditionNode.get("attribute").asText();
138-
String operatorKey = conditionNode.get("operator").asText();
138+
String operatorKey = conditionNode.get("operator").asText(null);
139139
OperatorType operator = OperatorType.fromString(operatorKey);
140140
if (operator == null) {
141141
log.warn("Unknown operator \"{}\"", operatorKey);
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
package cloud.eppo.ufc.dto.adapters;
2+
3+
import static cloud.eppo.Utils.getISODate;
4+
import static cloud.eppo.Utils.parseUtcISODateNode;
5+
6+
import com.fasterxml.jackson.core.JacksonException;
7+
import com.fasterxml.jackson.core.JsonGenerator;
8+
import com.fasterxml.jackson.core.JsonParser;
9+
import com.fasterxml.jackson.databind.DeserializationContext;
10+
import com.fasterxml.jackson.databind.JsonNode;
11+
import com.fasterxml.jackson.databind.SerializerProvider;
12+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
13+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
14+
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
18+
import java.io.IOException;
19+
import java.util.ArrayList;
20+
import java.util.Date;
21+
import java.util.HashMap;
22+
import java.util.HashSet;
23+
import java.util.Iterator;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.function.BiConsumer;
29+
30+
import cloud.eppo.api.EppoValue;
31+
import cloud.eppo.model.ShardRange;
32+
import cloud.eppo.ufc.dto.Allocation;
33+
import cloud.eppo.ufc.dto.BanditFlagVariation;
34+
import cloud.eppo.ufc.dto.BanditReference;
35+
import cloud.eppo.ufc.dto.FlagConfig;
36+
import cloud.eppo.ufc.dto.FlagConfigResponse;
37+
import cloud.eppo.ufc.dto.OperatorType;
38+
import cloud.eppo.ufc.dto.Shard;
39+
import cloud.eppo.ufc.dto.Split;
40+
import cloud.eppo.ufc.dto.TargetingCondition;
41+
import cloud.eppo.ufc.dto.TargetingRule;
42+
import cloud.eppo.ufc.dto.Variation;
43+
import cloud.eppo.ufc.dto.VariationType;
44+
45+
/**
46+
* Hand-rolled serializer so that we don't rely on annotations and method names, which can be
47+
* unreliable when ProGuard minification is in-use and not configured to protect
48+
* JSON-serialization-related classes and annotations.
49+
*/
50+
public class FlagConfigResponseSerializer extends StdSerializer<FlagConfigResponse> {
51+
private static final Logger log = LoggerFactory.getLogger(FlagConfigResponseSerializer.class);
52+
private final EppoValueSerializer eppoValueSerializer = new EppoValueSerializer();
53+
54+
protected FlagConfigResponseSerializer(Class<FlagConfigResponse> vc) {
55+
super(vc);
56+
}
57+
58+
public FlagConfigResponseSerializer() {
59+
this(null);
60+
}
61+
62+
@Override
63+
public void serialize(FlagConfigResponse src, JsonGenerator jgen, SerializerProvider provider)
64+
throws IOException, JacksonException {
65+
jgen.writeStartObject();
66+
final FlagConfigResponse.Format format = src.getFormat();
67+
if (format != null) {
68+
jgen.writeStringField("format", format.name());
69+
}
70+
final Map<String, FlagConfig> flags = src.getFlags();
71+
if (flags != null) {
72+
jgen.writeFieldName("flags");
73+
jgen.writeStartObject();
74+
for (Map.Entry<String, FlagConfig> entry : src.getFlags().entrySet()) {
75+
jgen.writeFieldName(entry.getKey());
76+
serializeFlag(entry.getValue(), jgen, provider);
77+
}
78+
jgen.writeEndObject();
79+
}
80+
final Map<String, BanditReference> banditReferences = src.getBanditReferences();
81+
if (banditReferences != null) {
82+
jgen.writeFieldName("banditReferences");
83+
jgen.writeStartObject();
84+
for (Map.Entry<String, BanditReference> entry : banditReferences.entrySet()) {
85+
jgen.writeFieldName(entry.getKey());
86+
serializeBanditReference(entry.getValue(), jgen);
87+
}
88+
jgen.writeEndObject();
89+
}
90+
jgen.writeEndObject();
91+
}
92+
93+
private void serializeFlag(FlagConfig flagConfig, JsonGenerator jgen, SerializerProvider provider)
94+
throws IOException {
95+
jgen.writeStartObject();
96+
jgen.writeStringField("key", flagConfig.getKey());
97+
jgen.writeBooleanField("enabled", flagConfig.isEnabled());
98+
jgen.writeNumberField("totalShards", flagConfig.getTotalShards());
99+
final VariationType variationType = flagConfig.getVariationType();
100+
if (variationType != null) {
101+
jgen.writeStringField("variationType", variationType.value);
102+
}
103+
final Map<String, Variation> variations = flagConfig.getVariations();
104+
if (variations != null) {
105+
jgen.writeFieldName("variations");
106+
jgen.writeStartObject();
107+
for (Map.Entry<String, Variation> entry : variations.entrySet()) {
108+
jgen.writeFieldName(entry.getKey());
109+
serializeVariation(entry.getValue(), jgen);
110+
}
111+
jgen.writeEndObject();
112+
}
113+
final List<Allocation> allocations = flagConfig.getAllocations();
114+
if (allocations != null) {
115+
jgen.writeFieldName("allocations");
116+
jgen.writeStartArray();
117+
for (Allocation allocation : allocations) {
118+
serializeAllocation(allocation, jgen, provider);
119+
}
120+
jgen.writeEndArray();
121+
}
122+
jgen.writeEndObject();
123+
}
124+
125+
private void serializeVariation(Variation variation, JsonGenerator jgen)
126+
throws IOException {
127+
jgen.writeStartObject();
128+
jgen.writeStringField("key", variation.getKey());
129+
jgen.writeObjectField("value", variation.getValue());
130+
jgen.writeEndObject();
131+
}
132+
133+
private void serializeAllocation(Allocation allocation, JsonGenerator jgen, SerializerProvider provider)
134+
throws IOException {
135+
jgen.writeStartObject();
136+
jgen.writeStringField("key", allocation.getKey());
137+
final Set<TargetingRule> rules = allocation.getRules();
138+
if (rules != null) {
139+
jgen.writeFieldName("rules");
140+
jgen.writeStartArray();
141+
for (TargetingRule rule : rules) {
142+
serializeTargetingRule(rule, jgen, provider);
143+
}
144+
jgen.writeEndArray();
145+
}
146+
final Date startAt = allocation.getStartAt();
147+
if (startAt != null) {
148+
jgen.writeStringField("startAt", getISODate(startAt));
149+
}
150+
final Date endAt = allocation.getEndAt();
151+
if (endAt != null) {
152+
jgen.writeStringField("endAt", getISODate(endAt));
153+
}
154+
final List<Split> splits = allocation.getSplits();
155+
if (splits != null) {
156+
jgen.writeFieldName("splits");
157+
jgen.writeStartArray();
158+
for (Split split : splits) {
159+
serializeSplit(split, jgen);
160+
}
161+
jgen.writeEndArray();
162+
}
163+
jgen.writeBooleanField("doLog", allocation.doLog());
164+
165+
jgen.writeEndObject();
166+
}
167+
168+
private void serializeTargetingRule(TargetingRule rule, JsonGenerator jgen, SerializerProvider provider)
169+
throws IOException {
170+
jgen.writeStartObject();
171+
final Set<TargetingCondition> conditions = rule.getConditions();
172+
if (conditions != null) {
173+
jgen.writeFieldName("conditions");
174+
jgen.writeStartArray();
175+
for (TargetingCondition condition : conditions) {
176+
jgen.writeStartObject();
177+
jgen.writeStringField("attribute", condition.getAttribute());
178+
final OperatorType operator = condition.getOperator();
179+
if (operator != null) {
180+
jgen.writeStringField("operator", operator.value);
181+
}
182+
final EppoValue value = condition.getValue();
183+
if (value != null) {
184+
jgen.writeFieldName("value");
185+
eppoValueSerializer.serialize(value, jgen, provider);
186+
}
187+
188+
jgen.writeEndObject();
189+
}
190+
jgen.writeEndArray();
191+
}
192+
jgen.writeEndObject();
193+
}
194+
195+
private void serializeSplit(Split split, JsonGenerator jgen)
196+
throws IOException {
197+
jgen.writeStartObject();
198+
jgen.writeStringField("variationKey", split.getVariationKey());
199+
final Set<Shard> shards = split.getShards();
200+
if (shards != null) {
201+
jgen.writeFieldName("shards");
202+
jgen.writeStartArray();
203+
for (Shard shard : shards) {
204+
serializeShard(shard, jgen);
205+
}
206+
jgen.writeEndArray();
207+
}
208+
Map<String, String> extraLogging = split.getExtraLogging();
209+
if (extraLogging != null) {
210+
jgen.writeFieldName("extraLogging");
211+
jgen.writeStartObject();
212+
for (Map.Entry<String, String> extraLog : extraLogging.entrySet()) {
213+
jgen.writeStringField(extraLog.getKey(), extraLog.getValue());
214+
}
215+
jgen.writeEndObject();
216+
}
217+
jgen.writeEndObject();
218+
}
219+
220+
private void serializeShard(Shard shard, JsonGenerator jgen)
221+
throws IOException {
222+
jgen.writeStartObject();
223+
jgen.writeStringField("salt", shard.getSalt());
224+
final Set<ShardRange> ranges = shard.getRanges();
225+
if (ranges != null) {
226+
jgen.writeFieldName("ranges");
227+
jgen.writeStartArray();
228+
for (ShardRange range : ranges) {
229+
jgen.writeStartObject();
230+
jgen.writeNumberField("start", range.getStart());
231+
jgen.writeNumberField("end", range.getEnd());
232+
jgen.writeEndObject();
233+
}
234+
jgen.writeEndArray();
235+
}
236+
jgen.writeEndObject();
237+
}
238+
239+
private void serializeBanditReference(BanditReference banditReference, JsonGenerator jgen)
240+
throws IOException {
241+
jgen.writeStartObject();
242+
jgen.writeStringField("modelVersion", banditReference.getModelVersion());
243+
List<BanditFlagVariation> flagVariations = banditReference.getFlagVariations();
244+
if (flagVariations != null) {
245+
jgen.writeFieldName("flagVariations");
246+
jgen.writeStartArray();
247+
for (BanditFlagVariation flagVariation : flagVariations) {
248+
jgen.writeStartObject();
249+
jgen.writeStringField("key", flagVariation.getBanditKey());
250+
jgen.writeStringField("flagKey", flagVariation.getFlagKey());
251+
jgen.writeStringField("allocationKey", flagVariation.getAllocationKey());
252+
jgen.writeStringField("variationKey", flagVariation.getVariationKey());
253+
jgen.writeStringField("variationValue", flagVariation.getVariationValue());
254+
jgen.writeEndObject();
255+
}
256+
jgen.writeEndArray();
257+
}
258+
jgen.writeEndObject();
259+
}
260+
}

src/test/java/cloud/eppo/ufc/deserializer/FlagConfigResponseDeserializerTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.io.File;
1010
import java.io.FileReader;
1111
import java.io.IOException;
12+
import java.io.StringReader;
1213
import java.util.ArrayList;
1314
import java.util.List;
1415
import java.util.Map;
@@ -23,6 +24,21 @@ public void testDeserializePlainText() throws IOException {
2324
FileReader fileReader = new FileReader(testUfc);
2425
FlagConfigResponse configResponse = mapper.readValue(fileReader, FlagConfigResponse.class);
2526

27+
assertFlagConfigResponse(configResponse);
28+
}
29+
30+
@Test
31+
public void testSerializePlainText() throws IOException {
32+
File testUfc = new File("src/test/resources/flags-v1.json");
33+
FileReader fileReader = new FileReader(testUfc);
34+
FlagConfigResponse initialConfigResponse = mapper.readValue(fileReader, FlagConfigResponse.class);
35+
String initialConfigResponseJson = mapper.writeValueAsString(initialConfigResponse);
36+
FlagConfigResponse rereadConfigResponse = mapper.readValue(new StringReader(initialConfigResponseJson), FlagConfigResponse.class);
37+
38+
assertFlagConfigResponse(rereadConfigResponse);
39+
}
40+
41+
private void assertFlagConfigResponse(FlagConfigResponse configResponse) {
2642
assertTrue(configResponse.getFlags().size() >= 13);
2743
assertTrue(configResponse.getFlags().containsKey("empty_flag"));
2844
assertTrue(configResponse.getFlags().containsKey("disabled_flag"));
@@ -114,4 +130,25 @@ public void testDeserializePlainText() throws IOException {
114130
assertEquals("off", offForAllSplit.getVariationKey());
115131
assertEquals(0, offForAllSplit.getShards().size());
116132
}
133+
134+
@Test
135+
public void testSerializePlainTextWithBandit() throws IOException {
136+
File testUfc = new File("src/test/resources/static/initial-flag-config-with-bandit.json");
137+
FileReader fileReader = new FileReader(testUfc);
138+
FlagConfigResponse initialConfigResponse = mapper.readValue(fileReader, FlagConfigResponse.class);
139+
String initialConfigResponseJson = mapper.writeValueAsString(initialConfigResponse);
140+
FlagConfigResponse rereadConfigResponse = mapper.readValue(new StringReader(initialConfigResponseJson), FlagConfigResponse.class);
141+
142+
assertEquals(1, rereadConfigResponse.getBanditReferences().size());
143+
BanditReference bannerBanditReference = rereadConfigResponse.getBanditReferences().get("banner_bandit");
144+
assertEquals("v123", bannerBanditReference.getModelVersion());
145+
146+
assertEquals(1, bannerBanditReference.getFlagVariations().size());
147+
BanditFlagVariation flagVariation = bannerBanditReference.getFlagVariations().get(0);
148+
assertEquals("banner_bandit", flagVariation.getBanditKey());
149+
assertEquals("banner_bandit_flag", flagVariation.getFlagKey());
150+
assertEquals("analysis", flagVariation.getAllocationKey());
151+
assertEquals("banner_bandit", flagVariation.getVariationKey());
152+
assertEquals("banner_bandit", flagVariation.getVariationValue());
153+
}
117154
}

0 commit comments

Comments
 (0)