Repository: open-feature/java-sdk
Pull request #913 (2024-04-30 13:35:50)

fix: consistent method chainability
-Client Interface: Modified `setEvaluationContext` and `addHooks` to return `Client` instances, enabling chaining.
-MutableContext: Updated `setTargetingKey` to return a `MutableContext` instance.
-OpenFeatureAPI: Altered `setEvaluationContext` to return `OpenFeatureAPI` instances.
-OpenFeatureClient: Updated both `addHooks` and `setEvaluationContext` to return `OpenFeatureClient` instances.
-Added unit tests to verify chainability for each updated method.

Fixes: https://github.com/open-feature/java-sdk/issues/906

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=913) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=open-feature_java-sdk&pullRequest=913&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=913&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-feature_java-sdk&pullRequest=913&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=913&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=913)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/open-feature/java-sdk/pull/913?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 95.21%. Comparing base [(`c7ebef3`)](https://app.codecov.io/gh/open-feature/java-sdk/commit/c7ebef3b8fe8e47f3921d28eef6adf1e3eccc3ba?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) to head [(`12bae94`)](https://app.codecov.io/gh/open-feature/java-sdk/pull/913?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).
> Report is 5 commits behind head on main.


<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main     #913      +/-   ##
============================================
+ Coverage     94.98%   95.21%   +0.22%     
- Complexity      383      384       +1     
============================================
  Files            36       36              
  Lines           877      877              
  Branches         53       53              
============================================
+ Hits            833      835       +2     
+ Misses           24       23       -1     
+ Partials         20       19       -1     
```

| [Flag](https://app.codecov.io/gh/open-feature/java-sdk/pull/913/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/open-feature/java-sdk/pull/913/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | `95.21% <100.00%> (+0.22%)` | :arrow_up: |

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.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/open-feature/java-sdk/pull/913?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature).

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=913) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=open-feature_java-sdk&pullRequest=913&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=913&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-feature_java-sdk&pullRequest=913&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=913&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=913)


toddbaert: I'll merge this tomorrow at the latest unless anyone has objections.
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=913) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=open-feature_java-sdk&pullRequest=913&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=913&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-feature_java-sdk&pullRequest=913&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-feature_java-sdk&pullRequest=913&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=open-feature_java-sdk&pullRequest=913)



Patch:
diff --git a/src/main/java/dev/openfeature/sdk/Client.java b/src/main/java/dev/openfeature/sdk/Client.java
index ebca0b1..4494180 100644
--- a/src/main/java/dev/openfeature/sdk/Client.java
+++ b/src/main/java/dev/openfeature/sdk/Client.java
@@ -18,7 +18,7 @@ public interface Client extends Features, EventBus<Client> {
      * Set the client-level evaluation context.
      * @param ctx Client level context.
      */
-    void setEvaluationContext(EvaluationContext ctx);
+    Client setEvaluationContext(EvaluationContext ctx);
 
     /**
      * Adds hooks for evaluation.
@@ -26,7 +26,7 @@ public interface Client extends Features, EventBus<Client> {
      *
      * @param hooks The hook to add.
      */
-    void addHooks(Hook... hooks);
+    Client addHooks(Hook... hooks);
 
     /**
      * Fetch the hooks associated to this client.
diff --git a/src/main/java/dev/openfeature/sdk/MutableContext.java b/src/main/java/dev/openfeature/sdk/MutableContext.java
index b63f9b3..7de394f 100644
--- a/src/main/java/dev/openfeature/sdk/MutableContext.java
+++ b/src/main/java/dev/openfeature/sdk/MutableContext.java
@@ -87,10 +87,11 @@ public class MutableContext implements EvaluationContext {
     /**
      * Override or set targeting key for this mutable context. Value should be non-null and non-empty to be accepted.
      */
-    public void setTargetingKey(String targetingKey) {
+    public MutableContext setTargetingKey(String targetingKey) {
         if (targetingKey != null && !targetingKey.trim().isEmpty()) {
             this.add(TARGETING_KEY, targetingKey);
         }
+        return this;
     }
 
 
diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
index eb9d3a7..896d60a 100644
--- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
+++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
@@ -83,10 +83,11 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
     /**
      * {@inheritDoc}
      */
-    public void setEvaluationContext(EvaluationContext evaluationContext) {
+    public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext) {
         try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
             this.evaluationContext = evaluationContext;
         }
+        return this;
     }
 
     /**
diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
index 3455c0c..ce763a3 100644
--- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
+++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
@@ -56,10 +56,11 @@ public class OpenFeatureClient implements Client {
      * {@inheritDoc}
      */
     @Override
-    public void addHooks(Hook... hooks) {
+    public OpenFeatureClient addHooks(Hook... hooks) {
         try (AutoCloseableLock __ = this.hooksLock.writeLockAutoCloseable()) {
             this.clientHooks.addAll(Arrays.asList(hooks));
         }
+        return this;
     }
 
     /**
@@ -76,10 +77,11 @@ public class OpenFeatureClient implements Client {
      * {@inheritDoc}
      */
     @Override
-    public void setEvaluationContext(EvaluationContext evaluationContext) {
+    public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContext) {
         try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) {
             this.evaluationContext = evaluationContext;
         }
+        return this;
     }
 
     /**


Test patch:
diff --git a/src/test/java/dev/openfeature/sdk/MutableContextTest.java b/src/test/java/dev/openfeature/sdk/MutableContextTest.java
index 1d462b1..df21e6e 100644
--- a/src/test/java/dev/openfeature/sdk/MutableContextTest.java
+++ b/src/test/java/dev/openfeature/sdk/MutableContextTest.java
@@ -116,4 +116,19 @@ class MutableContextTest {
         Structure value = key1.asStructure();
         assertArrayEquals(new Object[]{"key1_1"}, value.keySet().toArray());
     }
+
+    @DisplayName("Ensure mutations are chainable")
+    @Test
+    void shouldAllowChainingOfMutations() {
+        MutableContext context = new MutableContext();
+        context.add("key1", "val1")
+                .add("key2", 2)
+                .setTargetingKey("TARGETING_KEY")
+                .add("key3", 3.0);
+
+        assertEquals("TARGETING_KEY", context.getTargetingKey());
+        assertEquals("val1", context.getValue("key1").asString());
+        assertEquals(2, context.getValue("key2").asInteger());
+        assertEquals(3.0, context.getValue("key3").asDouble());        
+    }
 }
diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java
index 63a1dad..eceace2 100644
--- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java
+++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java
@@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.util.Collections;
+import java.util.HashMap;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -78,4 +79,12 @@ class OpenFeatureAPITest {
     void settingTransactionalContextPropagatorToNullErrors() {
         assertThatCode(() -> api.setTransactionContextPropagator(null)).isInstanceOf(IllegalArgumentException.class);
     }
+
+    @Test
+    void setEvaluationContextShouldAllowChaining() {
+        OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
+        EvaluationContext ctx = new ImmutableContext("targeting key", new HashMap<>());
+        OpenFeatureClient result = client.setEvaluationContext(ctx);
+        assertEquals(client, result);
+    }
 }
diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java
index 9036576..d6340a8 100644
--- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java
+++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java
@@ -3,12 +3,18 @@ package dev.openfeature.sdk;
 import java.util.*;
 
 import dev.openfeature.sdk.fixtures.HookFixtures;
+
 import org.junit.jupiter.api.*;
 import org.mockito.Mockito;
 import org.simplify4u.slf4jmock.LoggerMock;
 import org.slf4j.Logger;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.*;
 
 class OpenFeatureClientTest implements HookFixtures {
@@ -67,4 +73,28 @@ class OpenFeatureClientTest implements HookFixtures {
 
         assertThat(result.getValue()).isTrue();
     }
+
+    @Test
+    @DisplayName("addHooks should allow chaining by returning the same client instance")
+    void addHooksShouldAllowChaining() {
+        OpenFeatureAPI api = mock(OpenFeatureAPI.class);
+        OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
+        Hook<?> hook1 = Mockito.mock(Hook.class);
+        Hook<?> hook2 = Mockito.mock(Hook.class);
+
+        OpenFeatureClient result = client.addHooks(hook1, hook2);
+        assertEquals(client, result);  
+    }
+
+    @Test
+    @DisplayName("setEvaluationContext should allow chaining by returning the same client instance")
+    void setEvaluationContextShouldAllowChaining() {
+        OpenFeatureAPI api = mock(OpenFeatureAPI.class);
+        OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
+        EvaluationContext ctx = new ImmutableContext("targeting key", new HashMap<>());
+
+        OpenFeatureClient result = client.setEvaluationContext(ctx);
+        assertEquals(client, result);
+    }
+   
 }