Skip to content

feat: added implementation of immutable evaluation context #210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 31, 2023
4 changes: 4 additions & 0 deletions src/main/java/dev/openfeature/sdk/EvaluationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
public interface EvaluationContext extends Structure {
String getTargetingKey();

/**
* Mutating targeting key is not supported in all implementations and will be removed.
*/
@Deprecated
void setTargetingKey(String targetingKey);

/**
Expand Down
95 changes: 95 additions & 0 deletions src/main/java/dev/openfeature/sdk/ImmutableContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package dev.openfeature.sdk;

import java.util.HashMap;
import java.util.Map;

import lombok.Getter;
import lombok.ToString;
import lombok.experimental.Delegate;

/**
* The EvaluationContext is a container for arbitrary contextual data
* that can be used as a basis for dynamic evaluation.
* The ImmutableContext is an EvaluationContext implementation which is threadsafe, and whose attributes can
* not be modified after instantiation.
*/
@ToString
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
public final class ImmutableContext implements EvaluationContext {

@Getter
private final String targetingKey;
@Delegate
private final Structure structure;

/**
* Create an immutable context with an empty targeting_key and attributes provided.
*/
public ImmutableContext() {
this("", new HashMap<>());
}

/**
* Create an immutable context with given targeting_key provided.
*
* @param targetingKey targeting key
*/
public ImmutableContext(String targetingKey) {
this(targetingKey, new HashMap<>());
}

/**
* Create an immutable context with an attributes provided.
*
* @param attributes evaluation context attributes
*/
public ImmutableContext(Map<String, Value> attributes) {
this("", attributes);
}

/**
* Create an immutable context with given targetingKey and attributes provided.
*
* @param targetingKey targeting key
* @param attributes evaluation context attributes
*/
public ImmutableContext(String targetingKey, Map<String, Value> attributes) {
this.structure = new ImmutableStructure(attributes);
this.targetingKey = targetingKey;
}

/**
* Mutating targeting key is not supported in ImmutableContext and will be removed.
*/
@Override
@Deprecated
public void setTargetingKey(String targetingKey) {
throw new UnsupportedOperationException("changing of targeting key is not allowed");
}

/**
* Merges this EvaluationContext object with the passed EvaluationContext, overriding in case of conflict.
*
* @param overridingContext overriding context
* @return resulting merged context
*/
@Override
public EvaluationContext merge(EvaluationContext overridingContext) {
if (overridingContext == null) {
return new ImmutableContext(this.targetingKey, this.asMap());
}
String newTargetingKey = "";
if (this.getTargetingKey() != null && !this.getTargetingKey().trim().equals("")) {
newTargetingKey = this.getTargetingKey();
}

if (overridingContext.getTargetingKey() != null && !overridingContext.getTargetingKey().trim().equals("")) {
newTargetingKey = overridingContext.getTargetingKey();
}
Map<String, Value> merged = new HashMap<>();

merged.putAll(this.asMap());
merged.putAll(overridingContext.asMap());
return new ImmutableContext(newTargetingKey, merged);
}
}
87 changes: 87 additions & 0 deletions src/main/java/dev/openfeature/sdk/ImmutableStructure.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package dev.openfeature.sdk;

import lombok.EqualsAndHashCode;
import lombok.ToString;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* {@link ImmutableStructure} represents a potentially nested object type which is used to represent
* structured data.
* The ImmutableStructure is a Structure implementation which is threadsafe, and whose attributes can
* not be modified after instantiation.
*/
@ToString
@EqualsAndHashCode
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
public final class ImmutableStructure implements Structure {

private final Map<String, Value> attributes;

/**
* create an immutable structure with the empty attributes.
*/
public ImmutableStructure() {
this(new HashMap<>());
}

/**
* create immutable structure with the given attributes.
*
* @param attributes attributes.
*/
public ImmutableStructure(Map<String, Value> attributes) {
Map<String, Value> copy = attributes.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().clone()));
this.attributes = new HashMap<>(copy);
}

@Override
public Set<String> keySet() {
return new HashSet<>(this.attributes.keySet());
}

// getters
@Override
public Value getValue(String key) {
Value value = this.attributes.get(key);
return value.clone();
}

/**
* Get all values.
*
* @return all attributes on the structure
*/
@Override
public Map<String, Value> asMap() {
return attributes
.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> getValue(e.getKey())
));
}

/**
* Get all values, with primitives types.
*
* @return all attributes on the structure into a Map
*/
@Override
public Map<String, Object> asObjectMap() {
return attributes
.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> convertValue(getValue(e.getKey()))
));
}
}
56 changes: 3 additions & 53 deletions src/main/java/dev/openfeature/sdk/MutableStructure.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package dev.openfeature.sdk;

import lombok.EqualsAndHashCode;
import lombok.ToString;

import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
* {@link MutableStructure} represents a potentially nested object type which is used to represent
* structured data.
Expand Down Expand Up @@ -109,53 +108,4 @@ public Map<String, Object> asObjectMap() {
e -> convertValue(getValue(e.getKey()))
));
}

/**
* convertValue is converting the object type Value in a primitive type.
*
* @param value - Value object to convert
* @return an Object containing the primitive type.
*/
private Object convertValue(Value value) {
if (value.isBoolean()) {
return value.asBoolean();
}

if (value.isNumber()) {
Double valueAsDouble = value.asDouble();
if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
return value.asInteger();
}
return valueAsDouble;
}

if (value.isString()) {
return value.asString();
}

if (value.isInstant()) {
return value.asInstant();
}

if (value.isList()) {
return value.asList()
.stream()
.map(this::convertValue)
.collect(Collectors.toList());
}

if (value.isStructure()) {
Structure s = value.asStructure();
return s.asMap()
.keySet()
.stream()
.collect(
Collectors.toMap(
key -> key,
key -> convertValue(s.getValue(key))
)
);
}
throw new ValueNotConvertableError();
}
}
6 changes: 3 additions & 3 deletions src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
() -> FlagEvaluationOptions.builder().build());
Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
ctx = ObjectUtils.defaultIfNull(ctx, () -> new MutableContext());
ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext());


FlagEvaluationDetails<T> details = null;
Expand All @@ -120,10 +120,10 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
// merge of: API.context, client.context, invocation.context
apiContext = openfeatureApi.getEvaluationContext() != null
? openfeatureApi.getEvaluationContext()
: new MutableContext();
: new ImmutableContext();
clientContext = this.getEvaluationContext() != null
? this.getEvaluationContext()
: new MutableContext();
: new ImmutableContext();

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

Expand Down
52 changes: 52 additions & 0 deletions src/main/java/dev/openfeature/sdk/Structure.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package dev.openfeature.sdk;

import dev.openfeature.sdk.exceptions.ValueNotConvertableError;

import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* {@link Structure} represents a potentially nested object type which is used to represent
Expand Down Expand Up @@ -38,4 +41,53 @@ public interface Structure {
* @return all attributes on the structure into a Map
*/
Map<String, Object> asObjectMap();

/**
* convertValue is converting the object type Value in a primitive type.
*
* @param value - Value object to convert
* @return an Object containing the primitive type.
*/
default Object convertValue(Value value) {
if (value.isBoolean()) {
return value.asBoolean();
}

if (value.isNumber()) {
Double valueAsDouble = value.asDouble();
if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
return value.asInteger();
}
return valueAsDouble;
}

if (value.isString()) {
return value.asString();
}

if (value.isInstant()) {
return value.asInstant();
}

if (value.isList()) {
return value.asList()
.stream()
.map(this::convertValue)
.collect(Collectors.toList());
}

if (value.isStructure()) {
Structure s = value.asStructure();
return s.asMap()
.keySet()
.stream()
.collect(
Collectors.toMap(
key -> key,
key -> convertValue(s.getValue(key))
)
);
}
throw new ValueNotConvertableError();
}
}
Loading