Skip to content

Commit df1a083

Browse files
committed
Merge contexts from api and client
1 parent f5a49ea commit df1a083

File tree

6 files changed

+89
-3
lines changed

6 files changed

+89
-3
lines changed

src/main/java/dev/openfeature/javasdk/Client.java

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
88
public interface Client extends Features {
99
Metadata getMetadata();
1010

11+
/**
12+
* Return an optional client-level evaluation context.
13+
* @return {@link EvaluationContext}
14+
*/
15+
EvaluationContext getEvaluationContext();
16+
17+
/**
18+
* Set the client-level evaluation context.
19+
* @param ctx Client level context.
20+
*/
21+
void setEvaluationContext(EvaluationContext ctx);
22+
1123
/**
1224
* Adds hooks for evaluation.
1325
*

src/main/java/dev/openfeature/javasdk/EvaluationContext.java

+9
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ private <T> Map<String, T> getAttributesByType(FlagValueType type) {
5555

5656
private <T> T getAttributeByType(String key, FlagValueType type) {
5757
Pair<FlagValueType, Object> val = attributes.get(key);
58+
if (val == null) {
59+
return null;
60+
}
5861
if (val.getFirst() == type) {
5962
return (T) val.getSecond();
6063
}
@@ -106,6 +109,12 @@ public ZonedDateTime getDatetimeAttribute(String key) {
106109
*/
107110
public static EvaluationContext merge(EvaluationContext ctx1, EvaluationContext ctx2) {
108111
EvaluationContext ec = new EvaluationContext();
112+
if (ctx1 == null) {
113+
return ctx2;
114+
} else if (ctx2 == null) {
115+
return ctx1;
116+
}
117+
109118
ec.attributes.putAll(ctx1.attributes);
110119
ec.attributes.putAll(ctx2.attributes);
111120

src/main/java/dev/openfeature/javasdk/NoOpProvider.java

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ public class NoOpProvider implements FeatureProvider {
99
public static final String PASSED_IN_DEFAULT = "Passed in default";
1010
@Getter
1111
private final String name = "No-op Provider";
12+
private EvaluationContext ctx;
13+
14+
public EvaluationContext getMergedContext() {
15+
return ctx;
16+
}
1217

1318
@Override
1419
public Metadata getMetadata() {
@@ -22,6 +27,7 @@ public String getName() {
2227

2328
@Override
2429
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
30+
this.ctx = ctx;
2531
return ProviderEvaluation.<Boolean>builder()
2632
.value(defaultValue)
2733
.variant(PASSED_IN_DEFAULT)
@@ -31,6 +37,7 @@ public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defa
3137

3238
@Override
3339
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
40+
this.ctx = ctx;
3441
return ProviderEvaluation.<String>builder()
3542
.value(defaultValue)
3643
.variant(PASSED_IN_DEFAULT)
@@ -40,6 +47,7 @@ public ProviderEvaluation<String> getStringEvaluation(String key, String default
4047

4148
@Override
4249
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
50+
this.ctx = ctx;
4351
return ProviderEvaluation.<Integer>builder()
4452
.value(defaultValue)
4553
.variant(PASSED_IN_DEFAULT)
@@ -48,6 +56,7 @@ public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defa
4856
}
4957
@Override
5058
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
59+
this.ctx = ctx;
5160
return ProviderEvaluation.<Double>builder()
5261
.value(defaultValue)
5362
.variant(PASSED_IN_DEFAULT)
@@ -56,6 +65,7 @@ public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double default
5665
}
5766
@Override
5867
public <T> ProviderEvaluation<T> getObjectEvaluation(String key, T defaultValue, EvaluationContext invocationContext, FlagEvaluationOptions options) {
68+
this.ctx = ctx;
5969
return ProviderEvaluation.<T>builder()
6070
.value(defaultValue)
6171
.variant(PASSED_IN_DEFAULT)

src/main/java/dev/openfeature/javasdk/OpenFeatureAPI.java

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
public class OpenFeatureAPI {
1717
@Getter @Setter private FeatureProvider provider;
1818
private static OpenFeatureAPI api;
19+
20+
@Getter @Setter private EvaluationContext ctx;
1921
@Getter private List<Hook> apiHooks;
2022

2123
public static OpenFeatureAPI getInstance() {

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

+16-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import dev.openfeature.javasdk.exceptions.GeneralError;
66
import dev.openfeature.javasdk.internal.ObjectUtils;
77
import lombok.Getter;
8+
import lombok.Setter;
89
import lombok.extern.slf4j.Slf4j;
910

1011
@Slf4j
@@ -17,6 +18,9 @@ public class OpenFeatureClient implements Client {
1718
@Getter private final List<Hook> clientHooks;
1819
private final HookSupport hookSupport;
1920

21+
@Getter @Setter private EvaluationContext evaluationContext;
22+
23+
2024
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) {
2125
this.openfeatureApi = openFeatureAPI;
2226
this.name = name;
@@ -38,17 +42,26 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
3842
ctx = new EvaluationContext();
3943
}
4044

41-
// merge of: API.context, client.context, invocation.context
4245
HookContext<T> hookCtx = HookContext.from(key, type, this.getMetadata(), openfeatureApi.getProvider().getMetadata(), ctx, defaultValue);
4346

4447
List<Hook> mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getApiHooks());
4548

4649
FlagEvaluationDetails<T> details = null;
4750
try {
4851
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
49-
EvaluationContext invocationContext = EvaluationContext.merge(ctxFromHook, ctx);
5052

51-
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key, defaultValue, options, provider, invocationContext);
53+
EvaluationContext invocationCtx = EvaluationContext.merge(ctxFromHook, ctx);
54+
55+
// merge of: API.context, client.context, invocation.context
56+
EvaluationContext mergedCtx = EvaluationContext.merge(
57+
EvaluationContext.merge(
58+
openfeatureApi.getCtx(),
59+
this.getEvaluationContext()
60+
),
61+
invocationCtx
62+
);
63+
64+
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key, defaultValue, options, provider, mergedCtx);
5265

5366
details = FlagEvaluationDetails.from(providerEval, key);
5467
hookSupport.afterHooks(type, hookCtx, details, mergedHooks, hints);

src/test/java/dev/openfeature/javasdk/FlagEvaluationSpecTest.java

+40
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ private Client _client() {
2020
return api.getClient();
2121
}
2222

23+
@AfterEach void reset_ctx() {
24+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
25+
api.setCtx(null);
26+
}
27+
2328
@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.")
2429
@Test void global_singleton() {
2530
assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance());
@@ -209,6 +214,41 @@ private Client _client() {
209214
assertEquals(Reason.ERROR, result.getReason());
210215
}
211216

217+
@Specification(number="3.2.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.")
218+
@Specification(number="3.2.2", text="Evaluation context MUST be merged in the order: API (global) - client - invocation, with duplicate values being overwritten.")
219+
@Test void multi_layer_context_merges_correctly() {
220+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
221+
NoOpProvider provider = new NoOpProvider();
222+
api.setProvider(provider);
223+
224+
EvaluationContext apiCtx = new EvaluationContext();
225+
apiCtx.addStringAttribute("common", "1");
226+
apiCtx.addStringAttribute("common2", "1");
227+
apiCtx.addStringAttribute("api", "2");
228+
api.setCtx(apiCtx);
229+
230+
Client c = api.getClient();
231+
EvaluationContext clientCtx = new EvaluationContext();
232+
clientCtx.addStringAttribute("common", "3");
233+
clientCtx.addStringAttribute("common2", "3");
234+
clientCtx.addStringAttribute("client", "4");
235+
c.setEvaluationContext(clientCtx);
236+
237+
EvaluationContext invocationCtx = new EvaluationContext();
238+
clientCtx.addStringAttribute("common", "5");
239+
clientCtx.addStringAttribute("invocation", "6");
240+
241+
assertFalse(c.getBooleanValue("key", false, invocationCtx));
242+
243+
EvaluationContext merged = provider.getMergedContext();
244+
assertEquals("6", merged.getStringAttribute("invocation"));
245+
assertEquals("5", merged.getStringAttribute("common"), "invocation merge is incorrect");
246+
assertEquals("4", merged.getStringAttribute("client"));
247+
assertEquals("3", merged.getStringAttribute("common2"), "api client merge is incorrect");
248+
assertEquals("2", merged.getStringAttribute("api"));
249+
250+
}
251+
212252
@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.")
213253
@Specification(number="1.1.6", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.")
214254
@Disabled("Not sure how to test?")

0 commit comments

Comments
 (0)