Skip to content

Commit 269dfe4

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

File tree

12 files changed

+570
-80
lines changed

12 files changed

+570
-80
lines changed

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

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

33
import lombok.Builder;
4+
import lombok.Data;
45
import lombok.NonNull;
5-
import lombok.Value;
66
import lombok.With;
77

88
/**
99
* A data class to hold immutable context that {@link Hook} instances use.
1010
*
1111
* @param <T> the type for the flag being evaluated
1212
*/
13-
@Value
13+
@Data
1414
@Builder
1515
@With
1616
public class HookContext<T> {
@@ -26,7 +26,7 @@ public class HookContext<T> {
2626
Metadata providerMetadata;
2727

2828
/**
29-
* Builds a {@link HookContext} instances from request data.
29+
* Builds {@link HookContext} instances from request data.
3030
*
3131
* @param key feature flag key
3232
* @param type flag value type
@@ -53,4 +53,8 @@ public static <T> HookContext<T> from(
5353
.defaultValue(defaultValue)
5454
.build();
5555
}
56+
57+
HookData getHookData() {
58+
return null;
59+
}
5660
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dev.openfeature.sdk;
2+
3+
/**
4+
* A base decorator class for {@link HookContext} that enables dynamic enhancement of its functionality.
5+
* This class wraps an existing {@code HookContext<T>} instance and delegates method calls to it.
6+
*
7+
* @param <T> the type for the flag being evaluated
8+
*/
9+
class HookContextDecorator<T> extends HookContext<T> {
10+
11+
HookContext<T> decorated;
12+
13+
protected HookContextDecorator(HookContext<T> context) {
14+
super(context.getFlagKey(), context.getType(), context.getDefaultValue(), context.getCtx(),
15+
context.getClientMetadata(), context.getProviderMetadata());
16+
this.decorated = context;
17+
}
18+
19+
@Override
20+
public HookData getHookData() {
21+
return decorated.getHookData();
22+
}
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.openfeature.sdk;
2+
3+
/**
4+
* A concrete decorator for {@link HookContext} that adds {@link HookData} to the existing functionality.
5+
*
6+
* @param <T> the type for the flag being evaluated
7+
*/
8+
public class HookContextWithData<T> extends HookContextDecorator<T> {
9+
10+
private final HookData hookData;
11+
12+
public HookContextWithData(HookContext<T> context, HookData data) {
13+
super(context);
14+
this.hookData = data;
15+
}
16+
17+
@Override
18+
public HookData getHookData() {
19+
return hookData;
20+
}
21+
}
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 = new HookContextWithData(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 = new HookContextWithData(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 = new HookContextWithData(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

0 commit comments

Comments
 (0)