Skip to content

Commit 27f3e2a

Browse files
feat: separate hook support data from logic, PR suggestions
Signed-off-by: Alexandra Oberaigner <[email protected]>
1 parent 3ea9894 commit 27f3e2a

File tree

5 files changed

+111
-76
lines changed

5 files changed

+111
-76
lines changed

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

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

3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
34
import java.util.Objects;
45
import lombok.NonNull;
56

@@ -114,6 +115,7 @@ public Metadata getProviderMetadata() {
114115
return sharedContext.getProviderMetadata();
115116
}
116117

118+
@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Intentional exposure of hookData")
117119
public HookData getHookData() {
118120
return this.hookData;
119121
}
@@ -300,10 +302,10 @@ public HookContext<T> withHookData(HookData hookData) {
300302
*/
301303
@Deprecated
302304
public static class HookContextBuilder<T> {
303-
private @NonNull String flagKey;
304-
private @NonNull FlagValueType type;
305-
private @NonNull T defaultValue;
306-
private @NonNull EvaluationContext ctx;
305+
private String flagKey;
306+
private FlagValueType type;
307+
private T defaultValue;
308+
private EvaluationContext ctx;
307309
private ClientMetadata clientMetadata;
308310
private Metadata providerMetadata;
309311
private HookData hookData;
@@ -340,6 +342,7 @@ public HookContextBuilder<T> providerMetadata(Metadata providerMetadata) {
340342
return this;
341343
}
342344

345+
@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Intentional exposure of hookData")
343346
public HookContextBuilder<T> hookData(HookData hookData) {
344347
this.hookData = hookData;
345348
return this;

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

Lines changed: 15 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,37 @@
33
import java.util.ArrayList;
44
import java.util.Collections;
55
import java.util.List;
6-
import java.util.Map;
76
import java.util.Optional;
87
import lombok.extern.slf4j.Slf4j;
98

109
@Slf4j
1110
class HookSupport {
12-
private List<Pair<Hook, HookContext>> hooks;
13-
private EvaluationContext evaluationContext;
14-
private final Map<String, Object> hints;
1511

16-
HookSupport(
17-
List<Hook> hooks,
18-
SharedHookContext sharedContext,
19-
EvaluationContext evaluationContext,
20-
Map<String, Object> hints) {
21-
List<Pair<Hook, HookContext>> hookContextPairs = new ArrayList<>();
22-
for (Hook hook : hooks) {
23-
if (hook.supportsFlagValueType(sharedContext.getType())) {
24-
HookContext curContext = sharedContext.hookContextFor(evaluationContext, new DefaultHookData());
25-
hookContextPairs.add(Pair.of(hook, curContext));
26-
}
27-
}
28-
this.hooks = hookContextPairs;
29-
this.evaluationContext = evaluationContext;
30-
this.hints = hints;
31-
}
32-
33-
public EvaluationContext getEvaluationContext() {
34-
return evaluationContext;
35-
}
36-
37-
private void setEvaluationContext(EvaluationContext evaluationContext) {
38-
this.evaluationContext = evaluationContext;
39-
for (Pair<Hook, HookContext> hookContextPair : hooks) {
40-
hookContextPair.getValue().setCtx(evaluationContext);
41-
}
42-
}
43-
44-
public void executeBeforeHooks() {
12+
public void executeBeforeHooks(HookSupportData data) {
4513
// These traverse backwards from normal.
46-
List<Pair<Hook, HookContext>> reversedHooks = new ArrayList<>(hooks);
14+
List<Pair<Hook, HookContext>> reversedHooks = new ArrayList<>(data.getHooks());
4715
Collections.reverse(reversedHooks);
4816

4917
for (Pair<Hook, HookContext> hookContextPair : reversedHooks) {
5018
var hook = hookContextPair.getKey();
5119
var hookContext = hookContextPair.getValue();
5220

53-
Optional<EvaluationContext> returnedEvalContext =
54-
Optional.ofNullable(hook.before(hookContext, hints)).orElse(Optional.empty());
21+
Optional<EvaluationContext> returnedEvalContext = Optional.ofNullable(
22+
hook.before(hookContext, data.getHints()))
23+
.orElse(Optional.empty());
5524
if (returnedEvalContext.isPresent()) {
5625
// update shared evaluation context for all hooks
57-
setEvaluationContext(evaluationContext.merge(returnedEvalContext.get()));
26+
data.setEvaluationContext(data.getEvaluationContext().merge(returnedEvalContext.get()));
5827
}
5928
}
6029
}
6130

62-
public void executeErrorHooks(Exception error) {
63-
for (Pair<Hook, HookContext> hookContextPair : hooks) {
31+
public void executeErrorHooks(HookSupportData data, Exception error) {
32+
for (Pair<Hook, HookContext> hookContextPair : data.getHooks()) {
6433
var hook = hookContextPair.getKey();
6534
var hookContext = hookContextPair.getValue();
6635
try {
67-
hook.error(hookContext, error, hints);
36+
hook.error(hookContext, error, data.getHints());
6837
} catch (Exception e) {
6938
log.error(
7039
"Unhandled exception when running {} hook {} (only 'after' hooks should throw)",
@@ -76,20 +45,20 @@ public void executeErrorHooks(Exception error) {
7645
}
7746

7847
// after hooks can throw in order to do validation
79-
public <T> void executeAfterHooks(FlagEvaluationDetails<T> details) {
80-
for (Pair<Hook, HookContext> hookContextPair : hooks) {
48+
public <T> void executeAfterHooks(HookSupportData data, FlagEvaluationDetails<T> details) {
49+
for (Pair<Hook, HookContext> hookContextPair : data.getHooks()) {
8150
var hook = hookContextPair.getKey();
8251
var hookContext = hookContextPair.getValue();
83-
hook.after(hookContext, details, hints);
52+
hook.after(hookContext, details, data.getHints());
8453
}
8554
}
8655

87-
public <T> void executeAfterAllHooks(FlagEvaluationDetails<T> details) {
88-
for (Pair<Hook, HookContext> hookContextPair : hooks) {
56+
public <T> void executeAfterAllHooks(HookSupportData data, FlagEvaluationDetails<T> details) {
57+
for (Pair<Hook, HookContext> hookContextPair : data.getHooks()) {
8958
var hook = hookContextPair.getKey();
9059
var hookContext = hookContextPair.getValue();
9160
try {
92-
hook.finallyAfter(hookContext, details, hints);
61+
hook.finallyAfter(hookContext, details, data.getHints());
9362
} catch (Exception e) {
9463
log.error(
9564
"Unhandled exception when running {} hook {} (only 'after' hooks should throw)",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package dev.openfeature.sdk;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import lombok.Getter;
7+
8+
/**
9+
* Encapsulates data for hook execution per flag evaluation.
10+
*/
11+
@Getter
12+
class HookSupportData {
13+
14+
private List<Pair<Hook, HookContext>> hooks;
15+
private EvaluationContext evaluationContext;
16+
private Map<String, Object> hints;
17+
private boolean isInitialized = false;
18+
19+
HookSupportData() {}
20+
21+
void initialize(
22+
List<Hook> hooks,
23+
SharedHookContext sharedContext,
24+
EvaluationContext evaluationContext,
25+
Map<String, Object> hints) {
26+
List<Pair<Hook, HookContext>> hookContextPairs = new ArrayList<>();
27+
for (Hook hook : hooks) {
28+
if (hook.supportsFlagValueType(sharedContext.getType())) {
29+
HookContext curContext = sharedContext.hookContextFor(evaluationContext, new DefaultHookData());
30+
hookContextPairs.add(Pair.of(hook, curContext));
31+
}
32+
}
33+
this.hooks = hookContextPairs;
34+
this.evaluationContext = evaluationContext;
35+
this.hints = hints;
36+
isInitialized = true;
37+
}
38+
39+
public void setEvaluationContext(EvaluationContext evaluationContext) {
40+
this.evaluationContext = evaluationContext;
41+
for (Pair<Hook, HookContext> hookContextPair : hooks) {
42+
hookContextPair.getValue().setCtx(evaluationContext);
43+
}
44+
}
45+
}

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public class OpenFeatureClient implements Client {
5050
private final ConcurrentLinkedQueue<Hook> clientHooks;
5151
private final AtomicReference<EvaluationContext> evaluationContext = new AtomicReference<>();
5252

53+
private final HookSupport hookSupport;
54+
5355
/**
5456
* Deprecated public constructor. Use OpenFeature.API.getClient() instead.
5557
*
@@ -66,6 +68,7 @@ public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String ve
6668
this.openfeatureApi = openFeatureAPI;
6769
this.domain = domain;
6870
this.version = version;
71+
this.hookSupport = new HookSupport();
6972
this.clientHooks = new ConcurrentLinkedQueue<>();
7073
}
7174

@@ -162,7 +165,7 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
162165
var hints = Collections.unmodifiableMap(flagOptions.getHookHints());
163166

164167
FlagEvaluationDetails<T> details = null;
165-
HookSupport hookSupport = null;
168+
HookSupportData hookSupportData = new HookSupportData();
166169

167170
try {
168171
final var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain);
@@ -176,10 +179,12 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
176179
var sharedHookContext =
177180
new SharedHookContext(key, type, this.getMetadata(), provider.getMetadata(), defaultValue);
178181

182+
hookSupportData.initialize(mergedHooks, sharedHookContext, ctx, hints);
183+
179184
var evalContext = mergeEvaluationContext(ctx);
180-
hookSupport = new HookSupport(mergedHooks, sharedHookContext, evalContext, hints);
185+
hookSupportData.setEvaluationContext(evalContext);
181186

182-
hookSupport.executeBeforeHooks();
187+
hookSupport.executeBeforeHooks(hookSupportData);
183188

184189
// "short circuit" if the provider is in NOT_READY or FATAL state
185190
if (ProviderState.NOT_READY.equals(state)) {
@@ -190,16 +195,16 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
190195
}
191196

192197
var providerEval = (ProviderEvaluation<T>)
193-
createProviderEvaluation(type, key, defaultValue, provider, hookSupport.getEvaluationContext());
198+
createProviderEvaluation(type, key, defaultValue, provider, hookSupportData.getEvaluationContext());
194199

195200
details = FlagEvaluationDetails.from(providerEval, key);
196201
if (details.getErrorCode() != null) {
197202
var error =
198203
ExceptionUtils.instantiateErrorByErrorCode(details.getErrorCode(), details.getErrorMessage());
199204
enrichDetailsWithErrorDefaults(defaultValue, details);
200-
hookSupport.executeErrorHooks(error);
205+
hookSupport.executeErrorHooks(hookSupportData, error);
201206
} else {
202-
hookSupport.executeAfterHooks(details);
207+
hookSupport.executeAfterHooks(hookSupportData, details);
203208
}
204209
} catch (Exception e) {
205210
if (details == null) {
@@ -212,10 +217,12 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
212217
}
213218
details.setErrorMessage(e.getMessage());
214219
enrichDetailsWithErrorDefaults(defaultValue, details);
215-
hookSupport.executeErrorHooks(e);
220+
if (hookSupportData.isInitialized()) {
221+
hookSupport.executeErrorHooks(hookSupportData, e);
222+
}
216223
} finally {
217-
if (hookSupport != null) {
218-
hookSupport.executeAfterAllHooks(details);
224+
if (hookSupportData.isInitialized()) {
225+
hookSupport.executeAfterAllHooks(hookSupportData, details);
219226
}
220227
}
221228

src/test/java/dev/openfeature/sdk/HookSupportTest.java

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import org.junit.jupiter.params.provider.EnumSource;
2020

2121
class HookSupportTest implements HookFixtures {
22+
23+
private static final HookSupport hookSupport = new HookSupport();
24+
2225
@Test
2326
@DisplayName("should merge EvaluationContexts on before hooks correctly")
2427
void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() {
@@ -31,15 +34,16 @@ void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() {
3134
when(hook1.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("bla", "blubber")));
3235
when(hook2.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("foo", "bar")));
3336

34-
HookSupport executor = new HookSupport(
37+
var hookSupportData = new HookSupportData();
38+
hookSupportData.initialize(
3539
Arrays.asList(hook1, hook2),
3640
getBaseHookContextForType(FlagValueType.STRING),
3741
baseContext,
3842
Collections.emptyMap());
3943

40-
executor.executeBeforeHooks();
44+
hookSupport.executeBeforeHooks(hookSupportData);
4145

42-
EvaluationContext result = executor.getEvaluationContext();
46+
EvaluationContext result = hookSupportData.getEvaluationContext();
4347

4448
assertThat(result.getValue("bla").asString()).isEqualTo("blubber");
4549
assertThat(result.getValue("foo").asString()).isEqualTo("bar");
@@ -52,13 +56,14 @@ void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() {
5256
void shouldAlwaysCallGenericHook(FlagValueType flagValueType) {
5357
Hook<?> genericHook = mockGenericHook();
5458

55-
HookSupport hookSupport = new HookSupport(
59+
var hookSupportData = new HookSupportData();
60+
hookSupportData.initialize(
5661
List.of(genericHook),
5762
getBaseHookContextForType(flagValueType),
5863
ImmutableContext.EMPTY,
5964
Collections.emptyMap());
6065

61-
callAllHooks(hookSupport);
66+
callAllHooks(hookSupportData);
6267

6368
verify(genericHook).before(any(), any());
6469
verify(genericHook).after(any(), any(), any());
@@ -71,22 +76,25 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) {
7176
@DisplayName("should allow hooks to store and retrieve data across stages")
7277
void shouldPassDataAcrossStages(FlagValueType flagValueType) {
7378
var testHook = new TestHookWithData();
74-
HookSupport hookSupport = new HookSupport(
79+
var hookSupportData = new HookSupportData();
80+
hookSupportData.initialize(
7581
List.of(testHook),
7682
getBaseHookContextForType(flagValueType),
7783
ImmutableContext.EMPTY,
7884
Collections.emptyMap());
7985

80-
hookSupport.executeBeforeHooks();
86+
hookSupport.executeBeforeHooks(hookSupportData);
8187
assertHookData(testHook, "before");
8288

83-
hookSupport.executeAfterHooks(FlagEvaluationDetails.builder().build());
89+
hookSupport.executeAfterHooks(
90+
hookSupportData, FlagEvaluationDetails.builder().build());
8491
assertHookData(testHook, "before", "after");
8592

86-
hookSupport.executeAfterAllHooks(FlagEvaluationDetails.builder().build());
93+
hookSupport.executeAfterAllHooks(
94+
hookSupportData, FlagEvaluationDetails.builder().build());
8795
assertHookData(testHook, "before", "after", "finallyAfter");
8896

89-
hookSupport.executeErrorHooks(mock(Exception.class));
97+
hookSupport.executeErrorHooks(hookSupportData, mock(Exception.class));
9098
assertHookData(testHook, "before", "after", "finallyAfter", "error");
9199
}
92100

@@ -97,23 +105,26 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) {
97105
var testHook1 = new TestHookWithData(1);
98106
var testHook2 = new TestHookWithData(2);
99107

100-
HookSupport hookSupport = new HookSupport(
108+
var hookSupportData = new HookSupportData();
109+
hookSupportData.initialize(
101110
List.of(testHook1, testHook2),
102111
getBaseHookContextForType(flagValueType),
103112
ImmutableContext.EMPTY,
104113
Collections.emptyMap());
105114

106-
callAllHooks(hookSupport);
115+
callAllHooks(hookSupportData);
107116

108117
assertHookData(testHook1, 1, "before", "after", "finallyAfter", "error");
109118
assertHookData(testHook2, 2, "before", "after", "finallyAfter", "error");
110119
}
111120

112-
private static void callAllHooks(HookSupport hookSupport) {
113-
hookSupport.executeBeforeHooks();
114-
hookSupport.executeAfterHooks(FlagEvaluationDetails.builder().build());
115-
hookSupport.executeAfterAllHooks(FlagEvaluationDetails.builder().build());
116-
hookSupport.executeErrorHooks(mock(Exception.class));
121+
private static void callAllHooks(HookSupportData hookSupportData) {
122+
hookSupport.executeBeforeHooks(hookSupportData);
123+
hookSupport.executeAfterHooks(
124+
hookSupportData, FlagEvaluationDetails.builder().build());
125+
hookSupport.executeAfterAllHooks(
126+
hookSupportData, FlagEvaluationDetails.builder().build());
127+
hookSupport.executeErrorHooks(hookSupportData, mock(Exception.class));
117128
}
118129

119130
private static void assertHookData(TestHookWithData testHook, String... expectedKeys) {

0 commit comments

Comments
 (0)