Skip to content

Commit f820fd6

Browse files
committed
Wiring for mergeable evaluation contexts.
1 parent e6ed13d commit f820fd6

File tree

5 files changed

+43
-18
lines changed

5 files changed

+43
-18
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
package dev.openfeature.javasdk;
22

33
public class EvaluationContext {
4+
/**
5+
* Merges two EvaluationContext objects with the second overriding the first in case of conflict.
6+
*/
7+
public static EvaluationContext merge(EvaluationContext ctx1, EvaluationContext ctx2) {
8+
// TODO(abrahms): Actually implement this when we know what the fields of EC are.
9+
return ctx1;
10+
}
411
}

lib/src/main/java/dev/openfeature/javasdk/Hook.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import com.google.common.collect.ImmutableMap;
44

5+
import java.util.Optional;
6+
57
// TODO: interface? or abstract class?
68
public abstract class Hook<T> {
7-
public void before(HookContext<T> ctx, ImmutableMap<String, Object> hints) {}
9+
public Optional<EvaluationContext> before(HookContext<T> ctx, ImmutableMap<String, Object> hints) {
10+
return null;
11+
}
812
public void after(HookContext<T> ctx, FlagEvaluationDetails<T> details, ImmutableMap<String, Object> hints) {}
913
public void error(HookContext<T> ctx, Exception error, ImmutableMap<String, Object> hints) {}
1014
public void finallyAfter(HookContext<T> ctx, ImmutableMap<String, Object> hints) {}

lib/src/main/java/dev/openfeature/javasdk/HookContext.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import lombok.Builder;
44
import lombok.NonNull;
55
import lombok.Value;
6+
import lombok.With;
67

7-
@Value @Builder
8+
@Value @Builder @With
89
public class HookContext<T> {
910
@NonNull String flagKey;
1011
@NonNull FlagValueType type;

lib/src/main/java/dev/openfeature/javasdk/OpenFeatureClient.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.util.ArrayList;
1212
import java.util.Arrays;
1313
import java.util.List;
14+
import java.util.Optional;
1415

1516
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
1617
public class OpenFeatureClient implements Client {
@@ -56,13 +57,15 @@ <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defa
5657

5758
FlagEvaluationDetails<T> details = null;
5859
try {
59-
this.beforeHooks(hookCtx, mergedHooks, hints);
60+
EvaluationContext ctxFromHook = this.beforeHooks(hookCtx, mergedHooks, hints);
61+
EvaluationContext invocationContext = EvaluationContext.merge(ctxFromHook, ctx);
6062

6163
ProviderEvaluation<T> providerEval;
6264
if (type == FlagValueType.BOOLEAN) {
6365
// TODO: Can we guarantee that defaultValue is a boolean? If not, how to we handle that?
64-
providerEval = (ProviderEvaluation<T>) provider.getBooleanEvaluation(key, (Boolean) defaultValue, ctx, options);
66+
providerEval = (ProviderEvaluation<T>) provider.getBooleanEvaluation(key, (Boolean) defaultValue, invocationContext, options);
6567
} else {
68+
// TODO: Support other flag types.
6669
throw new GeneralError("Unknown flag type");
6770
}
6871

@@ -106,13 +109,17 @@ private <T> void afterHooks(HookContext hookContext, FlagEvaluationDetails<T> de
106109
}
107110
}
108111

109-
private HookContext beforeHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) {
112+
private EvaluationContext beforeHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) {
110113
// These traverse backwards from normal.
114+
EvaluationContext ctx = hookCtx.getCtx();
111115
for (Hook hook : Lists.reverse(hooks)) {
112-
hook.before(hookCtx, hints);
113-
// TODO: Merge returned context w/ hook context object
116+
Optional<EvaluationContext> newCtx = hook.before(hookCtx, hints);
117+
if (newCtx.isPresent()) {
118+
ctx = EvaluationContext.merge(ctx, newCtx.get());
119+
hookCtx = hookCtx.withCtx(ctx);
120+
}
114121
}
115-
return hookCtx;
122+
return ctx;
116123
}
117124

118125
@Override

lib/src/test/java/dev/openfeature/javasdk/HookSpecTests.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package dev.openfeature.javasdk;
22

33
import com.google.common.collect.ImmutableMap;
4-
import dev.openfeature.javasdk.*;
54
import lombok.SneakyThrows;
65
import org.junit.jupiter.api.AfterEach;
76
import org.junit.jupiter.api.Disabled;
@@ -11,6 +10,7 @@
1110
import java.util.ArrayList;
1211
import java.util.Arrays;
1312
import java.util.List;
13+
import java.util.Optional;
1414

1515
import static org.junit.jupiter.api.Assertions.*;
1616
import static org.mockito.ArgumentMatchers.any;
@@ -140,7 +140,7 @@ void emptyApiHooks() {
140140
.build();
141141
}
142142

143-
@Specification(spec="hooks", number="3.2", text="The before stage MUST run before flag evaluation occurs. It accepts a hook context (required) and HookHints (optional) as parameters and returns either a HookContext or nothing.")
143+
@Specification(spec="hooks", number="3.2", text="The before stage MUST run before flag evaluation occurs. It accepts a hook context (required) and HookHints (optional) as parameters and returns either a EvaluationContext or nothing.")
144144
@Test void before_runs_ahead_of_evaluation() {
145145
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
146146
api.setProvider(new AlwaysBrokenProvider());
@@ -175,7 +175,7 @@ void emptyApiHooks() {
175175
verify(h, times(0)).error(any(), any(), any());
176176
}
177177

178-
@Specification(spec="hooks", number="3.4", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag evaluation. It accepts hook context (required), exception for what went wrong (required), and HookHints (optional). It has no return value.")
178+
@Specification(spec="hooks", number="3.6", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag evaluation. It accepts hook context (required), exception for what went wrong (required), and HookHints (optional). It has no return value.")
179179
@Test void error_hook_run_during_non_finally_stage() {
180180
final boolean[] error_called = {false};
181181
Hook h = mock(Hook.class);
@@ -196,8 +196,9 @@ void emptyApiHooks() {
196196
api.setProvider(new NoOpProvider());
197197
api.registerHooks(new Hook<Boolean>() {
198198
@Override
199-
public void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
199+
public Optional<EvaluationContext> before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
200200
evalOrder.add("api before");
201+
return null;
201202
}
202203

203204
@Override
@@ -220,8 +221,9 @@ public void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object>
220221
Client c = api.getClient();
221222
c.registerHooks(new Hook<Boolean>() {
222223
@Override
223-
public void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
224+
public Optional<EvaluationContext> before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
224225
evalOrder.add("client before");
226+
return null;
225227
}
226228

227229
@Override
@@ -243,8 +245,9 @@ public void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object>
243245
c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder()
244246
.hook(new Hook<Boolean>() {
245247
@Override
246-
public void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
248+
public Optional<EvaluationContext> before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
247249
evalOrder.add("invocation before");
250+
return null;
248251
}
249252

250253
@Override
@@ -297,8 +300,9 @@ public void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object>
297300
Client client = api.getClient();
298301
Hook<Boolean> mutatingHook = new Hook<Boolean>() {
299302
@Override
300-
public void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
303+
public Optional<EvaluationContext> before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
301304
assertTrue(hints instanceof ImmutableMap);
305+
return null;
302306
}
303307

304308
@Override
@@ -332,8 +336,8 @@ public void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object>
332336
assertTrue(feo.getHookHints().isEmpty());
333337
}
334338

335-
@Specification(spec="hooks", number="3.3", text="The after stage MUST run after flag evaluation occurs. It accepts a hook context (required), flag evaluation details (required) and HookHints (optional). It has no return value.")
336-
@Specification(spec="hooks", number="3.5", text="The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and HookHints (optional). There is no return value.")
339+
@Specification(spec="hooks", number="3.5", text="The after stage MUST run after flag evaluation occurs. It accepts a hook context (required), flag evaluation details (required) and HookHints (optional). It has no return value.")
340+
@Specification(spec="hooks", number="3.7", text="The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and HookHints (optional). There is no return value.")
337341
@Test void flag_eval_hook_order() {
338342
Hook hook = mock(Hook.class);
339343
FeatureProvider provider = mock(FeatureProvider.class);
@@ -391,12 +395,14 @@ public void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object>
391395
verify(hook2, times(1)).error(any(), any(), any());
392396
}
393397

398+
@Specification(spec="hooks", number="3.3", text="Any `EvaluationContext` returned from a `before` hook **MUST** be passed to subsequent `before` hooks (via `HookContext`).")
399+
@Specification(spec="hooks", number="3.4", text="When `before` hooks have finished executing, any resulting `EvaluationContext` **MUST** be merged with the invocation `EvaluationContext` wherein the invocation `EvaluationContext` win any conflicts.")
394400
@Specification(spec="hooks", number="1.4", text="The evaluation context MUST be mutable only within the before hook.")
395401
@Specification(spec="hooks", number="3.1", text="Hooks MUST specify at least one stage.")
396402
@Test @Disabled void todo() {}
397403

398404
@SneakyThrows
399-
@Specification(spec="hooks", number="3.6", text="Condition: If finally is a reserved word in the language, finallyAfter SHOULD be used.")
405+
@Specification(spec="hooks", number="3.8", text="Condition: If finally is a reserved word in the language, finallyAfter SHOULD be used.")
400406
@Test void doesnt_use_finally() {
401407
try {
402408
Hook.class.getMethod("finally", HookContext.class, ImmutableMap.class);

0 commit comments

Comments
 (0)