-
Notifications
You must be signed in to change notification settings - Fork 74
feat: json logic operators for flagd in-process provider #434
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
Kavindu-Dodan
merged 13 commits into
open-feature:main
from
Kavindu-Dodan:feat/json-evaluators
Sep 14, 2023
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
a746e1b
fix resource loading
Kavindu-Dodan ef30c62
evaluators
Kavindu-Dodan ff14ff2
fractional eval and others
Kavindu-Dodan 0967c40
test improvcements
Kavindu-Dodan 9b2e63e
more tests
Kavindu-Dodan d6a2405
test fixes and unit tests
Kavindu-Dodan bc3a4eb
lint fixes
Kavindu-Dodan 266a370
fix pmd violations
Kavindu-Dodan b06e200
fix spotbug suggestions
Kavindu-Dodan d19bbae
fix pmd
Kavindu-Dodan 2386d16
fix fractional hash calculation
Kavindu-Dodan ba135f0
fix tests
Kavindu-Dodan f3354a9
use apache common murmur3
Kavindu-Dodan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
...n/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; | ||
|
|
||
| import io.github.jamsesso.jsonlogic.JsonLogicException; | ||
| import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; | ||
| import io.github.jamsesso.jsonlogic.evaluator.expressions.PreEvaluatedArgumentsExpression; | ||
| import lombok.Getter; | ||
| import lombok.extern.java.Log; | ||
| import org.apache.commons.codec.digest.MurmurHash3; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.logging.Level; | ||
|
|
||
| @Log | ||
| class Fractional implements PreEvaluatedArgumentsExpression { | ||
|
|
||
| public String key() { | ||
| return "fractional"; | ||
| } | ||
|
|
||
| public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationException { | ||
| if (arguments.size() < 2) { | ||
| return null; | ||
| } | ||
|
|
||
| final Operator.FlagProperties properties = new Operator.FlagProperties(data); | ||
|
|
||
| // check optional string target in first arg | ||
| Object arg1 = arguments.get(0); | ||
|
toddbaert marked this conversation as resolved.
|
||
|
|
||
| final String bucketBy; | ||
| final Object[] distibutions; | ||
|
|
||
| if (arg1 instanceof String) { | ||
| // first arg is a String, use for bucketing | ||
| bucketBy = (String) arg1; | ||
|
|
||
| Object[] source = arguments.toArray(); | ||
| distibutions = Arrays.copyOfRange(source, 1, source.length); | ||
| } else { | ||
| // fallback to targeting key if present | ||
| if (properties.getTargetingKey() == null) { | ||
| log.log(Level.FINE, "Missing fallback targeting key"); | ||
| return null; | ||
| } | ||
|
|
||
| bucketBy = properties.getTargetingKey(); | ||
| distibutions = arguments.toArray(); | ||
| } | ||
|
|
||
| final String hashKey = properties.getFlagKey() + bucketBy; | ||
| final List<FractionProperty> propertyList = new ArrayList<>(); | ||
|
|
||
| double distribution = 0; | ||
| try { | ||
| for (Object dist : distibutions) { | ||
| FractionProperty fractionProperty = new FractionProperty(dist); | ||
| propertyList.add(fractionProperty); | ||
| distribution += fractionProperty.getPercentage(); | ||
| } | ||
| } catch (JsonLogicException e) { | ||
| log.log(Level.FINE, "Error parsing fractional targeting rule", e); | ||
| return null; | ||
| } | ||
|
|
||
| if (distribution != 100) { | ||
| log.log(Level.FINE, "Fractional properties do not sum to 100"); | ||
| return null; | ||
| } | ||
|
|
||
| // find distribution | ||
| return distributeValue(hashKey, propertyList); | ||
| } | ||
|
|
||
| private static String distributeValue(final String hashKey, final List<FractionProperty> propertyList) | ||
| throws JsonLogicEvaluationException { | ||
| byte[] bytes = hashKey.getBytes(StandardCharsets.UTF_8); | ||
| int mmrHash = MurmurHash3.hash32x86(bytes, 0, bytes.length, 0); | ||
| int bucket = (int) ((Math.abs(mmrHash) * 1.0f / Integer.MAX_VALUE) * 100); | ||
|
|
||
| int bucketSum = 0; | ||
| for (FractionProperty p : propertyList) { | ||
| bucketSum += p.getPercentage(); | ||
|
|
||
| if (bucket < bucketSum) { | ||
| return p.getVariant(); | ||
| } | ||
| } | ||
|
|
||
| // this shall not be reached | ||
| throw new JsonLogicEvaluationException("Unable to find a correct bucket"); | ||
| } | ||
|
|
||
| @Getter | ||
| private static class FractionProperty { | ||
| private final String variant; | ||
| private final int percentage; | ||
|
|
||
| FractionProperty(final Object from) throws JsonLogicException { | ||
| if (!(from instanceof List<?>)) { | ||
| throw new JsonLogicException("Property is not an array"); | ||
| } | ||
|
|
||
| final List<?> array = (List) from; | ||
|
|
||
| if (array.size() != 2) { | ||
| throw new JsonLogicException("Fraction property does not have two elements"); | ||
| } | ||
|
|
||
| // first must be a string | ||
| if (!(array.get(0) instanceof String)) { | ||
| throw new JsonLogicException("First element of the fraction property is not a string variant"); | ||
| } | ||
|
|
||
| // second element must be a number | ||
| if (!(array.get(1) instanceof Number)) { | ||
| throw new JsonLogicException("Second element of the fraction property is not a number"); | ||
| } | ||
|
|
||
| variant = (String) array.get(0); | ||
| percentage = ((Number) array.get(1)).intValue(); | ||
| } | ||
|
|
||
| } | ||
| } | ||
77 changes: 77 additions & 0 deletions
77
...ain/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; | ||
|
|
||
| import dev.openfeature.sdk.EvaluationContext; | ||
| import io.github.jamsesso.jsonlogic.JsonLogic; | ||
| import io.github.jamsesso.jsonlogic.JsonLogicException; | ||
| import lombok.Getter; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Targeting operator wraps JsonLogic handlers and expose a simple API for external layers. | ||
| * This helps to isolate external dependencies to this package. | ||
| */ | ||
| public class Operator { | ||
|
|
||
| static final String FLAG_KEY = "$flagKey"; | ||
| static final String TARGET_KEY = "targetingKey"; | ||
|
|
||
| private final JsonLogic jsonLogicHandler; | ||
|
|
||
| /** | ||
| * Construct a targeting operator. | ||
| */ | ||
| public Operator() { | ||
| jsonLogicHandler = new JsonLogic(); | ||
| jsonLogicHandler.addOperation(new Fractional()); | ||
| jsonLogicHandler.addOperation(new SemVer()); | ||
| jsonLogicHandler.addOperation(new StringComp(StringComp.Type.STARTS_WITH)); | ||
| jsonLogicHandler.addOperation(new StringComp(StringComp.Type.ENDS_WITH)); | ||
| } | ||
|
|
||
| /** | ||
| * Apply this operator on the provided rule. | ||
| */ | ||
| public Object apply(final String flagKey, final String targetingRule, final EvaluationContext ctx) | ||
| throws TargetingRuleException { | ||
| final Map<String, Object> valueMap = ctx.asObjectMap(); | ||
| valueMap.put(FLAG_KEY, flagKey); | ||
|
|
||
| try { | ||
| return jsonLogicHandler.apply(targetingRule, valueMap); | ||
| } catch (JsonLogicException e) { | ||
| throw new TargetingRuleException("Error evaluating json logic", e); | ||
| } | ||
| } | ||
|
|
||
| @Getter | ||
| static class FlagProperties { | ||
| private final String flagKey; | ||
| private final String targetingKey; | ||
|
|
||
| FlagProperties(Object from) { | ||
| if (from instanceof Map) { | ||
| Map<?, ?> dataMap = (Map<?, ?>) from; | ||
|
|
||
| Object flagKey = dataMap.get(FLAG_KEY); | ||
|
|
||
| if (flagKey instanceof String) { | ||
| this.flagKey = (String) flagKey; | ||
| } else { | ||
| this.flagKey = null; | ||
| } | ||
|
|
||
| Object targetKey = dataMap.get(TARGET_KEY); | ||
|
|
||
| if (targetKey instanceof String) { | ||
| targetingKey = (String) targetKey; | ||
| } else { | ||
| targetingKey = null; | ||
| } | ||
| } else { | ||
| flagKey = null; | ||
| targetingKey = null; | ||
| } | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.