Merge contexts from api and client

This commit is contained in:
Justin Abrahms 2022-08-06 19:39:50 -07:00
parent f5a49ea164
commit df1a083398
No known key found for this signature in database
GPG Key ID: 599E2E12011DC474
6 changed files with 89 additions and 3 deletions

View File

@ -8,6 +8,18 @@ import java.util.List;
public interface Client extends Features {
Metadata getMetadata();
/**
* Return an optional client-level evaluation context.
* @return {@link EvaluationContext}
*/
EvaluationContext getEvaluationContext();
/**
* Set the client-level evaluation context.
* @param ctx Client level context.
*/
void setEvaluationContext(EvaluationContext ctx);
/**
* Adds hooks for evaluation.
*

View File

@ -55,6 +55,9 @@ public class EvaluationContext {
private <T> T getAttributeByType(String key, FlagValueType type) {
Pair<FlagValueType, Object> val = attributes.get(key);
if (val == null) {
return null;
}
if (val.getFirst() == type) {
return (T) val.getSecond();
}
@ -106,6 +109,12 @@ public class EvaluationContext {
*/
public static EvaluationContext merge(EvaluationContext ctx1, EvaluationContext ctx2) {
EvaluationContext ec = new EvaluationContext();
if (ctx1 == null) {
return ctx2;
} else if (ctx2 == null) {
return ctx1;
}
ec.attributes.putAll(ctx1.attributes);
ec.attributes.putAll(ctx2.attributes);

View File

@ -9,6 +9,11 @@ public class NoOpProvider implements FeatureProvider {
public static final String PASSED_IN_DEFAULT = "Passed in default";
@Getter
private final String name = "No-op Provider";
private EvaluationContext ctx;
public EvaluationContext getMergedContext() {
return ctx;
}
@Override
public Metadata getMetadata() {
@ -22,6 +27,7 @@ public class NoOpProvider implements FeatureProvider {
@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
this.ctx = ctx;
return ProviderEvaluation.<Boolean>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
@ -31,6 +37,7 @@ public class NoOpProvider implements FeatureProvider {
@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
this.ctx = ctx;
return ProviderEvaluation.<String>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
@ -40,6 +47,7 @@ public class NoOpProvider implements FeatureProvider {
@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
this.ctx = ctx;
return ProviderEvaluation.<Integer>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
@ -48,6 +56,7 @@ public class NoOpProvider implements FeatureProvider {
}
@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
this.ctx = ctx;
return ProviderEvaluation.<Double>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
@ -56,6 +65,7 @@ public class NoOpProvider implements FeatureProvider {
}
@Override
public <T> ProviderEvaluation<T> getObjectEvaluation(String key, T defaultValue, EvaluationContext invocationContext, FlagEvaluationOptions options) {
this.ctx = ctx;
return ProviderEvaluation.<T>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)

View File

@ -16,6 +16,8 @@ import java.util.List;
public class OpenFeatureAPI {
@Getter @Setter private FeatureProvider provider;
private static OpenFeatureAPI api;
@Getter @Setter private EvaluationContext ctx;
@Getter private List<Hook> apiHooks;
public static OpenFeatureAPI getInstance() {

View File

@ -5,6 +5,7 @@ import java.util.*;
import dev.openfeature.javasdk.exceptions.GeneralError;
import dev.openfeature.javasdk.internal.ObjectUtils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -17,6 +18,9 @@ public class OpenFeatureClient implements Client {
@Getter private final List<Hook> clientHooks;
private final HookSupport hookSupport;
@Getter @Setter private EvaluationContext evaluationContext;
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) {
this.openfeatureApi = openFeatureAPI;
this.name = name;
@ -38,7 +42,6 @@ public class OpenFeatureClient implements Client {
ctx = new EvaluationContext();
}
// merge of: API.context, client.context, invocation.context
HookContext<T> hookCtx = HookContext.from(key, type, this.getMetadata(), openfeatureApi.getProvider().getMetadata(), ctx, defaultValue);
List<Hook> mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getApiHooks());
@ -46,9 +49,19 @@ public class OpenFeatureClient implements Client {
FlagEvaluationDetails<T> details = null;
try {
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
EvaluationContext invocationContext = EvaluationContext.merge(ctxFromHook, ctx);
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key, defaultValue, options, provider, invocationContext);
EvaluationContext invocationCtx = EvaluationContext.merge(ctxFromHook, ctx);
// merge of: API.context, client.context, invocation.context
EvaluationContext mergedCtx = EvaluationContext.merge(
EvaluationContext.merge(
openfeatureApi.getCtx(),
this.getEvaluationContext()
),
invocationCtx
);
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key, defaultValue, options, provider, mergedCtx);
details = FlagEvaluationDetails.from(providerEval, key);
hookSupport.afterHooks(type, hookCtx, details, mergedHooks, hints);

View File

@ -20,6 +20,11 @@ class FlagEvaluationSpecTest implements HookFixtures {
return api.getClient();
}
@AfterEach void reset_ctx() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setCtx(null);
}
@Specification(number="1.1.1", text="The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.")
@Test void global_singleton() {
assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance());
@ -209,6 +214,41 @@ class FlagEvaluationSpecTest implements HookFixtures {
assertEquals(Reason.ERROR, result.getReason());
}
@Specification(number="3.2.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.")
@Specification(number="3.2.2", text="Evaluation context MUST be merged in the order: API (global) - client - invocation, with duplicate values being overwritten.")
@Test void multi_layer_context_merges_correctly() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
NoOpProvider provider = new NoOpProvider();
api.setProvider(provider);
EvaluationContext apiCtx = new EvaluationContext();
apiCtx.addStringAttribute("common", "1");
apiCtx.addStringAttribute("common2", "1");
apiCtx.addStringAttribute("api", "2");
api.setCtx(apiCtx);
Client c = api.getClient();
EvaluationContext clientCtx = new EvaluationContext();
clientCtx.addStringAttribute("common", "3");
clientCtx.addStringAttribute("common2", "3");
clientCtx.addStringAttribute("client", "4");
c.setEvaluationContext(clientCtx);
EvaluationContext invocationCtx = new EvaluationContext();
clientCtx.addStringAttribute("common", "5");
clientCtx.addStringAttribute("invocation", "6");
assertFalse(c.getBooleanValue("key", false, invocationCtx));
EvaluationContext merged = provider.getMergedContext();
assertEquals("6", merged.getStringAttribute("invocation"));
assertEquals("5", merged.getStringAttribute("common"), "invocation merge is incorrect");
assertEquals("4", merged.getStringAttribute("client"));
assertEquals("3", merged.getStringAttribute("common2"), "api client merge is incorrect");
assertEquals("2", merged.getStringAttribute("api"));
}
@Specification(number="1.3.3", text="The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.")
@Specification(number="1.1.6", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.")
@Disabled("Not sure how to test?")