Repository: open-feature/java-sdk
Pull request #65 (2022-09-09 15:34:00)

Collection of small, breaking changes
This is a collection of small (but breaking) changes. I hope these are the last breaking changes before a 1.0rc. I've broken it into 3 commits for easy understanding:

- add object constructor and accessor to `Value`: https://github.com/open-feature/java-sdk/commit/0152a1eef93ea1b5253ddae78718a9805c98aaf7
- use `Instant` instead of `ZonedDateTime` (https://github.com/open-feature/java-sdk/issues/59): https://github.com/open-feature/java-sdk/commit/3e6241422266825f267043e4acd116803c4939b0
- use `Value` instead of `Structure` in object resolver (https://github.com/open-feature/spec/issues/138#issuecomment-1241202093): https://github.com/open-feature/java-sdk/commit/5d262470e8ec47d2af35f0aabe55e8c969e992ac


Patch:
diff --git a/src/main/java/dev/openfeature/javasdk/EvaluationContext.java b/src/main/java/dev/openfeature/javasdk/EvaluationContext.java
index fe9740b..f7f11ad 100644
--- a/src/main/java/dev/openfeature/javasdk/EvaluationContext.java
+++ b/src/main/java/dev/openfeature/javasdk/EvaluationContext.java
@@ -1,6 +1,6 @@
 package dev.openfeature.javasdk;
 
-import java.time.ZonedDateTime;
+import java.time.Instant;
 import java.util.List;
 
 import lombok.Getter;
@@ -76,7 +76,7 @@ public class EvaluationContext {
         return this;
     }
 
-    public EvaluationContext add(String key, ZonedDateTime value) {
+    public EvaluationContext add(String key, Instant value) {
         this.structure.add(key, value);
         return this;
     }
@@ -123,7 +123,7 @@ public class EvaluationContext {
             return null;
         }
 
-        public Structure add(String ignoredKey, ZonedDateTime ignoredValue) {
+        public Structure add(String ignoredKey, Instant ignoredValue) {
             return null;
         }
     }
diff --git a/src/main/java/dev/openfeature/javasdk/FeatureProvider.java b/src/main/java/dev/openfeature/javasdk/FeatureProvider.java
index f679421..0938185 100644
--- a/src/main/java/dev/openfeature/javasdk/FeatureProvider.java
+++ b/src/main/java/dev/openfeature/javasdk/FeatureProvider.java
@@ -21,5 +21,5 @@ public interface FeatureProvider {
 
     ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx);
 
-    ProviderEvaluation<Structure> getObjectEvaluation(String key, Structure defaultValue, EvaluationContext ctx);
+    ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx);
 }
diff --git a/src/main/java/dev/openfeature/javasdk/Features.java b/src/main/java/dev/openfeature/javasdk/Features.java
index 61a1d39..79bd403 100644
--- a/src/main/java/dev/openfeature/javasdk/Features.java
+++ b/src/main/java/dev/openfeature/javasdk/Features.java
@@ -57,19 +57,19 @@ public interface Features {
     FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx,
                                                    FlagEvaluationOptions options);
 
-    Structure getObjectValue(String key, Structure defaultValue);
+    Value getObjectValue(String key, Value defaultValue);
 
-    Structure getObjectValue(String key, Structure defaultValue, EvaluationContext ctx);
+    Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx);
 
-    Structure getObjectValue(String key, Structure defaultValue, EvaluationContext ctx,
+    Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx,
             FlagEvaluationOptions options);
 
-    FlagEvaluationDetails<Structure> getObjectDetails(String key, Structure defaultValue);
+    FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue);
 
-    FlagEvaluationDetails<Structure> getObjectDetails(String key, Structure defaultValue,
+    FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue,
             EvaluationContext ctx);
 
-    FlagEvaluationDetails<Structure> getObjectDetails(String key, Structure defaultValue,
+    FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue,
             EvaluationContext ctx,
             FlagEvaluationOptions options);
 }
diff --git a/src/main/java/dev/openfeature/javasdk/NoOpProvider.java b/src/main/java/dev/openfeature/javasdk/NoOpProvider.java
index 35818a7..ad3c1c9 100644
--- a/src/main/java/dev/openfeature/javasdk/NoOpProvider.java
+++ b/src/main/java/dev/openfeature/javasdk/NoOpProvider.java
@@ -57,9 +57,9 @@ public class NoOpProvider implements FeatureProvider {
     }
 
     @Override
-    public ProviderEvaluation<Structure> getObjectEvaluation(String key, Structure defaultValue,
+    public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue,
                                                          EvaluationContext invocationContext) {
-        return ProviderEvaluation.<Structure>builder()
+        return ProviderEvaluation.<Value>builder()
                 .value(defaultValue)
                 .variant(PASSED_IN_DEFAULT)
                 .reason(Reason.DEFAULT)
diff --git a/src/main/java/dev/openfeature/javasdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/javasdk/OpenFeatureClient.java
index 6b8d7f2..44c4339 100644
--- a/src/main/java/dev/openfeature/javasdk/OpenFeatureClient.java
+++ b/src/main/java/dev/openfeature/javasdk/OpenFeatureClient.java
@@ -122,7 +122,7 @@ public class OpenFeatureClient implements Client {
             case DOUBLE:
                 return provider.getDoubleEvaluation(key, (Double) defaultValue, invocationContext);
             case OBJECT:
-                return provider.getObjectEvaluation(key, (Structure) defaultValue, invocationContext);
+                return provider.getObjectEvaluation(key, (Value) defaultValue, invocationContext);
             default:
                 throw new GeneralError("Unknown flag type");
         }
@@ -257,34 +257,34 @@ public class OpenFeatureClient implements Client {
     }
 
     @Override
-    public Structure getObjectValue(String key, Structure defaultValue) {
+    public Value getObjectValue(String key, Value defaultValue) {
         return getObjectDetails(key, defaultValue).getValue();
     }
 
     @Override
-    public Structure getObjectValue(String key, Structure defaultValue, EvaluationContext ctx) {
+    public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx) {
         return getObjectDetails(key, defaultValue, ctx).getValue();
     }
 
     @Override
-    public Structure getObjectValue(String key, Structure defaultValue, EvaluationContext ctx,
+    public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx,
             FlagEvaluationOptions options) {
         return getObjectDetails(key, defaultValue, ctx, options).getValue();
     }
 
     @Override
-    public FlagEvaluationDetails<Structure> getObjectDetails(String key, Structure defaultValue) {
+    public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue) {
         return getObjectDetails(key, defaultValue, null);
     }
 
     @Override
-    public FlagEvaluationDetails<Structure> getObjectDetails(String key, Structure defaultValue,
+    public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue,
                                 EvaluationContext ctx) {
         return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
     }
 
     @Override
-    public FlagEvaluationDetails<Structure> getObjectDetails(String key, Structure defaultValue, EvaluationContext ctx,
+    public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue, EvaluationContext ctx,
                                  FlagEvaluationOptions options) {
         return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options);
     }
diff --git a/src/main/java/dev/openfeature/javasdk/Structure.java b/src/main/java/dev/openfeature/javasdk/Structure.java
index 062750f..38ff2f8 100644
--- a/src/main/java/dev/openfeature/javasdk/Structure.java
+++ b/src/main/java/dev/openfeature/javasdk/Structure.java
@@ -1,6 +1,6 @@
 package dev.openfeature.javasdk;
 
-import java.time.ZonedDateTime;
+import java.time.Instant;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -73,7 +73,7 @@ public class Structure {
      * @param value date-time value
      * @return Structure
      */
-    public Structure add(String key, ZonedDateTime value) {
+    public Structure add(String key, Instant 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 426d4a1..859348a 100644
--- a/src/main/java/dev/openfeature/javasdk/Value.java
+++ b/src/main/java/dev/openfeature/javasdk/Value.java
@@ -1,13 +1,13 @@
 package dev.openfeature.javasdk;
 
-import java.time.ZonedDateTime;
+import java.time.Instant;
 import java.util.List;
 
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 /**
- * Values serve as a return type for provider objects.
+ * Values serve as a generic return type for structure data from providers.
  * Providers may deal in JSON, protobuf, XML or some other data-interchange format.
  * This intermediate representation provides a good medium of exchange.
  */
@@ -19,7 +19,27 @@ public class Value {
     private final Object innerObject;
 
     public Value() {
-        this.innerObject = null; 
+        this.innerObject = null;
+    }
+
+    /**
+     * Construct a new Value with an Object.
+     * @param value to be wrapped.
+     * @throws InstantiationException if value is not a valid type
+     *      (boolean, string, int, double, list, structure, instant)
+     */
+    public Value(Object value) throws InstantiationException {
+        // integer is a special case, convert those.
+        this.innerObject = value instanceof Integer ? ((Integer)value).doubleValue() : value;
+        if (!this.isNull()
+            && !this.isBoolean()
+            && !this.isString()
+            && !this.isNumber()
+            && !this.isStructure()
+            && !this.isList()
+            && !this.isInstant()) {
+            throw new InstantiationException("Invalid value type: " + value.getClass());
+        }
     }
 
     public Value(Value value) {
@@ -50,7 +70,7 @@ public class Value {
         this.innerObject = value; 
     }
 
-    public Value(ZonedDateTime value) {
+    public Value(Instant value) {
         this.innerObject = value;
     }
 
@@ -109,12 +129,12 @@ public class Value {
     }
 
     /** 
-     * Check if this Value represents a ZonedDateTime.
+     * Check if this Value represents an Instant.
      * 
      * @return boolean
      */
-    public boolean isZonedDateTime() {
-        return this.innerObject instanceof ZonedDateTime;
+    public boolean isInstant() {
+        return this.innerObject instanceof Instant;
     }
     
     /** 
@@ -131,6 +151,15 @@ public class Value {
         return null;
     }
     
+    /** 
+     * Retrieve the underlying object.
+     * 
+     * @return Object
+     */
+    public Object asObject() {
+        return this.innerObject;
+    }
+
     /** 
      * Retrieve the underlying String value, or null.
      * 
@@ -194,13 +223,13 @@ public class Value {
     }
 
     /** 
-     * Retrieve the underlying ZonedDateTime value, or null.
+     * Retrieve the underlying Instant value, or null.
      * 
-     * @return ZonedDateTime
+     * @return Instant
      */
-    public ZonedDateTime asZonedDateTime() {
-        if (this.isZonedDateTime()) {
-            return (ZonedDateTime)this.innerObject;
+    public Instant asInstant() {
+        if (this.isInstant()) {
+            return (Instant)this.innerObject;
         }
         return null;
     }


Test patch:
diff --git a/src/test/java/dev/openfeature/javasdk/AlwaysBrokenProvider.java b/src/test/java/dev/openfeature/javasdk/AlwaysBrokenProvider.java
index cd172af..f69a140 100644
--- a/src/test/java/dev/openfeature/javasdk/AlwaysBrokenProvider.java
+++ b/src/test/java/dev/openfeature/javasdk/AlwaysBrokenProvider.java
@@ -33,7 +33,7 @@ public class AlwaysBrokenProvider implements FeatureProvider {
     }
 
     @Override
-    public ProviderEvaluation<Structure> getObjectEvaluation(String key, Structure defaultValue, EvaluationContext invocationContext) {
+    public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) {
         throw new NotImplementedException("BORK");
     }
 }
diff --git a/src/test/java/dev/openfeature/javasdk/DoSomethingProvider.java b/src/test/java/dev/openfeature/javasdk/DoSomethingProvider.java
index 91600e0..1fd6d9d 100644
--- a/src/test/java/dev/openfeature/javasdk/DoSomethingProvider.java
+++ b/src/test/java/dev/openfeature/javasdk/DoSomethingProvider.java
@@ -44,9 +44,9 @@ public class DoSomethingProvider implements FeatureProvider {
     }
 
     @Override
-    public ProviderEvaluation<Structure> getObjectEvaluation(String key, Structure defaultValue, EvaluationContext invocationContext) {
+    public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) {
         savedContext = invocationContext;
-        return ProviderEvaluation.<Structure>builder()
+        return ProviderEvaluation.<Value>builder()
                 .value(null)
                 .build();
     }
diff --git a/src/test/java/dev/openfeature/javasdk/EvalContextTest.java b/src/test/java/dev/openfeature/javasdk/EvalContextTest.java
index 681a7e0..2950691 100644
--- a/src/test/java/dev/openfeature/javasdk/EvalContextTest.java
+++ b/src/test/java/dev/openfeature/javasdk/EvalContextTest.java
@@ -2,7 +2,7 @@ package dev.openfeature.javasdk;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import java.time.ZonedDateTime;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -34,9 +34,9 @@ public class EvalContextTest {
         ec.add("int", 4);
         assertEquals(4, ec.getValue("int").asInteger());
 
-        ZonedDateTime dt = ZonedDateTime.now();
+        Instant dt = Instant.now();
         ec.add("dt", dt);
-        assertEquals(dt, ec.getValue("dt").asZonedDateTime());
+        assertEquals(dt, ec.getValue("dt").asInstant());
     }
 
     @Specification(number="3.1.2", text="The evaluation context MUST support the inclusion of " +
@@ -72,7 +72,7 @@ public class EvalContextTest {
         ec.add("int", 4);
         ec.add("int2", 2);
 
-        ZonedDateTime dt = ZonedDateTime.now();
+        Instant dt = Instant.now();
         ec.add("dt", dt);
 
         ec.add("obj", new Structure().add("val1", 1).add("val2", "2"));
@@ -121,14 +121,14 @@ public class EvalContextTest {
                 .add("Double", (Double)null)
                 .add("Structure", (Structure)null)
                 .add("List", (List<Value>)null)
-                .add("ZonedDateTime", (ZonedDateTime)null);
+                .add("Instant", (Instant)null);
         assertEquals(6, ec.asMap().size());
         assertEquals(null, ec.getValue("Boolean").asBoolean());
         assertEquals(null, ec.getValue("String").asString());
         assertEquals(null, ec.getValue("Double").asDouble());
         assertEquals(null, ec.getValue("Structure").asStructure());
         assertEquals(null, ec.getValue("List").asList());
-        assertEquals(null, ec.getValue("ZonedDateTime").asString());
+        assertEquals(null, ec.getValue("Instant").asString());
     }
 
     @Test void merge_targeting_key() {
diff --git a/src/test/java/dev/openfeature/javasdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/javasdk/FlagEvaluationSpecTest.java
index de58b4f..2efeec1 100644
--- a/src/test/java/dev/openfeature/javasdk/FlagEvaluationSpecTest.java
+++ b/src/test/java/dev/openfeature/javasdk/FlagEvaluationSpecTest.java
@@ -115,9 +115,9 @@ class FlagEvaluationSpecTest implements HookFixtures {
         assertEquals(40.0, c.getDoubleValue(key, .4, new EvaluationContext()));
         assertEquals(40.0, c.getDoubleValue(key, .4, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
 
-        assertEquals(null, c.getObjectValue(key, new Structure()));
-        assertEquals(null, c.getObjectValue(key, new Structure(), new EvaluationContext()));
-        assertEquals(null, c.getObjectValue(key, new Structure(), new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(null, c.getObjectValue(key, new Value()));
+        assertEquals(null, c.getObjectValue(key, new Value(), new EvaluationContext()));
+        assertEquals(null, c.getObjectValue(key, new Value(), new EvaluationContext(), FlagEvaluationOptions.builder().build()));
     }
 
     @Specification(number="1.4.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.")
diff --git a/src/test/java/dev/openfeature/javasdk/NoOpProviderTest.java b/src/test/java/dev/openfeature/javasdk/NoOpProviderTest.java
index 4a45860..0853a29 100644
--- a/src/test/java/dev/openfeature/javasdk/NoOpProviderTest.java
+++ b/src/test/java/dev/openfeature/javasdk/NoOpProviderTest.java
@@ -30,10 +30,10 @@ public class NoOpProviderTest {
         assertEquals(0.4, eval.getValue());
     }
 
-    @Test void structure() {
+    @Test void value() {
         NoOpProvider p = new NoOpProvider();
-        Structure s = new Structure();
-        ProviderEvaluation<Structure> eval = p.getObjectEvaluation("key", s, null);
+        Value s = new Value();
+        ProviderEvaluation<Value> eval = p.getObjectEvaluation("key", s, null);
         assertEquals(s, eval.getValue());
     }
 }
diff --git a/src/test/java/dev/openfeature/javasdk/ProviderSpecTest.java b/src/test/java/dev/openfeature/javasdk/ProviderSpecTest.java
index 9151bad..b5d0c58 100644
--- a/src/test/java/dev/openfeature/javasdk/ProviderSpecTest.java
+++ b/src/test/java/dev/openfeature/javasdk/ProviderSpecTest.java
@@ -36,7 +36,7 @@ public class ProviderSpecTest {
         ProviderEvaluation<Boolean> boolean_result = p.getBooleanEvaluation("key", false, new EvaluationContext());
         assertNotNull(boolean_result.getValue());
 
-        ProviderEvaluation<Structure> object_result = p.getObjectEvaluation("key", new Structure(), new EvaluationContext());
+        ProviderEvaluation<Value> object_result = p.getObjectEvaluation("key", new Value(), new EvaluationContext());
         assertNotNull(object_result.getValue());
 
     }
diff --git a/src/test/java/dev/openfeature/javasdk/StructureTest.java b/src/test/java/dev/openfeature/javasdk/StructureTest.java
index 0232bc7..eccb23c 100644
--- a/src/test/java/dev/openfeature/javasdk/StructureTest.java
+++ b/src/test/java/dev/openfeature/javasdk/StructureTest.java
@@ -4,7 +4,7 @@ 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.time.Instant;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -44,7 +44,7 @@ public class StructureTest {
         String STRING_VAL = "val";
         int INT_VAL = 13;
         double DOUBLE_VAL = .5;
-        ZonedDateTime DATE_VAL = ZonedDateTime.now();
+        Instant DATE_VAL = Instant.now();
         Structure STRUCT_VAL = new Structure();
         List<Value> LIST_VAL = new ArrayList<Value>();
         Value VALUE_VAL = new Value();
@@ -63,7 +63,7 @@ public class StructureTest {
         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(DATE_VAL, structure.getValue(DATE_KEY).asInstant());
         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
index c86feb9..57cd6aa 100644
--- a/src/test/java/dev/openfeature/javasdk/ValueTest.java
+++ b/src/test/java/dev/openfeature/javasdk/ValueTest.java
@@ -1,9 +1,11 @@
 package dev.openfeature.javasdk;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 
-import java.time.ZonedDateTime;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -15,6 +17,47 @@ public class ValueTest {
         assertTrue(value.isNull());
     }
 
+    @Test public void objectArgShouldContainObject() {
+        try {
+            // int is a special case, see intObjectArgShouldConvertToInt()
+            List<Object> list = new ArrayList<>();
+            list.add(true);
+            list.add("val");
+            list.add(.5);
+            list.add(new Structure());
+            list.add(new ArrayList<Value>());
+            list.add(Instant.now());
+
+            int i = 0;
+            for (Object l: list) {
+                Value value = new Value(l);
+                assertEquals(list.get(i), value.asObject());
+                i++;
+            }
+        } catch (Exception e) {
+            fail("No exception expected.");
+        }
+    }
+
+    @Test public void intObjectArgShouldConvertToInt() {
+        try {
+            Object innerValue = 1;
+            Value value = new Value(innerValue);
+            assertEquals(innerValue, value.asInteger());
+        } catch (Exception e) {
+            fail("No exception expected.");
+        }
+    }
+
+    @Test public void invalidObjectArgShouldThrow() {
+
+        class Something {}
+
+        assertThrows(InstantiationException.class, () -> {
+            new Value(new Something());
+        });
+    }
+
     @Test public void boolArgShouldContainBool() {
         boolean innerValue = true;
         Value value = new Value(innerValue);
@@ -44,10 +87,10 @@ public class ValueTest {
     }
 
     @Test public void dateShouldContainDate() {
-        ZonedDateTime innerValue = ZonedDateTime.now();
+        Instant innerValue = Instant.now();
         Value value = new Value(innerValue);
-        assertTrue(value.isZonedDateTime());
-        assertEquals(innerValue, value.asZonedDateTime());
+        assertTrue(value.isInstant());
+        assertEquals(innerValue, value.asInstant());
     }
 
     @Test public void structureShouldContainStructure() {