Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 16 additions & 27 deletions src/main/java/dev/openfeature/sdk/HookSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ class HookSupport {
* Sets the {@link Hook}-{@link HookContext}-{@link Pair} list in the given data object with {@link HookContext}
* set to null. Filters hooks by supported {@link FlagValueType}.
*
* @param hookSupportData the data object to modify
* @param hooks the hooks to set
* @param type the flag value type to filter unsupported hooks
* @param hookSupportData the data object to modify
* @param hooks the hooks to set
* @param type the flag value type to filter unsupported hooks
*/
public void setHooks(HookSupportData hookSupportData, List<Hook> hooks, FlagValueType type) {
List<Pair<Hook, HookContext>> hookContextPairs = new ArrayList<>();
Expand All @@ -35,35 +35,20 @@ public void setHooks(HookSupportData hookSupportData, List<Hook> hooks, FlagValu
* Creates & sets a {@link HookContext} for every {@link Hook}-{@link HookContext}-{@link Pair}
* in the given data object with a new {@link HookData} instance.
*
* @param hookSupportData the data object to modify
* @param sharedContext the shared context from which the new {@link HookContext} is created
* @param hookSupportData the data object to modify
* @param sharedContext the shared context from which the new {@link HookContext} is created
*/
public void setHookContexts(HookSupportData hookSupportData, SharedHookContext sharedContext) {
public void setHookContexts(
HookSupportData hookSupportData,
SharedHookContext sharedContext,
LayeredEvaluationContext evaluationContext) {
for (int i = 0; i < hookSupportData.hooks.size(); i++) {
Pair<Hook, HookContext> hookContextPair = hookSupportData.hooks.get(i);
HookContext curHookContext = sharedContext.hookContextFor(null, new DefaultHookData());
HookContext curHookContext = sharedContext.hookContextFor(evaluationContext, new DefaultHookData());
hookContextPair.setValue(curHookContext);
}
}

/**
* Updates the evaluation context in the given data object's eval context and each hooks eval context.
*
* @param hookSupportData the data object to modify
* @param evaluationContext the new context to set
*/
public void updateEvaluationContext(HookSupportData hookSupportData, EvaluationContext evaluationContext) {
hookSupportData.evaluationContext = evaluationContext;
if (hookSupportData.hooks != null) {
for (Pair<Hook, HookContext> hookContextPair : hookSupportData.hooks) {
var curHookContext = hookContextPair.getValue();
if (curHookContext != null) {
curHookContext.setCtx(evaluationContext);
}
}
}
}

public void executeBeforeHooks(HookSupportData data) {
// These traverse backwards from normal.
List<Pair<Hook, HookContext>> reversedHooks = new ArrayList<>(data.getHooks());
Expand All @@ -77,8 +62,12 @@ public void executeBeforeHooks(HookSupportData data) {
hook.before(hookContext, data.getHints()))
.orElse(Optional.empty());
if (returnedEvalContext.isPresent()) {
// update shared evaluation context for all hooks
updateEvaluationContext(data, data.getEvaluationContext().merge(returnedEvalContext.get()));
var returnedContext = returnedEvalContext.get();
if (returnedContext.isEmpty()) {
continue;
}

data.evaluationContext.putAllHookContexts(returnedContext.asMap());
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/dev/openfeature/sdk/HookSupportData.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class HookSupportData {

List<Pair<Hook, HookContext>> hooks;
EvaluationContext evaluationContext;
LayeredEvaluationContext evaluationContext;
Map<String, Object> hints;

HookSupportData() {}
Expand Down
184 changes: 184 additions & 0 deletions src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package dev.openfeature.sdk;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
* LayeredEvaluationContext implements EvaluationContext by layering multiple contexts:
* API-level, Transaction-level, Client-level, Invocation-level, and Hook-level.
* The contexts are checked in that order for values, with Hook-level having the highest precedence.
*/
public class LayeredEvaluationContext implements EvaluationContext {
private final EvaluationContext apiContext;
private final EvaluationContext transactionContext;
private final EvaluationContext clientContext;
private final EvaluationContext invocationContext;
private final HashMap<String, Value> hookContext = new HashMap<>();
private final String targetingKey;

private Set<String> keySet = null;

/**
* Constructor for LayeredEvaluationContext.
*/
public LayeredEvaluationContext(
EvaluationContext apiContext,
EvaluationContext transactionContext,
EvaluationContext clientContext,
EvaluationContext invocationContext) {
this.apiContext = apiContext;
this.transactionContext = transactionContext;
this.clientContext = clientContext;
this.invocationContext = invocationContext;

if (invocationContext != null && invocationContext.getTargetingKey() != null) {
this.targetingKey = invocationContext.getTargetingKey();
} else if (clientContext != null && clientContext.getTargetingKey() != null) {
this.targetingKey = clientContext.getTargetingKey();
} else if (transactionContext != null && transactionContext.getTargetingKey() != null) {
this.targetingKey = transactionContext.getTargetingKey();
} else if (apiContext != null && apiContext.getTargetingKey() != null) {
this.targetingKey = apiContext.getTargetingKey();
} else {
this.targetingKey = null;
}
}

@Override
public String getTargetingKey() {
return targetingKey;
}

@Override
public EvaluationContext merge(EvaluationContext overridingContext) {
return null;
}

@Override
public boolean isEmpty() {
return hookContext.isEmpty()
&& (invocationContext == null || invocationContext.isEmpty())
&& (clientContext == null || clientContext.isEmpty())
&& (transactionContext == null || transactionContext.isEmpty())
&& (apiContext == null || apiContext.isEmpty());
}

@Override
public Set<String> keySet() {
return new HashSet<>(ensureKeySet());
}

private Set<String> ensureKeySet() {
if (this.keySet != null) {
return this.keySet;
}

var keys = new HashSet<>(hookContext.keySet());

if (invocationContext != null) {
keys.addAll(invocationContext.keySet());
}
if (clientContext != null) {
keys.addAll(clientContext.keySet());
}
if (transactionContext != null) {
keys.addAll(transactionContext.keySet());
}
if (apiContext != null) {
keys.addAll(apiContext.keySet());
}
this.keySet = keys;
return keys;
}

private Value getFromContext(EvaluationContext context, String key) {
if (context != null) {
return context.getValue(key);
}
return null;
}

private Value getFromContext(HashMap<String, Value> context, String key) {
if (context != null) {
return context.get(key);
}
return null;
}

@Override
public Value getValue(String key) {
var hookValue = getFromContext(hookContext, key);
if (hookValue != null) {
return hookValue;
}
var invocationValue = getFromContext(invocationContext, key);
if (invocationValue != null) {
return invocationValue;
}
var clientValue = getFromContext(clientContext, key);
if (clientValue != null) {
return clientValue;
}
var transactionValue = getFromContext(transactionContext, key);
if (transactionValue != null) {
return transactionValue;
}
return getFromContext(apiContext, key);
}

@Override
public Map<String, Value> asMap() {
var keySet = ensureKeySet();
var keys = keySet.size();
if (keys == 0) {
return new HashMap<>(1);
}
var map = new HashMap<String, Value>(keys);

for (String key : keySet) {
map.put(key, getValue(key));
}
return map;
}

@Override
public Map<String, Value> asUnmodifiableMap() {
var keySet = ensureKeySet();
var keys = keySet.size();
if (keys == 0) {
return Collections.emptyMap();
}
var map = new HashMap<String, Value>(keys);

for (String key : keySet) {
map.put(key, getValue(key));
}
return Collections.unmodifiableMap(map);
}

@Override
public Map<String, Object> asObjectMap() {
var keySet = ensureKeySet();
var keys = keySet.size();
if (keys == 0) {
return new HashMap<>(1);
}
var map = new HashMap<String, Object>(keys);

for (String key : keySet) {
map.put(key, convertValue(getValue(key)));
}
return map;
}

public void putHookContext(String key, Value value) {
this.hookContext.put(key, value);
}

public void putAllHookContexts(Map<String, Value> context) {
this.hookContext.putAll(context);
}
}
11 changes: 7 additions & 4 deletions src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
var flagOptions = ObjectUtils.defaultIfNull(
options, () -> FlagEvaluationOptions.builder().build());
hookSupportData.hints = Collections.unmodifiableMap(flagOptions.getHookHints());
var context = new LayeredEvaluationContext(
openfeatureApi.getEvaluationContext(),
openfeatureApi.getTransactionContext(),
evaluationContext.get(),
ctx);
hookSupportData.evaluationContext = context;

try {
final var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain);
Expand All @@ -180,10 +186,7 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(

var sharedHookContext =
new SharedHookContext(key, type, this.getMetadata(), provider.getMetadata(), defaultValue);
hookSupport.setHookContexts(hookSupportData, sharedHookContext);

var evalContext = mergeEvaluationContext(ctx);
hookSupport.updateEvaluationContext(hookSupportData, evalContext);
hookSupport.setHookContexts(hookSupportData, sharedHookContext, context);

hookSupport.executeBeforeHooks(hookSupportData);

Expand Down
2 changes: 1 addition & 1 deletion src/test/java/dev/openfeature/sdk/HookSpecTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ void mergeHappensCorrectly() {
invocationCtx,
FlagEvaluationOptions.builder().hook(hook).build());

ArgumentCaptor<ImmutableContext> captor = ArgumentCaptor.forClass(ImmutableContext.class);
ArgumentCaptor<LayeredEvaluationContext> captor = ArgumentCaptor.forClass(LayeredEvaluationContext.class);
verify(provider).getBooleanEvaluation(any(), any(), captor.capture());
EvaluationContext ec = captor.getValue();
assertEquals("works", ec.getValue("test").asString());
Expand Down
15 changes: 11 additions & 4 deletions src/test/java/dev/openfeature/sdk/HookSupportTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() {
when(hook1.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("bla", "blubber")));
when(hook2.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("foo", "bar")));

var layered = new LayeredEvaluationContext(baseEvalContext, null, null, null);
var sharedContext = getBaseHookContextForType(FlagValueType.STRING);
var hookSupportData = new HookSupportData();
hookSupportData.evaluationContext = layered;
hookSupport.setHooks(hookSupportData, Arrays.asList(hook1, hook2), FlagValueType.STRING);
hookSupport.setHookContexts(hookSupportData, sharedContext);
hookSupport.updateEvaluationContext(hookSupportData, baseEvalContext);
hookSupport.setHookContexts(hookSupportData, sharedContext, layered);

hookSupport.executeBeforeHooks(hookSupportData);

Expand Down Expand Up @@ -72,7 +73,10 @@ void shouldPassDataAcrossStages(FlagValueType flagValueType) {
var testHook = new TestHookWithData();
var hookSupportData = new HookSupportData();
hookSupport.setHooks(hookSupportData, List.of(testHook), flagValueType);
hookSupport.setHookContexts(hookSupportData, getBaseHookContextForType(flagValueType));
hookSupport.setHookContexts(
hookSupportData,
getBaseHookContextForType(flagValueType),
new LayeredEvaluationContext(null, null, null, null));

hookSupport.executeBeforeHooks(hookSupportData);
assertHookData(testHook, "before");
Expand All @@ -98,7 +102,10 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) {

var hookSupportData = new HookSupportData();
hookSupport.setHooks(hookSupportData, List.of(testHook1, testHook2), flagValueType);
hookSupport.setHookContexts(hookSupportData, getBaseHookContextForType(flagValueType));
hookSupport.setHookContexts(
hookSupportData,
getBaseHookContextForType(flagValueType),
new LayeredEvaluationContext(null, null, null, null));

callAllHooks(hookSupportData);

Expand Down
Loading
Loading