Skip to content

Commit 0ffba67

Browse files
feat: add hook data support
Signed-off-by: Alexandra Oberaigner <[email protected]>
1 parent 3ef41f5 commit 0ffba67

File tree

12 files changed

+540
-80
lines changed

12 files changed

+540
-80
lines changed

src/main/java/dev/openfeature/sdk/HookContext.java

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

33
import lombok.Builder;
4+
import lombok.Data;
45
import lombok.NonNull;
56
import lombok.Value;
67
import lombok.With;
8+
import lombok.experimental.NonFinal;
79

810
/**
911
* A data class to hold immutable context that {@link Hook} instances use.
1012
*
1113
* @param <T> the type for the flag being evaluated
1214
*/
13-
@Value
15+
@Data
1416
@Builder
1517
@With
1618
public class HookContext<T> {
@@ -25,8 +27,10 @@ public class HookContext<T> {
2527
ClientMetadata clientMetadata;
2628
Metadata providerMetadata;
2729

30+
HookData hookData;
31+
2832
/**
29-
* Builds a {@link HookContext} instances from request data.
33+
* Builds {@link HookContext} instances from request data.
3034
*
3135
* @param key feature flag key
3236
* @param type flag value type
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dev.openfeature.sdk;
2+
3+
public class HookContextWithData<T> extends HookContext<T> {
4+
5+
private HookContextWithData(String flagKey, FlagValueType type, T defaultValue,
6+
EvaluationContext ctx, ClientMetadata clientMetadata, Metadata providerMetadata, HookData data) {
7+
super(flagKey, type, defaultValue, ctx, clientMetadata, providerMetadata, data);
8+
}
9+
10+
public static <T> HookContextWithData<T> of(HookContext<T> context, HookData data) {
11+
return new HookContextWithData<>(context.getFlagKey(), context.getType(), context.getDefaultValue(), context.getCtx(), context.getClientMetadata(),
12+
context.getProviderMetadata(), data);
13+
}
14+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package dev.openfeature.sdk;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
/**
7+
* Hook data provides a way for hooks to maintain state across their execution stages.
8+
* Each hook instance gets its own isolated data store that persists only for the duration
9+
* of a single flag evaluation.
10+
*/
11+
public interface HookData {
12+
13+
/**
14+
* Sets a value for the given key.
15+
*
16+
* @param key the key to store the value under
17+
* @param value the value to store
18+
*/
19+
void set(String key, Object value);
20+
21+
/**
22+
* Gets the value for the given key.
23+
*
24+
* @param key the key to retrieve the value for
25+
* @return the value, or null if not found
26+
*/
27+
Object get(String key);
28+
29+
/**
30+
* Gets the value for the given key, cast to the specified type.
31+
*
32+
* @param <T> the type to cast to
33+
* @param key the key to retrieve the value for
34+
* @param type the class to cast to
35+
* @return the value cast to the specified type, or null if not found
36+
* @throws ClassCastException if the value cannot be cast to the specified type
37+
*/
38+
<T> T get(String key, Class<T> type);
39+
40+
/**
41+
* Default implementation uses a HashMap.
42+
*/
43+
static HookData create() {
44+
return new DefaultHookData();
45+
}
46+
47+
/**
48+
* Default implementation of HookData.
49+
*/
50+
class DefaultHookData implements HookData {
51+
private Map<String, Object> data;
52+
53+
@Override
54+
public void set(String key, Object value) {
55+
if (data == null) {
56+
data = new HashMap<>();
57+
}
58+
data.put(key, value);
59+
}
60+
61+
@Override
62+
public Object get(String key) {
63+
if (data == null) {
64+
return null;
65+
}
66+
return data.get(key);
67+
}
68+
69+
@Override
70+
public <T> T get(String key, Class<T> type) {
71+
Object value = get(key);
72+
if (value == null) {
73+
return null;
74+
}
75+
if (!type.isInstance(value)) {
76+
throw new ClassCastException("Value for key '" + key + "' is not of type " + type.getName());
77+
}
78+
return type.cast(value);
79+
}
80+
}
81+
}

src/main/java/dev/openfeature/sdk/HookSupport.java

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import java.util.List;
66
import java.util.Map;
77
import java.util.Optional;
8-
import java.util.function.Consumer;
8+
import java.util.function.BiConsumer;
99
import lombok.RequiredArgsConstructor;
1010
import lombok.extern.slf4j.Slf4j;
1111

@@ -15,52 +15,81 @@
1515
class HookSupport {
1616

1717
public EvaluationContext beforeHooks(
18-
FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks, Map<String, Object> hints) {
19-
return callBeforeHooks(flagValueType, hookCtx, hooks, hints);
18+
FlagValueType flagValueType,
19+
HookContext hookCtx,
20+
List<Pair<Hook, HookData>> hookDataPairs,
21+
Map<String, Object> hints) {
22+
return callBeforeHooks(flagValueType, hookCtx, hookDataPairs, hints);
2023
}
2124

2225
public void afterHooks(
2326
FlagValueType flagValueType,
2427
HookContext hookContext,
2528
FlagEvaluationDetails details,
26-
List<Hook> hooks,
29+
List<Pair<Hook, HookData>> hookDataPairs,
2730
Map<String, Object> hints) {
28-
executeHooksUnchecked(flagValueType, hooks, hook -> hook.after(hookContext, details, hints));
31+
executeHooksUnchecked(
32+
flagValueType, hookDataPairs, hookContext, (hook, ctx) -> hook.after(ctx, details, hints));
2933
}
3034

3135
public void afterAllHooks(
3236
FlagValueType flagValueType,
3337
HookContext hookCtx,
3438
FlagEvaluationDetails details,
35-
List<Hook> hooks,
39+
List<Pair<Hook, HookData>> hookDataPairs,
3640
Map<String, Object> hints) {
37-
executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, details, hints));
41+
executeHooks(
42+
flagValueType,
43+
hookDataPairs,
44+
hookCtx,
45+
"finally",
46+
(hook, ctx) -> hook.finallyAfter(ctx, details, hints));
3847
}
3948

4049
public void errorHooks(
4150
FlagValueType flagValueType,
4251
HookContext hookCtx,
4352
Exception e,
44-
List<Hook> hooks,
53+
List<Pair<Hook, HookData>> hookDataPairs,
4554
Map<String, Object> hints) {
46-
executeHooks(flagValueType, hooks, "error", hook -> hook.error(hookCtx, e, hints));
55+
executeHooks(flagValueType, hookDataPairs, hookCtx, "error", (hook, ctx) -> hook.error(ctx, e, hints));
56+
}
57+
58+
public List<Pair<Hook, HookData>> getHookDataPairs(List<Hook> hooks, FlagValueType flagValueType) {
59+
var pairs = new ArrayList<Pair<Hook, HookData>>();
60+
for (Hook hook : hooks) {
61+
if (hook.supportsFlagValueType(flagValueType)) {
62+
pairs.add(Pair.of(hook, HookData.create()));
63+
}
64+
}
65+
return pairs;
4766
}
4867

4968
private <T> void executeHooks(
50-
FlagValueType flagValueType, List<Hook> hooks, String hookMethod, Consumer<Hook<T>> hookCode) {
51-
if (hooks != null) {
52-
for (Hook hook : hooks) {
53-
if (hook.supportsFlagValueType(flagValueType)) {
54-
executeChecked(hook, hookCode, hookMethod);
55-
}
69+
FlagValueType flagValueType,
70+
List<Pair<Hook, HookData>> hookDataPairs,
71+
HookContext hookContext,
72+
String hookMethod,
73+
BiConsumer<Hook<T>, HookContext> hookCode) {
74+
if (hookDataPairs != null) {
75+
for (Pair<Hook, HookData> hookDataPair : hookDataPairs) {
76+
Hook hook = hookDataPair.getLeft();
77+
HookData hookData = hookDataPair.getRight();
78+
executeChecked(hook, hookData, hookContext, hookCode, hookMethod);
5679
}
5780
}
5881
}
5982

6083
// before, error, and finally hooks shouldn't throw
61-
private <T> void executeChecked(Hook<T> hook, Consumer<Hook<T>> hookCode, String hookMethod) {
84+
private <T> void executeChecked(
85+
Hook<T> hook,
86+
HookData hookData,
87+
HookContext hookContext,
88+
BiConsumer<Hook<T>, HookContext> hookCode,
89+
String hookMethod) {
6290
try {
63-
hookCode.accept(hook);
91+
var hookCtxWithData = HookContextWithData.of(hookContext, hookData);
92+
hookCode.accept(hook, hookCtxWithData);
6493
} catch (Exception exception) {
6594
log.error(
6695
"Unhandled exception when running {} hook {} (only 'after' hooks should throw)",
@@ -71,29 +100,41 @@ private <T> void executeChecked(Hook<T> hook, Consumer<Hook<T>> hookCode, String
71100
}
72101

73102
// after hooks can throw in order to do validation
74-
private <T> void executeHooksUnchecked(FlagValueType flagValueType, List<Hook> hooks, Consumer<Hook<T>> hookCode) {
75-
if (hooks != null) {
76-
for (Hook hook : hooks) {
77-
if (hook.supportsFlagValueType(flagValueType)) {
78-
hookCode.accept(hook);
79-
}
103+
private <T> void executeHooksUnchecked(
104+
FlagValueType flagValueType,
105+
List<Pair<Hook, HookData>> hookDataPairs,
106+
HookContext hookContext,
107+
BiConsumer<Hook<T>, HookContext> hookCode) {
108+
if (hookDataPairs != null) {
109+
for (Pair<Hook, HookData> hookDataPair : hookDataPairs) {
110+
Hook hook = hookDataPair.getLeft();
111+
HookData hookData = hookDataPair.getRight();
112+
var hookCtxWithData = HookContextWithData.of(hookContext, hookData);
113+
hookCode.accept(hook, hookCtxWithData);
80114
}
81115
}
82116
}
83117

84118
private EvaluationContext callBeforeHooks(
85-
FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks, Map<String, Object> hints) {
119+
FlagValueType flagValueType,
120+
HookContext hookCtx,
121+
List<Pair<Hook, HookData>> hookDataPairs,
122+
Map<String, Object> hints) {
86123
// These traverse backwards from normal.
87-
List<Hook> reversedHooks = new ArrayList<>(hooks);
124+
List<Pair<Hook, HookData>> reversedHooks = new ArrayList<>(hookDataPairs);
88125
Collections.reverse(reversedHooks);
89126
EvaluationContext context = hookCtx.getCtx();
90-
for (Hook hook : reversedHooks) {
91-
if (hook.supportsFlagValueType(flagValueType)) {
92-
Optional<EvaluationContext> optional =
93-
Optional.ofNullable(hook.before(hookCtx, hints)).orElse(Optional.empty());
94-
if (optional.isPresent()) {
95-
context = context.merge(optional.get());
96-
}
127+
128+
for (Pair<Hook, HookData> hookDataPair : reversedHooks) {
129+
Hook hook = hookDataPair.getLeft();
130+
HookData hookData = hookDataPair.getRight();
131+
132+
// Create a new context with this hook's data
133+
HookContext contextWithHookData = HookContextWithData.of(hookCtx, hookData);
134+
Optional<EvaluationContext> optional =
135+
Optional.ofNullable(hook.before(contextWithHookData, hints)).orElse(Optional.empty());
136+
if (optional.isPresent()) {
137+
context = context.merge(optional.get());
97138
}
98139
}
99140
return context;

src/main/java/dev/openfeature/sdk/ImmutableContext.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
2121
public final class ImmutableContext implements EvaluationContext {
2222

23+
public static final ImmutableContext EMPTY = new ImmutableContext();
24+
2325
@Delegate(excludes = DelegateExclusions.class)
2426
private final ImmutableStructure structure;
2527

src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -164,32 +164,27 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
164164
var hints = Collections.unmodifiableMap(flagOptions.getHookHints());
165165

166166
FlagEvaluationDetails<T> details = null;
167-
List<Hook> mergedHooks = null;
168-
HookContext<T> afterHookContext = null;
167+
List<Hook> mergedHooks;
168+
List<Pair<Hook, HookData>> hookDataPairs = null;
169+
HookContext<T> hookContext = null;
169170

170171
try {
171-
var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain);
172+
final var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain);
172173
// provider must be accessed once to maintain a consistent reference
173-
var provider = stateManager.getProvider();
174-
var state = stateManager.getState();
174+
final var provider = stateManager.getProvider();
175+
final var state = stateManager.getState();
176+
hookContext =
177+
HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), new ImmutableContext(), defaultValue);
178+
179+
// we are setting the evaluation context one after the other, so that we have a hook context in each
180+
// possible exception case.
181+
hookContext.setCtx(mergeEvaluationContext(ctx));
175182

176183
mergedHooks = ObjectUtils.merge(
177184
provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getMutableHooks());
178-
179-
var mergedCtx = hookSupport.beforeHooks(
180-
type,
181-
HookContext.from(
182-
key,
183-
type,
184-
this.getMetadata(),
185-
provider.getMetadata(),
186-
mergeEvaluationContext(ctx),
187-
defaultValue),
188-
mergedHooks,
189-
hints);
190-
191-
afterHookContext =
192-
HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), mergedCtx, defaultValue);
185+
hookDataPairs = hookSupport.getHookDataPairs(mergedHooks, type);
186+
var mergedCtx = hookSupport.beforeHooks(type, hookContext, hookDataPairs, hints);
187+
hookContext.setCtx(mergedCtx);
193188

194189
// "short circuit" if the provider is in NOT_READY or FATAL state
195190
if (ProviderState.NOT_READY.equals(state)) {
@@ -207,9 +202,9 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
207202
var error =
208203
ExceptionUtils.instantiateErrorByErrorCode(details.getErrorCode(), details.getErrorMessage());
209204
enrichDetailsWithErrorDefaults(defaultValue, details);
210-
hookSupport.errorHooks(type, afterHookContext, error, mergedHooks, hints);
205+
hookSupport.errorHooks(type, hookContext, error, hookDataPairs, hints);
211206
} else {
212-
hookSupport.afterHooks(type, afterHookContext, details, mergedHooks, hints);
207+
hookSupport.afterHooks(type, hookContext, details, hookDataPairs, hints);
213208
}
214209
} catch (Exception e) {
215210
if (details == null) {
@@ -222,9 +217,9 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
222217
}
223218
details.setErrorMessage(e.getMessage());
224219
enrichDetailsWithErrorDefaults(defaultValue, details);
225-
hookSupport.errorHooks(type, afterHookContext, e, mergedHooks, hints);
220+
hookSupport.errorHooks(type, hookContext, e, hookDataPairs, hints);
226221
} finally {
227-
hookSupport.afterAllHooks(type, afterHookContext, details, mergedHooks, hints);
222+
hookSupport.afterAllHooks(type, hookContext, details, hookDataPairs, hints);
228223
}
229224

230225
return details;

0 commit comments

Comments
 (0)