Skip to content

feat!: use evaluation context interface #112

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 8 commits into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 11 additions & 120 deletions src/main/java/dev/openfeature/sdk/EvaluationContext.java
Original file line number Diff line number Diff line change
@@ -1,130 +1,21 @@
package dev.openfeature.sdk;

import java.time.Instant;
import java.util.List;

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

@ToString
/**
* The EvaluationContext is a container for arbitrary contextual data
* that can be used as a basis for dynamic evaluation.
*/
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
public class EvaluationContext {

@Setter @Getter private String targetingKey;
@Delegate(excludes = HideDelegateAddMethods.class) private final Structure structure = new Structure();

public EvaluationContext() {
super();
this.targetingKey = "";
}

public EvaluationContext(String targetingKey) {
this();
this.targetingKey = targetingKey;
}
public interface EvaluationContext extends Structure {
String getTargetingKey();

void setTargetingKey(String targetingKey);

/**
* Merges two EvaluationContext objects with the second overriding the first in
* Merges this EvaluationContext object with the second overriding the this in
* case of conflict.
*
* @param ctx1 base context
* @param ctx2 overriding context
* @param overridingContext overriding context
* @return resulting merged context
*/
public static EvaluationContext merge(EvaluationContext ctx1, EvaluationContext ctx2) {
EvaluationContext ec = new EvaluationContext();
if (ctx1 == null) {
return ctx2;
} else if (ctx2 == null) {
return ctx1;
}

ec.structure.attributes.putAll(ctx1.structure.attributes);
ec.structure.attributes.putAll(ctx2.structure.attributes);

if (ctx1.getTargetingKey() != null && !ctx1.getTargetingKey().trim().equals("")) {
ec.setTargetingKey(ctx1.getTargetingKey());
}

if (ctx2.getTargetingKey() != null && !ctx2.getTargetingKey().trim().equals("")) {
ec.setTargetingKey(ctx2.getTargetingKey());
}

return ec;
}

// override @Delegate methods so that we can use "add" methods and still return EvaluationContext, not Structure
public EvaluationContext add(String key, Boolean value) {
this.structure.add(key, value);
return this;
}

public EvaluationContext add(String key, String value) {
this.structure.add(key, value);
return this;
}

public EvaluationContext add(String key, Integer value) {
this.structure.add(key, value);
return this;
}

public EvaluationContext add(String key, Double value) {
this.structure.add(key, value);
return this;
}

public EvaluationContext add(String key, Instant value) {
this.structure.add(key, value);
return this;
}

public EvaluationContext add(String key, Structure value) {
this.structure.add(key, value);
return this;
}

public EvaluationContext add(String key, List<Value> value) {
this.structure.add(key, value);
return this;
}

/**
* Hidden class to tell Lombok not to copy these methods over via delegation.
*/
private static class HideDelegateAddMethods {
public Structure add(String ignoredKey, Boolean ignoredValue) {
return null;
}

public Structure add(String ignoredKey, Double ignoredValue) {
return null;
}

public Structure add(String ignoredKey, String ignoredValue) {
return null;
}

public Structure add(String ignoredKey, Value ignoredValue) {
return null;
}

public Structure add(String ignoredKey, Integer ignoredValue) {
return null;
}

public Structure add(String ignoredKey, List<Value> ignoredValue) {
return null;
}

public Structure add(String ignoredKey, Structure ignoredValue) {
return null;
}

public Structure add(String ignoredKey, Instant ignoredValue) {
return null;
}
}
EvaluationContext merge(EvaluationContext overridingContext);
}
43 changes: 18 additions & 25 deletions src/main/java/dev/openfeature/sdk/HookSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,44 @@

@Slf4j
@RequiredArgsConstructor
@SuppressWarnings({"unchecked", "rawtypes"})
@SuppressWarnings({ "unchecked", "rawtypes" })
class HookSupport {

public void errorHooks(FlagValueType flagValueType, HookContext hookCtx, Exception e, List<Hook> hooks,
Map<String, Object> hints) {
Map<String, Object> hints) {
executeHooks(flagValueType, hooks, "error", hook -> hook.error(hookCtx, e, hints));
}

public void afterAllHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
Map<String, Object> hints) {
Map<String, Object> hints) {
executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, hints));
}

public void afterHooks(FlagValueType flagValueType, HookContext hookContext, FlagEvaluationDetails details,
List<Hook> hooks, Map<String, Object> hints) {
List<Hook> hooks, Map<String, Object> hints) {
executeHooksUnchecked(flagValueType, hooks, hook -> hook.after(hookContext, details, hints));
}

private <T> void executeHooks(
FlagValueType flagValueType, List<Hook> hooks,
String hookMethod,
Consumer<Hook<T>> hookCode
) {
Consumer<Hook<T>> hookCode) {
if (hooks != null) {
hooks
.stream()
.filter(hook -> hook.supportsFlagValueType(flagValueType))
.forEach(hook -> executeChecked(hook, hookCode, hookMethod));
.stream()
.filter(hook -> hook.supportsFlagValueType(flagValueType))
.forEach(hook -> executeChecked(hook, hookCode, hookMethod));
}
}

private <T> void executeHooksUnchecked(
FlagValueType flagValueType, List<Hook> hooks,
Consumer<Hook<T>> hookCode
) {
Consumer<Hook<T>> hookCode) {
if (hooks != null) {
hooks
.stream()
.filter(hook -> hook.supportsFlagValueType(flagValueType))
.forEach(hookCode::accept);
.stream()
.filter(hook -> hook.supportsFlagValueType(flagValueType))
.forEach(hookCode::accept);
}
}

Expand All @@ -63,13 +61,16 @@ private <T> void executeChecked(Hook<T> hook, Consumer<Hook<T>> hookCode, String
}

public EvaluationContext beforeHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
Map<String, Object> hints) {
Map<String, Object> hints) {
Stream<EvaluationContext> result = callBeforeHooks(flagValueType, hookCtx, hooks, hints);
return EvaluationContext.merge(hookCtx.getCtx(), collectContexts(result));
return hookCtx.getCtx().merge(
result.reduce(hookCtx.getCtx(), (EvaluationContext accumulated, EvaluationContext current) -> {
return accumulated.merge(current);
}));
}

private Stream<EvaluationContext> callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
List<Hook> hooks, Map<String, Object> hints) {
List<Hook> hooks, Map<String, Object> hints) {
// These traverse backwards from normal.
List<Hook> reversedHooks = IntStream
.range(0, hooks.size())
Expand All @@ -86,12 +87,4 @@ private Stream<EvaluationContext> callBeforeHooks(FlagValueType flagValueType, H
.map(Optional::get)
.map(EvaluationContext.class::cast);
}

//for whatever reason, an error `error: incompatible types: invalid method reference` is thrown on compilation
// with javac
//when the reduce call is appended directly as stream call chain above. moving it to its own method works however...
private EvaluationContext collectContexts(Stream<EvaluationContext> result) {
return result
.reduce(new EvaluationContext(), EvaluationContext::merge, EvaluationContext::merge);
}
}
148 changes: 148 additions & 0 deletions src/main/java/dev/openfeature/sdk/MutableContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package dev.openfeature.sdk;

import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import lombok.Getter;
import lombok.Setter;
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 MutableContext is an EvaluationContext implementation which is not threadsafe, and whose attributes can
* be modified after instantiation.
*/
@ToString
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
public class MutableContext implements EvaluationContext {

@Setter() @Getter private String targetingKey;
@Delegate(excludes = HideDelegateAddMethods.class) private final MutableStructure structure;

public MutableContext() {
this.structure = new MutableStructure();
this.targetingKey = "";
}

public MutableContext(String targetingKey) {
this();
this.targetingKey = targetingKey;
}

public MutableContext(Map<String, Value> attributes) {
this.structure = new MutableStructure(attributes);
this.targetingKey = "";
}

public MutableContext(String targetingKey, Map<String, Value> attributes) {
this(attributes);
this.targetingKey = targetingKey;
}

// override @Delegate methods so that we can use "add" methods and still return MutableContext, not Structure
public MutableContext add(String key, Boolean value) {
this.structure.add(key, value);
return this;
}

public MutableContext add(String key, String value) {
this.structure.add(key, value);
return this;
}

public MutableContext add(String key, Integer value) {
this.structure.add(key, value);
return this;
}

public MutableContext add(String key, Double value) {
this.structure.add(key, value);
return this;
}

public MutableContext add(String key, Instant value) {
this.structure.add(key, value);
return this;
}

public MutableContext add(String key, Structure value) {
this.structure.add(key, value);
return this;
}

public MutableContext add(String key, List<Value> value) {
this.structure.add(key, value);
return this;
}

/**
* Merges this EvaluationContext objects with the second overriding the this in
* case of conflict.
*
* @param overridingContext overriding context
* @return resulting merged context
*/
@Override
public EvaluationContext merge(EvaluationContext overridingContext) {
if (overridingContext == null) {
return new MutableContext(this.asMap());
}

Map<String, Value> merged = new HashMap<String, Value>();

merged.putAll(this.asMap());
merged.putAll(overridingContext.asMap());
EvaluationContext ec = new MutableContext(merged);

if (this.getTargetingKey() != null && !this.getTargetingKey().trim().equals("")) {
ec.setTargetingKey(this.getTargetingKey());
}

if (overridingContext.getTargetingKey() != null && !overridingContext.getTargetingKey().trim().equals("")) {
ec.setTargetingKey(overridingContext.getTargetingKey());
}

return ec;
}

/**
* Hidden class to tell Lombok not to copy these methods over via delegation.
*/
private static class HideDelegateAddMethods {
public MutableStructure add(String ignoredKey, Boolean ignoredValue) {
return null;
}

public MutableStructure add(String ignoredKey, Double ignoredValue) {
return null;
}

public MutableStructure add(String ignoredKey, String ignoredValue) {
return null;
}

public MutableStructure add(String ignoredKey, Value ignoredValue) {
return null;
}

public MutableStructure add(String ignoredKey, Integer ignoredValue) {
return null;
}

public MutableStructure add(String ignoredKey, List<Value> ignoredValue) {
return null;
}

public MutableStructure add(String ignoredKey, MutableStructure ignoredValue) {
return null;
}

public MutableStructure add(String ignoredKey, Instant ignoredValue) {
return null;
}
}
}
Loading