diff --git a/lib/build.gradle b/lib/build.gradle index f1790f0b..c0bcacd7 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -26,6 +26,8 @@ repositories { dependencies { implementation 'org.slf4j:slf4j-log4j12:1.7.29' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' + // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:30.1.1-jre' diff --git a/lib/src/main/java/dev/openfeature/javasdk/EvaluationContext.java b/lib/src/main/java/dev/openfeature/javasdk/EvaluationContext.java index 74d28a0e..7eb3b16d 100644 --- a/lib/src/main/java/dev/openfeature/javasdk/EvaluationContext.java +++ b/lib/src/main/java/dev/openfeature/javasdk/EvaluationContext.java @@ -1,9 +1,7 @@ package dev.openfeature.javasdk; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.*; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -13,14 +11,32 @@ import java.util.Map; @ToString @EqualsAndHashCode @SuppressWarnings("PMD.BeanMembersShouldSerialize") public class EvaluationContext { + @EqualsAndHashCode.Exclude private final ObjectMapper objMapper; @Setter @Getter private String targetingKey; private final Map integerAttributes; private final Map stringAttributes; + private final Map booleanAttributes; + final Map jsonAttributes; EvaluationContext() { + objMapper = new ObjectMapper(); this.targetingKey = ""; this.integerAttributes = new HashMap<>(); this.stringAttributes = new HashMap<>(); + booleanAttributes = new HashMap<>(); + jsonAttributes = new HashMap<>(); + } + + // TODO Not sure if I should have sneakythrows or checked exceptions here.. + @SneakyThrows + public void addStructureAttribute(String key, T value) { + jsonAttributes.put(key, objMapper.writeValueAsString(value)); + } + + @SneakyThrows + public T getStructureAttribute(String key, Class klass) { + String val = jsonAttributes.get(key); + return objMapper.readValue(val, klass); } public void addStringAttribute(String key, String value) { @@ -40,19 +56,17 @@ public class EvaluationContext { } public Boolean getBooleanAttribute(String key) { - return Boolean.valueOf(stringAttributes.get(key)); + return booleanAttributes.get(key); } public void addBooleanAttribute(String key, Boolean b) { - stringAttributes.put(key, b.toString()); + booleanAttributes.put(key, b); } public void addDatetimeAttribute(String key, ZonedDateTime value) { this.stringAttributes.put(key, value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); } - // TODO: addStructure or similar. - public ZonedDateTime getDatetimeAttribute(String key) { String attr = this.stringAttributes.get(key); if (attr == null) { @@ -66,21 +80,19 @@ public class EvaluationContext { */ public static EvaluationContext merge(EvaluationContext ctx1, EvaluationContext ctx2) { EvaluationContext ec = new EvaluationContext(); - for (Map.Entry e : ctx1.integerAttributes.entrySet()) { - ec.addIntegerAttribute(e.getKey(), e.getValue()); - } - for (Map.Entry e : ctx2.integerAttributes.entrySet()) { - ec.addIntegerAttribute(e.getKey(), e.getValue()); - } + ec.stringAttributes.putAll(ctx1.stringAttributes); + ec.stringAttributes.putAll(ctx2.stringAttributes); - for (Map.Entry e : ctx1.stringAttributes.entrySet()) { - ec.addStringAttribute(e.getKey(), e.getValue()); - } + ec.integerAttributes.putAll(ctx1.integerAttributes); + ec.integerAttributes.putAll(ctx2.integerAttributes); + + ec.booleanAttributes.putAll(ctx1.booleanAttributes); + ec.booleanAttributes.putAll(ctx2.booleanAttributes); + + ec.jsonAttributes.putAll(ctx1.jsonAttributes); + ec.jsonAttributes.putAll(ctx2.jsonAttributes); - for (Map.Entry e : ctx2.stringAttributes.entrySet()) { - ec.addStringAttribute(e.getKey(), e.getValue()); - } if (ctx1.getTargetingKey() != null) { ec.setTargetingKey(ctx1.getTargetingKey()); } diff --git a/lib/src/test/java/dev/openfeature/javasdk/EvalContextTests.java b/lib/src/test/java/dev/openfeature/javasdk/EvalContextTests.java index 92d9df7a..c71ffeb2 100644 --- a/lib/src/test/java/dev/openfeature/javasdk/EvalContextTests.java +++ b/lib/src/test/java/dev/openfeature/javasdk/EvalContextTests.java @@ -1,6 +1,5 @@ package dev.openfeature.javasdk; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.time.ZonedDateTime; @@ -40,6 +39,24 @@ public class EvalContextTests { @Specification(spec="Evaluation Context", number="3.2", text="The evaluation context MUST support the inclusion of " + "custom fields, having keys of type `string`, and " + "values of type `boolean | string | number | datetime | structure`.") - @Disabled("Structure support") - @Test void eval_context__structure() {} + @Test void eval_context__structure() { + Node n1 = new Node<>(); + n1.value = 4; + Node n2 = new Node<>(); + n2.value = 2; + n2.left = n1; + + EvaluationContext ec = new EvaluationContext(); + ec.addStructureAttribute("obj", n2); + + + String stringyObject = ec.jsonAttributes.get("obj"); + assertEquals("{\"left\":{\"left\":null,\"right\":null,\"value\":4},\"right\":null,\"value\":2}", stringyObject); + + Node nodeFromString = ec.getStructureAttribute("obj", Node.class); + assertEquals(n2, nodeFromString); + assertEquals(n1, nodeFromString.left); + assertEquals(2, nodeFromString.value); + assertEquals(4, nodeFromString.left.value); + } } diff --git a/lib/src/test/java/dev/openfeature/javasdk/Node.java b/lib/src/test/java/dev/openfeature/javasdk/Node.java new file mode 100644 index 00000000..31bf0cc8 --- /dev/null +++ b/lib/src/test/java/dev/openfeature/javasdk/Node.java @@ -0,0 +1,12 @@ +package dev.openfeature.javasdk; + +import lombok.Data; + +@Data +/** + * This is only for testing that object serialization works with both generics and nested classes. + */ +class Node { + Node left, right; + T value; +}