Support for hook hints.
This commit is contained in:
parent
a0172d57b4
commit
2520f7c051
|
|
@ -1,14 +1,15 @@
|
||||||
package javasdk;
|
package javasdk;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Singular;
|
import lombok.Singular;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data @Builder
|
@Data @Builder
|
||||||
public class FlagEvaluationOptions {
|
public class FlagEvaluationOptions {
|
||||||
@Singular private List<Hook> hooks;
|
@Singular private List<Hook> hooks;
|
||||||
private Map<String, Object> hookHints;
|
@Builder.Default
|
||||||
|
private ImmutableMap<String, Object> hookHints = ImmutableMap.of();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package javasdk;
|
package javasdk;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
|
||||||
// TODO: interface? or abstract class?
|
// TODO: interface? or abstract class?
|
||||||
public abstract class Hook<T> {
|
public abstract class Hook<T> {
|
||||||
void before(HookContext<T> ctx) {}
|
void before(HookContext<T> ctx, ImmutableMap<String, Object> hints) {}
|
||||||
void after(HookContext<T> ctx, FlagEvaluationDetails<T> details) {}
|
void after(HookContext<T> ctx, FlagEvaluationDetails<T> details, ImmutableMap<String, Object> hints) {}
|
||||||
void error(HookContext<T> ctx, Exception error) {}
|
void error(HookContext<T> ctx, Exception error, ImmutableMap<String, Object> hints) {}
|
||||||
void finallyAfter(HookContext<T> ctx) {}
|
void finallyAfter(HookContext<T> ctx, ImmutableMap<String, Object> hints) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package javasdk;
|
package javasdk;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import javasdk.exceptions.GeneralError;
|
import javasdk.exceptions.GeneralError;
|
||||||
import javasdk.exceptions.OpenFeatureError;
|
import javasdk.exceptions.OpenFeatureError;
|
||||||
|
|
@ -37,6 +38,9 @@ public class OpenFeatureClient implements Client {
|
||||||
if (ctx == null) {
|
if (ctx == null) {
|
||||||
ctx = new EvaluationContext();
|
ctx = new EvaluationContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImmutableMap<String, Object> hints = options.getHookHints();
|
||||||
|
|
||||||
// TODO: Context transformation?
|
// TODO: Context transformation?
|
||||||
HookContext hookCtx = HookContext.from(key, type, this, ctx, defaultValue);
|
HookContext hookCtx = HookContext.from(key, type, this, ctx, defaultValue);
|
||||||
|
|
||||||
|
|
@ -53,7 +57,7 @@ public class OpenFeatureClient implements Client {
|
||||||
|
|
||||||
FlagEvaluationDetails<T> details = null;
|
FlagEvaluationDetails<T> details = null;
|
||||||
try {
|
try {
|
||||||
this.beforeHooks(hookCtx, mergedHooks);
|
this.beforeHooks(hookCtx, mergedHooks, hints);
|
||||||
|
|
||||||
ProviderEvaluation<T> providerEval;
|
ProviderEvaluation<T> providerEval;
|
||||||
if (type == FlagValueType.BOOLEAN) {
|
if (type == FlagValueType.BOOLEAN) {
|
||||||
|
|
@ -64,7 +68,7 @@ public class OpenFeatureClient implements Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
details = FlagEvaluationDetails.from(providerEval, key, null);
|
details = FlagEvaluationDetails.from(providerEval, key, null);
|
||||||
this.afterHooks(hookCtx, details, mergedHooks);
|
this.afterHooks(hookCtx, details, mergedHooks, hints);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Unable to correctly evaluate flag with key {} due to exception {}", key, e.getMessage());
|
log.error("Unable to correctly evaluate flag with key {} due to exception {}", key, e.getMessage());
|
||||||
if (details == null) {
|
if (details == null) {
|
||||||
|
|
@ -77,36 +81,36 @@ public class OpenFeatureClient implements Client {
|
||||||
} else {
|
} else {
|
||||||
details.errorCode = ErrorCode.GENERAL;
|
details.errorCode = ErrorCode.GENERAL;
|
||||||
}
|
}
|
||||||
this.errorHooks(hookCtx, e, mergedHooks);
|
this.errorHooks(hookCtx, e, mergedHooks, hints);
|
||||||
} finally {
|
} finally {
|
||||||
this.afterAllHooks(hookCtx, mergedHooks);
|
this.afterAllHooks(hookCtx, mergedHooks, hints);
|
||||||
}
|
}
|
||||||
|
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void errorHooks(HookContext hookCtx, Exception e, List<Hook> hooks) {
|
private void errorHooks(HookContext hookCtx, Exception e, List<Hook> hooks, ImmutableMap<String, Object> hints) {
|
||||||
for (Hook hook : hooks) {
|
for (Hook hook : hooks) {
|
||||||
hook.error(hookCtx, e);
|
hook.error(hookCtx, e, hints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void afterAllHooks(HookContext hookCtx, List<Hook> hooks) {
|
private void afterAllHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) {
|
||||||
for (Hook hook : hooks) {
|
for (Hook hook : hooks) {
|
||||||
hook.finallyAfter(hookCtx);
|
hook.finallyAfter(hookCtx, hints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> void afterHooks(HookContext hookContext, FlagEvaluationDetails<T> details, List<Hook> hooks) {
|
private <T> void afterHooks(HookContext hookContext, FlagEvaluationDetails<T> details, List<Hook> hooks, ImmutableMap<String, Object> hints) {
|
||||||
for (Hook hook : hooks) {
|
for (Hook hook : hooks) {
|
||||||
hook.after(hookContext, details);
|
hook.after(hookContext, details, hints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HookContext beforeHooks(HookContext hookCtx, List<Hook> hooks) {
|
private HookContext beforeHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) {
|
||||||
// These traverse backwards from normal.
|
// These traverse backwards from normal.
|
||||||
for (Hook hook : Lists.reverse(hooks)) {
|
for (Hook hook : Lists.reverse(hooks)) {
|
||||||
hook.before(hookCtx);
|
hook.before(hookCtx, hints);
|
||||||
// TODO: Merge returned context w/ hook context object
|
// TODO: Merge returned context w/ hook context object
|
||||||
}
|
}
|
||||||
return hookCtx;
|
return hookCtx;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class DeveloperExperienceTest {
|
||||||
Client client = api.getClient();
|
Client client = api.getClient();
|
||||||
client.registerHooks(exampleHook);
|
client.registerHooks(exampleHook);
|
||||||
Boolean retval = client.getBooleanValue(flagKey, false);
|
Boolean retval = client.getBooleanValue(flagKey, false);
|
||||||
verify(exampleHook, times(1)).finallyAfter(any());
|
verify(exampleHook, times(1)).finallyAfter(any(), any());
|
||||||
assertFalse(retval);
|
assertFalse(retval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,8 +43,8 @@ class DeveloperExperienceTest {
|
||||||
client.registerHooks(clientHook);
|
client.registerHooks(clientHook);
|
||||||
Boolean retval = client.getBooleanValue(flagKey, false, new EvaluationContext(),
|
Boolean retval = client.getBooleanValue(flagKey, false, new EvaluationContext(),
|
||||||
FlagEvaluationOptions.builder().hook(evalHook).build());
|
FlagEvaluationOptions.builder().hook(evalHook).build());
|
||||||
verify(clientHook, times(1)).finallyAfter(any());
|
verify(clientHook, times(1)).finallyAfter(any(), any());
|
||||||
verify(evalHook, times(1)).finallyAfter(any());
|
verify(evalHook, times(1)).finallyAfter(any(), any());
|
||||||
assertFalse(retval);
|
assertFalse(retval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,8 +135,8 @@ public class FlagEvaluationSpecTests {
|
||||||
c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder()
|
c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder()
|
||||||
.hook(invocationHook)
|
.hook(invocationHook)
|
||||||
.build());
|
.build());
|
||||||
verify(clientHook, times(1)).before(any());
|
verify(clientHook, times(1)).before(any(), any());
|
||||||
verify(invocationHook, times(1)).before(any());
|
verify(invocationHook, times(1)).before(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Specification(spec="flag evaluation", number="1.18", text="Methods, functions, or operations on the client MUST " +
|
@Specification(spec="flag evaluation", number="1.18", text="Methods, functions, or operations on the client MUST " +
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ public class HookSpecTests {
|
||||||
client.getBooleanValue("key", false, new EvaluationContext(),
|
client.getBooleanValue("key", false, new EvaluationContext(),
|
||||||
FlagEvaluationOptions.builder().hook(evalHook).build());
|
FlagEvaluationOptions.builder().hook(evalHook).build());
|
||||||
|
|
||||||
verify(evalHook, times(1)).before(any());
|
verify(evalHook, times(1)).before(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Specification(spec="hooks", number="5.1", text="Flag evalution options MUST contain a list of hooks to evaluate.")
|
@Specification(spec="hooks", number="5.1", text="Flag evalution options MUST contain a list of hooks to evaluate.")
|
||||||
|
|
@ -160,7 +160,7 @@ public class HookSpecTests {
|
||||||
@Specification(spec="hooks", number="4.3", text="If an error occurs in the finally hook, it MUST NOT trigger the error hook.")
|
@Specification(spec="hooks", number="4.3", text="If an error occurs in the finally hook, it MUST NOT trigger the error hook.")
|
||||||
@Test void errors_in_finally() {
|
@Test void errors_in_finally() {
|
||||||
Hook<Boolean> h = mock(Hook.class);
|
Hook<Boolean> h = mock(Hook.class);
|
||||||
doThrow(RuntimeException.class).when(h).finallyAfter(any());
|
doThrow(RuntimeException.class).when(h).finallyAfter(any(), any());
|
||||||
|
|
||||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||||
api.setProvider(new NoOpProvider<>());
|
api.setProvider(new NoOpProvider<>());
|
||||||
|
|
@ -168,17 +168,17 @@ public class HookSpecTests {
|
||||||
|
|
||||||
assertThrows(RuntimeException.class, () -> c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder().hook(h).build()));
|
assertThrows(RuntimeException.class, () -> c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder().hook(h).build()));
|
||||||
|
|
||||||
verify(h, times(1)).finallyAfter(any());
|
verify(h, times(1)).finallyAfter(any(), any());
|
||||||
verify(h, times(0)).error(any(), any());
|
verify(h, times(0)).error(any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Specification(spec="hooks", number="3.4", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag evaluation. It accepts hook context (required), exception for what went wrong (required), and HookHints (optional). It has no return value.")
|
@Specification(spec="hooks", number="3.4", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag evaluation. It accepts hook context (required), exception for what went wrong (required), and HookHints (optional). It has no return value.")
|
||||||
@Test void error_hook_run_during_non_finally_stage() {
|
@Test void error_hook_run_during_non_finally_stage() {
|
||||||
final boolean[] error_called = {false};
|
final boolean[] error_called = {false};
|
||||||
Hook h = mock(Hook.class);
|
Hook h = mock(Hook.class);
|
||||||
doThrow(RuntimeException.class).when(h).finallyAfter(any());
|
doThrow(RuntimeException.class).when(h).finallyAfter(any(), any());
|
||||||
|
|
||||||
verify(h, times(0)).error(any(), any());
|
verify(h, times(0)).error(any(), any(), any());
|
||||||
}
|
}
|
||||||
@Specification(spec="hooks", number="4.2", text="Hooks MUST be evaluated in the following order:" +
|
@Specification(spec="hooks", number="4.2", text="Hooks MUST be evaluated in the following order:" +
|
||||||
"before: API, Client, Invocation" +
|
"before: API, Client, Invocation" +
|
||||||
|
|
@ -189,33 +189,33 @@ public class HookSpecTests {
|
||||||
List<String> evalOrder = new ArrayList<String>();
|
List<String> evalOrder = new ArrayList<String>();
|
||||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||||
api.setProvider(new NoOpProvider());
|
api.setProvider(new NoOpProvider());
|
||||||
api.registerHooks(new Hook() {
|
api.registerHooks(new Hook<Boolean>() {
|
||||||
@Override
|
@Override
|
||||||
void before(HookContext ctx) {
|
void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("api before");
|
evalOrder.add("api before");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void after(HookContext ctx, FlagEvaluationDetails details) {
|
void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("api after");
|
evalOrder.add("api after");
|
||||||
throw new RuntimeException(); // trigger error flows.
|
throw new RuntimeException(); // trigger error flows.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void error(HookContext ctx, Exception error) {
|
void error(HookContext<Boolean> ctx, Exception error, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("api error");
|
evalOrder.add("api error");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void finallyAfter(HookContext ctx) {
|
void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("api finally");
|
evalOrder.add("api finally");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Client c = api.getClient();
|
Client c = api.getClient();
|
||||||
c.registerHooks(new Hook() {
|
c.registerHooks(new Hook<Boolean>() {
|
||||||
@Override
|
@Override
|
||||||
void before(HookContext ctx) {
|
void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("client before");
|
evalOrder.add("client before");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,35 +225,35 @@ public class HookSpecTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void error(HookContext ctx, Exception error) {
|
void error(HookContext<Boolean> ctx, Exception error, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("client error");
|
evalOrder.add("client error");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void finallyAfter(HookContext ctx) {
|
void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("client finally");
|
evalOrder.add("client finally");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder()
|
c.getBooleanValue("key", false, null, FlagEvaluationOptions.builder()
|
||||||
.hook(new Hook() {
|
.hook(new Hook<Boolean>() {
|
||||||
@Override
|
@Override
|
||||||
void before(HookContext ctx) {
|
void before(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("invocation before");
|
evalOrder.add("invocation before");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void after(HookContext ctx, FlagEvaluationDetails details) {
|
void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("invocation after");
|
evalOrder.add("invocation after");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void error(HookContext ctx, Exception error) {
|
void error(HookContext<Boolean> ctx, Exception error, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("invocation error");
|
evalOrder.add("invocation error");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void finallyAfter(HookContext ctx) {
|
void finallyAfter(HookContext<Boolean> ctx, ImmutableMap<String, Object> hints) {
|
||||||
evalOrder.add("invocation finally");
|
evalOrder.add("invocation finally");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue