Skip to content

Commit 6c14d87

Browse files
thiyagu06toddbaert
andauthored
feat: added implementation of immutable evaluation context (#210)
added immutable context implementation Signed-off-by: thiyagu06 <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 43aea3b commit 6c14d87

19 files changed

+619
-175
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
public interface EvaluationContext extends Structure {
99
String getTargetingKey();
1010

11+
/**
12+
* Mutating targeting key is not supported in all implementations and will be removed.
13+
*/
14+
@Deprecated
1115
void setTargetingKey(String targetingKey);
1216

1317
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package dev.openfeature.sdk;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import lombok.Getter;
7+
import lombok.ToString;
8+
import lombok.experimental.Delegate;
9+
10+
/**
11+
* The EvaluationContext is a container for arbitrary contextual data
12+
* that can be used as a basis for dynamic evaluation.
13+
* The ImmutableContext is an EvaluationContext implementation which is threadsafe, and whose attributes can
14+
* not be modified after instantiation.
15+
*/
16+
@ToString
17+
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
18+
public final class ImmutableContext implements EvaluationContext {
19+
20+
@Getter
21+
private final String targetingKey;
22+
@Delegate
23+
private final Structure structure;
24+
25+
/**
26+
* Create an immutable context with an empty targeting_key and attributes provided.
27+
*/
28+
public ImmutableContext() {
29+
this("", new HashMap<>());
30+
}
31+
32+
/**
33+
* Create an immutable context with given targeting_key provided.
34+
*
35+
* @param targetingKey targeting key
36+
*/
37+
public ImmutableContext(String targetingKey) {
38+
this(targetingKey, new HashMap<>());
39+
}
40+
41+
/**
42+
* Create an immutable context with an attributes provided.
43+
*
44+
* @param attributes evaluation context attributes
45+
*/
46+
public ImmutableContext(Map<String, Value> attributes) {
47+
this("", attributes);
48+
}
49+
50+
/**
51+
* Create an immutable context with given targetingKey and attributes provided.
52+
*
53+
* @param targetingKey targeting key
54+
* @param attributes evaluation context attributes
55+
*/
56+
public ImmutableContext(String targetingKey, Map<String, Value> attributes) {
57+
this.structure = new ImmutableStructure(attributes);
58+
this.targetingKey = targetingKey;
59+
}
60+
61+
/**
62+
* Mutating targeting key is not supported in ImmutableContext and will be removed.
63+
*/
64+
@Override
65+
@Deprecated
66+
public void setTargetingKey(String targetingKey) {
67+
throw new UnsupportedOperationException("changing of targeting key is not allowed");
68+
}
69+
70+
/**
71+
* Merges this EvaluationContext object with the passed EvaluationContext, overriding in case of conflict.
72+
*
73+
* @param overridingContext overriding context
74+
* @return resulting merged context
75+
*/
76+
@Override
77+
public EvaluationContext merge(EvaluationContext overridingContext) {
78+
if (overridingContext == null) {
79+
return new ImmutableContext(this.targetingKey, this.asMap());
80+
}
81+
String newTargetingKey = "";
82+
if (this.getTargetingKey() != null && !this.getTargetingKey().trim().equals("")) {
83+
newTargetingKey = this.getTargetingKey();
84+
}
85+
86+
if (overridingContext.getTargetingKey() != null && !overridingContext.getTargetingKey().trim().equals("")) {
87+
newTargetingKey = overridingContext.getTargetingKey();
88+
}
89+
Map<String, Value> merged = new HashMap<>();
90+
91+
merged.putAll(this.asMap());
92+
merged.putAll(overridingContext.asMap());
93+
return new ImmutableContext(newTargetingKey, merged);
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package dev.openfeature.sdk;
2+
3+
import lombok.EqualsAndHashCode;
4+
import lombok.ToString;
5+
6+
import java.util.HashMap;
7+
import java.util.HashSet;
8+
import java.util.Map;
9+
import java.util.Set;
10+
import java.util.stream.Collectors;
11+
12+
/**
13+
* {@link ImmutableStructure} represents a potentially nested object type which is used to represent
14+
* structured data.
15+
* The ImmutableStructure is a Structure implementation which is threadsafe, and whose attributes can
16+
* not be modified after instantiation.
17+
*/
18+
@ToString
19+
@EqualsAndHashCode
20+
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
21+
public final class ImmutableStructure implements Structure {
22+
23+
private final Map<String, Value> attributes;
24+
25+
/**
26+
* create an immutable structure with the empty attributes.
27+
*/
28+
public ImmutableStructure() {
29+
this(new HashMap<>());
30+
}
31+
32+
/**
33+
* create immutable structure with the given attributes.
34+
*
35+
* @param attributes attributes.
36+
*/
37+
public ImmutableStructure(Map<String, Value> attributes) {
38+
Map<String, Value> copy = attributes.entrySet()
39+
.stream()
40+
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().clone()));
41+
this.attributes = new HashMap<>(copy);
42+
}
43+
44+
@Override
45+
public Set<String> keySet() {
46+
return new HashSet<>(this.attributes.keySet());
47+
}
48+
49+
// getters
50+
@Override
51+
public Value getValue(String key) {
52+
Value value = this.attributes.get(key);
53+
return value.clone();
54+
}
55+
56+
/**
57+
* Get all values.
58+
*
59+
* @return all attributes on the structure
60+
*/
61+
@Override
62+
public Map<String, Value> asMap() {
63+
return attributes
64+
.entrySet()
65+
.stream()
66+
.collect(Collectors.toMap(
67+
Map.Entry::getKey,
68+
e -> getValue(e.getKey())
69+
));
70+
}
71+
72+
/**
73+
* Get all values, with primitives types.
74+
*
75+
* @return all attributes on the structure into a Map
76+
*/
77+
@Override
78+
public Map<String, Object> asObjectMap() {
79+
return attributes
80+
.entrySet()
81+
.stream()
82+
.collect(Collectors.toMap(
83+
Map.Entry::getKey,
84+
e -> convertValue(getValue(e.getKey()))
85+
));
86+
}
87+
}

src/main/java/dev/openfeature/sdk/MutableStructure.java

+3-53
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
package dev.openfeature.sdk;
22

3+
import lombok.EqualsAndHashCode;
4+
import lombok.ToString;
5+
36
import java.time.Instant;
47
import java.util.HashMap;
58
import java.util.List;
69
import java.util.Map;
710
import java.util.Set;
811
import java.util.stream.Collectors;
912

10-
import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
11-
import lombok.EqualsAndHashCode;
12-
import lombok.ToString;
13-
1413
/**
1514
* {@link MutableStructure} represents a potentially nested object type which is used to represent
1615
* structured data.
@@ -109,53 +108,4 @@ public Map<String, Object> asObjectMap() {
109108
e -> convertValue(getValue(e.getKey()))
110109
));
111110
}
112-
113-
/**
114-
* convertValue is converting the object type Value in a primitive type.
115-
*
116-
* @param value - Value object to convert
117-
* @return an Object containing the primitive type.
118-
*/
119-
private Object convertValue(Value value) {
120-
if (value.isBoolean()) {
121-
return value.asBoolean();
122-
}
123-
124-
if (value.isNumber()) {
125-
Double valueAsDouble = value.asDouble();
126-
if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
127-
return value.asInteger();
128-
}
129-
return valueAsDouble;
130-
}
131-
132-
if (value.isString()) {
133-
return value.asString();
134-
}
135-
136-
if (value.isInstant()) {
137-
return value.asInstant();
138-
}
139-
140-
if (value.isList()) {
141-
return value.asList()
142-
.stream()
143-
.map(this::convertValue)
144-
.collect(Collectors.toList());
145-
}
146-
147-
if (value.isStructure()) {
148-
Structure s = value.asStructure();
149-
return s.asMap()
150-
.keySet()
151-
.stream()
152-
.collect(
153-
Collectors.toMap(
154-
key -> key,
155-
key -> convertValue(s.getValue(key))
156-
)
157-
);
158-
}
159-
throw new ValueNotConvertableError();
160-
}
161111
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
9393
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
9494
() -> FlagEvaluationOptions.builder().build());
9595
Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
96-
ctx = ObjectUtils.defaultIfNull(ctx, () -> new MutableContext());
96+
ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext());
9797

9898

9999
FlagEvaluationDetails<T> details = null;
@@ -120,10 +120,10 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
120120
// merge of: API.context, client.context, invocation.context
121121
apiContext = openfeatureApi.getEvaluationContext() != null
122122
? openfeatureApi.getEvaluationContext()
123-
: new MutableContext();
123+
: new ImmutableContext();
124124
clientContext = this.getEvaluationContext() != null
125125
? this.getEvaluationContext()
126-
: new MutableContext();
126+
: new ImmutableContext();
127127

128128
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
129129

src/main/java/dev/openfeature/sdk/Structure.java

+52
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package dev.openfeature.sdk;
22

3+
import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
4+
35
import java.util.Map;
46
import java.util.Set;
7+
import java.util.stream.Collectors;
58

69
/**
710
* {@link Structure} represents a potentially nested object type which is used to represent
@@ -38,4 +41,53 @@ public interface Structure {
3841
* @return all attributes on the structure into a Map
3942
*/
4043
Map<String, Object> asObjectMap();
44+
45+
/**
46+
* convertValue is converting the object type Value in a primitive type.
47+
*
48+
* @param value - Value object to convert
49+
* @return an Object containing the primitive type.
50+
*/
51+
default Object convertValue(Value value) {
52+
if (value.isBoolean()) {
53+
return value.asBoolean();
54+
}
55+
56+
if (value.isNumber()) {
57+
Double valueAsDouble = value.asDouble();
58+
if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
59+
return value.asInteger();
60+
}
61+
return valueAsDouble;
62+
}
63+
64+
if (value.isString()) {
65+
return value.asString();
66+
}
67+
68+
if (value.isInstant()) {
69+
return value.asInstant();
70+
}
71+
72+
if (value.isList()) {
73+
return value.asList()
74+
.stream()
75+
.map(this::convertValue)
76+
.collect(Collectors.toList());
77+
}
78+
79+
if (value.isStructure()) {
80+
Structure s = value.asStructure();
81+
return s.asMap()
82+
.keySet()
83+
.stream()
84+
.collect(
85+
Collectors.toMap(
86+
key -> key,
87+
key -> convertValue(s.getValue(key))
88+
)
89+
);
90+
}
91+
throw new ValueNotConvertableError();
92+
}
4193
}

0 commit comments

Comments
 (0)