diff --git a/.gitignore b/.gitignore index dc81d245b..116454b80 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ target .project .settings .DS_Store -dependency-reduced-pom.xml +dependency-reduced-pom.xml \ No newline at end of file diff --git a/client/pom.xml b/client/pom.xml index e2343ace1..efffe9272 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -167,6 +167,11 @@ + + io.split.client + targeting-engine + ${project.version} + io.split.client pluggable-storage diff --git a/client/src/main/java/io/split/client/CacheUpdaterService.java b/client/src/main/java/io/split/client/CacheUpdaterService.java index 63b426634..f65a4aa2a 100644 --- a/client/src/main/java/io/split/client/CacheUpdaterService.java +++ b/client/src/main/java/io/split/client/CacheUpdaterService.java @@ -1,19 +1,18 @@ package io.split.client; -import com.google.common.collect.Lists; import io.split.client.dtos.ConditionType; -import io.split.client.dtos.MatcherCombiner; import io.split.client.dtos.Partition; import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedSplit; -import io.split.engine.matchers.AllKeysMatcher; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.WhitelistMatcher; import io.split.grammar.Treatments; import io.split.storages.SplitCacheProducer; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -22,7 +21,7 @@ import java.util.HashMap; import java.util.stream.Collectors; -import static com.google.common.base.Preconditions.checkNotNull; +import java.util.Objects; public final class CacheUpdaterService { @@ -30,7 +29,7 @@ public final class CacheUpdaterService { private SplitCacheProducer _splitCacheProducer; public CacheUpdaterService(SplitCacheProducer splitCacheProducer) { - _splitCacheProducer = checkNotNull(splitCacheProducer); + _splitCacheProducer = Objects.requireNonNull(splitCacheProducer); } public void updateCache(Map map) { @@ -78,9 +77,10 @@ private List getConditions(String splitKey, ParsedSplit split, private ParsedCondition createWhitelistCondition(String splitKey, Partition partition) { ParsedCondition parsedCondition = new ParsedCondition(ConditionType.WHITELIST, - new CombiningMatcher(MatcherCombiner.AND, - Lists.newArrayList(new AttributeMatcher(null, new WhitelistMatcher(Lists.newArrayList(splitKey)), false))), - Lists.newArrayList(partition), splitKey); + new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList( + new AttributeMatcher(null, new WhitelistMatcher(Arrays.asList(splitKey)), false)))), + new ArrayList<>(Arrays.asList(partition)), splitKey); return parsedCondition; } @@ -89,9 +89,9 @@ private ParsedCondition createRolloutCondition(Partition partition) { rolloutPartition.treatment = "-"; rolloutPartition.size = 0; ParsedCondition parsedCondition = new ParsedCondition(ConditionType.ROLLOUT, - new CombiningMatcher(MatcherCombiner.AND, - Lists.newArrayList(new AttributeMatcher(null, new AllKeysMatcher(), false))), - Lists.newArrayList(partition, rolloutPartition), "LOCAL"); + new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))), + new ArrayList<>(Arrays.asList(partition, rolloutPartition)), "LOCAL"); return parsedCondition; } diff --git a/client/src/main/java/io/split/client/api/SplitView.java b/client/src/main/java/io/split/client/api/SplitView.java index cc217fe1f..9f8a25874 100644 --- a/client/src/main/java/io/split/client/api/SplitView.java +++ b/client/src/main/java/io/split/client/api/SplitView.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** @@ -51,7 +52,13 @@ public static SplitView fromParsedSplit(ParsedSplit parsedSplit) { splitView.configs = parsedSplit.configurations() == null? Collections.emptyMap() : parsedSplit.configurations() ; splitView.impressionsDisabled = parsedSplit.impressionsDisabled(); splitView.prerequisites = parsedSplit.prerequisitesMatcher() != null ? - parsedSplit.prerequisitesMatcher().getPrerequisites(): new ArrayList<>(); + parsedSplit.prerequisitesMatcher().getPrerequisites().stream() + .map(p -> { + Prerequisites prereq = new Prerequisites(); + prereq.featureFlagName = p.featureFlagName(); + prereq.treatments = p.treatments(); + return prereq; + }).collect(Collectors.toList()) : new ArrayList<>(); return splitView; } diff --git a/client/src/main/java/io/split/client/impressions/ImpressionHasher.java b/client/src/main/java/io/split/client/impressions/ImpressionHasher.java index 427b241fb..f6497dda5 100644 --- a/client/src/main/java/io/split/client/impressions/ImpressionHasher.java +++ b/client/src/main/java/io/split/client/impressions/ImpressionHasher.java @@ -1,6 +1,6 @@ package io.split.client.impressions; -import io.split.client.utils.MurmurHash3; +import io.split.rules.bucketing.MurmurHash3; public class ImpressionHasher { diff --git a/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java b/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java index 540acc5d3..b7de256b4 100644 --- a/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java +++ b/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java @@ -1,20 +1,26 @@ package io.split.engine.evaluator; +import io.split.client.dtos.ExcludedSegments; +import io.split.engine.experiments.ParsedCondition; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.rules.engine.EvaluationResult; import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheConsumer; -import static com.google.common.base.Preconditions.checkNotNull; +import java.util.List; +import java.util.Map; +import java.util.Objects; -public class EvaluationContext { +public class EvaluationContext implements io.split.rules.engine.EvaluationContext { private final Evaluator _evaluator; private final SegmentCacheConsumer _segmentCacheConsumer; private final RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; public EvaluationContext(Evaluator evaluator, SegmentCacheConsumer segmentCacheConsumer, RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { - _evaluator = checkNotNull(evaluator); - _segmentCacheConsumer = checkNotNull(segmentCacheConsumer); - _ruleBasedSegmentCacheConsumer = checkNotNull(ruleBasedSegmentCacheConsumer); + _evaluator = Objects.requireNonNull(evaluator); + _segmentCacheConsumer = Objects.requireNonNull(segmentCacheConsumer); + _ruleBasedSegmentCacheConsumer = Objects.requireNonNull(ruleBasedSegmentCacheConsumer); } public Evaluator getEvaluator() { @@ -28,4 +34,40 @@ public SegmentCacheConsumer getSegmentCache() { public RuleBasedSegmentCacheConsumer getRuleBasedSegmentCache() { return _ruleBasedSegmentCacheConsumer; } + + @Override + public EvaluationResult evaluate(String matchingKey, String bucketingKey, String ruleName, Map attributes) { + EvaluatorImp.TreatmentLabelAndChangeNumber r = _evaluator.evaluateFeature(matchingKey, bucketingKey, ruleName, attributes); + return new EvaluationResult(r.treatment, r.label, r.changeNumber, r.configurations, r.track); + } + + @Override + public boolean isInSegment(String segmentName, String key) { + return _segmentCacheConsumer.isInSegment(segmentName, key); + } + + @Override + public boolean isInRuleBasedSegment(String segmentName, String key, String bucketingKey, Map attributes) { + ParsedRuleBasedSegment parsedRuleBasedSegment = _ruleBasedSegmentCacheConsumer.get(segmentName); + if (parsedRuleBasedSegment == null) { + return false; + } + if (parsedRuleBasedSegment.excludedKeys().contains(key)) { + return false; + } + for (ExcludedSegments excludedSegment : parsedRuleBasedSegment.excludedSegments()) { + if (excludedSegment.isStandard() && _segmentCacheConsumer.isInSegment(excludedSegment.name, key)) { + return false; + } + if (excludedSegment.isRuleBased() && isInRuleBasedSegment(excludedSegment.name, key, bucketingKey, attributes)) { + return false; + } + } + for (ParsedCondition condition : parsedRuleBasedSegment.parsedConditions()) { + if (condition.matcher().match(key, bucketingKey, attributes, this)) { + return true; + } + } + return false; + } } diff --git a/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java b/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java index 8d7147aa6..603d4bddf 100644 --- a/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java +++ b/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java @@ -1,12 +1,13 @@ package io.split.engine.evaluator; -import io.split.client.dtos.ConditionType; import io.split.client.dtos.FallbackTreatment; import io.split.client.dtos.FallbackTreatmentCalculator; import io.split.client.exceptions.ChangeNumberExceptionWrapper; -import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedSplit; -import io.split.engine.splitter.Splitter; +import io.split.rules.engine.EvaluationResult; +import io.split.rules.engine.TargetingEngine; +import io.split.rules.engine.TargetingEngineImpl; +import io.split.rules.exceptions.VersionedExceptionWrapper; import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheConsumer; import io.split.storages.SplitCacheConsumer; @@ -19,7 +20,7 @@ import java.util.List; import java.util.Map; -import static com.google.common.base.Preconditions.checkNotNull; +import java.util.Objects; public class EvaluatorImp implements Evaluator { private static final Logger _log = LoggerFactory.getLogger(EvaluatorImp.class); @@ -28,15 +29,17 @@ public class EvaluatorImp implements Evaluator { private final EvaluationContext _evaluationContext; private final SplitCacheConsumer _splitCacheConsumer; private final FallbackTreatmentCalculator _fallbackTreatmentCalculator; + private final TargetingEngine _targetingEngine; private final String _evaluatorException = "Evaluator Exception"; public EvaluatorImp(SplitCacheConsumer splitCacheConsumer, SegmentCacheConsumer segmentCache, RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer, FallbackTreatmentCalculator fallbackTreatmentCalculator) { - _splitCacheConsumer = checkNotNull(splitCacheConsumer); - _segmentCacheConsumer = checkNotNull(segmentCache); + _splitCacheConsumer = Objects.requireNonNull(splitCacheConsumer); + _segmentCacheConsumer = Objects.requireNonNull(segmentCache); _evaluationContext = new EvaluationContext(this, _segmentCacheConsumer, ruleBasedSegmentCacheConsumer); _fallbackTreatmentCalculator = fallbackTreatmentCalculator; + _targetingEngine = new TargetingEngineImpl(); } @Override @@ -102,100 +105,23 @@ private List getFeatureFlagNamesByFlagSets(List flagSets) { /** * @param matchingKey MUST NOT be null - * @param bucketingKey + * @param bucketingKey may be null * @param parsedSplit MUST NOT be null - * @param attributes MUST NOT be null + * @param attributes may be null * @return * @throws ChangeNumberExceptionWrapper */ - private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bucketingKey, ParsedSplit parsedSplit, Map attributes) throws ChangeNumberExceptionWrapper { + private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bucketingKey, ParsedSplit parsedSplit, + Map attributes) throws ChangeNumberExceptionWrapper { try { - String config = getConfig(parsedSplit, parsedSplit.defaultTreatment()); - if (parsedSplit.killed()) { - return new TreatmentLabelAndChangeNumber( - parsedSplit.defaultTreatment(), - Labels.KILLED, - parsedSplit.changeNumber(), - config, - parsedSplit.impressionsDisabled()); - } - - String bk = getBucketingKey(bucketingKey, matchingKey); - - if (!parsedSplit.prerequisitesMatcher().match(matchingKey, bk, attributes, _evaluationContext)) { - return new TreatmentLabelAndChangeNumber( - parsedSplit.defaultTreatment(), - Labels.PREREQUISITES_NOT_MET, - parsedSplit.changeNumber(), - config, - parsedSplit.impressionsDisabled()); - } - - /* - * There are three parts to a single Feature flag: 1) Whitelists 2) Traffic Allocation - * 3) Rollout. The flag inRollout is there to understand when we move into the Rollout - * section. This is because we need to make sure that the Traffic Allocation - * computation happens after the whitelist but before the rollout. - */ - boolean inRollout = false; - - for (ParsedCondition parsedCondition : parsedSplit.parsedConditions()) { - - if (checkRollout(inRollout, parsedCondition)) { - - if (parsedSplit.trafficAllocation() < 100) { - // if the traffic allocation is 100%, no need to do anything special. - int bucket = Splitter.getBucket(bk, parsedSplit.trafficAllocationSeed(), parsedSplit.algo()); - - if (bucket > parsedSplit.trafficAllocation()) { - // out of split - config = getConfig(parsedSplit, parsedSplit.defaultTreatment()); - return new TreatmentLabelAndChangeNumber(parsedSplit.defaultTreatment(), Labels.NOT_IN_SPLIT, - parsedSplit.changeNumber(), config, parsedSplit.impressionsDisabled()); - } - - } - inRollout = true; - } - - if (parsedCondition.matcher().match(matchingKey, bucketingKey, attributes, _evaluationContext)) { - String treatment = Splitter.getTreatment(bk, parsedSplit.seed(), parsedCondition.partitions(), parsedSplit.algo()); - config = getConfig(parsedSplit, treatment); - return new TreatmentLabelAndChangeNumber( - treatment, - parsedCondition.label(), - parsedSplit.changeNumber(), - config, - parsedSplit.impressionsDisabled()); - } - } - - config = getConfig(parsedSplit, parsedSplit.defaultTreatment()); - - return new TreatmentLabelAndChangeNumber( - parsedSplit.defaultTreatment(), - Labels.DEFAULT_RULE, - parsedSplit.changeNumber(), - config, - parsedSplit.impressionsDisabled()); - } catch (Exception e) { - throw new ChangeNumberExceptionWrapper(e, parsedSplit.changeNumber()); + EvaluationResult r = _targetingEngine.evaluate(matchingKey, bucketingKey, + parsedSplit.targetingRule(), attributes, _evaluationContext); + return new TreatmentLabelAndChangeNumber(r.treatment, r.label, r.version, r.config, r.impressionsDisabled); + } catch (VersionedExceptionWrapper e) { + throw new ChangeNumberExceptionWrapper(e.wrappedException(), e.version()); } } - private boolean checkRollout(boolean inRollout, ParsedCondition parsedCondition) { - return (!inRollout && parsedCondition.conditionType() == ConditionType.ROLLOUT); - } - - private String getBucketingKey(String bucketingKey, String matchingKey) { - return (bucketingKey == null) ? matchingKey : bucketingKey; - } - - private String getConfig(ParsedSplit parsedSplit, String returnedTreatment) { - return parsedSplit.configurations() != null ? parsedSplit.configurations().get(returnedTreatment) : null; - } - private String getFallbackConfig(FallbackTreatment fallbackTreatment) { if (fallbackTreatment.getConfig() != null) { return fallbackTreatment.getConfig(); diff --git a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java index ad2e32a50..a99fcd7aa 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java @@ -2,7 +2,7 @@ import io.split.client.dtos.ConditionType; import io.split.client.dtos.Partition; -import io.split.engine.matchers.CombiningMatcher; +import io.split.rules.matchers.CombiningMatcher; import java.util.List; diff --git a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java index c00439700..f9f260e3d 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java @@ -1,10 +1,11 @@ package io.split.engine.experiments; -import com.google.common.collect.ImmutableList; import io.split.client.dtos.ExcludedSegments; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.UserDefinedSegmentMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.UserDefinedSegmentMatcher; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -12,7 +13,7 @@ public class ParsedRuleBasedSegment { private final String _ruleBasedSegment; - private final ImmutableList _parsedCondition; + private final List _parsedCondition; private final String _trafficTypeName; private final long _changeNumber; private final List _excludedKeys; @@ -45,7 +46,7 @@ public ParsedRuleBasedSegment( List excludedSegments ) { _ruleBasedSegment = ruleBasedSegment; - _parsedCondition = ImmutableList.copyOf(matcherAndSplits); + _parsedCondition = Collections.unmodifiableList(new ArrayList<>(matcherAndSplits)); _trafficTypeName = trafficTypeName; _changeNumber = changeNumber; _excludedKeys = excludedKeys; diff --git a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java index e202474f0..c6538b9dd 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java @@ -1,10 +1,12 @@ package io.split.engine.experiments; -import com.google.common.collect.ImmutableList; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.PrerequisitesMatcher; -import io.split.engine.matchers.RuleBasedSegmentMatcher; -import io.split.engine.matchers.UserDefinedSegmentMatcher; +import java.util.ArrayList; +import java.util.Collections; +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.Partition; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.PrerequisitesMatcher; +import io.split.rules.model.TargetingRule; import java.util.HashSet; import java.util.List; @@ -26,7 +28,7 @@ public class ParsedSplit { private final int _seed; private final boolean _killed; private final String _defaultTreatment; - private final ImmutableList _parsedCondition; + private final List _parsedCondition; private final String _trafficTypeName; private final long _changeNumber; private final int _trafficAllocation; @@ -36,6 +38,7 @@ public class ParsedSplit { private final HashSet _flagSets; private final boolean _impressionsDisabled; private PrerequisitesMatcher _prerequisitesMatcher; + private final TargetingRule _targetingRule; public static ParsedSplit createParsedSplitForTests( String feature, @@ -64,7 +67,9 @@ public static ParsedSplit createParsedSplitForTests( null, flagSets, impressionsDisabled, - prerequisitesMatcher + prerequisitesMatcher, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + changeNumber, 100, seed, algo, null, flagSets, impressionsDisabled, prerequisitesMatcher) ); } @@ -96,7 +101,9 @@ public static ParsedSplit createParsedSplitForTests( configurations, flagSets, impressionsDisabled, - prerequisitesMatcher + prerequisitesMatcher, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + changeNumber, 100, seed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher) ); } @@ -115,12 +122,37 @@ public ParsedSplit( HashSet flagSets, boolean impressionsDisabled, PrerequisitesMatcher prerequisitesMatcher + ) { + this(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, + trafficAllocation, trafficAllocationSeed, algo, configurations, flagSets, + impressionsDisabled, prerequisitesMatcher, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, + flagSets, impressionsDisabled, prerequisitesMatcher)); + } + + public ParsedSplit( + String feature, + int seed, + boolean killed, + String defaultTreatment, + List matcherAndSplits, + String trafficTypeName, + long changeNumber, + int trafficAllocation, + int trafficAllocationSeed, + int algo, + Map configurations, + HashSet flagSets, + boolean impressionsDisabled, + PrerequisitesMatcher prerequisitesMatcher, + TargetingRule targetingRule ) { _split = feature; _seed = seed; _killed = killed; _defaultTreatment = defaultTreatment; - _parsedCondition = ImmutableList.copyOf(matcherAndSplits); + _parsedCondition = Collections.unmodifiableList(new ArrayList<>(matcherAndSplits)); _trafficTypeName = trafficTypeName; _changeNumber = changeNumber; _algo = algo; @@ -133,6 +165,7 @@ public ParsedSplit( _flagSets = flagSets; _impressionsDisabled = impressionsDisabled; _prerequisitesMatcher = prerequisitesMatcher; + _targetingRule = targetingRule; } public String feature() { @@ -180,6 +213,7 @@ public boolean impressionsDisabled() { return _impressionsDisabled; } public PrerequisitesMatcher prerequisitesMatcher() { return _prerequisitesMatcher; } + public TargetingRule targetingRule() { return _targetingRule; } @Override public int hashCode() { @@ -253,34 +287,16 @@ public String toString() { public Set getSegmentsNames() { return parsedConditions().stream() .flatMap(parsedCondition -> parsedCondition.matcher().attributeMatchers().stream()) - .filter(ParsedSplit::isSegmentMatcher) - .map(ParsedSplit::asSegmentMatcherForEach) - .map(UserDefinedSegmentMatcher::getSegmentName) + .filter(AttributeMatcher::isUserDefinedSegmentMatcher) + .map(am -> am.asUserDefinedSegmentMatcher().getSegmentName()) .collect(Collectors.toSet()); } public Set getRuleBasedSegmentsNames() { return parsedConditions().stream() .flatMap(parsedCondition -> parsedCondition.matcher().attributeMatchers().stream()) - .filter(ParsedSplit::isRuleBasedSegmentMatcher) - .map(ParsedSplit::asRuleBasedSegmentMatcherForEach) - .map(RuleBasedSegmentMatcher::getSegmentName) + .filter(AttributeMatcher::isRuleBasedSegmentMatcher) + .map(am -> am.asRuleBasedSegmentMatcher().getSegmentName()) .collect(Collectors.toSet()); } - - private static boolean isSegmentMatcher(AttributeMatcher attributeMatcher) { - return ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate() instanceof UserDefinedSegmentMatcher; - } - - private static UserDefinedSegmentMatcher asSegmentMatcherForEach(AttributeMatcher attributeMatcher) { - return (UserDefinedSegmentMatcher) ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate(); - } - - private static boolean isRuleBasedSegmentMatcher(AttributeMatcher attributeMatcher) { - return ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate() instanceof RuleBasedSegmentMatcher; - } - - private static RuleBasedSegmentMatcher asRuleBasedSegmentMatcherForEach(AttributeMatcher attributeMatcher) { - return (RuleBasedSegmentMatcher) ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate(); - } } diff --git a/client/src/main/java/io/split/engine/experiments/ParserUtils.java b/client/src/main/java/io/split/engine/experiments/ParserUtils.java index 3b1355123..92a0ba788 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -1,41 +1,42 @@ package io.split.engine.experiments; -import com.google.common.collect.Lists; +import io.split.client.dtos.DataType; +import io.split.client.dtos.MatcherCombiner; import io.split.client.dtos.MatcherType; import io.split.client.dtos.Partition; import io.split.client.dtos.MatcherGroup; import io.split.client.dtos.ConditionType; import io.split.client.dtos.Matcher; import io.split.engine.evaluator.Labels; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.AllKeysMatcher; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.UserDefinedSegmentMatcher; -import io.split.engine.matchers.EqualToMatcher; -import io.split.engine.matchers.GreaterThanOrEqualToMatcher; -import io.split.engine.matchers.LessThanOrEqualToMatcher; -import io.split.engine.matchers.BetweenMatcher; -import io.split.engine.matchers.DependencyMatcher; -import io.split.engine.matchers.BooleanMatcher; -import io.split.engine.matchers.EqualToSemverMatcher; -import io.split.engine.matchers.GreaterThanOrEqualToSemverMatcher; -import io.split.engine.matchers.LessThanOrEqualToSemverMatcher; -import io.split.engine.matchers.InListSemverMatcher; -import io.split.engine.matchers.BetweenSemverMatcher; -import io.split.engine.matchers.RuleBasedSegmentMatcher; -import io.split.engine.matchers.collections.ContainsAllOfSetMatcher; -import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; -import io.split.engine.matchers.collections.EqualToSetMatcher; -import io.split.engine.matchers.collections.PartOfSetMatcher; -import io.split.engine.matchers.strings.WhitelistMatcher; -import io.split.engine.matchers.strings.StartsWithAnyOfMatcher; -import io.split.engine.matchers.strings.EndsWithAnyOfMatcher; -import io.split.engine.matchers.strings.ContainsAnyOfMatcher; -import io.split.engine.matchers.strings.RegularExpressionMatcher; - +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.UserDefinedSegmentMatcher; +import io.split.rules.matchers.EqualToMatcher; +import io.split.rules.matchers.GreaterThanOrEqualToMatcher; +import io.split.rules.matchers.LessThanOrEqualToMatcher; +import io.split.rules.matchers.BetweenMatcher; +import io.split.rules.matchers.DependencyMatcher; +import io.split.rules.matchers.BooleanMatcher; +import io.split.rules.matchers.EqualToSemverMatcher; +import io.split.rules.matchers.GreaterThanOrEqualToSemverMatcher; +import io.split.rules.matchers.LessThanOrEqualToSemverMatcher; +import io.split.rules.matchers.InListSemverMatcher; +import io.split.rules.matchers.BetweenSemverMatcher; +import io.split.rules.matchers.RuleBasedSegmentMatcher; +import io.split.rules.matchers.collections.ContainsAllOfSetMatcher; +import io.split.rules.matchers.collections.ContainsAnyOfSetMatcher; +import io.split.rules.matchers.collections.EqualToSetMatcher; +import io.split.rules.matchers.collections.PartOfSetMatcher; +import io.split.rules.matchers.WhitelistMatcher; +import io.split.rules.matchers.strings.StartsWithAnyOfMatcher; +import io.split.rules.matchers.strings.EndsWithAnyOfMatcher; +import io.split.rules.matchers.strings.ContainsAnyOfMatcher; +import io.split.rules.matchers.strings.RegularExpressionMatcher; + +import java.util.ArrayList; import java.util.List; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; public final class ParserUtils { @@ -59,7 +60,7 @@ public static boolean checkUnsupportedMatcherExist(List matchers) { } public static ParsedCondition getTemplateCondition() { - List templatePartitions = Lists.newArrayList(); + List templatePartitions = new ArrayList<>(); Partition partition = new Partition(); partition.treatment = "control"; partition.size = 100; @@ -73,20 +74,28 @@ public static ParsedCondition getTemplateCondition() { public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { List matchers = matcherGroup.matchers; - checkArgument(!matchers.isEmpty()); + if (matchers.isEmpty()) throw new IllegalArgumentException(); - List toCombine = Lists.newArrayList(); + List toCombine = new ArrayList<>(); for (Matcher matcher : matchers) { toCombine.add(toMatcher(matcher)); } - return new CombiningMatcher(matcherGroup.combiner, toCombine); + return new CombiningMatcher(toCombiner(matcherGroup.combiner), toCombine); } + private static io.split.rules.model.DataType toRulesDataType(io.split.client.dtos.DataType dt) { + return io.split.rules.model.DataType.valueOf(dt.name()); + } + + private static CombiningMatcher.Combiner toCombiner(MatcherCombiner combiner) { + return CombiningMatcher.Combiner.valueOf(combiner.name()); + } + public static AttributeMatcher toMatcher(Matcher matcher) { - io.split.engine.matchers.Matcher delegate = null; + io.split.rules.matchers.Matcher delegate = null; switch (matcher.matcherType) { case ALL_KEYS: delegate = new AllKeysMatcher(); @@ -102,19 +111,22 @@ public static AttributeMatcher toMatcher(Matcher matcher) { break; case EQUAL_TO: checkNotNull(matcher.unaryNumericMatcherData); - delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); break; case GREATER_THAN_OR_EQUAL_TO: checkNotNull(matcher.unaryNumericMatcherData); - delegate = new GreaterThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + delegate = new GreaterThanOrEqualToMatcher( + matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); break; case LESS_THAN_OR_EQUAL_TO: checkNotNull(matcher.unaryNumericMatcherData); - delegate = new LessThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + delegate = new LessThanOrEqualToMatcher( + matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); break; case BETWEEN: checkNotNull(matcher.betweenMatcherData); - delegate = new BetweenMatcher(matcher.betweenMatcherData.start, matcher.betweenMatcherData.end, matcher.betweenMatcherData.dataType); + delegate = new BetweenMatcher(matcher.betweenMatcherData.start, + matcher.betweenMatcherData.end, toRulesDataType(matcher.betweenMatcherData.dataType)); break; case EQUAL_TO_SET: checkNotNull(matcher.whitelistMatcherData); @@ -133,55 +145,43 @@ public static AttributeMatcher toMatcher(Matcher matcher) { delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case STARTS_WITH: - checkNotNull(matcher.whitelistMatcherData); delegate = new StartsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); break; case ENDS_WITH: - checkNotNull(matcher.whitelistMatcherData); delegate = new EndsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); break; case CONTAINS_STRING: - checkNotNull(matcher.whitelistMatcherData); delegate = new ContainsAnyOfMatcher(matcher.whitelistMatcherData.whitelist); break; case MATCHES_STRING: - checkNotNull(matcher.stringMatcherData); delegate = new RegularExpressionMatcher(matcher.stringMatcherData); break; case IN_SPLIT_TREATMENT: - checkNotNull(matcher.dependencyMatcherData, - "MatcherType is " + matcher.matcherType - + ". matcher.dependencyMatcherData() MUST NOT BE null"); + if (matcher.dependencyMatcherData == null) throw new NullPointerException( + "MatcherType is " + matcher.matcherType + ". matcher.dependencyMatcherData() MUST NOT BE null"); delegate = new DependencyMatcher(matcher.dependencyMatcherData.split, matcher.dependencyMatcherData.treatments); break; case EQUAL_TO_BOOLEAN: - checkNotNull(matcher.booleanMatcherData, - "MatcherType is " + matcher.matcherType - + ". matcher.booleanMatcherData() MUST NOT BE null"); + if (matcher.booleanMatcherData == null) throw new NullPointerException( + "MatcherType is " + matcher.matcherType + ". matcher.booleanMatcherData() MUST NOT BE null"); delegate = new BooleanMatcher(matcher.booleanMatcherData); break; case EQUAL_TO_SEMVER: - checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for EQUAL_TO_SEMVER matcher type"); delegate = new EqualToSemverMatcher(matcher.stringMatcherData); break; case GREATER_THAN_OR_EQUAL_TO_SEMVER: - checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for GREATER_THAN_OR_EQUAL_TO_SEMVER matcher type"); delegate = new GreaterThanOrEqualToSemverMatcher(matcher.stringMatcherData); break; case LESS_THAN_OR_EQUAL_TO_SEMVER: - checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for LESS_THAN_OR_EQUAL_SEMVER matcher type"); delegate = new LessThanOrEqualToSemverMatcher(matcher.stringMatcherData); break; case IN_LIST_SEMVER: - checkNotNull(matcher.whitelistMatcherData, "whitelistMatcherData is required for IN_LIST_SEMVER matcher type"); delegate = new InListSemverMatcher(matcher.whitelistMatcherData.whitelist); break; case BETWEEN_SEMVER: - checkNotNull(matcher.betweenStringMatcherData, "betweenStringMatcherData is required for BETWEEN_SEMVER matcher type"); delegate = new BetweenSemverMatcher(matcher.betweenStringMatcherData.start, matcher.betweenStringMatcherData.end); break; case IN_RULE_BASED_SEGMENT: - checkNotNull(matcher.userDefinedSegmentMatcherData); String ruleBasedSegmentName = matcher.userDefinedSegmentMatcherData.segmentName; delegate = new RuleBasedSegmentMatcher(ruleBasedSegmentName); break; @@ -189,8 +189,6 @@ public static AttributeMatcher toMatcher(Matcher matcher) { throw new IllegalArgumentException("Unknown matcher type: " + matcher.matcherType); } - checkNotNull(delegate, "We were not able to create a matcher for: " + matcher.matcherType); - String attribute = null; if (matcher.keySelector != null && matcher.keySelector.attribute != null) { attribute = matcher.keySelector.attribute; diff --git a/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java b/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java index b67c5e354..31e223098 100644 --- a/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java +++ b/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java @@ -1,12 +1,12 @@ package io.split.engine.experiments; -import com.google.common.collect.Lists; import io.split.client.dtos.Condition; import io.split.client.dtos.RuleBasedSegment; -import io.split.engine.matchers.CombiningMatcher; +import io.split.rules.matchers.CombiningMatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; import static io.split.engine.experiments.ParserUtils.checkUnsupportedMatcherExist; @@ -30,7 +30,7 @@ public ParsedRuleBasedSegment parse(RuleBasedSegment ruleBasedSegment) { } private ParsedRuleBasedSegment parseWithoutExceptionHandling(RuleBasedSegment ruleBasedSegment) { - List parsedConditionList = Lists.newArrayList(); + List parsedConditionList = new ArrayList<>(); for (Condition condition : ruleBasedSegment.conditions) { if (checkUnsupportedMatcherExist(condition.matcherGroup.matchers)) { _log.error("Unsupported matcher type found for rule based segment: " + ruleBasedSegment.name + diff --git a/client/src/main/java/io/split/engine/experiments/SplitParser.java b/client/src/main/java/io/split/engine/experiments/SplitParser.java index 5771c9ae4..6494c5287 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitParser.java +++ b/client/src/main/java/io/split/engine/experiments/SplitParser.java @@ -1,18 +1,22 @@ package io.split.engine.experiments; -import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import io.split.client.dtos.Condition; import io.split.client.dtos.Partition; import io.split.client.dtos.Split; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.PrerequisitesMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.PrerequisitesMatcher; +import io.split.rules.model.Prerequisite; +import io.split.rules.model.TargetingRule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; -import java.util.Objects; - import static io.split.engine.experiments.ParserUtils.checkUnsupportedMatcherExist; import static io.split.engine.experiments.ParserUtils.getTemplateCondition; import static io.split.engine.experiments.ParserUtils.toMatcher; @@ -39,7 +43,8 @@ public ParsedSplit parse(Split split) { } private ParsedSplit parseWithoutExceptionHandling(Split split) { - List parsedConditionList = Lists.newArrayList(); + List parsedConditionList = new ArrayList<>(); + List targetingConditionList = new ArrayList<>(); if (Objects.isNull(split.impressionsDisabled)) { _log.debug("impressionsDisabled field not detected for Feature flag `" + split.name + "`, setting it to `false`."); split.impressionsDisabled = false; @@ -49,13 +54,42 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) { if (checkUnsupportedMatcherExist(condition.matcherGroup.matchers)) { _log.error("Unsupported matcher type found for feature flag: " + split.name + " , will revert to default template matcher."); parsedConditionList.clear(); + targetingConditionList.clear(); parsedConditionList.add(getTemplateCondition()); + io.split.rules.model.Condition templateCondition = toTargetingCondition(getTemplateCondition()); + targetingConditionList.add(templateCondition); break; } CombiningMatcher matcher = toMatcher(condition.matcherGroup); parsedConditionList.add(new ParsedCondition(condition.conditionType, matcher, partitions, condition.label)); + targetingConditionList.add(new io.split.rules.model.Condition( + toTargetingConditionType(condition.conditionType), + matcher, + toTargetingPartitions(partitions), + condition.label)); } + List prerequisites = split.prerequisites == null ? Collections.emptyList() : + split.prerequisites.stream() + .map(p -> new Prerequisite(p.featureFlagName, p.treatments)) + .collect(Collectors.toList()); + + TargetingRule targetingRule = new TargetingRule( + split.name, + split.seed, + split.killed, + split.defaultTreatment, + targetingConditionList, + split.trafficTypeName, + split.changeNumber, + split.trafficAllocation, + split.trafficAllocationSeed, + split.algo, + split.configurations, + split.sets == null ? new HashSet<>() : split.sets, + split.impressionsDisabled, + prerequisites); + return new ParsedSplit( split.name, split.seed, @@ -70,6 +104,28 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) { split.configurations, split.sets, split.impressionsDisabled, - new PrerequisitesMatcher(split.prerequisites)); + new PrerequisitesMatcher(prerequisites), + targetingRule); + } + + private static io.split.rules.model.ConditionType toTargetingConditionType(io.split.client.dtos.ConditionType type) { + return type == io.split.client.dtos.ConditionType.ROLLOUT + ? io.split.rules.model.ConditionType.ROLLOUT + : io.split.rules.model.ConditionType.WHITELIST; + } + + private static List toTargetingPartitions(List partitions) { + if (partitions == null) return Collections.emptyList(); + return partitions.stream() + .map(p -> new io.split.rules.model.Partition(p.treatment, p.size)) + .collect(Collectors.toList()); + } + + private static io.split.rules.model.Condition toTargetingCondition(ParsedCondition parsedCondition) { + return new io.split.rules.model.Condition( + toTargetingConditionType(parsedCondition.conditionType()), + parsedCondition.matcher(), + toTargetingPartitions(parsedCondition.partitions()), + parsedCondition.label()); } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java b/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java new file mode 100644 index 000000000..daea047a4 --- /dev/null +++ b/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java @@ -0,0 +1,59 @@ +package io.split.engine.experiments; + +import io.split.rules.matchers.PrerequisitesMatcher; +import io.split.rules.model.Condition; +import io.split.rules.model.ConditionType; +import io.split.rules.model.Partition; +import io.split.rules.model.Prerequisite; +import io.split.rules.model.TargetingRule; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public final class TargetingRuleFactory { + + private TargetingRuleFactory() { + throw new IllegalStateException("Utility class"); + } + + public static TargetingRule buildTargetingRule( + String feature, int seed, boolean killed, String defaultTreatment, + List matcherAndSplits, String trafficTypeName, long changeNumber, + int trafficAllocation, int trafficAllocationSeed, int algo, + Map configurations, HashSet flagSets, + boolean impressionsDisabled, PrerequisitesMatcher prerequisitesMatcher) { + + List conditions = matcherAndSplits == null + ? Collections.emptyList() + : matcherAndSplits.stream() + .map(TargetingRuleFactory::toTargetingCondition) + .collect(Collectors.toList()); + + List prereqs = prerequisitesMatcher == null + ? Collections.emptyList() + : prerequisitesMatcher.getPrerequisites() == null + ? Collections.emptyList() + : Collections.unmodifiableList(prerequisitesMatcher.getPrerequisites()); + + return new TargetingRule(feature, seed, killed, defaultTreatment, conditions, trafficTypeName, + changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, + flagSets == null ? new HashSet<>() : flagSets, impressionsDisabled, prereqs); + } + + private static Condition toTargetingCondition(ParsedCondition c) { + List partitions = c.partitions() == null + ? Collections.emptyList() + : c.partitions().stream() + .map(p -> new Partition(p.treatment, p.size)) + .collect(Collectors.toList()); + + ConditionType condType = c.conditionType() == io.split.client.dtos.ConditionType.ROLLOUT + ? ConditionType.ROLLOUT + : ConditionType.WHITELIST; + + return new Condition(condType, c.matcher(), partitions, c.label()); + } +} diff --git a/client/src/main/java/io/split/engine/matchers/CombiningMatcher.java b/client/src/main/java/io/split/engine/matchers/CombiningMatcher.java deleted file mode 100644 index 4097ef851..000000000 --- a/client/src/main/java/io/split/engine/matchers/CombiningMatcher.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.split.engine.matchers; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import io.split.client.dtos.MatcherCombiner; -import io.split.engine.evaluator.EvaluationContext; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import static com.google.common.base.Preconditions.checkArgument; - -/** - * Combines the results of multiple matchers using the logical OR or AND. - * - * @author adil - */ -public class CombiningMatcher { - - private final ImmutableList _delegates; - private final MatcherCombiner _combiner; - - public static CombiningMatcher of(Matcher matcher) { - return new CombiningMatcher(MatcherCombiner.AND, - Lists.newArrayList(AttributeMatcher.vanilla(matcher))); - } - - public static CombiningMatcher of(String attribute, Matcher matcher) { - return new CombiningMatcher(MatcherCombiner.AND, - Lists.newArrayList(new AttributeMatcher(attribute, matcher, false))); - } - - public CombiningMatcher(MatcherCombiner combiner, List delegates) { - _delegates = ImmutableList.copyOf(delegates); - _combiner = combiner; - - checkArgument(_delegates.size() > 0); - } - - public boolean match(String key, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (_delegates.isEmpty()) { - return false; - } - - switch (_combiner) { - case AND: - return and(key, bucketingKey, attributes, evaluationContext); - default: - throw new IllegalArgumentException("Unknown combiner: " + _combiner); - } - - } - - private boolean and(String key, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - boolean result = true; - for (AttributeMatcher delegate : _delegates) { - result &= (delegate.match(key, bucketingKey, attributes, evaluationContext)); - } - return result; - } - - public ImmutableList attributeMatchers() { - return _delegates; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("if"); - boolean first = true; - for (AttributeMatcher matcher : _delegates) { - if (!first) { - bldr.append(" " + _combiner); - } - bldr.append(" "); - bldr.append(matcher); - first = false; - } - return bldr.toString(); - } - - @Override - public int hashCode() { - return Objects.hash(_combiner, _delegates); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof CombiningMatcher)) return false; - - CombiningMatcher other = (CombiningMatcher) obj; - - return _combiner.equals(other._combiner) && _delegates.equals(other._delegates); - } -} diff --git a/client/src/main/java/io/split/engine/matchers/Matcher.java b/client/src/main/java/io/split/engine/matchers/Matcher.java deleted file mode 100644 index ecdee1e78..000000000 --- a/client/src/main/java/io/split/engine/matchers/Matcher.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -public interface Matcher { - boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext); -} diff --git a/client/src/main/java/io/split/engine/matchers/RuleBasedSegmentMatcher.java b/client/src/main/java/io/split/engine/matchers/RuleBasedSegmentMatcher.java deleted file mode 100644 index 4c74527be..000000000 --- a/client/src/main/java/io/split/engine/matchers/RuleBasedSegmentMatcher.java +++ /dev/null @@ -1,105 +0,0 @@ -package io.split.engine.matchers; - -import io.split.client.dtos.ExcludedSegments; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.experiments.ParsedCondition; -import io.split.engine.experiments.ParsedRuleBasedSegment; - -import java.util.List; -import java.util.Map; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * A matcher that checks if the key is part of a user defined segment. This class - * assumes that the logic for refreshing what keys are part of a segment is delegated - * to SegmentFetcher. - * - * @author adil - */ -public class RuleBasedSegmentMatcher implements Matcher { - private final String _segmentName; - - public RuleBasedSegmentMatcher(String segmentName) { - _segmentName = checkNotNull(segmentName); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (!(matchValue instanceof String)) { - return false; - } - ParsedRuleBasedSegment parsedRuleBasedSegment = evaluationContext.getRuleBasedSegmentCache().get(_segmentName); - if (parsedRuleBasedSegment == null) { - return false; - } - - if (parsedRuleBasedSegment.excludedKeys().contains(matchValue)) { - return false; - } - - if (matchExcludedSegments(parsedRuleBasedSegment.excludedSegments(), matchValue, bucketingKey, attributes, evaluationContext)) { - return false; - } - - return matchConditions(parsedRuleBasedSegment.parsedConditions(), matchValue, bucketingKey, attributes, evaluationContext); - } - - private boolean matchExcludedSegments(List excludedSegments, Object matchValue, String bucketingKey, - Map attributes, EvaluationContext evaluationContext) { - for (ExcludedSegments excludedSegment: excludedSegments) { - if (excludedSegment.isStandard() && evaluationContext.getSegmentCache().isInSegment(excludedSegment.name, (String) matchValue)) { - return true; - } - - if (excludedSegment.isRuleBased()) { - RuleBasedSegmentMatcher excludedRbsMatcher = new RuleBasedSegmentMatcher(excludedSegment.name); - if (excludedRbsMatcher.match(matchValue, bucketingKey, attributes, evaluationContext)) { - return true; - } - } - } - - return false; - } - - private boolean matchConditions(List conditions, Object matchValue, String bucketingKey, - Map attributes, EvaluationContext evaluationContext) { - for (ParsedCondition parsedCondition : conditions) { - if (parsedCondition.matcher().match((String) matchValue, bucketingKey, attributes, evaluationContext)) { - return true; - } - } - return false; - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _segmentName.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof RuleBasedSegmentMatcher)) return false; - - RuleBasedSegmentMatcher other = (RuleBasedSegmentMatcher) obj; - - return _segmentName.equals(other._segmentName); - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("in segment "); - bldr.append(_segmentName); - return bldr.toString(); - } - - public String getSegmentName() { - return _segmentName; - } -} diff --git a/client/src/main/java/io/split/engine/matchers/UserDefinedSegmentMatcher.java b/client/src/main/java/io/split/engine/matchers/UserDefinedSegmentMatcher.java deleted file mode 100644 index 1ba1c5c2c..000000000 --- a/client/src/main/java/io/split/engine/matchers/UserDefinedSegmentMatcher.java +++ /dev/null @@ -1,62 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * A matcher that checks if the key is part of a user defined segment. This class - * assumes that the logic for refreshing what keys are part of a segment is delegated - * to SegmentFetcher. - * - * @author adil - */ -public class UserDefinedSegmentMatcher implements Matcher { - private final String _segmentName; - - public UserDefinedSegmentMatcher(String segmentName) { - _segmentName = checkNotNull(segmentName); - } - - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (!(matchValue instanceof String)) { - return false; - } - - return evaluationContext.getSegmentCache().isInSegment(_segmentName, (String) matchValue); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _segmentName.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof UserDefinedSegmentMatcher)) return false; - - UserDefinedSegmentMatcher other = (UserDefinedSegmentMatcher) obj; - - return _segmentName.equals(other._segmentName); - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("in segment "); - bldr.append(_segmentName); - return bldr.toString(); - } - - public String getSegmentName() { - return _segmentName; - } -} diff --git a/client/src/main/java/io/split/engine/splitter/Splitter.java b/client/src/main/java/io/split/engine/splitter/Splitter.java index c867a81db..eb990166f 100644 --- a/client/src/main/java/io/split/engine/splitter/Splitter.java +++ b/client/src/main/java/io/split/engine/splitter/Splitter.java @@ -1,7 +1,7 @@ package io.split.engine.splitter; import io.split.client.dtos.Partition; -import io.split.client.utils.MurmurHash3; +import io.split.rules.bucketing.MurmurHash3; import io.split.grammar.Treatments; import java.util.List; diff --git a/client/src/test/java/io/split/client/SplitClientImplTest.java b/client/src/test/java/io/split/client/SplitClientImplTest.java index 26a850574..6f2b0bd8a 100644 --- a/client/src/test/java/io/split/client/SplitClientImplTest.java +++ b/client/src/test/java/io/split/client/SplitClientImplTest.java @@ -5,18 +5,18 @@ import io.split.client.api.Key; import io.split.client.api.SplitResult; import io.split.client.dtos.*; -import io.split.client.events.EventsStorageProducer; +import io.split.rules.model.DataType;import io.split.client.events.EventsStorageProducer; import io.split.client.events.NoopEventsStorageImp; import io.split.client.impressions.Impression; import io.split.client.impressions.ImpressionsManager; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; -import io.split.engine.matchers.PrerequisitesMatcher; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.EqualToMatcher; -import io.split.engine.matchers.GreaterThanOrEqualToMatcher; -import io.split.engine.matchers.AllKeysMatcher; -import io.split.engine.matchers.DependencyMatcher; +import io.split.rules.matchers.PrerequisitesMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.EqualToMatcher; +import io.split.rules.matchers.GreaterThanOrEqualToMatcher; +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.DependencyMatcher; import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheConsumer; import io.split.storages.SplitCacheConsumer; @@ -24,8 +24,8 @@ import io.split.engine.SDKReadinessGates; import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedSplit; -import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; -import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.rules.matchers.collections.ContainsAnyOfSetMatcher; +import io.split.rules.matchers.WhitelistMatcher; import io.split.grammar.Treatments; import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.TelemetryStorage; diff --git a/client/src/test/java/io/split/client/SplitManagerImplTest.java b/client/src/test/java/io/split/client/SplitManagerImplTest.java index f3c04454f..a09b7ae1b 100644 --- a/client/src/test/java/io/split/client/SplitManagerImplTest.java +++ b/client/src/test/java/io/split/client/SplitManagerImplTest.java @@ -12,9 +12,9 @@ import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedSplit; import io.split.engine.experiments.SplitParser; -import io.split.engine.matchers.AllKeysMatcher; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.PrerequisitesMatcher; +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.PrerequisitesMatcher; import io.split.grammar.Treatments; import io.split.storages.SplitCacheConsumer; import io.split.telemetry.storage.InMemoryTelemetryStorage; @@ -71,8 +71,9 @@ public void splitCallWithExistentSplit() { Prerequisites prereq = new Prerequisites(); prereq.featureFlagName = "feature1"; prereq.treatments = Lists.newArrayList("on"); + io.split.rules.model.Prerequisite prerequisite = new io.split.rules.model.Prerequisite(prereq.featureFlagName, prereq.treatments); ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, new HashSet<>(), false, - new PrerequisitesMatcher(Lists.newArrayList(prereq))); + new PrerequisitesMatcher(Lists.newArrayList(prerequisite))); when(splitCacheConsumer.get(existent)).thenReturn(response); SplitManagerImpl splitManager = new SplitManagerImpl(splitCacheConsumer, diff --git a/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java b/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java index 5b0a024a6..791c65fd0 100644 --- a/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java +++ b/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java @@ -10,12 +10,12 @@ import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedRuleBasedSegment; import io.split.engine.experiments.ParsedSplit; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.PrerequisitesMatcher; -import io.split.engine.matchers.RuleBasedSegmentMatcher; -import io.split.engine.matchers.strings.EndsWithAnyOfMatcher; -import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.PrerequisitesMatcher; +import io.split.rules.matchers.RuleBasedSegmentMatcher; +import io.split.rules.matchers.strings.EndsWithAnyOfMatcher; +import io.split.rules.matchers.WhitelistMatcher; import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SegmentCache; import io.split.storages.SplitCache; @@ -192,9 +192,9 @@ private Evaluator buildEvaluatorAndLoadCache(boolean killed, int trafficAllocati AttributeMatcher endsWithMatcher = AttributeMatcher.vanilla(new EndsWithAnyOfMatcher(Lists.newArrayList("@test.io", "@mail.io"))); AttributeMatcher ruleBasedSegmentMatcher = AttributeMatcher.vanilla(new RuleBasedSegmentMatcher("sample_rule_based_segment")); - CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); - CombiningMatcher endsWithCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(endsWithMatcher)); - CombiningMatcher ruleBasedSegmentCombinerMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ruleBasedSegmentMatcher)); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(whiteListMatcher)); + CombiningMatcher endsWithCombiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(endsWithMatcher)); + CombiningMatcher ruleBasedSegmentCombinerMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(ruleBasedSegmentMatcher)); ParsedCondition whitelistCondition = new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, partitions, TEST_LABEL_VALUE_WHITELIST); ParsedCondition rollOutCondition = new ParsedCondition(ConditionType.ROLLOUT, endsWithCombiningMatcher, partitions, TEST_LABEL_VALUE_ROLL_OUT); diff --git a/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java b/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java index 33ebf6d65..fd0faf25a 100644 --- a/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java +++ b/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java @@ -2,10 +2,11 @@ import io.split.client.dtos.*; import io.split.client.utils.Json; +import io.split.rules.model.Prerequisite; import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedSplit; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.PrerequisitesMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.PrerequisitesMatcher; import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheConsumer; import io.split.storages.SplitCacheConsumer; @@ -196,7 +197,7 @@ public void evaluateWithPrerequisites() { _partitions.add(partition); ParsedCondition condition = new ParsedCondition(ConditionType.WHITELIST, _matcher, _partitions, "test whitelist label"); _conditions.add(condition); - List prerequisites = Arrays.asList(Json.fromJson("{\"n\": \"split1\", \"ts\": [\"" + TREATMENT_VALUE + "\"]}", Prerequisites.class)); + List prerequisites = Arrays.asList(new Prerequisite("split1", Arrays.asList(TREATMENT_VALUE))); ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(prerequisites)); ParsedSplit split1 = new ParsedSplit("split1", 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); diff --git a/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java b/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java index 253636814..32a0e1dde 100644 --- a/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java +++ b/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java @@ -8,9 +8,9 @@ import io.split.client.dtos.SplitChange; import io.split.client.utils.Json; import io.split.client.utils.RuleBasedSegmentsToUpdate; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.UserDefinedSegmentMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.UserDefinedSegmentMatcher; import org.junit.Assert; import org.junit.Test; @@ -29,7 +29,7 @@ public void works() { excludedSegments.add(new ExcludedSegments("standard","segment2")); AttributeMatcher segmentMatcher = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher("employees")); - CombiningMatcher segmentCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(segmentMatcher)); + CombiningMatcher segmentCombiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(segmentMatcher)); ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("another_rule_based_segment", Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, segmentCombiningMatcher, null, "label")), "user", 123, Lists.newArrayList("mauro@test.io", "gaston@test.io"), excludedSegments); diff --git a/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java b/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java index add3eb2a5..df09569d0 100644 --- a/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java +++ b/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java @@ -1,20 +1,28 @@ package io.split.engine.experiments; import com.google.common.collect.Lists; -import io.split.client.dtos.*; +import io.split.client.dtos.Condition; +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.SegmentChange; +import io.split.client.dtos.RuleBasedSegment; +import io.split.rules.model.TargetingRule; import io.split.client.dtos.Matcher; +import io.split.client.dtos.MatcherType; +import io.split.client.dtos.Partition; +import io.split.client.dtos.DataType; +import io.split.client.dtos.SplitChange; import io.split.client.utils.Json; import io.split.client.utils.RuleBasedSegmentsToUpdate; import io.split.engine.ConditionsTestUtil; import io.split.engine.evaluator.Labels; -import io.split.engine.matchers.*; -import io.split.engine.matchers.collections.ContainsAllOfSetMatcher; -import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; -import io.split.engine.matchers.collections.EqualToSetMatcher; -import io.split.engine.matchers.collections.PartOfSetMatcher; -import io.split.engine.matchers.strings.ContainsAnyOfMatcher; -import io.split.engine.matchers.strings.EndsWithAnyOfMatcher; -import io.split.engine.matchers.strings.StartsWithAnyOfMatcher; +import io.split.rules.matchers.*; +import io.split.rules.matchers.collections.ContainsAllOfSetMatcher; +import io.split.rules.matchers.collections.ContainsAnyOfSetMatcher; +import io.split.rules.matchers.collections.EqualToSetMatcher; +import io.split.rules.matchers.collections.PartOfSetMatcher; +import io.split.rules.matchers.strings.ContainsAnyOfMatcher; +import io.split.rules.matchers.strings.EndsWithAnyOfMatcher; +import io.split.rules.matchers.strings.StartsWithAnyOfMatcher; import io.split.engine.segments.SegmentChangeFetcher; import io.split.grammar.Treatments; import io.split.storages.SegmentCache; @@ -68,7 +76,7 @@ public void works() { AttributeMatcher employeesMatcherLogic = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher(EMPLOYEES)); AttributeMatcher notSalesPeopleMatcherLogic = new AttributeMatcher(null, new UserDefinedSegmentMatcher(SALES_PEOPLE), true); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(employeesMatcherLogic, notSalesPeopleMatcherLogic)); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(employeesMatcherLogic, notSalesPeopleMatcherLogic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -167,8 +175,8 @@ public void worksWithAttributes() { ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); AttributeMatcher employeesMatcherLogic = new AttributeMatcher("name", new UserDefinedSegmentMatcher(EMPLOYEES), false); - AttributeMatcher creationDateNotOlderThanAPointLogic = new AttributeMatcher("creation_date", new GreaterThanOrEqualToMatcher(1457386741L, DataType.DATETIME), true); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(employeesMatcherLogic, creationDateNotOlderThanAPointLogic)); + AttributeMatcher creationDateNotOlderThanAPointLogic = new AttributeMatcher("creation_date", new GreaterThanOrEqualToMatcher(1457386741L, io.split.rules.model.DataType.DATETIME), true); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(employeesMatcherLogic, creationDateNotOlderThanAPointLogic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -185,7 +193,7 @@ public void lessThanOrEqualTo() { SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.LESS_THAN_OR_EQUAL_TO, DataType.NUMBER, 10L, false); + Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.LESS_THAN_OR_EQUAL_TO, io.split.client.dtos.DataType.NUMBER, 10L, false); Condition c = ConditionsTestUtil.and(ageLessThan10, null); List conditions = Lists.newArrayList(c); @@ -194,8 +202,8 @@ public void lessThanOrEqualTo() { RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first-name", conditions, 1); ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); - AttributeMatcher ageLessThan10Logic = new AttributeMatcher("age", new LessThanOrEqualToMatcher(10, DataType.NUMBER), false); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageLessThan10Logic)); + AttributeMatcher ageLessThan10Logic = new AttributeMatcher("age", new LessThanOrEqualToMatcher(10, io.split.rules.model.DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(ageLessThan10Logic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -212,7 +220,7 @@ public void equalTo() { SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, DataType.NUMBER, 10L, true); + Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, io.split.client.dtos.DataType.NUMBER, 10L, true); Condition c = ConditionsTestUtil.and(ageLessThan10, null); List conditions = Lists.newArrayList(c); @@ -220,8 +228,8 @@ public void equalTo() { RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first-name", conditions, 1); ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); - AttributeMatcher equalToMatcher = new AttributeMatcher("age", new EqualToMatcher(10, DataType.NUMBER), true); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(equalToMatcher)); + AttributeMatcher equalToMatcher = new AttributeMatcher("age", new EqualToMatcher(10, io.split.rules.model.DataType.NUMBER), true); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(equalToMatcher)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -238,7 +246,7 @@ public void equalToNegativeNumber() { SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - Matcher equalToNegative10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, DataType.NUMBER, -10L, false); + Matcher equalToNegative10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, io.split.client.dtos.DataType.NUMBER, -10L, false); Condition c = ConditionsTestUtil.and(equalToNegative10, null); List conditions = Lists.newArrayList(c); @@ -246,8 +254,8 @@ public void equalToNegativeNumber() { RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first-name", conditions, 1); ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); - AttributeMatcher ageEqualTo10Logic = new AttributeMatcher("age", new EqualToMatcher(-10, DataType.NUMBER), false); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageEqualTo10Logic)); + AttributeMatcher ageEqualTo10Logic = new AttributeMatcher("age", new EqualToMatcher(-10, io.split.rules.model.DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(ageEqualTo10Logic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -278,8 +286,8 @@ public void between() { RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first-name", conditions, 1); ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); - AttributeMatcher ageBetween10And11Logic = new AttributeMatcher("age", new BetweenMatcher(10, 12, DataType.NUMBER), false); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageBetween10And11Logic)); + AttributeMatcher ageBetween10And11Logic = new AttributeMatcher("age", new BetweenMatcher(10, 12, io.split.rules.model.DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(ageBetween10And11Logic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -520,7 +528,7 @@ public void InListSemverMatcher() throws IOException { assertTrue(false); } - public void setMatcherTest(Condition c, io.split.engine.matchers.Matcher m) { + public void setMatcherTest(Condition c, io.split.rules.matchers.Matcher m) { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); @@ -534,7 +542,7 @@ public void setMatcherTest(Condition c, io.split.engine.matchers.Matcher m) { ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); AttributeMatcher attrMatcher = new AttributeMatcher("products", m, false); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(attrMatcher)); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(attrMatcher)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); diff --git a/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java b/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java index a6c2468ab..d388d7b6d 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java @@ -10,8 +10,8 @@ import io.split.client.dtos.*; import io.split.engine.ConditionsTestUtil; import io.split.engine.common.FetchOptions; -import io.split.engine.matchers.AllKeysMatcher; -import io.split.engine.matchers.CombiningMatcher; +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.CombiningMatcher; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTask; import io.split.engine.segments.SegmentSynchronizationTaskImp; diff --git a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java index d9e945bfa..068d60ccc 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java @@ -11,26 +11,26 @@ import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; import io.split.client.dtos.Status; -import io.split.engine.matchers.PrerequisitesMatcher; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.BetweenMatcher; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.EqualToMatcher; -import io.split.engine.matchers.GreaterThanOrEqualToMatcher; -import io.split.engine.matchers.LessThanOrEqualToMatcher; -import io.split.engine.matchers.UserDefinedSegmentMatcher; +import io.split.rules.matchers.PrerequisitesMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.BetweenMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.EqualToMatcher; +import io.split.rules.matchers.GreaterThanOrEqualToMatcher; +import io.split.rules.matchers.LessThanOrEqualToMatcher; +import io.split.rules.matchers.UserDefinedSegmentMatcher; import io.split.storages.SegmentCache; import io.split.storages.memory.SegmentCacheInMemoryImpl; import io.split.client.utils.Json; import io.split.engine.evaluator.Labels; import io.split.engine.ConditionsTestUtil; -import io.split.engine.matchers.collections.ContainsAllOfSetMatcher; -import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; -import io.split.engine.matchers.collections.EqualToSetMatcher; -import io.split.engine.matchers.collections.PartOfSetMatcher; -import io.split.engine.matchers.strings.ContainsAnyOfMatcher; -import io.split.engine.matchers.strings.EndsWithAnyOfMatcher; -import io.split.engine.matchers.strings.StartsWithAnyOfMatcher; +import io.split.rules.matchers.collections.ContainsAllOfSetMatcher; +import io.split.rules.matchers.collections.ContainsAnyOfSetMatcher; +import io.split.rules.matchers.collections.EqualToSetMatcher; +import io.split.rules.matchers.collections.PartOfSetMatcher; +import io.split.rules.matchers.strings.ContainsAnyOfMatcher; +import io.split.rules.matchers.strings.EndsWithAnyOfMatcher; +import io.split.rules.matchers.strings.StartsWithAnyOfMatcher; import io.split.engine.segments.SegmentChangeFetcher; import io.split.grammar.Treatments; import org.junit.Assert; @@ -91,7 +91,7 @@ public void works() { AttributeMatcher employeesMatcherLogic = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher(EMPLOYEES)); AttributeMatcher notSalesPeopleMatcherLogic = new AttributeMatcher(null, new UserDefinedSegmentMatcher(SALES_PEOPLE), true); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(employeesMatcherLogic, notSalesPeopleMatcherLogic)); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(employeesMatcherLogic, notSalesPeopleMatcherLogic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -134,7 +134,7 @@ public void worksWithConfig() { AttributeMatcher employeesMatcherLogic = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher(EMPLOYEES)); AttributeMatcher notSalesPeopleMatcherLogic = new AttributeMatcher(null, new UserDefinedSegmentMatcher(SALES_PEOPLE), true); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(employeesMatcherLogic, notSalesPeopleMatcherLogic)); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(employeesMatcherLogic, notSalesPeopleMatcherLogic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -256,8 +256,8 @@ public void worksWithAttributes() { ParsedSplit actual = parser.parse(split); AttributeMatcher employeesMatcherLogic = new AttributeMatcher("name", new UserDefinedSegmentMatcher(EMPLOYEES), false); - AttributeMatcher creationDateNotOlderThanAPointLogic = new AttributeMatcher("creation_date", new GreaterThanOrEqualToMatcher(1457386741L, DataType.DATETIME), true); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(employeesMatcherLogic, creationDateNotOlderThanAPointLogic)); + AttributeMatcher creationDateNotOlderThanAPointLogic = new AttributeMatcher("creation_date", new GreaterThanOrEqualToMatcher(1457386741L, io.split.rules.model.DataType.DATETIME), true); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(employeesMatcherLogic, creationDateNotOlderThanAPointLogic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -276,7 +276,7 @@ public void lessThanOrEqualTo() { SplitParser parser = new SplitParser(); - Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.LESS_THAN_OR_EQUAL_TO, DataType.NUMBER, 10L, false); + Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.LESS_THAN_OR_EQUAL_TO, io.split.client.dtos.DataType.NUMBER, 10L, false); List partitions = Lists.newArrayList(ConditionsTestUtil.partition("on", 100)); @@ -289,8 +289,8 @@ public void lessThanOrEqualTo() { ParsedSplit actual = parser.parse(split); - AttributeMatcher ageLessThan10Logic = new AttributeMatcher("age", new LessThanOrEqualToMatcher(10, DataType.NUMBER), false); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageLessThan10Logic)); + AttributeMatcher ageLessThan10Logic = new AttributeMatcher("age", new LessThanOrEqualToMatcher(10, io.split.rules.model.DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(ageLessThan10Logic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -309,7 +309,7 @@ public void equalTo() { SplitParser parser = new SplitParser(); - Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, DataType.NUMBER, 10L, true); + Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, io.split.client.dtos.DataType.NUMBER, 10L, true); List partitions = Lists.newArrayList(ConditionsTestUtil.partition("on", 100)); @@ -321,8 +321,8 @@ public void equalTo() { ParsedSplit actual = parser.parse(split); - AttributeMatcher equalToMatcher = new AttributeMatcher("age", new EqualToMatcher(10, DataType.NUMBER), true); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(equalToMatcher)); + AttributeMatcher equalToMatcher = new AttributeMatcher("age", new EqualToMatcher(10, io.split.rules.model.DataType.NUMBER), true); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(equalToMatcher)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -340,7 +340,7 @@ public void equalToNegativeNumber() { SplitParser parser = new SplitParser(); - Matcher equalToNegative10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, DataType.NUMBER, -10L, false); + Matcher equalToNegative10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, io.split.client.dtos.DataType.NUMBER, -10L, false); List partitions = Lists.newArrayList(ConditionsTestUtil.partition("on", 100)); @@ -352,8 +352,8 @@ public void equalToNegativeNumber() { ParsedSplit actual = parser.parse(split); - AttributeMatcher ageEqualTo10Logic = new AttributeMatcher("age", new EqualToMatcher(-10, DataType.NUMBER), false); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageEqualTo10Logic)); + AttributeMatcher ageEqualTo10Logic = new AttributeMatcher("age", new EqualToMatcher(-10, io.split.rules.model.DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(ageEqualTo10Logic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -388,8 +388,8 @@ public void between() { ParsedSplit actual = parser.parse(split); - AttributeMatcher ageBetween10And11Logic = new AttributeMatcher("age", new BetweenMatcher(10, 12, DataType.NUMBER), false); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageBetween10And11Logic)); + AttributeMatcher ageBetween10And11Logic = new AttributeMatcher("age", new BetweenMatcher(10, 12, io.split.rules.model.DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(ageBetween10And11Logic)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); @@ -684,7 +684,7 @@ public void ImpressionToggleParseTest() throws IOException { assertTrue(check3); } - public void setMatcherTest(Condition c, io.split.engine.matchers.Matcher m) { + public void setMatcherTest(Condition c, io.split.rules.matchers.Matcher m) { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); @@ -705,7 +705,7 @@ public void setMatcherTest(Condition c, io.split.engine.matchers.Matcher m) { ParsedSplit actual = parser.parse(split); AttributeMatcher attrMatcher = new AttributeMatcher("products", m, false); - CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(attrMatcher)); + CombiningMatcher combiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(attrMatcher)); ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); diff --git a/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java b/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java new file mode 100644 index 000000000..cc1da5220 --- /dev/null +++ b/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java @@ -0,0 +1,320 @@ +package io.split.engine.experiments; + +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.Partition; +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.model.Condition; +import io.split.rules.model.TargetingRule; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + +public class TargetingRuleFactoryTest { + + private Map _configurations; + private HashSet _flagSets; + + @Before + public void setUp() { + _configurations = new HashMap<>(); + _configurations.put("on", "{\"color\": \"blue\"}"); + _flagSets = new HashSet<>(Arrays.asList("set1", "set2")); + } + + @Test + public void testBuildTargetingRule_withNullConditions_returnsEmptyList() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + null, + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + _flagSets, + false, + null + ); + + assertNotNull(rule); + assertEquals("feature1", rule.name()); + assertTrue(rule.conditions().isEmpty()); + } + + @Test + public void testBuildTargetingRule_withValidConditions_mapsCorrectly() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + Partition dtoPartition = new Partition(); + dtoPartition.treatment = "on"; + dtoPartition.size = 100; + + ParsedCondition parsedCondition = new ParsedCondition( + ConditionType.ROLLOUT, + matcher, + Arrays.asList(dtoPartition), + "label1" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + Arrays.asList(parsedCondition), + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + _flagSets, + false, + null + ); + + assertNotNull(rule); + assertEquals("feature1", rule.name()); + assertEquals(1, rule.conditions().size()); + + Condition condition = rule.conditions().get(0); + assertEquals(io.split.rules.model.ConditionType.ROLLOUT, condition.conditionType()); + assertEquals("label1", condition.label()); + assertEquals(1, condition.partitions().size()); + + io.split.rules.model.Partition partition = condition.partitions().get(0); + assertEquals("on", partition.treatment); + assertEquals(100, partition.size); + } + + @Test + public void testBuildTargetingRule_withNullPrerequisites_returnsEmptyPrerequisiteList() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + new ArrayList<>(), + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + _flagSets, + false, + null + ); + + assertNotNull(rule); + assertTrue(rule.prerequisites().isEmpty()); + } + + @Test + public void testBuildTargetingRule_withNullFlagSets_createsEmptySet() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + new ArrayList<>(), + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + null, + false, + null + ); + + assertNotNull(rule); + assertNotNull(rule.flagSets()); + assertTrue(rule.flagSets().isEmpty()); + } + + @Test + public void testBuildTargetingRule_withFlagSets_preservesSet() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + new ArrayList<>(), + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + _flagSets, + false, + null + ); + + assertNotNull(rule); + assertEquals(_flagSets, rule.flagSets()); + } + + @Test + public void testBuildTargetingRule_withRolloutCondition_mapsConditionTypeCorrectly() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + Partition dtoPartition = new Partition(); + dtoPartition.treatment = "on"; + dtoPartition.size = 50; + + ParsedCondition rolloutCondition = new ParsedCondition( + ConditionType.ROLLOUT, + matcher, + Arrays.asList(dtoPartition), + "rollout_label" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", 12345, false, "control", + Arrays.asList(rolloutCondition), + "user_type", 999L, 100, 456, 1, + _configurations, _flagSets, false, null + ); + + Condition condition = rule.conditions().get(0); + assertEquals(io.split.rules.model.ConditionType.ROLLOUT, condition.conditionType()); + } + + @Test + public void testBuildTargetingRule_withWhitelistCondition_mapsConditionTypeCorrectly() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + Partition dtoPartition = new Partition(); + dtoPartition.treatment = "special"; + dtoPartition.size = 100; + + ParsedCondition whitelistCondition = new ParsedCondition( + ConditionType.WHITELIST, + matcher, + Arrays.asList(dtoPartition), + "whitelist_label" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", 12345, false, "control", + Arrays.asList(whitelistCondition), + "user_type", 999L, 100, 456, 1, + _configurations, _flagSets, false, null + ); + + Condition condition = rule.conditions().get(0); + assertEquals(io.split.rules.model.ConditionType.WHITELIST, condition.conditionType()); + } + + @Test + public void testBuildTargetingRule_withMultiplePartitions_mapsAllPartitions() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + Partition partition1 = new Partition(); + partition1.treatment = "on"; + partition1.size = 50; + + Partition partition2 = new Partition(); + partition2.treatment = "off"; + partition2.size = 50; + + ParsedCondition parsedCondition = new ParsedCondition( + ConditionType.ROLLOUT, + matcher, + Arrays.asList(partition1, partition2), + "multi_partition_label" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", 12345, false, "control", + Arrays.asList(parsedCondition), + "user_type", 999L, 100, 456, 1, + _configurations, _flagSets, false, null + ); + + Condition condition = rule.conditions().get(0); + assertEquals(2, condition.partitions().size()); + assertEquals("on", condition.partitions().get(0).treatment); + assertEquals(50, condition.partitions().get(0).size); + assertEquals("off", condition.partitions().get(1).treatment); + assertEquals(50, condition.partitions().get(1).size); + } + + @Test + public void testBuildTargetingRule_withNullPartitions_returnsEmptyPartitionList() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + ParsedCondition parsedCondition = new ParsedCondition( + ConditionType.ROLLOUT, + matcher, + null, + "null_partitions_label" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", 12345, false, "control", + Arrays.asList(parsedCondition), + "user_type", 999L, 100, 456, 1, + _configurations, _flagSets, false, null + ); + + Condition condition = rule.conditions().get(0); + assertTrue(condition.partitions().isEmpty()); + } + + @Test + public void testBuildTargetingRule_preservesAllNonMappedFields() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "my_feature", + 98765, + true, + "killed", + new ArrayList<>(), + "account", + 555L, + 75, + 321, + 2, + _configurations, + _flagSets, + true, + null + ); + + assertEquals("my_feature", rule.name()); + assertEquals(98765, rule.seed()); + assertTrue(rule.killed()); + assertEquals("killed", rule.defaultTreatment()); + assertEquals("account", rule.trafficTypeName()); + assertEquals(555L, rule.changeNumber()); + assertEquals(75, rule.trafficAllocation()); + assertEquals(321, rule.trafficAllocationSeed()); + assertEquals(2, rule.algo()); + assertEquals(_configurations, rule.configurations()); + assertTrue(rule.impressionsDisabled()); + } +} diff --git a/client/src/test/java/io/split/engine/matchers/AllKeysMatcherTest.java b/client/src/test/java/io/split/engine/matchers/AllKeysMatcherTest.java index edfa73614..9aae3587d 100644 --- a/client/src/test/java/io/split/engine/matchers/AllKeysMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/AllKeysMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import org.apache.commons.lang3.RandomStringUtils; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/AttributeMatcherTest.java b/client/src/test/java/io/split/engine/matchers/AttributeMatcherTest.java index 9f535790d..fe6612b6b 100644 --- a/client/src/test/java/io/split/engine/matchers/AttributeMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/AttributeMatcherTest.java @@ -1,10 +1,12 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import io.split.client.dtos.DataType; -import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.rules.model.DataType; +import io.split.rules.matchers.WhitelistMatcher; import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/BetweenMatcherTest.java b/client/src/test/java/io/split/engine/matchers/BetweenMatcherTest.java index 22bc3b449..496c30a1d 100644 --- a/client/src/test/java/io/split/engine/matchers/BetweenMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/BetweenMatcherTest.java @@ -1,6 +1,8 @@ package io.split.engine.matchers; -import io.split.client.dtos.DataType; +import io.split.rules.matchers.*; + +import io.split.rules.model.DataType; import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/BetweenSemverMatcherTest.java b/client/src/test/java/io/split/engine/matchers/BetweenSemverMatcherTest.java index 41a4f76d4..2afcb1a1b 100644 --- a/client/src/test/java/io/split/engine/matchers/BetweenSemverMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/BetweenSemverMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import org.junit.Test; import static org.junit.Assert.assertTrue; diff --git a/client/src/test/java/io/split/engine/matchers/BooleanMatcherTest.java b/client/src/test/java/io/split/engine/matchers/BooleanMatcherTest.java index 14889af68..1963eefc5 100644 --- a/client/src/test/java/io/split/engine/matchers/BooleanMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/BooleanMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import org.junit.Test; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/client/src/test/java/io/split/engine/matchers/CombiningMatcherTest.java b/client/src/test/java/io/split/engine/matchers/CombiningMatcherTest.java index 3946aed50..868ab1f72 100644 --- a/client/src/test/java/io/split/engine/matchers/CombiningMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/CombiningMatcherTest.java @@ -1,8 +1,10 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import com.google.common.collect.Lists; import io.split.client.dtos.MatcherCombiner; -import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.rules.matchers.WhitelistMatcher; import org.junit.Assert; import org.junit.Test; @@ -20,7 +22,7 @@ public void worksAnd() { AttributeMatcher matcher1 = AttributeMatcher.vanilla(new AllKeysMatcher()); AttributeMatcher matcher2 = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("a", "b"))); - CombiningMatcher combiner = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(matcher1, matcher2)); + CombiningMatcher combiner = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(matcher1, matcher2)); Assert.assertTrue(combiner.match("a", null, null, null)); Assert.assertTrue(combiner.match("b", null, Collections.emptyMap(), null)); diff --git a/client/src/test/java/io/split/engine/matchers/EqualToMatcherTest.java b/client/src/test/java/io/split/engine/matchers/EqualToMatcherTest.java index f8320e511..edb182a7e 100644 --- a/client/src/test/java/io/split/engine/matchers/EqualToMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/EqualToMatcherTest.java @@ -1,6 +1,8 @@ package io.split.engine.matchers; -import io.split.client.dtos.DataType; +import io.split.rules.matchers.*; + +import io.split.rules.model.DataType; import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/EqualToSemverMatcherTest.java b/client/src/test/java/io/split/engine/matchers/EqualToSemverMatcherTest.java index a5a41e2bb..9e718ca6d 100644 --- a/client/src/test/java/io/split/engine/matchers/EqualToSemverMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/EqualToSemverMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import org.junit.Test; import static org.junit.Assert.assertTrue; diff --git a/client/src/test/java/io/split/engine/matchers/GreaterThanOrEqualToMatcherTest.java b/client/src/test/java/io/split/engine/matchers/GreaterThanOrEqualToMatcherTest.java index bbe37fdd7..a6e61f308 100644 --- a/client/src/test/java/io/split/engine/matchers/GreaterThanOrEqualToMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/GreaterThanOrEqualToMatcherTest.java @@ -1,6 +1,8 @@ package io.split.engine.matchers; -import io.split.client.dtos.DataType; +import io.split.rules.matchers.*; + +import io.split.rules.model.DataType; import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcherTest.java b/client/src/test/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcherTest.java index 753034c70..4b77fda8d 100644 --- a/client/src/test/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import org.junit.Test; import static org.junit.Assert.assertTrue; diff --git a/client/src/test/java/io/split/engine/matchers/InListSemverMatcherTest.java b/client/src/test/java/io/split/engine/matchers/InListSemverMatcherTest.java index c01371251..69234b00d 100644 --- a/client/src/test/java/io/split/engine/matchers/InListSemverMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/InListSemverMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import com.google.common.collect.Lists; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/LessThanOrEqualToMatcherTest.java b/client/src/test/java/io/split/engine/matchers/LessThanOrEqualToMatcherTest.java index 47853ed4c..310c1bdce 100644 --- a/client/src/test/java/io/split/engine/matchers/LessThanOrEqualToMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/LessThanOrEqualToMatcherTest.java @@ -1,6 +1,8 @@ package io.split.engine.matchers; -import io.split.client.dtos.DataType; +import io.split.rules.matchers.*; + +import io.split.rules.model.DataType; import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcherTest.java b/client/src/test/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcherTest.java index 349a608ae..662af5869 100644 --- a/client/src/test/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import org.junit.Test; import static org.junit.Assert.assertTrue; diff --git a/client/src/test/java/io/split/engine/matchers/NegatableMatcherTest.java b/client/src/test/java/io/split/engine/matchers/NegatableMatcherTest.java index f80f38739..78ebcde04 100644 --- a/client/src/test/java/io/split/engine/matchers/NegatableMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/NegatableMatcherTest.java @@ -1,9 +1,11 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import com.google.common.collect.Lists; import io.split.engine.evaluator.EvaluationContext; import io.split.engine.evaluator.Evaluator; -import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.rules.matchers.WhitelistMatcher; import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SegmentCache; import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; diff --git a/client/src/test/java/io/split/engine/matchers/PrerequisitesMatcherTest.java b/client/src/test/java/io/split/engine/matchers/PrerequisitesMatcherTest.java index 4fe92d045..cc031ed77 100644 --- a/client/src/test/java/io/split/engine/matchers/PrerequisitesMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/PrerequisitesMatcherTest.java @@ -1,7 +1,8 @@ package io.split.engine.matchers; -import io.split.client.dtos.Prerequisites; -import io.split.client.utils.Json; +import io.split.rules.matchers.*; + +import io.split.rules.model.Prerequisite; import io.split.engine.evaluator.EvaluationContext; import io.split.engine.evaluator.Evaluator; import io.split.engine.evaluator.EvaluatorImp; @@ -23,7 +24,7 @@ public class PrerequisitesMatcherTest { public void works() { Evaluator evaluator = Mockito.mock(Evaluator.class); EvaluationContext evaluationContext = new EvaluationContext(evaluator, Mockito.mock(SegmentCache.class), Mockito.mock(RuleBasedSegmentCache.class)); - List prerequisites = Arrays.asList(Json.fromJson("{\"n\": \"split1\", \"ts\": [\"on\"]}", Prerequisites.class), Json.fromJson("{\"n\": \"split2\", \"ts\": [\"off\"]}", Prerequisites.class)); + List prerequisites = Arrays.asList(new Prerequisite("split1", Arrays.asList("on")), new Prerequisite("split2", Arrays.asList("off"))); PrerequisitesMatcher matcher = new PrerequisitesMatcher(prerequisites); Assert.assertEquals("prerequisites: split1 [on], split2 [off]", matcher.toString()); PrerequisitesMatcher matcher2 = new PrerequisitesMatcher(prerequisites); @@ -43,7 +44,7 @@ public void invalidParams() { Evaluator evaluator = Mockito.mock(Evaluator.class); EvaluationContext evaluationContext = new EvaluationContext(evaluator, Mockito.mock(SegmentCache.class), Mockito.mock(RuleBasedSegmentCache.class)); - List prerequisites = Arrays.asList(Json.fromJson("{\"n\": \"split1\", \"ts\": [\"on\"]}", Prerequisites.class), Json.fromJson("{\"n\": \"split2\", \"ts\": [\"off\"]}", Prerequisites.class)); + List prerequisites = Arrays.asList(new Prerequisite("split1", Arrays.asList("on")), new Prerequisite("split2", Arrays.asList("off"))); PrerequisitesMatcher matcher = new PrerequisitesMatcher(prerequisites); Mockito.when(evaluator.evaluateFeature("user", "user", "split1", null)).thenReturn(new EvaluatorImp.TreatmentLabelAndChangeNumber("on", "")); Assert.assertFalse(matcher.match(null, null, null, evaluationContext)); diff --git a/client/src/test/java/io/split/engine/matchers/RuleBasedSegmentMatcherTest.java b/client/src/test/java/io/split/engine/matchers/RuleBasedSegmentMatcherTest.java index 7d5d0c48b..9c8823beb 100644 --- a/client/src/test/java/io/split/engine/matchers/RuleBasedSegmentMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/RuleBasedSegmentMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import com.google.common.collect.Lists; import io.split.client.dtos.ConditionType; import io.split.client.dtos.MatcherCombiner; @@ -11,7 +13,7 @@ import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedRuleBasedSegment; import io.split.engine.experiments.RuleBasedSegmentParser; -import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.rules.matchers.WhitelistMatcher; import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SegmentCache; import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; @@ -39,10 +41,10 @@ public void works() { RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); EvaluationContext evaluationContext = new EvaluationContext(evaluator, segmentCache, ruleBasedSegmentCache); AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); - CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(whiteListMatcher)); AttributeMatcher ruleBasedSegmentMatcher = AttributeMatcher.vanilla(new RuleBasedSegmentMatcher("sample_rule_based_segment")); - CombiningMatcher ruleBasedSegmentCombinerMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ruleBasedSegmentMatcher)); + CombiningMatcher ruleBasedSegmentCombinerMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(ruleBasedSegmentMatcher)); ParsedCondition ruleBasedSegmentCondition = new ParsedCondition(ConditionType.ROLLOUT, ruleBasedSegmentCombinerMatcher, null, "test rbs rule"); ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("sample_rule_based_segment", Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, "whitelist label")),"user", diff --git a/client/src/test/java/io/split/engine/matchers/SemverTest.java b/client/src/test/java/io/split/engine/matchers/SemverTest.java index 40da82643..147533b7d 100644 --- a/client/src/test/java/io/split/engine/matchers/SemverTest.java +++ b/client/src/test/java/io/split/engine/matchers/SemverTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers; +import io.split.rules.matchers.*; + import org.junit.Test; import java.io.*; diff --git a/client/src/test/java/io/split/engine/matchers/TransformersTest.java b/client/src/test/java/io/split/engine/matchers/TransformersTest.java index fcca8ced1..19bacbb52 100644 --- a/client/src/test/java/io/split/engine/matchers/TransformersTest.java +++ b/client/src/test/java/io/split/engine/matchers/TransformersTest.java @@ -4,9 +4,9 @@ import java.util.Calendar; -import static io.split.engine.matchers.Transformers.asDate; -import static io.split.engine.matchers.Transformers.asDateHourMinute; -import static io.split.engine.matchers.Transformers.asLong; +import static io.split.rules.matchers.Transformers.asDate; +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; diff --git a/client/src/test/java/io/split/engine/matchers/UserDefinedSegmentMatcherTest.java b/client/src/test/java/io/split/engine/matchers/UserDefinedSegmentMatcherTest.java index b957f73d0..1596c7184 100644 --- a/client/src/test/java/io/split/engine/matchers/UserDefinedSegmentMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/UserDefinedSegmentMatcherTest.java @@ -1,5 +1,6 @@ package io.split.engine.matchers; +import io.split.rules.matchers.UserDefinedSegmentMatcher; import com.google.common.collect.Sets; import io.split.engine.evaluator.EvaluationContext; import io.split.engine.evaluator.Evaluator; diff --git a/client/src/test/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcherTest.java b/client/src/test/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcherTest.java index 1c84cf2e6..12a3e0f9a 100644 --- a/client/src/test/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcherTest.java @@ -1,5 +1,6 @@ package io.split.engine.matchers.collections; +import io.split.rules.matchers.collections.ContainsAllOfSetMatcher; import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcherTest.java b/client/src/test/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcherTest.java index 520959aee..93c513e99 100644 --- a/client/src/test/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers.collections; +import io.split.rules.matchers.collections.ContainsAnyOfSetMatcher; + import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/collections/EqualToSetMatcherTest.java b/client/src/test/java/io/split/engine/matchers/collections/EqualToSetMatcherTest.java index ceb3b4b11..46dd34806 100644 --- a/client/src/test/java/io/split/engine/matchers/collections/EqualToSetMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/collections/EqualToSetMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers.collections; +import io.split.rules.matchers.collections.*; + import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/collections/PartOfSetMatcherTest.java b/client/src/test/java/io/split/engine/matchers/collections/PartOfSetMatcherTest.java index 0a734e884..1d4fbc96e 100644 --- a/client/src/test/java/io/split/engine/matchers/collections/PartOfSetMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/collections/PartOfSetMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers.collections; +import io.split.rules.matchers.collections.*; + import org.junit.Assert; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/matchers/strings/ContainsAnyOfMatcherTest.java b/client/src/test/java/io/split/engine/matchers/strings/ContainsAnyOfMatcherTest.java index 41a4b53b0..b62ff0744 100644 --- a/client/src/test/java/io/split/engine/matchers/strings/ContainsAnyOfMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/strings/ContainsAnyOfMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers.strings; +import io.split.rules.matchers.strings.*; + import org.junit.Test; import java.util.ArrayList; diff --git a/client/src/test/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcherTest.java b/client/src/test/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcherTest.java index 6c0417ba1..532c77cc5 100644 --- a/client/src/test/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers.strings; +import io.split.rules.matchers.strings.*; + import org.junit.Test; import java.util.ArrayList; diff --git a/client/src/test/java/io/split/engine/matchers/strings/RegularExpressionMatcherTest.java b/client/src/test/java/io/split/engine/matchers/strings/RegularExpressionMatcherTest.java index 16c899282..6d6a617a2 100644 --- a/client/src/test/java/io/split/engine/matchers/strings/RegularExpressionMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/strings/RegularExpressionMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers.strings; +import io.split.rules.matchers.strings.*; + import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; diff --git a/client/src/test/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcherTest.java b/client/src/test/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcherTest.java index 7e7e183eb..1b69ea8d0 100644 --- a/client/src/test/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcherTest.java @@ -1,5 +1,7 @@ package io.split.engine.matchers.strings; +import io.split.rules.matchers.strings.*; + import org.junit.Test; import java.util.ArrayList; diff --git a/client/src/test/java/io/split/engine/matchers/strings/WhitelistMatcherTest.java b/client/src/test/java/io/split/engine/matchers/strings/WhitelistMatcherTest.java index d284674e3..349a1f509 100644 --- a/client/src/test/java/io/split/engine/matchers/strings/WhitelistMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/strings/WhitelistMatcherTest.java @@ -1,5 +1,6 @@ package io.split.engine.matchers.strings; +import io.split.rules.matchers.WhitelistMatcher; import com.google.common.collect.Lists; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/splitter/HashConsistencyTest.java b/client/src/test/java/io/split/engine/splitter/HashConsistencyTest.java index cb2200294..8541db3da 100644 --- a/client/src/test/java/io/split/engine/splitter/HashConsistencyTest.java +++ b/client/src/test/java/io/split/engine/splitter/HashConsistencyTest.java @@ -2,7 +2,7 @@ import com.google.common.base.Charsets; import com.google.common.hash.Hashing; -import io.split.client.utils.MurmurHash3; +import io.split.rules.bucketing.MurmurHash3; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; diff --git a/client/src/test/java/io/split/engine/splitter/MyHash.java b/client/src/test/java/io/split/engine/splitter/MyHash.java index 0d24405da..ce1ecc1af 100644 --- a/client/src/test/java/io/split/engine/splitter/MyHash.java +++ b/client/src/test/java/io/split/engine/splitter/MyHash.java @@ -1,7 +1,7 @@ package io.split.engine.splitter; import com.google.common.hash.Hashing; -import io.split.client.utils.MurmurHash3; +import io.split.rules.bucketing.MurmurHash3; import java.nio.charset.Charset; diff --git a/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java b/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java index 1f7c9a8c7..959c4b2e9 100644 --- a/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java +++ b/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java @@ -13,8 +13,8 @@ import io.split.engine.experiments.ParsedRuleBasedSegment; import io.split.engine.experiments.RuleBasedSegmentParser; import io.split.engine.experiments.SplitParser; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.CombiningMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.CombiningMatcher; import io.split.engine.sse.dtos.CommonChangeNotification; import io.split.engine.sse.dtos.RawMessageNotification; import io.split.engine.sse.dtos.GenericNotificationData; @@ -103,9 +103,9 @@ public void testRefreshSplitsArchiveFF() { @Test public void testUpdateRuleBasedSegmentsWithCorrectFF() { - io.split.engine.matchers.Matcher matcher = (matchValue, bucketingKey, attributes, evaluationContext) -> false; + io.split.rules.matchers.Matcher matcher = (matchValue, bucketingKey, attributes, evaluationContext) -> false; ParsedCondition parsedCondition = new ParsedCondition(ConditionType.ROLLOUT, - new CombiningMatcher(MatcherCombiner.AND, Arrays.asList(new AttributeMatcher("email", matcher, false))), + new CombiningMatcher(CombiningMatcher.Combiner.AND, Arrays.asList(new AttributeMatcher("email", matcher, false))), null, "my label"); ParsedRuleBasedSegment parsedRBS = new ParsedRuleBasedSegment("sample_rule_based_segment", diff --git a/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java b/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java index 5589d71da..8c7859856 100644 --- a/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java +++ b/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java @@ -7,8 +7,8 @@ import io.split.engine.ConditionsTestUtil; import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedSplit; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.UserDefinedSegmentMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.UserDefinedSegmentMatcher; import io.split.grammar.Treatments; import org.junit.Assert; import org.junit.Before; diff --git a/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java b/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java index 492cc8aeb..d32d1172c 100644 --- a/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java +++ b/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java @@ -7,10 +7,10 @@ import io.split.engine.experiments.ParsedCondition; import io.split.client.dtos.ConditionType; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.UserDefinedSegmentMatcher; -import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.matchers.UserDefinedSegmentMatcher; +import io.split.rules.matchers.WhitelistMatcher; import junit.framework.TestCase; import org.junit.Test; import com.google.common.collect.Lists; @@ -26,7 +26,7 @@ public class RuleBasedSegmentCacheInMemoryImplTest extends TestCase { public void testAddAndDeleteSegment(){ RuleBasedSegmentCacheInMemoryImp ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); - CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(whiteListMatcher)); ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("sample_rule_based_segment", Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, "label")),"user", 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), Lists.newArrayList()); @@ -49,7 +49,7 @@ public void testMultipleSegment(){ RuleBasedSegmentCacheInMemoryImp ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); - CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(whiteListMatcher)); ParsedRuleBasedSegment parsedRuleBasedSegment1 = new ParsedRuleBasedSegment("sample_rule_based_segment", Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, "label")),"user", 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), excludedSegments); @@ -58,7 +58,7 @@ public void testMultipleSegment(){ excludedSegments.add(new ExcludedSegments("standard","segment1")); excludedSegments.add(new ExcludedSegments("standard","segment2")); AttributeMatcher segmentMatcher = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher("employees")); - CombiningMatcher segmentCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(segmentMatcher)); + CombiningMatcher segmentCombiningMatcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, Lists.newArrayList(segmentMatcher)); ParsedRuleBasedSegment parsedRuleBasedSegment2 = new ParsedRuleBasedSegment("another_rule_based_segment", Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, segmentCombiningMatcher, null, "label")),"user", 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), excludedSegments); diff --git a/pom.xml b/pom.xml index b388caf0e..8df4d7ef9 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,7 @@ 1.8 + targeting-engine pluggable-storage redis-wrapper testing diff --git a/targeting-engine/pom.xml b/targeting-engine/pom.xml new file mode 100644 index 000000000..d362412df --- /dev/null +++ b/targeting-engine/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + io.split.client + java-client-parent + 4.18.3 + + + 4.18.3 + targeting-engine + jar + Targeting Engine + A generic, zero-dependency targeting rules engine extracted from the Split Java SDK + + + + junit + junit + 4.13.1 + test + + + org.mockito + mockito-core + 5.14.2 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/targeting-engine/src/main/java/io/split/rules/bucketing/Bucketer.java b/targeting-engine/src/main/java/io/split/rules/bucketing/Bucketer.java new file mode 100644 index 000000000..23594924f --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/bucketing/Bucketer.java @@ -0,0 +1,78 @@ +package io.split.rules.bucketing; + +import io.split.rules.model.Partition; + +import java.util.List; + +/** + * Hashes keys into buckets and selects treatments from partition lists. + */ +public final class Bucketer { + private static final int ALGO_LEGACY = 1; + private static final int ALGO_MURMUR = 2; + private static final String CONTROL = "control"; + + /** + * Returns the treatment for the given key, seed, partitions, and algorithm. + * Returns "control" if no partition matches. + */ + public static String getTreatment(String key, int seed, List partitions, int algo) { + if (partitions.isEmpty()) { + return CONTROL; + } + if (hundredPercentOneTreatment(partitions)) { + return partitions.get(0).treatment; + } + return selectTreatment(bucket(hash(key, seed, algo)), partitions); + } + + /** + * Returns a bucket between 1 and 100, inclusive. + */ + public static int getBucket(String key, int seed, int algo) { + return bucket(hash(key, seed, algo)); + } + + static long hash(String key, int seed, int algo) { + switch (algo) { + case ALGO_MURMUR: + return murmurHash(key, seed); + case ALGO_LEGACY: + default: + return legacyHash(key, seed); + } + } + + static long murmurHash(String key, int seed) { + return MurmurHash3.murmurhash3_x86_32(key, 0, key.length(), seed); + } + + static int legacyHash(String key, int seed) { + int h = 0; + for (int i = 0; i < key.length(); i++) { + h = 31 * h + key.charAt(i); + } + return h ^ seed; + } + + static int bucket(long hash) { + return (int) (Math.abs(hash % 100) + 1); + } + + private static String selectTreatment(int bucket, List partitions) { + int covered = 0; + for (Partition partition : partitions) { + covered += partition.size; + if (covered >= bucket) { + return partition.treatment; + } + } + return CONTROL; + } + + private static boolean hundredPercentOneTreatment(List partitions) { + return partitions.size() == 1 && partitions.get(0).size == 100; + } + + private Bucketer() {} +} diff --git a/client/src/main/java/io/split/client/utils/MurmurHash3.java b/targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java similarity index 99% rename from client/src/main/java/io/split/client/utils/MurmurHash3.java rename to targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java index 94943515f..da0376d8e 100644 --- a/client/src/main/java/io/split/client/utils/MurmurHash3.java +++ b/targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java @@ -1,4 +1,4 @@ -package io.split.client.utils; +package io.split.rules.bucketing; /** * The MurmurHash3 algorithm was created by Austin Appleby and placed in the public domain. diff --git a/targeting-engine/src/main/java/io/split/rules/engine/EvaluationContext.java b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationContext.java new file mode 100644 index 000000000..4cfae4f47 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationContext.java @@ -0,0 +1,25 @@ +package io.split.rules.engine; + +import java.util.Map; + +/** + * Provides recursive evaluation and segment membership checks to matchers. + * Each SDK implements this interface to bridge to its own storage and evaluator. + */ +public interface EvaluationContext { + + /** + * Evaluates a targeting rule by name. Used by DependencyMatcher and PrerequisitesMatcher. + */ + EvaluationResult evaluate(String matchingKey, String bucketingKey, String ruleName, Map attributes); + + /** + * Checks if the given key is a member of a standard segment. + */ + boolean isInSegment(String segmentName, String key); + + /** + * Checks if the given key is a member of a rule-based segment. + */ + boolean isInRuleBasedSegment(String segmentName, String key, String bucketingKey, Map attributes); +} diff --git a/targeting-engine/src/main/java/io/split/rules/engine/EvaluationLabels.java b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationLabels.java new file mode 100644 index 000000000..5d89abc9d --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationLabels.java @@ -0,0 +1,14 @@ +package io.split.rules.engine; + +public final class EvaluationLabels { + public static final String NOT_IN_SPLIT = "not in split"; + public static final String DEFAULT_RULE = "default rule"; + public static final String KILLED = "killed"; + public static final String DEFINITION_NOT_FOUND = "definition not found"; + public static final String EXCEPTION = "exception"; + public static final String UNSUPPORTED_MATCHER = "targeting rule type unsupported by sdk"; + public static final String PREREQUISITES_NOT_MET = "prerequisites not met"; + public static final String NOT_READY = "not ready"; + + private EvaluationLabels() {} +} diff --git a/targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java new file mode 100644 index 000000000..0d9481f6c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java @@ -0,0 +1,25 @@ +package io.split.rules.engine; + +public final class EvaluationResult { + public final String treatment; + public final String label; + public final Long version; + public final String config; + public final boolean impressionsDisabled; + + public EvaluationResult(String treatment, String label) { + this(treatment, label, null, null, false); + } + + public EvaluationResult(String treatment, String label, Long version) { + this(treatment, label, version, null, false); + } + + public EvaluationResult(String treatment, String label, Long version, String config, boolean impressionsDisabled) { + this.treatment = treatment; + this.label = label; + this.version = version; + this.config = config; + this.impressionsDisabled = impressionsDisabled; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngine.java b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngine.java new file mode 100644 index 000000000..6664650ba --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngine.java @@ -0,0 +1,16 @@ +package io.split.rules.engine; + +import io.split.rules.exceptions.VersionedExceptionWrapper; +import io.split.rules.model.TargetingRule; + +import java.util.Map; + +/** + * Evaluates a targeting rule against a key and attributes. + * This is the core contract of the targeting engine. + */ +public interface TargetingEngine { + EvaluationResult evaluate(String matchingKey, String bucketingKey, + TargetingRule rule, Map attributes, + EvaluationContext context) throws VersionedExceptionWrapper; +} diff --git a/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java new file mode 100644 index 000000000..590d31789 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java @@ -0,0 +1,74 @@ +package io.split.rules.engine; + +import io.split.rules.bucketing.Bucketer; +import io.split.rules.exceptions.VersionedExceptionWrapper; +import io.split.rules.model.Condition; +import io.split.rules.model.ConditionType; +import io.split.rules.model.TargetingRule; + +import java.util.Map; + +public final class TargetingEngineImpl implements TargetingEngine { + + @Override + public EvaluationResult evaluate(String matchingKey, String bucketingKey, + TargetingRule rule, Map attributes, + EvaluationContext context) throws VersionedExceptionWrapper { + try { + String config = getConfig(rule, rule.defaultTreatment()); + + // 1. Killed rule → return default treatment + if (rule.killed()) { + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.KILLED, + rule.changeNumber(), config, rule.impressionsDisabled()); + } + + // 2. Bucketing key resolution + String bk = bucketingKey != null ? bucketingKey : matchingKey; + + // 3. Prerequisites check + if (!rule.prerequisitesMatcher().match(matchingKey, bk, attributes, context)) { + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.PREREQUISITES_NOT_MET, + rule.changeNumber(), config, rule.impressionsDisabled()); + } + + // 4. Iterate conditions + boolean inRollout = false; + for (Condition condition : rule.conditions()) { + + // 4a. Traffic allocation check (once, before first ROLLOUT condition) + if (!inRollout && condition.conditionType() == ConditionType.ROLLOUT) { + if (rule.trafficAllocation() < 100) { + int bucket = Bucketer.getBucket(bk, rule.trafficAllocationSeed(), rule.algo()); + if (bucket > rule.trafficAllocation()) { + config = getConfig(rule, rule.defaultTreatment()); + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.NOT_IN_SPLIT, + rule.changeNumber(), config, rule.impressionsDisabled()); + } + } + inRollout = true; + } + + // 4b. Condition match → select treatment + if (condition.matcher().match(matchingKey, bucketingKey, attributes, context)) { + String treatment = Bucketer.getTreatment(bk, rule.seed(), condition.partitions(), rule.algo()); + config = getConfig(rule, treatment); + return new EvaluationResult(treatment, condition.label(), + rule.changeNumber(), config, rule.impressionsDisabled()); + } + } + + // 5. No condition matched → default rule + config = getConfig(rule, rule.defaultTreatment()); + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.DEFAULT_RULE, + rule.changeNumber(), config, rule.impressionsDisabled()); + + } catch (Exception e) { + throw new VersionedExceptionWrapper(e, rule.changeNumber()); + } + } + + private String getConfig(TargetingRule rule, String treatment) { + return rule.configurations() != null ? rule.configurations().get(treatment) : null; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java b/targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java new file mode 100644 index 000000000..0501f7f78 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java @@ -0,0 +1,19 @@ +package io.split.rules.exceptions; + +public class VersionedExceptionWrapper extends Exception { + private final Exception _wrappedException; + private final Long _version; + + public VersionedExceptionWrapper(Exception wrappedException, Long version) { + _wrappedException = wrappedException; + _version = version; + } + + public Exception wrappedException() { + return _wrappedException; + } + + public Long version() { + return _version; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/logging/Logger.java b/targeting-engine/src/main/java/io/split/rules/logging/Logger.java new file mode 100644 index 000000000..89b822a41 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/logging/Logger.java @@ -0,0 +1,8 @@ +package io.split.rules.logging; + +public interface Logger { + void debug(String message); + void warn(String message); + void error(String message); + void error(String message, Throwable t); +} diff --git a/client/src/main/java/io/split/engine/matchers/AllKeysMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java similarity index 90% rename from client/src/main/java/io/split/engine/matchers/AllKeysMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java index 790224ab1..dd9d9df57 100644 --- a/client/src/main/java/io/split/engine/matchers/AllKeysMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java @@ -1,6 +1,6 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.Map; diff --git a/client/src/main/java/io/split/engine/matchers/AttributeMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java similarity index 52% rename from client/src/main/java/io/split/engine/matchers/AttributeMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java index 92deb0140..6ea30f10f 100644 --- a/client/src/main/java/io/split/engine/matchers/AttributeMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java @@ -1,86 +1,70 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.Map; import java.util.Objects; -/** - * Created by adilaijaz on 3/4/16. - */ - public final class AttributeMatcher { - private final String _attribute; private final Matcher _matcher; - public static AttributeMatcher vanilla(Matcher matcher) { return new AttributeMatcher(null, matcher, false); } public AttributeMatcher(String attribute, Matcher matcher, boolean negate) { _attribute = attribute; - if (matcher == null) { - throw new IllegalArgumentException("Null matcher"); - } + if (matcher == null) throw new IllegalArgumentException("Null matcher"); _matcher = new NegatableMatcher(matcher, negate); } - public boolean match(String key, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + public boolean match(String key, String bucketingKey, Map attributes, EvaluationContext context) { if (_attribute == null) { - return _matcher.match(key, bucketingKey, attributes, evaluationContext); + return _matcher.match(key, bucketingKey, attributes, context); } - - if (attributes == null) { - return false; - } - + if (attributes == null) return false; Object value = attributes.get(_attribute); - if (value == null) { - return false; - } + if (value == null) return false; + return _matcher.match(value, bucketingKey, null, null); + } + public String attribute() { return _attribute; } + public Matcher matcher() { return _matcher; } - return _matcher.match(value, bucketingKey, null, null); + public boolean isUserDefinedSegmentMatcher() { + return ((NegatableMatcher) _matcher).delegate() instanceof UserDefinedSegmentMatcher; } - @Override - public int hashCode() { - return Objects.hash(_attribute, _matcher); + public UserDefinedSegmentMatcher asUserDefinedSegmentMatcher() { + return (UserDefinedSegmentMatcher) ((NegatableMatcher) _matcher).delegate(); } - public String attribute() { - return _attribute; + public boolean isRuleBasedSegmentMatcher() { + return ((NegatableMatcher) _matcher).delegate() instanceof RuleBasedSegmentMatcher; } - public Matcher matcher() { - return _matcher; + public RuleBasedSegmentMatcher asRuleBasedSegmentMatcher() { + return (RuleBasedSegmentMatcher) ((NegatableMatcher) _matcher).delegate(); } + @Override + public int hashCode() { return Objects.hash(_attribute, _matcher); } + @Override public boolean equals(Object obj) { if (obj == null) return false; if (this == obj) return true; if (!(obj instanceof AttributeMatcher)) return false; - AttributeMatcher other = (AttributeMatcher) obj; - - return Objects.equals(_attribute, other._attribute) - && _matcher.equals(other._matcher); + return Objects.equals(_attribute, other._attribute) && _matcher.equals(other._matcher); } @Override public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("key"); - if (_attribute != null) { - bldr.append("."); - bldr.append(_attribute); - } - - bldr.append(" is"); - bldr.append(_matcher); + StringBuilder bldr = new StringBuilder("key"); + if (_attribute != null) bldr.append(".").append(_attribute); + bldr.append(" is").append(_matcher); return bldr.toString(); } @@ -93,44 +77,29 @@ public NegatableMatcher(Matcher matcher, boolean negate) { _delegate = matcher; } - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - boolean result = _delegate.match(matchValue, bucketingKey, attributes, evaluationContext); - return (_negate) ? !result : result; + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext context) { + boolean result = _delegate.match(matchValue, bucketingKey, attributes, context); + return _negate ? !result : result; } + public Matcher delegate() { return _delegate; } + @Override - public int hashCode() { - return Objects.hash(_negate, _delegate); - } + public int hashCode() { return Objects.hash(_negate, _delegate); } @Override public boolean equals(Object obj) { if (obj == null) return false; if (this == obj) return true; if (!(obj instanceof NegatableMatcher)) return false; - NegatableMatcher other = (NegatableMatcher) obj; - - return _negate == other._negate - && _delegate.equals(other._delegate); + return _negate == other._negate && _delegate.equals(other._delegate); } @Override public String toString() { - StringBuilder bldr = new StringBuilder(); - if (_negate) { - bldr.append(" not"); - } - bldr.append(" "); - bldr.append(_delegate); - return bldr.toString(); - } - - public Matcher delegate() { - return _delegate; + return (_negate ? " not " : " ") + _delegate; } } - } diff --git a/client/src/main/java/io/split/engine/matchers/BetweenMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java similarity index 88% rename from client/src/main/java/io/split/engine/matchers/BetweenMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java index a0ccfc1b7..14de73b23 100644 --- a/client/src/main/java/io/split/engine/matchers/BetweenMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java @@ -1,12 +1,12 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.client.dtos.DataType; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; import java.util.Map; -import static io.split.engine.matchers.Transformers.asDateHourMinute; -import static io.split.engine.matchers.Transformers.asLong; +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; /** * Supports the logic: if user.age is between x and y diff --git a/client/src/main/java/io/split/engine/matchers/BetweenSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java similarity index 95% rename from client/src/main/java/io/split/engine/matchers/BetweenSemverMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java index 326e21830..e393c2372 100644 --- a/client/src/main/java/io/split/engine/matchers/BetweenSemverMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java @@ -1,6 +1,6 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.Map; diff --git a/client/src/main/java/io/split/engine/matchers/BooleanMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java similarity index 87% rename from client/src/main/java/io/split/engine/matchers/BooleanMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java index 79d5a303f..59800896b 100644 --- a/client/src/main/java/io/split/engine/matchers/BooleanMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java @@ -1,10 +1,10 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.Map; -import static io.split.engine.matchers.Transformers.asBoolean; +import static io.split.rules.matchers.Transformers.asBoolean; public class BooleanMatcher implements Matcher { private boolean _booleanValue; diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/CombiningMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/CombiningMatcher.java new file mode 100644 index 000000000..be1730e6f --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/CombiningMatcher.java @@ -0,0 +1,73 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class CombiningMatcher { + + public enum Combiner { AND } + + private final List _delegates; + private final Combiner _combiner; + + public static CombiningMatcher of(Matcher matcher) { + return new CombiningMatcher(Combiner.AND, + new ArrayList<>(Arrays.asList(AttributeMatcher.vanilla(matcher)))); + } + + public static CombiningMatcher of(String attribute, Matcher matcher) { + return new CombiningMatcher(Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(attribute, matcher, false)))); + } + + public CombiningMatcher(Combiner combiner, List delegates) { + if (delegates == null || delegates.isEmpty()) throw new IllegalArgumentException("Delegates must not be empty"); + _delegates = Collections.unmodifiableList(new ArrayList<>(delegates)); + _combiner = combiner; + } + + public boolean match(String key, String bucketingKey, Map attributes, EvaluationContext context) { + if (_delegates.isEmpty()) return false; + switch (_combiner) { + case AND: + for (AttributeMatcher d : _delegates) { + if (!d.match(key, bucketingKey, attributes, context)) return false; + } + return true; + default: + throw new IllegalArgumentException("Unknown combiner: " + _combiner); + } + } + + public List attributeMatchers() { return _delegates; } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder("if"); + boolean first = true; + for (AttributeMatcher m : _delegates) { + if (!first) bldr.append(" ").append(_combiner); + bldr.append(" ").append(m); + first = false; + } + return bldr.toString(); + } + + @Override + public int hashCode() { return Objects.hash(_combiner, _delegates); } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof CombiningMatcher)) return false; + CombiningMatcher other = (CombiningMatcher) obj; + return _combiner.equals(other._combiner) && _delegates.equals(other._delegates); + } +} diff --git a/client/src/main/java/io/split/engine/matchers/DependencyMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java similarity index 87% rename from client/src/main/java/io/split/engine/matchers/DependencyMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java index a3c3c4640..8d4b521c9 100644 --- a/client/src/main/java/io/split/engine/matchers/DependencyMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java @@ -1,6 +1,6 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.List; import java.util.Map; @@ -28,7 +28,7 @@ public boolean match(Object matchValue, String bucketingKey, Map return false; } - String result = evaluationContext.getEvaluator().evaluateFeature((String) matchValue, bucketingKey, _featureFlag, attributes).treatment; + String result = evaluationContext.evaluate((String) matchValue, bucketingKey, _featureFlag, attributes).treatment; return _treatments.contains(result); } diff --git a/client/src/main/java/io/split/engine/matchers/EqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java similarity index 86% rename from client/src/main/java/io/split/engine/matchers/EqualToMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java index 9a1e32f37..8792648bd 100644 --- a/client/src/main/java/io/split/engine/matchers/EqualToMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java @@ -1,12 +1,12 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.client.dtos.DataType; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; import java.util.Map; -import static io.split.engine.matchers.Transformers.asDate; -import static io.split.engine.matchers.Transformers.asLong; +import static io.split.rules.matchers.Transformers.asDate; +import static io.split.rules.matchers.Transformers.asLong; /** * Created by adilaijaz on 3/7/16. diff --git a/client/src/main/java/io/split/engine/matchers/EqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java similarity index 93% rename from client/src/main/java/io/split/engine/matchers/EqualToSemverMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java index 64d9135d2..0af499ae3 100644 --- a/client/src/main/java/io/split/engine/matchers/EqualToSemverMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java @@ -1,6 +1,6 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.Map; diff --git a/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java similarity index 87% rename from client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java index 1b83dc2c3..3c1a0b864 100644 --- a/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java @@ -1,12 +1,12 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.client.dtos.DataType; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; import java.util.Map; -import static io.split.engine.matchers.Transformers.asDateHourMinute; -import static io.split.engine.matchers.Transformers.asLong; +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; /** * Created by adilaijaz on 3/7/16. diff --git a/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java similarity index 94% rename from client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java index ffc714cca..6d92594a0 100644 --- a/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java @@ -1,6 +1,6 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.Map; diff --git a/client/src/main/java/io/split/engine/matchers/InListSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java similarity index 95% rename from client/src/main/java/io/split/engine/matchers/InListSemverMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java index 69fd1ea45..f1d1422e8 100644 --- a/client/src/main/java/io/split/engine/matchers/InListSemverMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java @@ -1,6 +1,6 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.Collection; import java.util.HashSet; diff --git a/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java similarity index 87% rename from client/src/main/java/io/split/engine/matchers/LessThanOrEqualToMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java index 24a74aaba..432821651 100644 --- a/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java @@ -1,12 +1,12 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.client.dtos.DataType; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; import java.util.Map; -import static io.split.engine.matchers.Transformers.asDateHourMinute; -import static io.split.engine.matchers.Transformers.asLong; +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; /** * Created by adilaijaz on 3/7/16. diff --git a/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java similarity index 94% rename from client/src/main/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java index dd05f8c4d..0c6b499ac 100644 --- a/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java @@ -1,6 +1,6 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; import java.util.Map; diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/Matcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/Matcher.java new file mode 100644 index 000000000..347270073 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/Matcher.java @@ -0,0 +1,9 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public interface Matcher { + boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext context); +} diff --git a/client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java similarity index 66% rename from client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java index 122784498..5951c9fb0 100644 --- a/client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java @@ -1,7 +1,7 @@ -package io.split.engine.matchers; +package io.split.rules.matchers; -import io.split.client.dtos.Prerequisites; -import io.split.engine.evaluator.EvaluationContext; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.model.Prerequisite; import java.util.List; import java.util.Map; @@ -9,13 +9,13 @@ import java.util.stream.Collectors; public class PrerequisitesMatcher implements Matcher { - private List _prerequisites; + private List _prerequisites; - public PrerequisitesMatcher(List prerequisites) { + public PrerequisitesMatcher(List prerequisites) { _prerequisites = prerequisites; } - public List getPrerequisites() { return _prerequisites; } + public List getPrerequisites() { return _prerequisites; } @Override public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { @@ -31,10 +31,10 @@ public boolean match(Object matchValue, String bucketingKey, Map return true; } - for (Prerequisites prerequisites : _prerequisites) { - String treatment = evaluationContext.getEvaluator().evaluateFeature((String) matchValue, bucketingKey, - prerequisites.featureFlagName, attributes). treatment; - if (!prerequisites.treatments.contains(treatment)) { + for (Prerequisite prerequisites : _prerequisites) { + String treatment = evaluationContext.evaluate((String) matchValue, bucketingKey, + prerequisites.featureFlagName(), attributes).treatment; + if (!prerequisites.treatments().contains(treatment)) { return false; } } @@ -46,8 +46,8 @@ public String toString() { StringBuilder bldr = new StringBuilder(); bldr.append("prerequisites: "); if (this._prerequisites != null) { - bldr.append(this._prerequisites.stream().map(pr -> pr.featureFlagName + " " + - pr.treatments.toString()).map(Object::toString).collect(Collectors.joining(", "))); + bldr.append(this._prerequisites.stream().map(pr -> pr.featureFlagName() + " " + + pr.treatments().toString()).map(Object::toString).collect(Collectors.joining(", "))); } return bldr.toString(); } diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/RuleBasedSegmentMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/RuleBasedSegmentMatcher.java new file mode 100644 index 000000000..66347ce1d --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/RuleBasedSegmentMatcher.java @@ -0,0 +1,41 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; +import java.util.Objects; + +/** + * Checks if the key is a member of a rule-based segment. + * Delegates to EvaluationContext.isInRuleBasedSegment() — the SDK provides the actual evaluation logic + * (excluded keys, excluded segments, condition matching). + */ +public final class RuleBasedSegmentMatcher implements Matcher { + private final String _segmentName; + + public RuleBasedSegmentMatcher(String segmentName) { + _segmentName = Objects.requireNonNull(segmentName); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext context) { + if (!(matchValue instanceof String)) return false; + return context.isInRuleBasedSegment(_segmentName, (String) matchValue, bucketingKey, attributes); + } + + public String getSegmentName() { return _segmentName; } + + @Override + public int hashCode() { return 31 * 17 + _segmentName.hashCode(); } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof RuleBasedSegmentMatcher)) return false; + return _segmentName.equals(((RuleBasedSegmentMatcher) obj)._segmentName); + } + + @Override + public String toString() { return "in rule-based segment " + _segmentName; } +} diff --git a/client/src/main/java/io/split/engine/matchers/Semver.java b/targeting-engine/src/main/java/io/split/rules/matchers/Semver.java similarity index 82% rename from client/src/main/java/io/split/engine/matchers/Semver.java rename to targeting-engine/src/main/java/io/split/rules/matchers/Semver.java index 7a85a0d72..a9f7f217f 100644 --- a/client/src/main/java/io/split/engine/matchers/Semver.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/Semver.java @@ -1,16 +1,12 @@ -package io.split.engine.matchers; - -import io.split.client.exceptions.SemverParseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +package io.split.rules.matchers; import java.util.Arrays; public class Semver { private static final String METADATA_DELIMITER = "+"; private static final String PRERELEASE_DELIMITER = "-"; - private static final String VALUE_DELIMITER = "\\."; - private static final Logger _log = LoggerFactory.getLogger(Semver.class); + private static final String VALUE_DELIMITER_REGEX = "\\."; + private static final String VALUE_DELIMITER = "."; private Long _major; private Long _minor; @@ -21,11 +17,10 @@ public class Semver { private String _version; public static Semver build(String version) { - if (version.isEmpty()) return null; + if (version == null || version.isEmpty()) return null; try { return new Semver(version); } catch (Exception ex) { - _log.error("An error occurred during the creation of a Semver instance:", ex.getMessage()); return null; } } @@ -107,40 +102,40 @@ private int adjustNumber(int number) { if (number < 0) return -1; return 0; } - private Semver(String version) throws SemverParseException { + private Semver(String version) { String vWithoutMetadata = setAndRemoveMetadataIfExists(version); String vWithoutPreRelease = setAndRemovePreReleaseIfExists(vWithoutMetadata); setMajorMinorAndPatch(vWithoutPreRelease); _version = setVersion(); } - private String setAndRemoveMetadataIfExists(String version) throws SemverParseException { + private String setAndRemoveMetadataIfExists(String version) { int index = version.indexOf(METADATA_DELIMITER); if (index == -1) { return version; } _metadata = version.substring(index+1); if (_metadata == null || _metadata.isEmpty()) { - throw new SemverParseException("Unable to convert to Semver, incorrect pre release data"); + throw new IllegalArgumentException("Unable to convert to Semver, incorrect pre release data"); } return version.substring(0, index); } - private String setAndRemovePreReleaseIfExists(String vWithoutMetadata) throws SemverParseException { + private String setAndRemovePreReleaseIfExists(String vWithoutMetadata) { int index = vWithoutMetadata.indexOf(PRERELEASE_DELIMITER); if (index == -1) { _isStable = true; return vWithoutMetadata; } String preReleaseData = vWithoutMetadata.substring(index+1); - _preRelease = preReleaseData.split(VALUE_DELIMITER); + _preRelease = preReleaseData.split(VALUE_DELIMITER_REGEX); if (_preRelease == null || Arrays.stream(_preRelease).allMatch(pr -> pr == null || pr.isEmpty())) { - throw new SemverParseException("Unable to convert to Semver, incorrect pre release data"); + throw new IllegalArgumentException("Unable to convert to Semver, incorrect pre release data"); } return vWithoutMetadata.substring(0, index); } - private void setMajorMinorAndPatch(String version) throws SemverParseException { - String[] vParts = version.split(VALUE_DELIMITER); + private void setMajorMinorAndPatch(String version) { + String[] vParts = version.split(VALUE_DELIMITER_REGEX); if (vParts.length != 3) - throw new SemverParseException("Unable to convert to Semver, incorrect format: " + version); + throw new IllegalArgumentException("Unable to convert to Semver, incorrect format: " + version); _major = Long.parseLong(vParts[0]); _minor = Long.parseLong(vParts[1]); _patch = Long.parseLong(vParts[2]); diff --git a/client/src/main/java/io/split/engine/matchers/Transformers.java b/targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java similarity index 93% rename from client/src/main/java/io/split/engine/matchers/Transformers.java rename to targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java index 17d9101fb..b34e60991 100644 --- a/client/src/main/java/io/split/engine/matchers/Transformers.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java @@ -1,7 +1,6 @@ -package io.split.engine.matchers; - -import com.google.common.collect.Sets; +package io.split.rules.matchers; +import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.HashSet; @@ -12,7 +11,7 @@ * Created by adilaijaz on 3/7/16. */ public class Transformers { - private static Set VALID_BOOLEAN_STRINGS = Sets.newHashSet("true", "false"); + private static Set VALID_BOOLEAN_STRINGS = new HashSet<>(Arrays.asList("true", "false")); private static TimeZone UTC = TimeZone.getTimeZone("UTC"); public static Long asLong(Object obj) { diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/UserDefinedSegmentMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/UserDefinedSegmentMatcher.java new file mode 100644 index 000000000..fc95318a4 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/UserDefinedSegmentMatcher.java @@ -0,0 +1,40 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; +import java.util.Objects; + +/** + * Checks if the key is a member of a standard (user-defined) segment. + * Delegates to EvaluationContext.isInSegment() — the SDK provides the actual storage lookup. + */ +public final class UserDefinedSegmentMatcher implements Matcher { + private final String _segmentName; + + public UserDefinedSegmentMatcher(String segmentName) { + _segmentName = Objects.requireNonNull(segmentName); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext context) { + if (!(matchValue instanceof String)) return false; + return context.isInSegment(_segmentName, (String) matchValue); + } + + public String getSegmentName() { return _segmentName; } + + @Override + public int hashCode() { return 31 * 17 + _segmentName.hashCode(); } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof UserDefinedSegmentMatcher)) return false; + return _segmentName.equals(((UserDefinedSegmentMatcher) obj)._segmentName); + } + + @Override + public String toString() { return "in segment " + _segmentName; } +} diff --git a/client/src/main/java/io/split/engine/matchers/strings/WhitelistMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java similarity index 92% rename from client/src/main/java/io/split/engine/matchers/strings/WhitelistMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java index 5068c1437..64fb4753c 100644 --- a/client/src/main/java/io/split/engine/matchers/strings/WhitelistMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java @@ -1,7 +1,6 @@ -package io.split.engine.matchers.strings; +package io.split.rules.matchers; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; import java.util.Collection; import java.util.HashSet; diff --git a/client/src/main/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java similarity index 89% rename from client/src/main/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java index 5f4f9433a..65814087c 100644 --- a/client/src/main/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java @@ -1,14 +1,14 @@ -package io.split.engine.matchers.collections; +package io.split.rules.matchers.collections; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; -import static io.split.engine.matchers.Transformers.toSetOfStrings; +import static io.split.rules.matchers.Transformers.toSetOfStrings; /** * Created by adilaijaz on 3/7/16. diff --git a/client/src/main/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java similarity index 89% rename from client/src/main/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java index 3a2514401..2288020e1 100644 --- a/client/src/main/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java @@ -1,14 +1,14 @@ -package io.split.engine.matchers.collections; +package io.split.rules.matchers.collections; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; -import static io.split.engine.matchers.Transformers.toSetOfStrings; +import static io.split.rules.matchers.Transformers.toSetOfStrings; /** * Created by adilaijaz on 3/7/16. diff --git a/client/src/main/java/io/split/engine/matchers/collections/EqualToSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java similarity index 88% rename from client/src/main/java/io/split/engine/matchers/collections/EqualToSetMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java index 4a09c9efc..212b9f0c7 100644 --- a/client/src/main/java/io/split/engine/matchers/collections/EqualToSetMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java @@ -1,14 +1,14 @@ -package io.split.engine.matchers.collections; +package io.split.rules.matchers.collections; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; -import static io.split.engine.matchers.Transformers.toSetOfStrings; +import static io.split.rules.matchers.Transformers.toSetOfStrings; /** * Created by adilaijaz on 3/7/16. diff --git a/client/src/main/java/io/split/engine/matchers/collections/PartOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java similarity index 88% rename from client/src/main/java/io/split/engine/matchers/collections/PartOfSetMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java index 8bb5f1399..54aa9730b 100644 --- a/client/src/main/java/io/split/engine/matchers/collections/PartOfSetMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java @@ -1,14 +1,14 @@ -package io.split.engine.matchers.collections; +package io.split.rules.matchers.collections; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; -import static io.split.engine.matchers.Transformers.toSetOfStrings; +import static io.split.rules.matchers.Transformers.toSetOfStrings; /** * Created by adilaijaz on 3/7/16. diff --git a/client/src/main/java/io/split/engine/matchers/strings/ContainsAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java similarity index 93% rename from client/src/main/java/io/split/engine/matchers/strings/ContainsAnyOfMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java index b8cbe8fca..755a33aad 100644 --- a/client/src/main/java/io/split/engine/matchers/strings/ContainsAnyOfMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java @@ -1,7 +1,7 @@ -package io.split.engine.matchers.strings; +package io.split.rules.matchers.strings; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; import java.util.Collection; import java.util.HashSet; diff --git a/client/src/main/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java similarity index 93% rename from client/src/main/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java index 32ac9f7f3..64b67881a 100644 --- a/client/src/main/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java @@ -1,7 +1,7 @@ -package io.split.engine.matchers.strings; +package io.split.rules.matchers.strings; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; import java.util.Collection; import java.util.HashSet; diff --git a/client/src/main/java/io/split/engine/matchers/strings/RegularExpressionMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java similarity index 90% rename from client/src/main/java/io/split/engine/matchers/strings/RegularExpressionMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java index f64b3264b..5ae33cee4 100644 --- a/client/src/main/java/io/split/engine/matchers/strings/RegularExpressionMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java @@ -1,7 +1,7 @@ -package io.split.engine.matchers.strings; +package io.split.rules.matchers.strings; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; import java.util.Map; import java.util.regex.Pattern; diff --git a/client/src/main/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java similarity index 93% rename from client/src/main/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcher.java rename to targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java index 7f1ed2cad..fb85d8fbc 100644 --- a/client/src/main/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcher.java +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java @@ -1,7 +1,7 @@ -package io.split.engine.matchers.strings; +package io.split.rules.matchers.strings; -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; import java.util.Collection; import java.util.HashSet; diff --git a/targeting-engine/src/main/java/io/split/rules/model/Condition.java b/targeting-engine/src/main/java/io/split/rules/model/Condition.java new file mode 100644 index 000000000..5dd868a99 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/Condition.java @@ -0,0 +1,36 @@ +package io.split.rules.model; + +import io.split.rules.matchers.CombiningMatcher; + +import java.util.Collections; +import java.util.List; + +public final class Condition { + private final ConditionType _conditionType; + private final CombiningMatcher _matcher; + private final List _partitions; + private final String _label; + + public Condition(ConditionType conditionType, CombiningMatcher matcher, List partitions, String label) { + _conditionType = conditionType; + _matcher = matcher; + _partitions = partitions != null ? Collections.unmodifiableList(partitions) : Collections.emptyList(); + _label = label; + } + + public ConditionType conditionType() { + return _conditionType; + } + + public CombiningMatcher matcher() { + return _matcher; + } + + public List partitions() { + return _partitions; + } + + public String label() { + return _label; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/ConditionType.java b/targeting-engine/src/main/java/io/split/rules/model/ConditionType.java new file mode 100644 index 000000000..96ad57f6c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/ConditionType.java @@ -0,0 +1,6 @@ +package io.split.rules.model; + +public enum ConditionType { + WHITELIST, + ROLLOUT +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/DataType.java b/targeting-engine/src/main/java/io/split/rules/model/DataType.java new file mode 100644 index 000000000..a7ffbad06 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/DataType.java @@ -0,0 +1,7 @@ +package io.split.rules.model; + +public enum DataType { + NUMBER, + DATETIME, + STRING +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/Partition.java b/targeting-engine/src/main/java/io/split/rules/model/Partition.java new file mode 100644 index 000000000..45d9e8d5c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/Partition.java @@ -0,0 +1,11 @@ +package io.split.rules.model; + +public final class Partition { + public final String treatment; + public final int size; + + public Partition(String treatment, int size) { + this.treatment = treatment; + this.size = size; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/Prerequisite.java b/targeting-engine/src/main/java/io/split/rules/model/Prerequisite.java new file mode 100644 index 000000000..3b4367d91 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/Prerequisite.java @@ -0,0 +1,37 @@ +package io.split.rules.model; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public final class Prerequisite { + private final String _featureFlagName; + private final List _treatments; + + public Prerequisite(String featureFlagName, List treatments) { + _featureFlagName = Objects.requireNonNull(featureFlagName); + _treatments = Collections.unmodifiableList(treatments); + } + + public String featureFlagName() { + return _featureFlagName; + } + + public List treatments() { + return _treatments; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Prerequisite that = (Prerequisite) o; + return Objects.equals(_featureFlagName, that._featureFlagName) + && Objects.equals(_treatments, that._treatments); + } + + @Override + public int hashCode() { + return Objects.hash(_featureFlagName, _treatments); + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java b/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java new file mode 100644 index 000000000..922948b85 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java @@ -0,0 +1,73 @@ +package io.split.rules.model; + +import io.split.rules.matchers.PrerequisitesMatcher; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A fully-parsed targeting rule (analogous to ParsedSplit). + * Contains all the information needed to evaluate feature flag targeting. + */ +public final class TargetingRule { + private final String _name; + private final int _seed; + private final boolean _killed; + private final String _defaultTreatment; + private final List _conditions; + private final String _trafficTypeName; + private final long _changeNumber; + private final int _trafficAllocation; + private final int _trafficAllocationSeed; + private final int _algo; + private final Map _configurations; + private final Set _flagSets; + private final boolean _impressionsDisabled; + private final List _prerequisites; + private final PrerequisitesMatcher _prerequisitesMatcher; + + public TargetingRule(String name, int seed, boolean killed, String defaultTreatment, + List conditions, String trafficTypeName, long changeNumber, + int trafficAllocation, int trafficAllocationSeed, int algo, + Map configurations, Set flagSets, + boolean impressionsDisabled, List prerequisites) { + _name = Objects.requireNonNull(name); + _seed = seed; + _killed = killed; + _defaultTreatment = Objects.requireNonNull(defaultTreatment); + _conditions = conditions != null + ? Collections.unmodifiableList(conditions) + : Collections.emptyList(); + _trafficTypeName = trafficTypeName; + _changeNumber = changeNumber; + _trafficAllocation = trafficAllocation; + _trafficAllocationSeed = trafficAllocationSeed; + _algo = algo; + _configurations = configurations; + _flagSets = flagSets; + _impressionsDisabled = impressionsDisabled; + _prerequisites = prerequisites != null + ? Collections.unmodifiableList(prerequisites) + : Collections.emptyList(); + _prerequisitesMatcher = new PrerequisitesMatcher(_prerequisites); + } + + public String name() { return _name; } + public int seed() { return _seed; } + public boolean killed() { return _killed; } + public String defaultTreatment() { return _defaultTreatment; } + public List conditions() { return _conditions; } + public String trafficTypeName() { return _trafficTypeName; } + public long changeNumber() { return _changeNumber; } + public int trafficAllocation() { return _trafficAllocation; } + public int trafficAllocationSeed() { return _trafficAllocationSeed; } + public int algo() { return _algo; } + public Map configurations() { return _configurations; } + public Set flagSets() { return _flagSets; } + public boolean impressionsDisabled() { return _impressionsDisabled; } + public List prerequisites() { return _prerequisites; } + public PrerequisitesMatcher prerequisitesMatcher() { return _prerequisitesMatcher; } +} diff --git a/targeting-engine/src/test/java/io/split/rules/bucketing/BucketerTest.java b/targeting-engine/src/test/java/io/split/rules/bucketing/BucketerTest.java new file mode 100644 index 000000000..edda8d386 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/bucketing/BucketerTest.java @@ -0,0 +1,88 @@ +package io.split.rules.bucketing; + +import io.split.rules.model.Partition; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class BucketerTest { + + @Test + public void getBucketIsBetween1And100Inclusive() { + int bucket = Bucketer.getBucket("somekey", 12345, 2); + assertTrue("bucket should be >= 1", bucket >= 1); + assertTrue("bucket should be <= 100", bucket <= 100); + } + + @Test + public void getBucketLegacyAlgoReturnsSameResultForSameInput() { + int b1 = Bucketer.getBucket("user1", 123, 1); + int b2 = Bucketer.getBucket("user1", 123, 1); + assertEquals(b1, b2); + } + + @Test + public void getBucketMurmurAlgoReturnsSameResultForSameInput() { + int b1 = Bucketer.getBucket("user1", 123, 2); + int b2 = Bucketer.getBucket("user1", 123, 2); + assertEquals(b1, b2); + } + + @Test + public void getTreatmentReturnsControlForEmptyPartitions() { + List empty = Collections.emptyList(); + assertEquals("control", Bucketer.getTreatment("key", 123, empty, 2)); + } + + @Test + public void getTreatmentReturnsSingleTreatmentWhen100Percent() { + List partitions = Collections.singletonList(new Partition("on", 100)); + assertEquals("on", Bucketer.getTreatment("key", 123, partitions, 2)); + } + + @Test + public void getTreatmentSelectsFromPartitionsBasedOnBucket() { + List partitions = Arrays.asList( + new Partition("on", 50), + new Partition("off", 50) + ); + // Verify it returns one of the two treatments + String t = Bucketer.getTreatment("user1", 12345, partitions, 2); + assertTrue("on".equals(t) || "off".equals(t)); + } + + @Test + public void getTreatmentReturnsControlWhenBucketExceedsAllPartitions() { + // partitions that don't sum to 100 — bucket could exceed them + List partitions = Collections.singletonList(new Partition("on", 1)); + // Most keys should get "control" with 1% partition + long controlCount = 0; + for (int i = 0; i < 200; i++) { + if ("control".equals(Bucketer.getTreatment("user" + i, 12345, partitions, 2))) { + controlCount++; + } + } + assertTrue("Most keys should get control with 1% partition", controlCount > 150); + } + + @Test + public void bucketMathIsCorrect() { + // bucket() returns (Math.abs(hash % 100) + 1) + assertEquals(1, Bucketer.bucket(0)); + assertEquals(1, Bucketer.bucket(100)); + assertEquals(50, Bucketer.bucket(49)); + assertEquals(100, Bucketer.bucket(99)); + } + + @Test + public void knownMurmurHashValue() { + // Verify consistent murmur hash across platforms + long hash = Bucketer.murmurHash("testKey", 12345); + assertEquals(hash, Bucketer.murmurHash("testKey", 12345)); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java b/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java new file mode 100644 index 000000000..4e76fd644 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java @@ -0,0 +1,161 @@ +package io.split.rules.engine; + +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.model.Condition; +import io.split.rules.model.ConditionType; +import io.split.rules.model.Partition; +import io.split.rules.model.Prerequisite; +import io.split.rules.model.TargetingRule; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class TargetingEngineImplTest { + + private TargetingEngine _engine; + private EvaluationContext _context; + + @Before + public void setUp() { + _engine = new TargetingEngineImpl(); + _context = Mockito.mock(EvaluationContext.class); + } + + private TargetingRule buildRule(boolean killed, List conditions, List prerequisites, + int trafficAllocation) { + return new TargetingRule( + "test_flag", 12345, killed, "off", + conditions, "user", 1L, + trafficAllocation, 12345, 2, + null, new HashSet<>(), false, + prerequisites + ); + } + + private Condition rolloutCondition(String treatment) { + return new Condition( + ConditionType.ROLLOUT, + CombiningMatcher.of(new AllKeysMatcher()), + Collections.singletonList(new Partition(treatment, 100)), + "test label" + ); + } + + private Condition whitelistCondition(String treatment) { + return new Condition( + ConditionType.WHITELIST, + CombiningMatcher.of(new AllKeysMatcher()), + Collections.singletonList(new Partition(treatment, 100)), + "whitelist label" + ); + } + + @Test + public void killedRuleReturnsDefaultTreatment() throws Exception { + TargetingRule rule = buildRule(true, Collections.singletonList(rolloutCondition("on")), null, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("off", result.treatment); + assertEquals(EvaluationLabels.KILLED, result.label); + assertEquals(Long.valueOf(1L), result.version); + } + + @Test + public void emptyConditionsReturnsDefaultRule() throws Exception { + TargetingRule rule = buildRule(false, Collections.emptyList(), null, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("off", result.treatment); + assertEquals(EvaluationLabels.DEFAULT_RULE, result.label); + } + + @Test + public void matchingConditionReturnsTreatment() throws Exception { + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), null, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("on", result.treatment); + assertEquals("test label", result.label); + } + + @Test + public void whitelistConditionMatchesBeforeTrafficAllocation() throws Exception { + TargetingRule rule = buildRule(false, Collections.singletonList(whitelistCondition("on")), null, 0); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("on", result.treatment); + assertEquals("whitelist label", result.label); + } + + @Test + public void trafficAllocationZeroReturnsNotInSplit() throws Exception { + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), null, 0); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("off", result.treatment); + assertEquals(EvaluationLabels.NOT_IN_SPLIT, result.label); + } + + @Test + public void prerequisitesNotMetReturnsDefaultTreatment() throws Exception { + Mockito.when(_context.evaluate("user1", "user1", "prereq_flag", null)) + .thenReturn(new EvaluationResult("off", EvaluationLabels.DEFAULT_RULE)); + List prereqs = Collections.singletonList( + new Prerequisite("prereq_flag", Collections.singletonList("on")) + ); + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), prereqs, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("off", result.treatment); + assertEquals(EvaluationLabels.PREREQUISITES_NOT_MET, result.label); + } + + @Test + public void prerequisitesMetProceedsToEvaluation() throws Exception { + Mockito.when(_context.evaluate("user1", "user1", "prereq_flag", null)) + .thenReturn(new EvaluationResult("on", EvaluationLabels.DEFAULT_RULE)); + List prereqs = Collections.singletonList( + new Prerequisite("prereq_flag", Collections.singletonList("on")) + ); + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), prereqs, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("on", result.treatment); + } + + @Test + public void bucketingKeyUsedWhenProvided() throws Exception { + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), null, 100); + EvaluationResult result = _engine.evaluate("user1", "bucket_user", rule, null, _context); + assertEquals("on", result.treatment); + } + + @Test + public void configReturnedForTreatment() throws Exception { + Map configs = new HashMap<>(); + configs.put("on", "{\"color\":\"red\"}"); + TargetingRule rule = new TargetingRule( + "test_flag", 12345, false, "off", + Collections.singletonList(rolloutCondition("on")), "user", 1L, + 100, 12345, 2, configs, new HashSet<>(), false, null + ); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("on", result.treatment); + assertEquals("{\"color\":\"red\"}", result.config); + } + + @Test + public void impressionsDisabledPreserved() throws Exception { + TargetingRule rule = new TargetingRule( + "test_flag", 12345, false, "off", + Collections.singletonList(rolloutCondition("on")), "user", 1L, + 100, 12345, 2, null, new HashSet<>(), true, null + ); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals(true, result.impressionsDisabled); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/matchers/AllKeysMatcherTest.java b/targeting-engine/src/test/java/io/split/rules/matchers/AllKeysMatcherTest.java new file mode 100644 index 000000000..e7704d2a2 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/matchers/AllKeysMatcherTest.java @@ -0,0 +1,26 @@ +package io.split.rules.matchers; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AllKeysMatcherTest { + + private final AllKeysMatcher _matcher = new AllKeysMatcher(); + + @Test + public void matchesNonNullValue() { + assertTrue(_matcher.match("anything", null, null, null)); + } + + @Test + public void doesNotMatchNull() { + assertFalse(_matcher.match(null, null, null, null)); + } + + @Test + public void equalityHolds() { + assertTrue(_matcher.equals(new AllKeysMatcher())); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/matchers/BetweenMatcherTest.java b/targeting-engine/src/test/java/io/split/rules/matchers/BetweenMatcherTest.java new file mode 100644 index 000000000..68aeae594 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/matchers/BetweenMatcherTest.java @@ -0,0 +1,37 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class BetweenMatcherTest { + + @Test + public void matchesValueInRange() { + BetweenMatcher m = new BetweenMatcher(10L, 20L, DataType.NUMBER); + assertTrue(m.match(15L, null, null, null)); + assertTrue(m.match(10L, null, null, null)); + assertTrue(m.match(20L, null, null, null)); + } + + @Test + public void doesNotMatchValueOutOfRange() { + BetweenMatcher m = new BetweenMatcher(10L, 20L, DataType.NUMBER); + assertFalse(m.match(9L, null, null, null)); + assertFalse(m.match(21L, null, null, null)); + } + + @Test + public void doesNotMatchNull() { + BetweenMatcher m = new BetweenMatcher(10L, 20L, DataType.NUMBER); + assertFalse(m.match(null, null, null, null)); + } + + @Test + public void matchesIntegerInRange() { + BetweenMatcher m = new BetweenMatcher(10L, 20L, DataType.NUMBER); + assertTrue(m.match(15, null, null, null)); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/matchers/BooleanMatcherTest.java b/targeting-engine/src/test/java/io/split/rules/matchers/BooleanMatcherTest.java new file mode 100644 index 000000000..75e6efd81 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/matchers/BooleanMatcherTest.java @@ -0,0 +1,34 @@ +package io.split.rules.matchers; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class BooleanMatcherTest { + + @Test + public void matchesTrueWhenValueIsTrue() { + BooleanMatcher m = new BooleanMatcher(true); + assertTrue(m.match(true, null, null, null)); + assertTrue(m.match("true", null, null, null)); + } + + @Test + public void matchesFalseWhenValueIsFalse() { + BooleanMatcher m = new BooleanMatcher(false); + assertTrue(m.match(false, null, null, null)); + assertTrue(m.match("false", null, null, null)); + } + + @Test + public void doesNotMatchOpposite() { + assertTrue(new BooleanMatcher(true).match(true, null, null, null)); + assertFalse(new BooleanMatcher(true).match(false, null, null, null)); + } + + @Test + public void doesNotMatchNull() { + assertFalse(new BooleanMatcher(true).match(null, null, null, null)); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/matchers/SemverTest.java b/targeting-engine/src/test/java/io/split/rules/matchers/SemverTest.java new file mode 100644 index 000000000..5ab49d374 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/matchers/SemverTest.java @@ -0,0 +1,52 @@ +package io.split.rules.matchers; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SemverTest { + + @Test + public void buildsValidSemver() { + Semver s = Semver.build("1.2.3"); + assertNotNull(s); + assertEquals("1.2.3", s.version()); + assertEquals(Long.valueOf(1), s.major()); + assertEquals(Long.valueOf(2), s.minor()); + assertEquals(Long.valueOf(3), s.patch()); + } + + @Test + public void buildsWithPreRelease() { + Semver s = Semver.build("1.0.0-alpha.1"); + assertNotNull(s); + assertFalse(s.isStable()); + } + + @Test + public void returnsNullForEmpty() { + assertNull(Semver.build("")); + } + + @Test + public void returnsNullForInvalidFormat() { + assertNull(Semver.build("notasemver")); + } + + @Test + public void compareReturnsZeroForEqual() { + assertEquals(0, Semver.build("1.2.3").compare(Semver.build("1.2.3"))); + } + + @Test + public void compareOrdersCorrectly() { + assertTrue(Semver.build("2.0.0").compare(Semver.build("1.0.0")) > 0); + assertTrue(Semver.build("1.0.0").compare(Semver.build("2.0.0")) < 0); + assertTrue(Semver.build("1.1.0").compare(Semver.build("1.0.0")) > 0); + } + + @Test + public void stableIsGreaterThanPreRelease() { + assertTrue(Semver.build("1.0.0").compare(Semver.build("1.0.0-alpha")) > 0); + } +}