Skip to content

Commit ae85278

Browse files
chrfwowtoddbaert
andauthored
feat: Add evaluation details to finally hook stage #1246 (#1262)
Signed-off-by: christian.lutnik <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 3c97b7b commit ae85278

File tree

8 files changed

+106
-81
lines changed

8 files changed

+106
-81
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ class MyHook implements Hook {
426426
}
427427

428428
@Override
429-
public void finallyAfter(HookContext ctx, Map hints) {
429+
public void finallyAfter(HookContext ctx, FlagEvaluationDetails details, Map hints) {
430430
// code that runs regardless of success or error
431431
}
432432
};

Diff for: src/main/java/dev/openfeature/sdk/Hook.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ default void error(HookContext<T> ctx, Exception error, Map<String, Object> hint
4646
* @param ctx Information about the particular flag evaluation
4747
* @param hints An immutable mapping of data for users to communicate to the hooks.
4848
*/
49-
default void finallyAfter(HookContext<T> ctx, Map<String, Object> hints) {}
49+
default void finallyAfter(HookContext<T> ctx, FlagEvaluationDetails<T> details, Map<String, Object> hints) {}
5050

5151
default boolean supportsFlagValueType(FlagValueType flagValueType) {
5252
return true;

Diff for: src/main/java/dev/openfeature/sdk/HookSupport.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ public void afterHooks(
2929
}
3030

3131
public void afterAllHooks(
32-
FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks, Map<String, Object> hints) {
33-
executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, hints));
32+
FlagValueType flagValueType,
33+
HookContext hookCtx,
34+
FlagEvaluationDetails details,
35+
List<Hook> hooks,
36+
Map<String, Object> hints) {
37+
executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, details, hints));
3438
}
3539

3640
public void errorHooks(

Diff for: src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
228228
enrichDetailsWithErrorDefaults(defaultValue, details);
229229
hookSupport.errorHooks(type, afterHookContext, e, mergedHooks, hints);
230230
} finally {
231-
hookSupport.afterAllHooks(type, afterHookContext, mergedHooks, hints);
231+
hookSupport.afterAllHooks(type, afterHookContext, details, mergedHooks, hints);
232232
}
233233

234234
return details;

Diff for: src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ void clientHooks() {
3939
Client client = api.getClient();
4040
client.addHooks(exampleHook);
4141
Boolean retval = client.getBooleanValue(flagKey, false);
42-
verify(exampleHook, times(1)).finallyAfter(any(), any());
42+
verify(exampleHook, times(1)).finallyAfter(any(), any(), any());
4343
assertFalse(retval);
4444
}
4545

@@ -57,8 +57,8 @@ void evalHooks() {
5757
false,
5858
null,
5959
FlagEvaluationOptions.builder().hook(evalHook).build());
60-
verify(clientHook, times(1)).finallyAfter(any(), any());
61-
verify(evalHook, times(1)).finallyAfter(any(), any());
60+
verify(clientHook, times(1)).finallyAfter(any(), any(), any());
61+
verify(evalHook, times(1)).finallyAfter(any(), any(), any());
6262
assertFalse(retval);
6363
}
6464

Diff for: src/test/java/dev/openfeature/sdk/HookSpecTest.java

+86-14
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
package dev.openfeature.sdk;
22

3+
import static org.assertj.core.api.Assertions.assertThat;
34
import static org.assertj.core.api.Assertions.assertThatCode;
45
import static org.assertj.core.api.Assertions.fail;
5-
import static org.junit.jupiter.api.Assertions.*;
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
8+
import static org.junit.jupiter.api.Assertions.assertNotNull;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
610
import static org.mockito.ArgumentMatchers.any;
7-
import static org.mockito.Mockito.*;
11+
import static org.mockito.Mockito.doThrow;
12+
import static org.mockito.Mockito.inOrder;
13+
import static org.mockito.Mockito.mock;
14+
import static org.mockito.Mockito.never;
15+
import static org.mockito.Mockito.times;
16+
import static org.mockito.Mockito.verify;
17+
import static org.mockito.Mockito.when;
818

919
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
1020
import dev.openfeature.sdk.fixtures.HookFixtures;
@@ -187,7 +197,7 @@ void feo_has_hook_list() {
187197
void error_hook_run_during_non_finally_stage() {
188198
final boolean[] error_called = {false};
189199
Hook h = mockBooleanHook();
190-
doThrow(RuntimeException.class).when(h).finallyAfter(any(), any());
200+
doThrow(RuntimeException.class).when(h).finallyAfter(any(), any(), any());
191201

192202
verify(h, times(0)).error(any(), any(), any());
193203
}
@@ -219,7 +229,7 @@ void error_hook_must_run_if_resolution_details_returns_an_error_code() {
219229

220230
verify(hook, times(1)).before(any(), any());
221231
verify(hook, times(1)).error(any(), captor.capture(), any());
222-
verify(hook, times(1)).finallyAfter(any(), any());
232+
verify(hook, times(1)).finallyAfter(any(), any(), any());
223233
verify(hook, never()).after(any(), any(), any());
224234

225235
Exception exception = captor.getValue();
@@ -274,7 +284,10 @@ public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object>
274284
}
275285

276286
@Override
277-
public void finallyAfter(HookContext<Boolean> ctx, Map<String, Object> hints) {
287+
public void finallyAfter(
288+
HookContext<Boolean> ctx,
289+
FlagEvaluationDetails<Boolean> details,
290+
Map<String, Object> hints) {
278291
evalOrder.add("provider finally");
279292
}
280293
});
@@ -300,7 +313,8 @@ public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object>
300313
}
301314

302315
@Override
303-
public void finallyAfter(HookContext<Boolean> ctx, Map<String, Object> hints) {
316+
public void finallyAfter(
317+
HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
304318
evalOrder.add("api finally");
305319
}
306320
});
@@ -325,7 +339,8 @@ public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object>
325339
}
326340

327341
@Override
328-
public void finallyAfter(HookContext<Boolean> ctx, Map<String, Object> hints) {
342+
public void finallyAfter(
343+
HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
329344
evalOrder.add("client finally");
330345
}
331346
});
@@ -357,7 +372,10 @@ public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object>
357372
}
358373

359374
@Override
360-
public void finallyAfter(HookContext<Boolean> ctx, Map<String, Object> hints) {
375+
public void finallyAfter(
376+
HookContext<Boolean> ctx,
377+
FlagEvaluationDetails<Boolean> details,
378+
Map<String, Object> hints) {
361379
evalOrder.add("invocation finally");
362380
}
363381
})
@@ -462,7 +480,8 @@ public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object>
462480
}
463481

464482
@Override
465-
public void finallyAfter(HookContext<Boolean> ctx, Map<String, Object> hints) {
483+
public void finallyAfter(
484+
HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
466485
assertThatCode(() -> hints.put(hintKey, "changed value"))
467486
.isInstanceOf(UnsupportedOperationException.class);
468487
}
@@ -509,7 +528,7 @@ void flag_eval_hook_order() {
509528
order.verify(hook).before(any(), any());
510529
order.verify(provider).getBooleanEvaluation(any(), any(), any());
511530
order.verify(hook).after(any(), any(), any());
512-
order.verify(hook).finallyAfter(any(), any());
531+
order.verify(hook).finallyAfter(any(), any(), any());
513532
}
514533

515534
@Specification(
@@ -550,6 +569,58 @@ void error_hooks__after() {
550569
verify(hook, times(1)).error(any(), any(), any());
551570
}
552571

572+
@Test
573+
void erroneous_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() {
574+
Hook hook = mockBooleanHook();
575+
doThrow(RuntimeException.class).when(hook).after(any(), any(), any());
576+
String flagKey = "test-flag-key";
577+
Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider());
578+
client.getBooleanValue(
579+
flagKey,
580+
true,
581+
new ImmutableContext(),
582+
FlagEvaluationOptions.builder().hook(hook).build());
583+
584+
ArgumentCaptor<FlagEvaluationDetails<Boolean>> captor = ArgumentCaptor.forClass(FlagEvaluationDetails.class);
585+
verify(hook).finallyAfter(any(), captor.capture(), any());
586+
587+
FlagEvaluationDetails<Boolean> evaluationDetails = captor.getValue();
588+
assertThat(evaluationDetails).isNotNull();
589+
590+
assertThat(evaluationDetails.getErrorCode()).isEqualTo(ErrorCode.GENERAL);
591+
assertThat(evaluationDetails.getReason()).isEqualTo("ERROR");
592+
assertThat(evaluationDetails.getVariant()).isEqualTo("Passed in default");
593+
assertThat(evaluationDetails.getFlagKey()).isEqualTo(flagKey);
594+
assertThat(evaluationDetails.getFlagMetadata())
595+
.isEqualTo(ImmutableMetadata.builder().build());
596+
assertThat(evaluationDetails.getValue()).isTrue();
597+
}
598+
599+
@Test
600+
void successful_flagResolution_setsAppropriateFieldsInFlagEvaluationDetails() {
601+
Hook hook = mockBooleanHook();
602+
String flagKey = "test-flag-key";
603+
Client client = getClient(TestEventsProvider.newInitializedTestEventsProvider());
604+
client.getBooleanValue(
605+
flagKey,
606+
true,
607+
new ImmutableContext(),
608+
FlagEvaluationOptions.builder().hook(hook).build());
609+
610+
ArgumentCaptor<FlagEvaluationDetails<Boolean>> captor = ArgumentCaptor.forClass(FlagEvaluationDetails.class);
611+
verify(hook).finallyAfter(any(), captor.capture(), any());
612+
613+
FlagEvaluationDetails<Boolean> evaluationDetails = captor.getValue();
614+
assertThat(evaluationDetails).isNotNull();
615+
assertThat(evaluationDetails.getErrorCode()).isNull();
616+
assertThat(evaluationDetails.getReason()).isEqualTo("DEFAULT");
617+
assertThat(evaluationDetails.getVariant()).isEqualTo("Passed in default");
618+
assertThat(evaluationDetails.getFlagKey()).isEqualTo(flagKey);
619+
assertThat(evaluationDetails.getFlagMetadata())
620+
.isEqualTo(ImmutableMetadata.builder().build());
621+
assertThat(evaluationDetails.getValue()).isTrue();
622+
}
623+
553624
@Test
554625
void multi_hooks_early_out__before() {
555626
Hook<Boolean> hook = mockBooleanHook();
@@ -649,7 +720,7 @@ void mergeHappensCorrectly() {
649720
void first_finally_broken() {
650721
Hook hook = mockBooleanHook();
651722
doThrow(RuntimeException.class).when(hook).before(any(), any());
652-
doThrow(RuntimeException.class).when(hook).finallyAfter(any(), any());
723+
doThrow(RuntimeException.class).when(hook).finallyAfter(any(), any(), any());
653724
Hook hook2 = mockBooleanHook();
654725
InOrder order = inOrder(hook, hook2);
655726

@@ -661,8 +732,8 @@ void first_finally_broken() {
661732
FlagEvaluationOptions.builder().hook(hook2).hook(hook).build());
662733

663734
order.verify(hook).before(any(), any());
664-
order.verify(hook2).finallyAfter(any(), any());
665-
order.verify(hook).finallyAfter(any(), any());
735+
order.verify(hook2).finallyAfter(any(), any(), any());
736+
order.verify(hook).finallyAfter(any(), any(), any());
666737
}
667738

668739
@Specification(
@@ -711,7 +782,8 @@ void doesnt_use_finally() {
711782
.as("Not possible. Finally is a reserved word.")
712783
.isInstanceOf(NoSuchMethodException.class);
713784

714-
assertThatCode(() -> Hook.class.getMethod("finallyAfter", HookContext.class, Map.class))
785+
assertThatCode(() ->
786+
Hook.class.getMethod("finallyAfter", HookContext.class, FlagEvaluationDetails.class, Map.class))
715787
.doesNotThrowAnyException();
716788
}
717789
}

Diff for: src/test/java/dev/openfeature/sdk/HookSupportTest.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) {
6464
Collections.singletonList(genericHook),
6565
Collections.emptyMap());
6666
hookSupport.afterAllHooks(
67-
flagValueType, hookContext, Collections.singletonList(genericHook), Collections.emptyMap());
67+
flagValueType,
68+
hookContext,
69+
FlagEvaluationDetails.builder().build(),
70+
Collections.singletonList(genericHook),
71+
Collections.emptyMap());
6872
hookSupport.errorHooks(
6973
flagValueType,
7074
hookContext,
@@ -74,7 +78,7 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) {
7478

7579
verify(genericHook).before(any(), any());
7680
verify(genericHook).after(any(), any(), any());
77-
verify(genericHook).finallyAfter(any(), any());
81+
verify(genericHook).finallyAfter(any(), any(), any());
7882
verify(genericHook).error(any(), any(), any());
7983
}
8084

Diff for: src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java

+2-57
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import static org.junit.jupiter.api.Assertions.assertThrows;
66
import static org.mockito.ArgumentMatchers.any;
77
import static org.mockito.ArgumentMatchers.anyString;
8-
import static org.mockito.Mockito.*;
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.never;
910

1011
import dev.openfeature.sdk.exceptions.FatalError;
1112
import dev.openfeature.sdk.fixtures.HookFixtures;
1213
import dev.openfeature.sdk.testutils.TestEventsProvider;
1314
import java.util.HashMap;
14-
import java.util.concurrent.atomic.AtomicBoolean;
1515
import org.junit.jupiter.api.AfterEach;
1616
import org.junit.jupiter.api.BeforeEach;
1717
import org.junit.jupiter.api.DisplayName;
@@ -104,59 +104,4 @@ void shouldNotCallEvaluationMethodsWhenProviderIsInNotReadyState() {
104104

105105
assertThat(details.getErrorCode()).isEqualTo(ErrorCode.PROVIDER_NOT_READY);
106106
}
107-
108-
private static class MockProvider implements FeatureProvider {
109-
private final AtomicBoolean evaluationCalled = new AtomicBoolean();
110-
private final ProviderState providerState;
111-
112-
public MockProvider(ProviderState providerState) {
113-
this.providerState = providerState;
114-
}
115-
116-
public boolean isEvaluationCalled() {
117-
return evaluationCalled.get();
118-
}
119-
120-
@Override
121-
public ProviderState getState() {
122-
return providerState;
123-
}
124-
125-
@Override
126-
public Metadata getMetadata() {
127-
return null;
128-
}
129-
130-
@Override
131-
public ProviderEvaluation<Boolean> getBooleanEvaluation(
132-
String key, Boolean defaultValue, EvaluationContext ctx) {
133-
evaluationCalled.set(true);
134-
return null;
135-
}
136-
137-
@Override
138-
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
139-
evaluationCalled.set(true);
140-
return null;
141-
}
142-
143-
@Override
144-
public ProviderEvaluation<Integer> getIntegerEvaluation(
145-
String key, Integer defaultValue, EvaluationContext ctx) {
146-
evaluationCalled.set(true);
147-
return null;
148-
}
149-
150-
@Override
151-
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
152-
evaluationCalled.set(true);
153-
return null;
154-
}
155-
156-
@Override
157-
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
158-
evaluationCalled.set(true);
159-
return null;
160-
}
161-
}
162107
}

0 commit comments

Comments
 (0)