Repository: open-feature/java-sdk
Pull request #61 (2022-09-07 16:30:14)

breaking: store all numbers as doubles
- As per https://github.com/open-feature/java-sdk/issues/58, `Value` now stores all numbers as doubles. `Value.isDouble()`/`Value.isInteger()` consolidated to `Value.isNumber()`. `Value.asInteger()` now returns the rounded inner double using `Math.round()`.

Also includes a few non-breaking changes:

- new `add` overload for adding Value type to struct: `Structure.add(String key, Value value)`
- new `Value()` constructor to construct a null Value.
- specific testing for Structure/Value.

Closes: https://github.com/open-feature/java-sdk/issues/58


Patch:
diff --git a/src/main/java/dev/openfeature/javasdk/Structure.java b/src/main/java/dev/openfeature/javasdk/Structure.java
index 05a6655..062750f 100644
--- a/src/main/java/dev/openfeature/javasdk/Structure.java
+++ b/src/main/java/dev/openfeature/javasdk/Structure.java
@@ -41,6 +41,11 @@ public class Structure {
     }
 
     // adders
+    public Structure add(String key, Value value) {
+        attributes.put(key, value);
+        return this;
+    }
+
     public Structure add(String key, Boolean value) {
         attributes.put(key, new Value(value));
         return this;
diff --git a/src/main/java/dev/openfeature/javasdk/Value.java b/src/main/java/dev/openfeature/javasdk/Value.java
index 786ee29..426d4a1 100644
--- a/src/main/java/dev/openfeature/javasdk/Value.java
+++ b/src/main/java/dev/openfeature/javasdk/Value.java
@@ -7,9 +7,9 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 /**
- * Values server as a return type for provider objects. Providers may deal in protobufs or JSON in the backend and
- * have no reasonable way to convert that into a type that users care about (e.g. an instance of `T`). This
- * intermediate representation should provide a good medium of exchange.
+ * Values serve as a return type for provider objects.
+ * Providers may deal in JSON, protobuf, XML or some other data-interchange format.
+ * This intermediate representation provides a good medium of exchange.
  */
 @ToString
 @EqualsAndHashCode
@@ -18,6 +18,10 @@ public class Value {
 
     private final Object innerObject;
 
+    public Value() {
+        this.innerObject = null; 
+    }
+
     public Value(Value value) {
         this.innerObject = value.innerObject; 
     }
@@ -31,7 +35,7 @@ public class Value {
     }
 
     public Value(Integer value) {
-        this.innerObject = value; 
+        this.innerObject = value.doubleValue(); 
     }
 
     public Value(Double value) {
@@ -51,38 +55,38 @@ public class Value {
     }
 
     /** 
-     * Check if this Value represents a Boolean.
+     * Check if this Value represents null.
      * 
      * @return boolean
      */
-    public boolean isBoolean() {
-        return this.innerObject instanceof Boolean;
+    public boolean isNull() {
+        return this.innerObject == null;
     }
 
     /** 
-     * Check if this Value represents a String.
+     * Check if this Value represents a Boolean.
      * 
      * @return boolean
      */
-    public boolean isString() {
-        return this.innerObject instanceof String;
+    public boolean isBoolean() {
+        return this.innerObject instanceof Boolean;
     }
 
     /** 
-     * Check if this Value represents an Integer.
+     * Check if this Value represents a String.
      * 
      * @return boolean
      */
-    public boolean isInteger() {
-        return this.innerObject instanceof Integer;
+    public boolean isString() {
+        return this.innerObject instanceof String;
     }
 
     /** 
-     * Check if this Value represents a Double.
+     * Check if this Value represents a numeric value.
      * 
      * @return boolean
      */
-    public boolean isDouble() {
+    public boolean isNumber() {
         return this.innerObject instanceof Double;
     }
 
@@ -140,24 +144,25 @@ public class Value {
     }
 
     /** 
-     * Retrieve the underlying Integer value, or null.
+     * Retrieve the underlying numeric value as an Integer, or null.
+     * If the value is not an integer, it will be rounded using Math.round().
      * 
      * @return Integer
      */
     public Integer asInteger() {
-        if (this.isInteger()) {
-            return (Integer)this.innerObject;
+        if (this.isNumber()) {
+            return (int)Math.round((Double)this.innerObject);
         }
         return null;
     }
     
     /** 
-     * Retrieve the underlying Double value, or null.
+     * Retrieve the underlying numeric value as a Double, or null.
      * 
      * @return Double
      */
     public Double asDouble() {
-        if (this.isDouble()) {
+        if (this.isNumber()) {
             return (Double)this.innerObject;
         }
         return null;


Test patch:
diff --git a/src/test/java/dev/openfeature/javasdk/StructureTest.java b/src/test/java/dev/openfeature/javasdk/StructureTest.java
new file mode 100644
index 0000000..0232bc7
--- /dev/null
+++ b/src/test/java/dev/openfeature/javasdk/StructureTest.java
@@ -0,0 +1,71 @@
+package dev.openfeature.javasdk;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+public class StructureTest {
+    @Test public void noArgShouldContainEmptyAttributes() {
+        Structure structure = new Structure();
+        assertEquals(0, structure.asMap().keySet().size());
+    }
+
+    @Test public void mapArgShouldContainNewMap() {
+        String KEY = "key";
+        Map<String, Value> map = new HashMap<String, Value>() {
+            {
+                put(KEY, new Value(KEY));
+            }
+        };
+        Structure structure = new Structure(map);
+        assertEquals(KEY, structure.asMap().get(KEY).asString());
+        assertNotSame(structure.asMap(), map); // should be a copy
+    }
+
+    @Test public void addAndGetAddAndReturnValues() {
+        String BOOL_KEY = "bool";
+        String STRING_KEY = "string";
+        String INT_KEY = "int";
+        String DOUBLE_KEY = "double";
+        String DATE_KEY = "date";
+        String STRUCT_KEY = "struct";
+        String LIST_KEY = "list";
+        String VALUE_KEY = "value";
+
+        boolean BOOL_VAL = true;
+        String STRING_VAL = "val";
+        int INT_VAL = 13;
+        double DOUBLE_VAL = .5;
+        ZonedDateTime DATE_VAL = ZonedDateTime.now();
+        Structure STRUCT_VAL = new Structure();
+        List<Value> LIST_VAL = new ArrayList<Value>();
+        Value VALUE_VAL = new Value();
+
+        Structure structure = new Structure();
+        structure.add(BOOL_KEY, BOOL_VAL);
+        structure.add(STRING_KEY, STRING_VAL);
+        structure.add(INT_KEY, INT_VAL);
+        structure.add(DOUBLE_KEY, DOUBLE_VAL);
+        structure.add(DATE_KEY, DATE_VAL);
+        structure.add(STRUCT_KEY, STRUCT_VAL);
+        structure.add(LIST_KEY, LIST_VAL);
+        structure.add(VALUE_KEY, VALUE_VAL);
+
+        assertEquals(BOOL_VAL, structure.getValue(BOOL_KEY).asBoolean());
+        assertEquals(STRING_VAL, structure.getValue(STRING_KEY).asString());
+        assertEquals(INT_VAL, structure.getValue(INT_KEY).asInteger());
+        assertEquals(DOUBLE_VAL, structure.getValue(DOUBLE_KEY).asDouble());
+        assertEquals(DATE_VAL, structure.getValue(DATE_KEY).asZonedDateTime());
+        assertEquals(STRUCT_VAL, structure.getValue(STRUCT_KEY).asStructure());
+        assertEquals(LIST_VAL, structure.getValue(LIST_KEY).asList());
+        assertTrue(structure.getValue(VALUE_KEY).isNull());
+    }
+}
diff --git a/src/test/java/dev/openfeature/javasdk/ValueTest.java b/src/test/java/dev/openfeature/javasdk/ValueTest.java
new file mode 100644
index 0000000..c86feb9
--- /dev/null
+++ b/src/test/java/dev/openfeature/javasdk/ValueTest.java
@@ -0,0 +1,70 @@
+package dev.openfeature.javasdk;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+public class ValueTest {
+    @Test public void noArgShouldContainNull() {
+        Value value = new Value();
+        assertTrue(value.isNull());
+    }
+
+    @Test public void boolArgShouldContainBool() {
+        boolean innerValue = true;
+        Value value = new Value(innerValue);
+        assertTrue(value.isBoolean());
+        assertEquals(innerValue, value.asBoolean());
+    }
+
+    @Test public void numericArgShouldReturnDoubleOrInt() {
+        double innerDoubleValue = .75;
+        Value doubleValue = new Value(innerDoubleValue);
+        assertTrue(doubleValue.isNumber());
+        assertEquals(1, doubleValue.asInteger());     // should be rounded
+        assertEquals(.75, doubleValue.asDouble());
+
+        int innerIntValue = 100;
+        Value intValue = new Value(innerIntValue);
+        assertTrue(intValue.isNumber());
+        assertEquals(innerIntValue, intValue.asInteger());
+        assertEquals(innerIntValue, intValue.asDouble());
+    }
+
+    @Test public void stringArgShouldContainString() {
+        String innerValue = "hi!";
+        Value value = new Value(innerValue);
+        assertTrue(value.isString());
+        assertEquals(innerValue, value.asString());
+    }
+
+    @Test public void dateShouldContainDate() {
+        ZonedDateTime innerValue = ZonedDateTime.now();
+        Value value = new Value(innerValue);
+        assertTrue(value.isZonedDateTime());
+        assertEquals(innerValue, value.asZonedDateTime());
+    }
+
+    @Test public void structureShouldContainStructure() {
+        String INNER_KEY = "key";
+        String INNER_VALUE = "val";
+        Structure innerValue = new Structure().add(INNER_KEY, INNER_VALUE);
+        Value value = new Value(innerValue);
+        assertTrue(value.isStructure());
+        assertEquals(INNER_VALUE, value.asStructure().getValue(INNER_KEY).asString());
+    }
+
+    @Test public void listArgShouldContainList() {
+        String ITEM_VALUE = "val";
+        List<Value> innerValue = new ArrayList<Value>();
+        innerValue.add(new Value(ITEM_VALUE));
+        Value value = new Value(innerValue);
+        assertTrue(value.isList());
+        assertEquals(ITEM_VALUE, value.asList().get(0).asString());
+    }
+}