Repository: open-feature/java-sdk
Pull request #75 (2022-09-16 13:57:40)

feat: Add asObjectMap to get the EvaluationContext as Map<String,Object>
Add a new method called `asObjectMap` that convert the `EvaluationContext` into a `Map<String,Object>`.

This utility method will be used by the providers when they have to convert the `EvaluationContext` to transform them into the query they need to send to the backend system.

codecov-commenter: # [Codecov](https://codecov.io/gh/open-feature/java-sdk/pull/75?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
> Merging [#75](https://codecov.io/gh/open-feature/java-sdk/pull/75?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (7f7da25) into [main](https://codecov.io/gh/open-feature/java-sdk/commit/5db90f1a3898bd2248a5b77cc58b8f3b0e474eaf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (5db90f1) will **decrease** coverage by `0.08%`.
> The diff coverage is `90.32%`.

> :exclamation: Current head 7f7da25 differs from pull request most recent head 7f96af5. Consider uploading reports for the commit 7f96af5 to get more accurate results

```diff
@@             Coverage Diff              @@
##               main      #75      +/-   ##
============================================
- Coverage     91.32%   91.24%   -0.09%     
- Complexity      162      173      +11     
============================================
  Files            19       19              
  Lines           346      377      +31     
  Branches         16       22       +6     
============================================
+ Hits            316      344      +28     
- Misses           21       22       +1     
- Partials          9       11       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `91.24% <90.32%> (-0.09%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/open-feature/java-sdk/pull/75?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Coverage Δ | |
|---|---|---|
| [...c/main/java/dev/openfeature/javasdk/Structure.java](https://codecov.io/gh/open-feature/java-sdk/pull/75/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9TdHJ1Y3R1cmUuamF2YQ==) | `92.85% <90.32%> (-3.15%)` | :arrow_down: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature)


Patch:
diff --git a/src/main/java/dev/openfeature/javasdk/Structure.java b/src/main/java/dev/openfeature/javasdk/Structure.java
index 38ff2f8..87491e6 100644
--- a/src/main/java/dev/openfeature/javasdk/Structure.java
+++ b/src/main/java/dev/openfeature/javasdk/Structure.java
@@ -1,11 +1,10 @@
 package dev.openfeature.javasdk;
 
 import java.time.Instant;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
 
+import dev.openfeature.javasdk.exceptions.ValueNotConvertableError;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
@@ -66,9 +65,9 @@ public class Structure {
         return this;
     }
 
-    /** 
+    /**
      * Add date-time relevant key.
-     * 
+     *
      * @param key feature key
      * @param value date-time value
      * @return Structure
@@ -90,10 +89,73 @@ public class Structure {
 
     /**
      * Get all values.
-     * 
+     *
      * @return all attributes on the structure
      */
     public Map<String, Value> asMap() {
         return new HashMap<>(this.attributes);
     }
+
+    /**
+     * Get all values, with primitives types.
+     *
+     * @return all attributes on the structure into a Map
+     */
+    public Map<String, Object> asObjectMap() {
+        return attributes
+            .entrySet()
+            .stream()
+            .collect(Collectors.toMap(
+                Map.Entry::getKey,
+                e -> convertValue(getValue(e.getKey()))
+            ));
+    }
+
+    /**
+     * convertValue is converting the object type Value in a primitive type.
+     * @param value - Value object to convert
+     * @return an Object containing the primitive type.
+     */
+    private Object convertValue(Value value) {
+        if (value.isBoolean()) {
+            return value.asBoolean();
+        }
+
+        if (value.isNumber()) {
+            Double valueAsDouble = value.asDouble();
+            if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
+                return value.asInteger();
+            }
+            return valueAsDouble;
+        }
+
+        if (value.isString()) {
+            return value.asString();
+        }
+
+        if (value.isInstant()) {
+            return value.asInstant();
+        }
+
+        if (value.isList()) {
+            return value.asList()
+                    .stream()
+                    .map(this::convertValue)
+                    .collect(Collectors.toList());
+        }
+
+        if (value.isStructure()) {
+            Structure s = value.asStructure();
+            return s.asMap()
+                    .keySet()
+                    .stream()
+                    .collect(
+                        Collectors.toMap(
+                            key -> key,
+                            key -> convertValue(s.getValue(key))
+                        )
+                    );
+        }
+        throw new ValueNotConvertableError();
+    }
 }
diff --git a/src/main/java/dev/openfeature/javasdk/exceptions/ValueNotConvertableError.java b/src/main/java/dev/openfeature/javasdk/exceptions/ValueNotConvertableError.java
new file mode 100644
index 0000000..1cbff41
--- /dev/null
+++ b/src/main/java/dev/openfeature/javasdk/exceptions/ValueNotConvertableError.java
@@ -0,0 +1,12 @@
+package dev.openfeature.javasdk.exceptions;
+
+import dev.openfeature.javasdk.ErrorCode;
+import lombok.Getter;
+import lombok.experimental.StandardException;
+
+@StandardException
+public class ValueNotConvertableError extends OpenFeatureError {
+    private static final long serialVersionUID = 1L;
+    @Getter
+    private final ErrorCode errorCode = ErrorCode.GENERAL;
+}


Test patch:
diff --git a/src/test/java/dev/openfeature/javasdk/EvalContextTest.java b/src/test/java/dev/openfeature/javasdk/EvalContextTest.java
index 2950691..3cb2078 100644
--- a/src/test/java/dev/openfeature/javasdk/EvalContextTest.java
+++ b/src/test/java/dev/openfeature/javasdk/EvalContextTest.java
@@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -148,4 +149,55 @@ public class EvalContextTest {
         ctxMerged = EvaluationContext.merge(ctx1, ctx2);
         assertEquals(key1, ctxMerged.getTargetingKey());
     }
+
+    @Test void asObjectMap() {
+        String key1 = "key1";
+        EvaluationContext ctx = new EvaluationContext(key1);
+        ctx.add("stringItem", "stringValue");
+        ctx.add("boolItem", false);
+        ctx.add("integerItem", 1);
+        ctx.add("doubleItem", 1.2);
+        ctx.add("instantItem",  Instant.ofEpochSecond(1663331342));
+        List<Value> listItem = new ArrayList<>();
+        listItem.add(new Value("item1"));
+        listItem.add(new Value("item2"));
+        ctx.add("listItem", listItem);
+        List<Value> listItem2 = new ArrayList<>();
+        listItem2.add(new Value(true));
+        listItem2.add(new Value(false));
+        ctx.add("listItem2", listItem2);
+        Map<String, Value> structureValue = new HashMap<>();
+        structureValue.put("structStringItem", new Value("stringValue"));
+        structureValue.put("structBoolItem", new Value(false));
+        structureValue.put("structIntegerItem", new Value(1));
+        structureValue.put("structDoubleItem", new Value(1.2));
+        structureValue.put("structInstantItem",  new Value(Instant.ofEpochSecond(1663331342)));
+        Structure structure = new Structure(structureValue);
+        ctx.add("structureItem", structure);
+
+
+        Map<String, Object> want = new HashMap<>();
+        want.put("stringItem", "stringValue");
+        want.put("boolItem", false);
+        want.put("integerItem", 1);
+        want.put("doubleItem", 1.2);
+        want.put("instantItem",  Instant.ofEpochSecond(1663331342));
+        List<String> wantListItem = new ArrayList<>();
+        wantListItem.add("item1");
+        wantListItem.add("item2");
+        want.put("listItem", wantListItem);
+        List<Boolean> wantListItem2 = new ArrayList<>();
+        wantListItem2.add(true);
+        wantListItem2.add(false);
+        want.put("listItem2", wantListItem2);
+        Map<String, Object> wantStructureValue = new HashMap<>();
+        wantStructureValue.put("structStringItem", "stringValue");
+        wantStructureValue.put("structBoolItem", false);
+        wantStructureValue.put("structIntegerItem", 1);
+        wantStructureValue.put("structDoubleItem", 1.2);
+        wantStructureValue.put("structInstantItem",  Instant.ofEpochSecond(1663331342));
+        want.put("structureItem",wantStructureValue);
+
+        assertEquals(want,ctx.asObjectMap());
+    }
 }