Skip to content

Commit b57e554

Browse files
author
Justin Abrahms
authored
Merge pull request #5 from open-feature/eval-context
Evaluation Context support
2 parents 6448e72 + 49e3749 commit b57e554

File tree

8 files changed

+239
-25
lines changed

8 files changed

+239
-25
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,94 @@
11
package dev.openfeature.javasdk;
22

3+
import lombok.EqualsAndHashCode;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
import lombok.ToString;
7+
8+
import java.time.ZonedDateTime;
9+
import java.time.format.DateTimeFormatter;
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
13+
@ToString @EqualsAndHashCode
14+
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
315
public class EvaluationContext {
16+
@Setter @Getter private String targetingKey;
17+
private final Map<String, Integer> integerAttributes;
18+
private final Map<String, String> stringAttributes;
19+
20+
EvaluationContext() {
21+
this.targetingKey = "";
22+
this.integerAttributes = new HashMap<>();
23+
this.stringAttributes = new HashMap<>();
24+
}
25+
26+
public void addStringAttribute(String key, String value) {
27+
stringAttributes.put(key, value);
28+
}
29+
30+
public String getStringAttribute(String key) {
31+
return stringAttributes.get(key);
32+
}
33+
34+
public void addIntegerAttribute(String key, Integer value) {
35+
integerAttributes.put(key, value);
36+
}
37+
38+
public Integer getIntegerAttribute(String key) {
39+
return integerAttributes.get(key);
40+
}
41+
42+
public Boolean getBooleanAttribute(String key) {
43+
return Boolean.valueOf(stringAttributes.get(key));
44+
}
45+
46+
public void addBooleanAttribute(String key, Boolean b) {
47+
stringAttributes.put(key, b.toString());
48+
}
49+
50+
public void addDatetimeAttribute(String key, ZonedDateTime value) {
51+
this.stringAttributes.put(key, value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
52+
}
53+
54+
// TODO: addStructure or similar.
55+
56+
public ZonedDateTime getDatetimeAttribute(String key) {
57+
String attr = this.stringAttributes.get(key);
58+
if (attr == null) {
59+
return null;
60+
}
61+
return ZonedDateTime.parse(attr, DateTimeFormatter.ISO_ZONED_DATE_TIME);
62+
}
63+
64+
/**
65+
* Merges two EvaluationContext objects with the second overriding the first in case of conflict.
66+
*/
67+
public static EvaluationContext merge(EvaluationContext ctx1, EvaluationContext ctx2) {
68+
EvaluationContext ec = new EvaluationContext();
69+
for (Map.Entry<String, Integer> e : ctx1.integerAttributes.entrySet()) {
70+
ec.addIntegerAttribute(e.getKey(), e.getValue());
71+
}
72+
73+
for (Map.Entry<String, Integer> e : ctx2.integerAttributes.entrySet()) {
74+
ec.addIntegerAttribute(e.getKey(), e.getValue());
75+
}
76+
77+
for (Map.Entry<String, String> e : ctx1.stringAttributes.entrySet()) {
78+
ec.addStringAttribute(e.getKey(), e.getValue());
79+
}
80+
81+
for (Map.Entry<String, String> e : ctx2.stringAttributes.entrySet()) {
82+
ec.addStringAttribute(e.getKey(), e.getValue());
83+
}
84+
if (ctx1.getTargetingKey() != null) {
85+
ec.setTargetingKey(ctx1.getTargetingKey());
86+
}
87+
88+
if (ctx2.getTargetingKey() != null) {
89+
ec.setTargetingKey(ctx2.getTargetingKey());
90+
}
91+
92+
return ec;
93+
}
494
}

lib/src/main/java/dev/openfeature/javasdk/Hook.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import com.google.common.collect.ImmutableMap;
44

5+
import java.util.Optional;
6+
57
// TODO: interface? or abstract class?
68
public abstract class Hook<T> {
7-
public void before(HookContext<T> ctx, ImmutableMap<String, Object> hints) {}
9+
public Optional<EvaluationContext> before(HookContext<T> ctx, ImmutableMap<String, Object> hints) {
10+
return Optional.empty();
11+
}
812
public void after(HookContext<T> ctx, FlagEvaluationDetails<T> details, ImmutableMap<String, Object> hints) {}
913
public void error(HookContext<T> ctx, Exception error, ImmutableMap<String, Object> hints) {}
1014
public void finallyAfter(HookContext<T> ctx, ImmutableMap<String, Object> hints) {}

lib/src/main/java/dev/openfeature/javasdk/HookContext.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import lombok.Builder;
44
import lombok.NonNull;
55
import lombok.Value;
6+
import lombok.With;
67

7-
@Value @Builder
8+
@Value @Builder @With
89
public class HookContext<T> {
910
@NonNull String flagKey;
1011
@NonNull FlagValueType type;

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.util.ArrayList;
1212
import java.util.Arrays;
1313
import java.util.List;
14+
import java.util.Optional;
1415

1516
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
1617
public class OpenFeatureClient implements Client {
@@ -34,11 +35,12 @@ public void registerHooks(Hook... hooks) {
3435

3536
<T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
3637
FeatureProvider provider = this.openfeatureApi.getProvider();
38+
ImmutableMap<String, Object> hints = options.getHookHints();
3739
if (ctx == null) {
3840
ctx = new EvaluationContext();
3941
}
4042

41-
ImmutableMap<String, Object> hints = options.getHookHints();
43+
// merge of: API.context, client.context, invocation.context
4244

4345
// TODO: Context transformation?
4446
HookContext hookCtx = HookContext.from(key, type, this, ctx, defaultValue);
@@ -56,13 +58,15 @@ <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defa
5658

5759
FlagEvaluationDetails<T> details = null;
5860
try {
59-
this.beforeHooks(hookCtx, mergedHooks, hints);
61+
EvaluationContext ctxFromHook = this.beforeHooks(hookCtx, mergedHooks, hints);
62+
EvaluationContext invocationContext = EvaluationContext.merge(ctxFromHook, ctx);
6063

6164
ProviderEvaluation<T> providerEval;
6265
if (type == FlagValueType.BOOLEAN) {
6366
// TODO: Can we guarantee that defaultValue is a boolean? If not, how to we handle that?
64-
providerEval = (ProviderEvaluation<T>) provider.getBooleanEvaluation(key, (Boolean) defaultValue, ctx, options);
67+
providerEval = (ProviderEvaluation<T>) provider.getBooleanEvaluation(key, (Boolean) defaultValue, invocationContext, options);
6568
} else {
69+
// TODO: Support other flag types.
6670
throw new GeneralError("Unknown flag type");
6771
}
6872

@@ -106,13 +110,17 @@ private <T> void afterHooks(HookContext hookContext, FlagEvaluationDetails<T> de
106110
}
107111
}
108112

109-
private HookContext beforeHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) {
113+
private EvaluationContext beforeHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) {
110114
// These traverse backwards from normal.
115+
EvaluationContext ctx = hookCtx.getCtx();
111116
for (Hook hook : Lists.reverse(hooks)) {
112-
hook.before(hookCtx, hints);
113-
// TODO: Merge returned context w/ hook context object
117+
Optional<EvaluationContext> newCtx = hook.before(hookCtx, hints);
118+
if (newCtx != null && newCtx.isPresent()) {
119+
ctx = EvaluationContext.merge(ctx, newCtx.get());
120+
hookCtx = hookCtx.withCtx(ctx);
121+
}
114122
}
115-
return hookCtx;
123+
return ctx;
116124
}
117125

118126
@Override
@@ -162,12 +170,12 @@ public String getStringValue(String key, String defaultValue, EvaluationContext
162170

163171
@Override
164172
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue) {
165-
return getStringDetails(key, defaultValue, new EvaluationContext());
173+
return getStringDetails(key, defaultValue, null);
166174
}
167175

168176
@Override
169177
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx) {
170-
return getStringDetails(key, defaultValue, new EvaluationContext(), FlagEvaluationOptions.builder().build());
178+
return getStringDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
171179
}
172180

173181
@Override

lib/src/test/java/dev/openfeature/javasdk/DeveloperExperienceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class DeveloperExperienceTest {
4141
api.setProvider(new NoOpProvider());
4242
Client client = api.getClient();
4343
client.registerHooks(clientHook);
44-
Boolean retval = client.getBooleanValue(flagKey, false, new EvaluationContext(),
44+
Boolean retval = client.getBooleanValue(flagKey, false, null,
4545
FlagEvaluationOptions.builder().hook(evalHook).build());
4646
verify(clientHook, times(1)).finallyAfter(any(), any());
4747
verify(evalHook, times(1)).finallyAfter(any(), any());
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package dev.openfeature.javasdk;
2+
3+
import org.junit.jupiter.api.Disabled;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.time.ZonedDateTime;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
public class EvalContextTests {
11+
@Specification(spec="Evaluation Context", number="3.1",
12+
text="The `evaluation context` structure **MUST** define an optional `targeting key` field of " +
13+
"type string, identifying the subject of the flag evaluation.")
14+
@Test void requires_targeting_key() {
15+
EvaluationContext ec = new EvaluationContext();
16+
ec.setTargetingKey("targeting-key");
17+
assertEquals("targeting-key", ec.getTargetingKey());
18+
}
19+
20+
@Specification(spec="Evaluation Context", number="3.2", text="The evaluation context MUST support the inclusion of " +
21+
"custom fields, having keys of type `string`, and " +
22+
"values of type `boolean | string | number | datetime | structure`.")
23+
@Test void eval_context() {
24+
EvaluationContext ec = new EvaluationContext();
25+
26+
ec.addStringAttribute("str", "test");
27+
assertEquals("test", ec.getStringAttribute("str"));
28+
29+
ec.addBooleanAttribute("bool", true);
30+
assertEquals(true, ec.getBooleanAttribute("bool"));
31+
32+
ec.addIntegerAttribute("int", 4);
33+
assertEquals(4, ec.getIntegerAttribute("int"));
34+
35+
ZonedDateTime dt = ZonedDateTime.now();
36+
ec.addDatetimeAttribute("dt", dt);
37+
assertEquals(dt, ec.getDatetimeAttribute("dt"));
38+
}
39+
40+
@Specification(spec="Evaluation Context", number="3.2", text="The evaluation context MUST support the inclusion of " +
41+
"custom fields, having keys of type `string`, and " +
42+
"values of type `boolean | string | number | datetime | structure`.")
43+
@Disabled("Structure support")
44+
@Test void eval_context__structure() {}
45+
}

lib/src/test/java/dev/openfeature/javasdk/FlagEvaluationSpecTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package dev.openfeature.javasdk;
22

3-
import dev.openfeature.javasdk.*;
43
import org.junit.jupiter.api.Disabled;
54
import org.junit.jupiter.api.Test;
65

@@ -132,6 +131,7 @@ Client _client() {
132131
"unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.")
133132
@Disabled
134133
@Test void detail_flags() {
134+
// TODO: Add tests re: detail functions.
135135
throw new NotImplementedException();
136136
}
137137

0 commit comments

Comments
 (0)