Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/cloud/eppo/ufc/dto/adapters/EppoModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class EppoModule {
public static SimpleModule eppoModule() {
SimpleModule module = new SimpleModule();
module.addDeserializer(FlagConfigResponse.class, new FlagConfigResponseDeserializer());
module.addSerializer(FlagConfigResponse.class, new FlagConfigResponseSerializer());
module.addDeserializer(
BanditParametersResponse.class, new BanditParametersResponseDeserializer());
module.addDeserializer(EppoValue.class, new EppoValueDeserializer());
Expand Down
39 changes: 26 additions & 13 deletions src/main/java/cloud/eppo/ufc/dto/adapters/EppoValueSerializer.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cloud.eppo.ufc.dto.adapters;

import cloud.eppo.api.EppoValue;
import cloud.eppo.ufc.dto.EppoValueType;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
Expand All @@ -18,20 +20,31 @@ public EppoValueSerializer() {
@Override
public void serialize(EppoValue src, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
if (src.isBoolean()) {
jgen.writeBoolean(src.booleanValue());
}
if (src.isNumeric()) {
jgen.writeNumber(src.doubleValue());
}
if (src.isString()) {
jgen.writeString(src.stringValue());
}
if (src.isStringArray()) {
String[] arr = src.stringArrayValue().toArray(new String[0]);
jgen.writeArray(arr, 0, arr.length);
} else {
final EppoValueType type = src.getType();
if (type == null) {
// this should never happen, but if it does,
// we need to write something so that we're valid JSON
// so a null value is safest.
jgen.writeNull();
} else {
switch (src.getType()) {
case NULL:
jgen.writeNull();
break;
case BOOLEAN:
jgen.writeBoolean(src.booleanValue());
break;
case NUMBER:
jgen.writeNumber(src.doubleValue());
break;
case STRING:
jgen.writeString(src.stringValue());
break;
case ARRAY_OF_STRING:
String[] arr = src.stringArrayValue().toArray(new String[0]);
jgen.writeArray(arr, 0, arr.length);
break;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ private Set<TargetingRule> deserializeTargetingRules(JsonNode jsonNode) {
Set<TargetingCondition> conditions = new HashSet<>();
for (JsonNode conditionNode : ruleNode.get("conditions")) {
String attribute = conditionNode.get("attribute").asText();
String operatorKey = conditionNode.get("operator").asText();
String operatorKey = conditionNode.get("operator").asText(null);
OperatorType operator = OperatorType.fromString(operatorKey);
if (operator == null) {
log.warn("Unknown operator \"{}\"", operatorKey);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package cloud.eppo.ufc.dto.adapters;

import static cloud.eppo.Utils.getISODate;
import static cloud.eppo.Utils.parseUtcISODateNode;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;

import cloud.eppo.api.EppoValue;
import cloud.eppo.model.ShardRange;
import cloud.eppo.ufc.dto.Allocation;
import cloud.eppo.ufc.dto.BanditFlagVariation;
import cloud.eppo.ufc.dto.BanditReference;
import cloud.eppo.ufc.dto.FlagConfig;
import cloud.eppo.ufc.dto.FlagConfigResponse;
import cloud.eppo.ufc.dto.OperatorType;
import cloud.eppo.ufc.dto.Shard;
import cloud.eppo.ufc.dto.Split;
import cloud.eppo.ufc.dto.TargetingCondition;
import cloud.eppo.ufc.dto.TargetingRule;
import cloud.eppo.ufc.dto.Variation;
import cloud.eppo.ufc.dto.VariationType;

/**
* Hand-rolled serializer so that we don't rely on annotations and method names, which can be
* unreliable when ProGuard minification is in-use and not configured to protect
* JSON-serialization-related classes and annotations.
*/
public class FlagConfigResponseSerializer extends StdSerializer<FlagConfigResponse> {
private static final Logger log = LoggerFactory.getLogger(FlagConfigResponseSerializer.class);
private final EppoValueSerializer eppoValueSerializer = new EppoValueSerializer();

protected FlagConfigResponseSerializer(Class<FlagConfigResponse> vc) {
super(vc);
}

public FlagConfigResponseSerializer() {
this(null);
}

@Override
public void serialize(FlagConfigResponse src, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JacksonException {
jgen.writeStartObject();
final FlagConfigResponse.Format format = src.getFormat();
if (format != null) {
jgen.writeStringField("format", format.name());
}
final Map<String, FlagConfig> flags = src.getFlags();
if (flags != null) {
jgen.writeFieldName("flags");
jgen.writeStartObject();
for (Map.Entry<String, FlagConfig> entry : src.getFlags().entrySet()) {
jgen.writeFieldName(entry.getKey());
serializeFlag(entry.getValue(), jgen, provider);
}
jgen.writeEndObject();
}
final Map<String, BanditReference> banditReferences = src.getBanditReferences();
if (banditReferences != null) {
jgen.writeFieldName("banditReferences");
jgen.writeStartObject();
for (Map.Entry<String, BanditReference> entry : banditReferences.entrySet()) {
jgen.writeFieldName(entry.getKey());
serializeBanditReference(entry.getValue(), jgen);
}
jgen.writeEndObject();
}
jgen.writeEndObject();
}

private void serializeFlag(FlagConfig flagConfig, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeStartObject();
jgen.writeStringField("key", flagConfig.getKey());
jgen.writeBooleanField("enabled", flagConfig.isEnabled());
jgen.writeNumberField("totalShards", flagConfig.getTotalShards());
final VariationType variationType = flagConfig.getVariationType();
if (variationType != null) {
jgen.writeStringField("variationType", variationType.value);
}
final Map<String, Variation> variations = flagConfig.getVariations();
if (variations != null) {
jgen.writeFieldName("variations");
jgen.writeStartObject();
for (Map.Entry<String, Variation> entry : variations.entrySet()) {
jgen.writeFieldName(entry.getKey());
serializeVariation(entry.getValue(), jgen);
}
jgen.writeEndObject();
}
final List<Allocation> allocations = flagConfig.getAllocations();
if (allocations != null) {
jgen.writeFieldName("allocations");
jgen.writeStartArray();
for (Allocation allocation : allocations) {
serializeAllocation(allocation, jgen, provider);
}
jgen.writeEndArray();
}
jgen.writeEndObject();
}

private void serializeVariation(Variation variation, JsonGenerator jgen)
throws IOException {
jgen.writeStartObject();
jgen.writeStringField("key", variation.getKey());
jgen.writeObjectField("value", variation.getValue());
jgen.writeEndObject();
}

private void serializeAllocation(Allocation allocation, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeStartObject();
jgen.writeStringField("key", allocation.getKey());
final Set<TargetingRule> rules = allocation.getRules();
if (rules != null) {
jgen.writeFieldName("rules");
jgen.writeStartArray();
for (TargetingRule rule : rules) {
serializeTargetingRule(rule, jgen, provider);
}
jgen.writeEndArray();
}
final Date startAt = allocation.getStartAt();
if (startAt != null) {
jgen.writeStringField("startAt", getISODate(startAt));
}
final Date endAt = allocation.getEndAt();
if (endAt != null) {
jgen.writeStringField("endAt", getISODate(endAt));
}
final List<Split> splits = allocation.getSplits();
if (splits != null) {
jgen.writeFieldName("splits");
jgen.writeStartArray();
for (Split split : splits) {
serializeSplit(split, jgen);
}
jgen.writeEndArray();
}
jgen.writeBooleanField("doLog", allocation.doLog());

jgen.writeEndObject();
}

private void serializeTargetingRule(TargetingRule rule, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeStartObject();
final Set<TargetingCondition> conditions = rule.getConditions();
if (conditions != null) {
jgen.writeFieldName("conditions");
jgen.writeStartArray();
for (TargetingCondition condition : conditions) {
jgen.writeStartObject();
jgen.writeStringField("attribute", condition.getAttribute());
final OperatorType operator = condition.getOperator();
if (operator != null) {
jgen.writeStringField("operator", operator.value);
}
final EppoValue value = condition.getValue();
if (value != null) {
jgen.writeFieldName("value");
eppoValueSerializer.serialize(value, jgen, provider);
}

jgen.writeEndObject();
}
jgen.writeEndArray();
}
jgen.writeEndObject();
}

private void serializeSplit(Split split, JsonGenerator jgen)
throws IOException {
jgen.writeStartObject();
jgen.writeStringField("variationKey", split.getVariationKey());
final Set<Shard> shards = split.getShards();
if (shards != null) {
jgen.writeFieldName("shards");
jgen.writeStartArray();
for (Shard shard : shards) {
serializeShard(shard, jgen);
}
jgen.writeEndArray();
}
Map<String, String> extraLogging = split.getExtraLogging();
if (extraLogging != null) {
jgen.writeFieldName("extraLogging");
jgen.writeStartObject();
for (Map.Entry<String, String> extraLog : extraLogging.entrySet()) {
jgen.writeStringField(extraLog.getKey(), extraLog.getValue());
}
jgen.writeEndObject();
}
jgen.writeEndObject();
}

private void serializeShard(Shard shard, JsonGenerator jgen)
throws IOException {
jgen.writeStartObject();
jgen.writeStringField("salt", shard.getSalt());
final Set<ShardRange> ranges = shard.getRanges();
if (ranges != null) {
jgen.writeFieldName("ranges");
jgen.writeStartArray();
for (ShardRange range : ranges) {
jgen.writeStartObject();
jgen.writeNumberField("start", range.getStart());
jgen.writeNumberField("end", range.getEnd());
jgen.writeEndObject();
}
jgen.writeEndArray();
}
jgen.writeEndObject();
}

private void serializeBanditReference(BanditReference banditReference, JsonGenerator jgen)
throws IOException {
jgen.writeStartObject();
jgen.writeStringField("modelVersion", banditReference.getModelVersion());
List<BanditFlagVariation> flagVariations = banditReference.getFlagVariations();
if (flagVariations != null) {
jgen.writeFieldName("flagVariations");
jgen.writeStartArray();
for (BanditFlagVariation flagVariation : flagVariations) {
jgen.writeStartObject();
jgen.writeStringField("key", flagVariation.getBanditKey());
jgen.writeStringField("flagKey", flagVariation.getFlagKey());
jgen.writeStringField("allocationKey", flagVariation.getAllocationKey());
jgen.writeStringField("variationKey", flagVariation.getVariationKey());
jgen.writeStringField("variationValue", flagVariation.getVariationValue());
jgen.writeEndObject();
}
jgen.writeEndArray();
}
jgen.writeEndObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand All @@ -23,6 +24,21 @@ public void testDeserializePlainText() throws IOException {
FileReader fileReader = new FileReader(testUfc);
FlagConfigResponse configResponse = mapper.readValue(fileReader, FlagConfigResponse.class);

assertFlagConfigResponse(configResponse);
}

@Test
public void testSerializePlainText() throws IOException {
File testUfc = new File("src/test/resources/flags-v1.json");
FileReader fileReader = new FileReader(testUfc);
FlagConfigResponse initialConfigResponse = mapper.readValue(fileReader, FlagConfigResponse.class);
String initialConfigResponseJson = mapper.writeValueAsString(initialConfigResponse);
FlagConfigResponse rereadConfigResponse = mapper.readValue(new StringReader(initialConfigResponseJson), FlagConfigResponse.class);

assertFlagConfigResponse(rereadConfigResponse);
}

private void assertFlagConfigResponse(FlagConfigResponse configResponse) {
assertTrue(configResponse.getFlags().size() >= 13);
assertTrue(configResponse.getFlags().containsKey("empty_flag"));
assertTrue(configResponse.getFlags().containsKey("disabled_flag"));
Expand Down Expand Up @@ -114,4 +130,25 @@ public void testDeserializePlainText() throws IOException {
assertEquals("off", offForAllSplit.getVariationKey());
assertEquals(0, offForAllSplit.getShards().size());
}

@Test
public void testSerializePlainTextWithBandit() throws IOException {
File testUfc = new File("src/test/resources/static/initial-flag-config-with-bandit.json");
FileReader fileReader = new FileReader(testUfc);
FlagConfigResponse initialConfigResponse = mapper.readValue(fileReader, FlagConfigResponse.class);
String initialConfigResponseJson = mapper.writeValueAsString(initialConfigResponse);
FlagConfigResponse rereadConfigResponse = mapper.readValue(new StringReader(initialConfigResponseJson), FlagConfigResponse.class);

assertEquals(1, rereadConfigResponse.getBanditReferences().size());
BanditReference bannerBanditReference = rereadConfigResponse.getBanditReferences().get("banner_bandit");
assertEquals("v123", bannerBanditReference.getModelVersion());

assertEquals(1, bannerBanditReference.getFlagVariations().size());
BanditFlagVariation flagVariation = bannerBanditReference.getFlagVariations().get(0);
assertEquals("banner_bandit", flagVariation.getBanditKey());
assertEquals("banner_bandit_flag", flagVariation.getFlagKey());
assertEquals("analysis", flagVariation.getAllocationKey());
assertEquals("banner_bandit", flagVariation.getVariationKey());
assertEquals("banner_bandit", flagVariation.getVariationValue());
}
}