From 68731e95c82ea29a088beaa713d0ceea83d6c39f Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 1 Apr 2026 15:28:05 -0300 Subject: [PATCH 1/9] WIP evaluator AI-Session-Id: 095253e8-7e1c-4578-9779-bf96395021cf AI-Tool: claude-code AI-Model: unknown --- .gitignore | 2 +- client/pom.xml | 5 + .../io/split/client/CacheUpdaterService.java | 27 +- .../java/io/split/client/api/SplitView.java | 9 +- .../client/impressions/ImpressionHasher.java | 2 +- .../io/split/client/utils/MurmurHash3.java | 302 ------------------ .../engine/evaluator/EvaluationContext.java | 52 ++- .../split/engine/evaluator/EvaluatorImp.java | 110 ++----- .../engine/experiments/ParsedCondition.java | 2 +- .../experiments/ParsedRuleBasedSegment.java | 9 +- .../split/engine/experiments/ParsedSplit.java | 111 +++++-- .../split/engine/experiments/ParserUtils.java | 115 +++---- .../experiments/RuleBasedSegmentParser.java | 5 +- .../split/engine/experiments/SplitParser.java | 72 ++++- .../split/engine/matchers/AllKeysMatcher.java | 39 --- .../engine/matchers/AttributeMatcher.java | 136 -------- .../split/engine/matchers/BetweenMatcher.java | 84 ----- .../engine/matchers/BetweenSemverMatcher.java | 58 ---- .../split/engine/matchers/BooleanMatcher.java | 46 --- .../engine/matchers/CombiningMatcher.java | 98 ------ .../engine/matchers/DependencyMatcher.java | 63 ---- .../split/engine/matchers/EqualToMatcher.java | 71 ---- .../engine/matchers/EqualToSemverMatcher.java | 54 ---- .../matchers/GreaterThanOrEqualToMatcher.java | 74 ----- .../GreaterThanOrEqualToSemverMatcher.java | 54 ---- .../engine/matchers/InListSemverMatcher.java | 77 ----- .../matchers/LessThanOrEqualToMatcher.java | 73 ----- .../LessThanOrEqualToSemverMatcher.java | 54 ---- .../io/split/engine/matchers/Matcher.java | 9 - .../engine/matchers/PrerequisitesMatcher.java | 71 ---- .../matchers/RuleBasedSegmentMatcher.java | 105 ------ .../java/io/split/engine/matchers/Semver.java | 176 ---------- .../split/engine/matchers/Transformers.java | 104 ------ .../matchers/UserDefinedSegmentMatcher.java | 62 ---- .../collections/ContainsAllOfSetMatcher.java | 70 ---- .../collections/ContainsAnyOfSetMatcher.java | 75 ----- .../collections/EqualToSetMatcher.java | 68 ---- .../collections/PartOfSetMatcher.java | 72 ----- .../strings/ContainsAnyOfMatcher.java | 83 ----- .../strings/EndsWithAnyOfMatcher.java | 83 ----- .../strings/RegularExpressionMatcher.java | 51 --- .../strings/StartsWithAnyOfMatcher.java | 83 ----- .../matchers/strings/WhitelistMatcher.java | 67 ---- .../io/split/engine/splitter/Splitter.java | 2 +- .../io/split/client/SplitClientImplTest.java | 18 +- .../io/split/client/SplitManagerImplTest.java | 9 +- .../evaluator/EvaluatorIntegrationTest.java | 18 +- .../split/engine/evaluator/EvaluatorTest.java | 7 +- .../ParsedRuleBasedSegmentTest.java | 8 +- .../RuleBasedSegmentParserTest.java | 58 ++-- .../engine/experiments/SplitFetcherTest.java | 4 +- .../engine/experiments/SplitParserTest.java | 64 ++-- .../engine/matchers/AllKeysMatcherTest.java | 2 + .../engine/matchers/AttributeMatcherTest.java | 6 +- .../engine/matchers/BetweenMatcherTest.java | 4 +- .../matchers/BetweenSemverMatcherTest.java | 2 + .../engine/matchers/BooleanMatcherTest.java | 2 + .../engine/matchers/CombiningMatcherTest.java | 6 +- .../engine/matchers/EqualToMatcherTest.java | 4 +- .../matchers/EqualToSemverMatcherTest.java | 2 + .../GreaterThanOrEqualToMatcherTest.java | 4 +- ...GreaterThanOrEqualToSemverMatcherTest.java | 2 + .../matchers/InListSemverMatcherTest.java | 2 + .../LessThanOrEqualToMatcherTest.java | 4 +- .../LessThanOrEqualToSemverMatcherTest.java | 2 + .../engine/matchers/NegatableMatcherTest.java | 4 +- .../matchers/PrerequisitesMatcherTest.java | 9 +- .../matchers/RuleBasedSegmentMatcherTest.java | 8 +- .../io/split/engine/matchers/SemverTest.java | 2 + .../engine/matchers/TransformersTest.java | 6 +- .../UserDefinedSegmentMatcherTest.java | 1 + .../ContainsAllOfSetMatcherTest.java | 1 + .../ContainsAnyOfSetMatcherTest.java | 2 + .../collections/EqualToSetMatcherTest.java | 2 + .../collections/PartOfSetMatcherTest.java | 2 + .../strings/ContainsAnyOfMatcherTest.java | 2 + .../strings/EndsWithAnyOfMatcherTest.java | 2 + .../strings/RegularExpressionMatcherTest.java | 2 + .../strings/StartsWithAnyOfMatcherTest.java | 2 + .../strings/WhitelistMatcherTest.java | 1 + .../engine/splitter/HashConsistencyTest.java | 2 +- .../java/io/split/engine/splitter/MyHash.java | 2 +- .../sse/workers/FeatureFlagWorkerImpTest.java | 8 +- .../storages/memory/InMemoryCacheTest.java | 4 +- ...RuleBasedSegmentCacheInMemoryImplTest.java | 14 +- pom.xml | 1 + 86 files changed, 476 insertions(+), 2814 deletions(-) delete mode 100644 client/src/main/java/io/split/client/utils/MurmurHash3.java delete mode 100644 client/src/main/java/io/split/engine/matchers/AllKeysMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/AttributeMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/BetweenMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/BetweenSemverMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/BooleanMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/CombiningMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/DependencyMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/EqualToMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/EqualToSemverMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/InListSemverMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/LessThanOrEqualToMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/Matcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/RuleBasedSegmentMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/Semver.java delete mode 100644 client/src/main/java/io/split/engine/matchers/Transformers.java delete mode 100644 client/src/main/java/io/split/engine/matchers/UserDefinedSegmentMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/collections/EqualToSetMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/collections/PartOfSetMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/strings/ContainsAnyOfMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/strings/RegularExpressionMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcher.java delete mode 100644 client/src/main/java/io/split/engine/matchers/strings/WhitelistMatcher.java 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..db8db3b1e 100644 --- a/client/src/main/java/io/split/client/CacheUpdaterService.java +++ b/client/src/main/java/io/split/client/CacheUpdaterService.java @@ -1,15 +1,13 @@ 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; @@ -22,7 +20,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 +28,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 +76,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 java.util.ArrayList<>(java.util.Arrays.asList( + new AttributeMatcher(null, new WhitelistMatcher(java.util.Arrays.asList(splitKey)), false)))), + new java.util.ArrayList<>(java.util.Arrays.asList(partition)), splitKey); return parsedCondition; } @@ -89,9 +88,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 java.util.ArrayList<>(java.util.Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))), + new java.util.ArrayList<>(java.util.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/client/utils/MurmurHash3.java b/client/src/main/java/io/split/client/utils/MurmurHash3.java deleted file mode 100644 index 94943515f..000000000 --- a/client/src/main/java/io/split/client/utils/MurmurHash3.java +++ /dev/null @@ -1,302 +0,0 @@ -package io.split.client.utils; - -/** - * The MurmurHash3 algorithm was created by Austin Appleby and placed in the public domain. - * This java port was authored by Yonik Seeley and also placed into the public domain. - * The author hereby disclaims copyright to this source code. - *

- * This produces exactly the same hash values as the final C++ - * version of MurmurHash3 and is thus suitable for producing the same hash values across - * platforms. - *

- * The 32 bit x86 version of this hash should be the fastest variant for relatively short keys like ids. - * murmurhash3_x64_128 is a good choice for longer strings or if you need more than 32 bits of hash. - *

- * Note - The x86 and x64 versions do _not_ produce the same results, as the - * algorithms are optimized for their respective platforms. - *

- * See http://github.com/yonik/java_util for future updates to this file. - */ -public final class MurmurHash3 { - - /** - * 128 bits of state - */ - public static final class LongPair { - public long val1; - public long val2; - } - - public static final int fmix32(int h) { - h ^= h >>> 16; - h *= 0x85ebca6b; - h ^= h >>> 13; - h *= 0xc2b2ae35; - h ^= h >>> 16; - return h; - } - - public static final long fmix64(long k) { - k ^= k >>> 33; - k *= 0xff51afd7ed558ccdL; - k ^= k >>> 33; - k *= 0xc4ceb9fe1a85ec53L; - k ^= k >>> 33; - return k; - } - - /** - * Gets a long from a byte buffer in little endian byte order. - */ - public static final long getLongLittleEndian(byte[] buf, int offset) { - return ((long) buf[offset + 7] << 56) // no mask needed - | ((buf[offset + 6] & 0xffL) << 48) - | ((buf[offset + 5] & 0xffL) << 40) - | ((buf[offset + 4] & 0xffL) << 32) - | ((buf[offset + 3] & 0xffL) << 24) - | ((buf[offset + 2] & 0xffL) << 16) - | ((buf[offset + 1] & 0xffL) << 8) - | ((buf[offset] & 0xffL)); // no shift needed - } - - - /** - * Returns the MurmurHash3_x86_32 hash of the UTF-8 bytes of the String without actually encoding - * the string to a temporary buffer. This is more than 2x faster than hashing the result - * of String.getBytes(). - */ - public static long murmurhash3_x86_32(CharSequence data, int offset, int len, int seed) { - - final int c1 = 0xcc9e2d51; - final int c2 = 0x1b873593; - - int h1 = seed; - - int pos = offset; - int end = offset + len; - int k1 = 0; - int k2 = 0; - int shift = 0; - int bits = 0; - int nBytes = 0; // length in UTF8 bytes - - - while (pos < end) { - int code = data.charAt(pos++); - if (code < 0x80) { - k2 = code; - bits = 8; - - } else if (code < 0x800) { - k2 = (0xC0 | (code >> 6)) - | ((0x80 | (code & 0x3F)) << 8); - bits = 16; - } else if (code < 0xD800 || code > 0xDFFF || pos >= end) { - // we check for pos>=end to encode an unpaired surrogate as 3 bytes. - k2 = (0xE0 | (code >> 12)) - | ((0x80 | ((code >> 6) & 0x3F)) << 8) - | ((0x80 | (code & 0x3F)) << 16); - bits = 24; - } else { - // surrogate pair - // int utf32 = pos < end ? (int) data.charAt(pos++) : 0; - int utf32 = (int) data.charAt(pos++); - utf32 = ((code - 0xD7C0) << 10) + (utf32 & 0x3FF); - k2 = (0xff & (0xF0 | (utf32 >> 18))) - | ((0x80 | ((utf32 >> 12) & 0x3F))) << 8 - | ((0x80 | ((utf32 >> 6) & 0x3F))) << 16 - | (0x80 | (utf32 & 0x3F)) << 24; - bits = 32; - } - - - k1 |= k2 << shift; - - // int used_bits = 32 - shift; // how many bits of k2 were used in k1. - // int unused_bits = bits - used_bits; // (bits-(32-shift)) == bits+shift-32 == bits-newshift - - shift += bits; - if (shift >= 32) { - // mix after we have a complete word - - k1 *= c1; - k1 = (k1 << 15) | (k1 >>> 17); // ROTL32(k1,15); - k1 *= c2; - - h1 ^= k1; - h1 = (h1 << 13) | (h1 >>> 19); // ROTL32(h1,13); - h1 = h1 * 5 + 0xe6546b64; - - shift -= 32; - // unfortunately, java won't let you shift 32 bits off, so we need to check for 0 - if (shift != 0) { - k1 = k2 >>> (bits - shift); // bits used == bits - newshift - } else { - k1 = 0; - } - nBytes += 4; - } - - } // inner - - // handle tail - if (shift > 0) { - nBytes += shift >> 3; - k1 *= c1; - k1 = (k1 << 15) | (k1 >>> 17); // ROTL32(k1,15); - k1 *= c2; - h1 ^= k1; - } - - // finalization - h1 ^= nBytes; - - // fmix(h1); - h1 ^= h1 >>> 16; - h1 *= 0x85ebca6b; - h1 ^= h1 >>> 13; - h1 *= 0xc2b2ae35; - h1 ^= h1 >>> 16; - - return h1 & 0xFFFFFFFFL; - } - - // The following set of methods and constants are borrowed from: - // `This method is borrowed from `org.apache.commons.codec.digest.MurmurHash3` - - // Constants for 128-bit variant - private static final long C1 = 0x87c37b91114253d5L; - private static final long C2 = 0x4cf5ad432745937fL; - private static final int R1 = 31; - private static final int R2 = 27; - private static final int R3 = 33; - private static final int M = 5; - private static final int N1 = 0x52dce729; - private static final int N2 = 0x38495ab5; - - /** - * Gets the little-endian long from 8 bytes starting at the specified index. - * - * @param data The data - * @param index The index - * @return The little-endian long - */ - private static long getLittleEndianLong(final byte[] data, final int index) { - return (((long) data[index ] & 0xff) ) | - (((long) data[index + 1] & 0xff) << 8) | - (((long) data[index + 2] & 0xff) << 16) | - (((long) data[index + 3] & 0xff) << 24) | - (((long) data[index + 4] & 0xff) << 32) | - (((long) data[index + 5] & 0xff) << 40) | - (((long) data[index + 6] & 0xff) << 48) | - (((long) data[index + 7] & 0xff) << 56); - } - - public static long[] hash128x64(final byte[] data) { - return hash128x64(data, 0, data.length, 0); - } - - /** - * Generates 128-bit hash from the byte array with the given offset, length and seed. - * - *

This is an implementation of the 128-bit hash function {@code MurmurHash3_x64_128} - * from from Austin Applyby's original MurmurHash3 {@code c++} code in SMHasher.

- * - * @param data The input byte array - * @param offset The first element of array - * @param length The length of array - * @param seed The initial seed value - * @return The 128-bit hash (2 longs) - */ - public static long[] hash128x64(final byte[] data, final int offset, final int length, final long seed) { - long h1 = seed; - long h2 = seed; - final int nblocks = length >> 4; - - // body - for (int i = 0; i < nblocks; i++) { - final int index = offset + (i << 4); - long k1 = getLittleEndianLong(data, index); - long k2 = getLittleEndianLong(data, index + 8); - - // mix functions for k1 - k1 *= C1; - k1 = Long.rotateLeft(k1, R1); - k1 *= C2; - h1 ^= k1; - h1 = Long.rotateLeft(h1, R2); - h1 += h2; - h1 = h1 * M + N1; - - // mix functions for k2 - k2 *= C2; - k2 = Long.rotateLeft(k2, R3); - k2 *= C1; - h2 ^= k2; - h2 = Long.rotateLeft(h2, R1); - h2 += h1; - h2 = h2 * M + N2; - } - - // tail - long k1 = 0; - long k2 = 0; - final int index = offset + (nblocks << 4); - switch (offset + length - index) { - case 15: - k2 ^= ((long) data[index + 14] & 0xff) << 48; - case 14: - k2 ^= ((long) data[index + 13] & 0xff) << 40; - case 13: - k2 ^= ((long) data[index + 12] & 0xff) << 32; - case 12: - k2 ^= ((long) data[index + 11] & 0xff) << 24; - case 11: - k2 ^= ((long) data[index + 10] & 0xff) << 16; - case 10: - k2 ^= ((long) data[index + 9] & 0xff) << 8; - case 9: - k2 ^= data[index + 8] & 0xff; - k2 *= C2; - k2 = Long.rotateLeft(k2, R3); - k2 *= C1; - h2 ^= k2; - - case 8: - k1 ^= ((long) data[index + 7] & 0xff) << 56; - case 7: - k1 ^= ((long) data[index + 6] & 0xff) << 48; - case 6: - k1 ^= ((long) data[index + 5] & 0xff) << 40; - case 5: - k1 ^= ((long) data[index + 4] & 0xff) << 32; - case 4: - k1 ^= ((long) data[index + 3] & 0xff) << 24; - case 3: - k1 ^= ((long) data[index + 2] & 0xff) << 16; - case 2: - k1 ^= ((long) data[index + 1] & 0xff) << 8; - case 1: - k1 ^= data[index] & 0xff; - k1 *= C1; - k1 = Long.rotateLeft(k1, R1); - k1 *= C2; - h1 ^= k1; - } - - // finalization - h1 ^= length; - h2 ^= length; - - h1 += h2; - h2 += h1; - - h1 = fmix64(h1); - h2 = fmix64(h2); - - h1 += h2; - h2 += h1; - - return new long[] { h1, h2 }; - } -} \ No newline at end of file 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..e58d1d762 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java @@ -1,9 +1,8 @@ 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.List; import java.util.Set; @@ -12,7 +11,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 +44,7 @@ public ParsedRuleBasedSegment( List excludedSegments ) { _ruleBasedSegment = ruleBasedSegment; - _parsedCondition = ImmutableList.copyOf(matcherAndSplits); + _parsedCondition = java.util.Collections.unmodifiableList(new java.util.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..4e20ed98e 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, + 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, + 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, + 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() { @@ -250,37 +284,52 @@ public String toString() { } + private 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(ParsedSplit::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 java.util.HashSet<>() : flagSets, impressionsDisabled, prereqs); + } + + private static io.split.rules.model.Condition toTargetingCondition(ParsedCondition c) { + List partitions = c.partitions() == null + ? Collections.emptyList() + : c.partitions().stream() + .map(p -> new io.split.rules.model.Partition(p.treatment, p.size)) + .collect(Collectors.toList()); + io.split.rules.model.ConditionType condType = c.conditionType() == ConditionType.ROLLOUT + ? io.split.rules.model.ConditionType.ROLLOUT + : io.split.rules.model.ConditionType.WHITELIST; + return new io.split.rules.model.Condition(condType, c.matcher(), partitions, c.label()); + } + 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..03cc4ec14 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -1,43 +1,41 @@ package io.split.engine.experiments; -import com.google.common.collect.Lists; +import io.split.client.dtos.DataType; 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 { private ParserUtils() { @@ -59,7 +57,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,115 +71,100 @@ 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(CombiningMatcher.Combiner.AND, toCombine); } + private static io.split.rules.model.DataType toRulesDataType(DataType dt) { + return io.split.rules.model.DataType.valueOf(dt.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(); break; case IN_SEGMENT: - checkNotNull(matcher.userDefinedSegmentMatcherData); String segmentName = matcher.userDefinedSegmentMatcherData.segmentName; delegate = new UserDefinedSegmentMatcher(segmentName); break; case WHITELIST: - checkNotNull(matcher.whitelistMatcherData); delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); 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); delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); break; case PART_OF_SET: - checkNotNull(matcher.whitelistMatcherData); delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case CONTAINS_ALL_OF_SET: - checkNotNull(matcher.whitelistMatcherData); delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case CONTAINS_ANY_OF_SET: - checkNotNull(matcher.whitelistMatcherData); 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 +172,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..1bf62dba5 100644 --- a/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java +++ b/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java @@ -1,9 +1,8 @@ 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; @@ -30,7 +29,7 @@ public ParsedRuleBasedSegment parse(RuleBasedSegment ruleBasedSegment) { } private ParsedRuleBasedSegment parseWithoutExceptionHandling(RuleBasedSegment ruleBasedSegment) { - List parsedConditionList = Lists.newArrayList(); + List parsedConditionList = new java.util.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..0cc589d34 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 java.util.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/matchers/AllKeysMatcher.java b/client/src/main/java/io/split/engine/matchers/AllKeysMatcher.java deleted file mode 100644 index 790224ab1..000000000 --- a/client/src/main/java/io/split/engine/matchers/AllKeysMatcher.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -/** - * A matcher that matches all keys. It returns true for everything. - * - * @author adil - */ -public final class AllKeysMatcher implements Matcher { - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - return true; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof AllKeysMatcher)) return false; - return true; - } - - @Override - public int hashCode() { - return 17; - } - - @Override - public String toString() { - return "in segment all"; - } -} diff --git a/client/src/main/java/io/split/engine/matchers/AttributeMatcher.java b/client/src/main/java/io/split/engine/matchers/AttributeMatcher.java deleted file mode 100644 index 92deb0140..000000000 --- a/client/src/main/java/io/split/engine/matchers/AttributeMatcher.java +++ /dev/null @@ -1,136 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.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"); - } - _matcher = new NegatableMatcher(matcher, negate); - } - - public boolean match(String key, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (_attribute == null) { - return _matcher.match(key, bucketingKey, attributes, evaluationContext); - } - - if (attributes == null) { - return false; - } - - Object value = attributes.get(_attribute); - if (value == null) { - return false; - } - - - return _matcher.match(value, bucketingKey, null, null); - } - - @Override - public int hashCode() { - return Objects.hash(_attribute, _matcher); - } - - public String attribute() { - return _attribute; - } - - public Matcher matcher() { - return _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); - } - - @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); - return bldr.toString(); - } - - public static final class NegatableMatcher implements Matcher { - private final boolean _negate; - private final Matcher _delegate; - - public NegatableMatcher(Matcher matcher, boolean negate) { - _negate = 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; - } - - @Override - 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); - } - - @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; - } - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/BetweenMatcher.java b/client/src/main/java/io/split/engine/matchers/BetweenMatcher.java deleted file mode 100644 index a0ccfc1b7..000000000 --- a/client/src/main/java/io/split/engine/matchers/BetweenMatcher.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.split.engine.matchers; - -import io.split.client.dtos.DataType; -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -import static io.split.engine.matchers.Transformers.asDateHourMinute; -import static io.split.engine.matchers.Transformers.asLong; - -/** - * Supports the logic: if user.age is between x and y - * - * @author adil - */ -public class BetweenMatcher implements Matcher { - private final long _start; - private final long _end; - private final long _normalizedStart; - private final long _normalizedEnd; - - private final DataType _dataType; - - public BetweenMatcher(long start, long end, DataType dataType) { - _start = start; - _end = end; - _dataType = dataType; - - if (_dataType == DataType.DATETIME) { - _normalizedStart = asDateHourMinute(_start); - _normalizedEnd = asDateHourMinute(_end); - } else { - _normalizedStart = _start; - _normalizedEnd = _end; - } - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - Long keyAsLong; - - if (_dataType == DataType.DATETIME) { - keyAsLong = asDateHourMinute(matchValue); - } else { - keyAsLong = asLong(matchValue); - } - - if (keyAsLong == null) { - return false; - } - - return keyAsLong >= _normalizedStart && keyAsLong <= _normalizedEnd; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("between "); - bldr.append(_start); - bldr.append(" and "); - bldr.append(_end); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + (int)(_start ^ (_start >>> 32)); - result = 31 * result + (int)(_end ^ (_end >>> 32)); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof BetweenMatcher)) return false; - - BetweenMatcher other = (BetweenMatcher) obj; - - return _start == other._start && _end == other._end; - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/BetweenSemverMatcher.java b/client/src/main/java/io/split/engine/matchers/BetweenSemverMatcher.java deleted file mode 100644 index 326e21830..000000000 --- a/client/src/main/java/io/split/engine/matchers/BetweenSemverMatcher.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -public class BetweenSemverMatcher implements Matcher { - - private final Semver _semverStart; - private final Semver _semverEnd; - - public BetweenSemverMatcher(String semverStart, String semverEnd) { - _semverStart = Semver.build(semverStart); - _semverEnd = Semver.build(semverEnd); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (!(matchValue instanceof String) || _semverStart == null || _semverEnd == null) { - return false; - } - Semver matchSemver = Semver.build(matchValue.toString()); - if (matchSemver == null) { - return false; - } - - return matchSemver.compare(_semverStart) >= 0 && matchSemver.compare(_semverEnd) <= 0; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("between semver "); - bldr.append(_semverStart.version()); - bldr.append(" and "); - bldr.append(_semverEnd.version()); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _semverStart.hashCode() + _semverEnd.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof BetweenSemverMatcher)) return false; - - BetweenSemverMatcher other = (BetweenSemverMatcher) obj; - - return _semverStart == other._semverStart && _semverEnd == other._semverEnd; - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/BooleanMatcher.java b/client/src/main/java/io/split/engine/matchers/BooleanMatcher.java deleted file mode 100644 index 79d5a303f..000000000 --- a/client/src/main/java/io/split/engine/matchers/BooleanMatcher.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -import static io.split.engine.matchers.Transformers.asBoolean; - -public class BooleanMatcher implements Matcher { - private boolean _booleanValue; - - public BooleanMatcher(boolean booleanValue) { - _booleanValue = booleanValue; - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - Boolean valueAsBoolean = asBoolean(matchValue); - - return valueAsBoolean != null && valueAsBoolean == _booleanValue; - } - - @Override - public String toString() { - return "is " + _booleanValue; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - BooleanMatcher that = (BooleanMatcher) o; - - return _booleanValue == that._booleanValue; - } - - @Override - public int hashCode() { - return (_booleanValue ? 1 : 0); - } -} 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/DependencyMatcher.java b/client/src/main/java/io/split/engine/matchers/DependencyMatcher.java deleted file mode 100644 index a3c3c4640..000000000 --- a/client/src/main/java/io/split/engine/matchers/DependencyMatcher.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Supports the logic: if user is in split "feature" treatments ["on","off"] - */ -public class DependencyMatcher implements Matcher { - private String _featureFlag; - private List _treatments; - - public DependencyMatcher(String featureFlag, List treatments) { - _featureFlag = featureFlag; - _treatments = treatments; - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof String)) { - return false; - } - - String result = evaluationContext.getEvaluator().evaluateFeature((String) matchValue, bucketingKey, _featureFlag, attributes).treatment; - - return _treatments.contains(result); - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("in split \""); - bldr.append(this._featureFlag); - bldr.append("\" treatment "); - bldr.append(this._treatments); - return bldr.toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - DependencyMatcher that = (DependencyMatcher) o; - - if (!Objects.equals(_featureFlag, that._featureFlag)) return false; - return Objects.equals(_treatments, that._treatments); - } - - @Override - public int hashCode() { - int result = _featureFlag != null ? _featureFlag.hashCode() : 0; - result = 31 * result + (_treatments != null ? _treatments.hashCode() : 0); - return result; - } -} diff --git a/client/src/main/java/io/split/engine/matchers/EqualToMatcher.java b/client/src/main/java/io/split/engine/matchers/EqualToMatcher.java deleted file mode 100644 index 9a1e32f37..000000000 --- a/client/src/main/java/io/split/engine/matchers/EqualToMatcher.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.split.engine.matchers; - -import io.split.client.dtos.DataType; -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -import static io.split.engine.matchers.Transformers.asDate; -import static io.split.engine.matchers.Transformers.asLong; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class EqualToMatcher implements Matcher { - - private final long _compareTo; - private final long _normalizedCompareTo; - private final DataType _dataType; - - public EqualToMatcher(long compareTo, DataType dataType) { - _compareTo = compareTo; - _dataType = dataType; - - if (_dataType == DataType.DATETIME) { - _normalizedCompareTo = asDate(_compareTo); - } else { - _normalizedCompareTo = _compareTo; - } - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - Long keyAsLong; - - if (_dataType == DataType.DATETIME) { - keyAsLong = asDate(matchValue); - } else { - keyAsLong = asLong(matchValue); - } - - return keyAsLong != null && keyAsLong == _normalizedCompareTo; - } - - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("== "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof EqualToMatcher)) return false; - - EqualToMatcher other = (EqualToMatcher) obj; - - return _compareTo == other._compareTo; - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/EqualToSemverMatcher.java b/client/src/main/java/io/split/engine/matchers/EqualToSemverMatcher.java deleted file mode 100644 index 64d9135d2..000000000 --- a/client/src/main/java/io/split/engine/matchers/EqualToSemverMatcher.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -public class EqualToSemverMatcher implements Matcher { - - private final Semver _semVer; - - public EqualToSemverMatcher(String semVer) { - _semVer = Semver.build(semVer); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (!(matchValue instanceof String) || _semVer == null) { - return false; - } - Semver matchSemver = Semver.build(matchValue.toString()); - if (matchSemver == null) { - return false; - } - - return matchSemver.version().equals(_semVer.version()); - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("== semver "); - bldr.append(_semVer.version()); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _semVer.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof EqualToSemverMatcher)) return false; - - EqualToSemverMatcher other = (EqualToSemverMatcher) obj; - - return _semVer == other._semVer; - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToMatcher.java b/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToMatcher.java deleted file mode 100644 index 1b83dc2c3..000000000 --- a/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToMatcher.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.split.engine.matchers; - -import io.split.client.dtos.DataType; -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -import static io.split.engine.matchers.Transformers.asDateHourMinute; -import static io.split.engine.matchers.Transformers.asLong; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class GreaterThanOrEqualToMatcher implements Matcher { - - private final long _compareTo; - private final long _normalizedCompareTo; - private final DataType _dataType; - - public GreaterThanOrEqualToMatcher(long compareTo, DataType dataType) { - _compareTo = compareTo; - _dataType = dataType; - - if (_dataType == DataType.DATETIME) { - _normalizedCompareTo = asDateHourMinute(_compareTo); - } else { - _normalizedCompareTo = _compareTo; - } - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - Long keyAsLong; - - if (_dataType == DataType.DATETIME) { - keyAsLong = asDateHourMinute(matchValue); - } else { - keyAsLong = asLong(matchValue); - } - - if (keyAsLong == null) { - return false; - } - - return keyAsLong >= _normalizedCompareTo; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append(">= "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof GreaterThanOrEqualToMatcher)) return false; - - GreaterThanOrEqualToMatcher other = (GreaterThanOrEqualToMatcher) obj; - - return _compareTo == other._compareTo; - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcher.java b/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcher.java deleted file mode 100644 index ffc714cca..000000000 --- a/client/src/main/java/io/split/engine/matchers/GreaterThanOrEqualToSemverMatcher.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -public class GreaterThanOrEqualToSemverMatcher implements Matcher { - - private final Semver _semVer; - - public GreaterThanOrEqualToSemverMatcher(String semVer) { - _semVer = Semver.build(semVer); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (!(matchValue instanceof String)|| _semVer == null) { - return false; - } - Semver matchSemver = Semver.build(matchValue.toString()); - if (matchSemver == null) { - return false; - } - - return matchSemver.compare(_semVer) >= 0; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append(">= semver "); - bldr.append(_semVer.version()); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _semVer.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof GreaterThanOrEqualToSemverMatcher)) return false; - - GreaterThanOrEqualToSemverMatcher other = (GreaterThanOrEqualToSemverMatcher) obj; - - return _semVer == other._semVer; - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/InListSemverMatcher.java b/client/src/main/java/io/split/engine/matchers/InListSemverMatcher.java deleted file mode 100644 index 69fd1ea45..000000000 --- a/client/src/main/java/io/split/engine/matchers/InListSemverMatcher.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -public class InListSemverMatcher implements Matcher { - - private final Set _semverlist = new HashSet<>(); - - public InListSemverMatcher(Collection whitelist) { - for (String item : whitelist) { - Semver semver = Semver.build(item); - if (semver == null) continue; - - _semverlist.add(semver); - } - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (!(matchValue instanceof String) || _semverlist.isEmpty()) { - return false; - } - Semver matchSemver = Semver.build(matchValue.toString()); - if (matchSemver == null) { - return false; - } - - for (Semver semverItem : _semverlist) { - if (semverItem.version().equals(matchSemver.version())) return true; - } - return false; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("in semver list ["); - boolean first = true; - - for (Semver item : _semverlist) { - if (!first) { - bldr.append(','); - } - bldr.append('"'); - bldr.append(item.version()); - bldr.append('"'); - first = false; - } - - bldr.append("]"); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _semverlist.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof InListSemverMatcher)) return false; - - InListSemverMatcher other = (InListSemverMatcher) obj; - - return _semverlist == other._semverlist; - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToMatcher.java b/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToMatcher.java deleted file mode 100644 index 24a74aaba..000000000 --- a/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToMatcher.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.split.engine.matchers; - -import io.split.client.dtos.DataType; -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -import static io.split.engine.matchers.Transformers.asDateHourMinute; -import static io.split.engine.matchers.Transformers.asLong; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class LessThanOrEqualToMatcher implements Matcher { - private final long _compareTo; - private final long _normalizedCompareTo; - private final DataType _dataType; - - public LessThanOrEqualToMatcher(long compareTo, DataType dataType) { - _compareTo = compareTo; - _dataType = dataType; - - if (_dataType == DataType.DATETIME) { - _normalizedCompareTo = asDateHourMinute(_compareTo); - } else { - _normalizedCompareTo = _compareTo; - } - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - Long keyAsLong; - - if (_dataType == DataType.DATETIME) { - keyAsLong = asDateHourMinute(matchValue); - } else { - keyAsLong = asLong(matchValue); - } - - if (keyAsLong == null) { - return false; - } - - return keyAsLong <= _normalizedCompareTo; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("<= "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof LessThanOrEqualToMatcher)) return false; - - LessThanOrEqualToMatcher other = (LessThanOrEqualToMatcher) obj; - - return _compareTo == other._compareTo; - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcher.java b/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcher.java deleted file mode 100644 index dd05f8c4d..000000000 --- a/client/src/main/java/io/split/engine/matchers/LessThanOrEqualToSemverMatcher.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.split.engine.matchers; - -import io.split.engine.evaluator.EvaluationContext; - -import java.util.Map; - -public class LessThanOrEqualToSemverMatcher implements Matcher { - - private final Semver _semVer; - - public LessThanOrEqualToSemverMatcher(String semVer) { - _semVer = Semver.build(semVer); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (!(matchValue instanceof String) || _semVer == null) { - return false; - } - Semver matchSemver = Semver.build(matchValue.toString()); - if (matchSemver == null) { - return false; - } - - return matchSemver.compare(_semVer) <= 0; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("<= semver "); - bldr.append(_semVer.version()); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _semVer.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof LessThanOrEqualToSemverMatcher)) return false; - - LessThanOrEqualToSemverMatcher other = (LessThanOrEqualToSemverMatcher) obj; - - return _semVer == other._semVer; - } - -} 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/PrerequisitesMatcher.java b/client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java deleted file mode 100644 index 122784498..000000000 --- a/client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.split.engine.matchers; - -import io.split.client.dtos.Prerequisites; -import io.split.engine.evaluator.EvaluationContext; - -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -public class PrerequisitesMatcher implements Matcher { - private List _prerequisites; - - public PrerequisitesMatcher(List prerequisites) { - _prerequisites = prerequisites; - } - - public List getPrerequisites() { return _prerequisites; } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof String)) { - return false; - } - - if (_prerequisites == null) { - return true; - } - - for (Prerequisites prerequisites : _prerequisites) { - String treatment = evaluationContext.getEvaluator().evaluateFeature((String) matchValue, bucketingKey, - prerequisites.featureFlagName, attributes). treatment; - if (!prerequisites.treatments.contains(treatment)) { - return false; - } - } - return true; - } - - @Override - 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(", "))); - } - return bldr.toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PrerequisitesMatcher that = (PrerequisitesMatcher) o; - - return Objects.equals(_prerequisites, that._prerequisites); - } - - @Override - public int hashCode() { - int result = _prerequisites != null ? _prerequisites.hashCode() : 0; - result = 31 * result + (_prerequisites != null ? _prerequisites.hashCode() : 0); - return result; - } -} 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/Semver.java b/client/src/main/java/io/split/engine/matchers/Semver.java deleted file mode 100644 index 7a85a0d72..000000000 --- a/client/src/main/java/io/split/engine/matchers/Semver.java +++ /dev/null @@ -1,176 +0,0 @@ -package io.split.engine.matchers; - -import io.split.client.exceptions.SemverParseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -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 Long _major; - private Long _minor; - private Long _patch; - private String[] _preRelease = new String[] {}; - private boolean _isStable; - private String _metadata; - private String _version; - - public static Semver build(String version) { - if (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; - } - } - - public String version() { - return _version; - } - - public Long major() { - return _major; - } - - public Long minor() { - return _minor; - } - - public Long patch() { - return _patch; - } - - public String[] prerelease() { - return _preRelease; - } - - public String metadata() { - return _metadata; - } - - public boolean isStable() { - return _isStable; - } - - /** - * Precedence comparision between 2 Semver objects. - * - * @return the value {@code 0} if {@code this == toCompare}; - * a value less than {@code 0} if {@code this < toCompare}; and - * a value greater than {@code 0} if {@code this > toCompare} - */ - public int compare(Semver toCompare) { - if (_version.equals(toCompare.version())) { - return 0; - } - // Compare major, minor, and patch versions numerically - int result = Long.compare(_major, toCompare.major()); - if (result != 0) { - return result; - } - result = Long.compare(_minor, toCompare.minor()); - if (result != 0) { - return result; - } - result = Long.compare(_patch, toCompare.patch()); - if (result != 0) { - return result; - } - if (!_isStable && toCompare.isStable()) { - return -1; - } else if (_isStable && !toCompare.isStable()) { - return 1; - } - // Compare pre-release versions lexically - int minLength = Math.min(_preRelease.length, toCompare.prerelease().length); - for (int i = 0; i < minLength; i++) { - if (_preRelease[i].equals(toCompare.prerelease()[i])) { - continue; - } - if ( isNumeric(_preRelease[i]) && isNumeric(toCompare._preRelease[i])) { - return Long.compare(Integer.parseInt(_preRelease[i]), Long.parseLong(toCompare._preRelease[i])); - } - return adjustNumber(_preRelease[i].compareTo(toCompare._preRelease[i])); - } - // Compare lengths of pre-release versions - return Integer.compare(_preRelease.length, toCompare._preRelease.length); - } - - private int adjustNumber(int number) { - if (number > 0) return 1; - if (number < 0) return -1; - return 0; - } - private Semver(String version) throws SemverParseException { - String vWithoutMetadata = setAndRemoveMetadataIfExists(version); - String vWithoutPreRelease = setAndRemovePreReleaseIfExists(vWithoutMetadata); - setMajorMinorAndPatch(vWithoutPreRelease); - _version = setVersion(); - } - private String setAndRemoveMetadataIfExists(String version) throws SemverParseException { - 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"); - } - return version.substring(0, index); - } - private String setAndRemovePreReleaseIfExists(String vWithoutMetadata) throws SemverParseException { - int index = vWithoutMetadata.indexOf(PRERELEASE_DELIMITER); - if (index == -1) { - _isStable = true; - return vWithoutMetadata; - } - String preReleaseData = vWithoutMetadata.substring(index+1); - _preRelease = preReleaseData.split(VALUE_DELIMITER); - if (_preRelease == null || Arrays.stream(_preRelease).allMatch(pr -> pr == null || pr.isEmpty())) { - throw new SemverParseException("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); - if (vParts.length != 3) - throw new SemverParseException("Unable to convert to Semver, incorrect format: " + version); - _major = Long.parseLong(vParts[0]); - _minor = Long.parseLong(vParts[1]); - _patch = Long.parseLong(vParts[2]); - } - - private String setVersion() { - String toReturn = _major + VALUE_DELIMITER + _minor + VALUE_DELIMITER + _patch; - if (_preRelease != null && _preRelease.length != 0) - { - for (int i = 0; i < _preRelease.length; i++) - { - if (isNumeric(_preRelease[i])) - { - _preRelease[i] = Long.toString(Long.parseLong(_preRelease[i])); - } - } - toReturn = toReturn + PRERELEASE_DELIMITER + String.join(VALUE_DELIMITER, _preRelease); - } - if (_metadata != null && !_metadata.isEmpty()) { - toReturn = toReturn + METADATA_DELIMITER + _metadata; - } - return toReturn; - } - - private static boolean isNumeric(String str) { - try { - Double.parseDouble(str); - return true; - } catch(NumberFormatException e){ - return false; - } - } -} diff --git a/client/src/main/java/io/split/engine/matchers/Transformers.java b/client/src/main/java/io/split/engine/matchers/Transformers.java deleted file mode 100644 index 17d9101fb..000000000 --- a/client/src/main/java/io/split/engine/matchers/Transformers.java +++ /dev/null @@ -1,104 +0,0 @@ -package io.split.engine.matchers; - -import com.google.common.collect.Sets; - -import java.util.Calendar; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; -import java.util.TimeZone; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class Transformers { - private static Set VALID_BOOLEAN_STRINGS = Sets.newHashSet("true", "false"); - private static TimeZone UTC = TimeZone.getTimeZone("UTC"); - - public static Long asLong(Object obj) { - if (obj == null) { - return null; - } - - if (obj instanceof Integer) { - return ((Integer) obj).longValue(); - } - - if (obj instanceof Long) { - return ((Long) obj).longValue(); - } - - return null; - } - - public static Long asDate(Object obj) { - Calendar c = toCalendar(obj); - - if (c == null) { - return null; - } - - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - - return c.getTimeInMillis(); - } - - public static Long asDateHourMinute(Object obj) { - - Calendar c = toCalendar(obj); - - if (c == null) { - return null; - } - - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - - return c.getTimeInMillis(); - } - - public static Boolean asBoolean(Object obj) { - if (obj == null) { - return null; - } - - if (obj instanceof Boolean) { - return (Boolean) obj; - } - - if (obj instanceof String) { - if (VALID_BOOLEAN_STRINGS.contains(((String) obj).toLowerCase())) { - return Boolean.parseBoolean((String) obj); - } - } - - return null; - } - - private static Calendar toCalendar(Object obj) { - Long millisecondsSinceEpoch = asLong(obj); - - if (millisecondsSinceEpoch == null) { - return null; - } - - Calendar c = Calendar.getInstance(); - c.setTimeZone(UTC); - c.setTimeInMillis(millisecondsSinceEpoch.longValue()); - - return c; - } - - - public static Set toSetOfStrings(Collection key) { - Set result = new HashSet(key.size()); - for (Object o : key) { - result.add(o.toString()); - } - return result; - } - -} 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/matchers/collections/ContainsAllOfSetMatcher.java b/client/src/main/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcher.java deleted file mode 100644 index 5f4f9433a..000000000 --- a/client/src/main/java/io/split/engine/matchers/collections/ContainsAllOfSetMatcher.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.split.engine.matchers.collections; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.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; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class ContainsAllOfSetMatcher implements Matcher { - private final Set _compareTo = new HashSet<>(); - - public ContainsAllOfSetMatcher(Collection compareTo) { - if (compareTo == null) { - throw new IllegalArgumentException("Null whitelist"); - } - _compareTo.addAll(compareTo); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof Collection)) { - return false; - } - - if (_compareTo.isEmpty()) { - return false; - } - - Set keyAsSet = toSetOfStrings((Collection) matchValue); - return keyAsSet.containsAll(_compareTo); - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("contains all of "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _compareTo.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof ContainsAllOfSetMatcher)) return false; - - ContainsAllOfSetMatcher other = (ContainsAllOfSetMatcher) obj; - - return _compareTo.equals(other._compareTo); - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcher.java b/client/src/main/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcher.java deleted file mode 100644 index 3a2514401..000000000 --- a/client/src/main/java/io/split/engine/matchers/collections/ContainsAnyOfSetMatcher.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.split.engine.matchers.collections; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.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; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class ContainsAnyOfSetMatcher implements Matcher { - - private final Set _compareTo = new HashSet<>(); - - public ContainsAnyOfSetMatcher(Collection compareTo) { - if (compareTo == null) { - throw new IllegalArgumentException("Null whitelist"); - } - _compareTo.addAll(compareTo); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof Collection)) { - return false; - } - - Set keyAsSet = toSetOfStrings((Collection) matchValue); - - for (String s : _compareTo) { - if ((keyAsSet.contains(s))) { - return true; - } - } - - return false; - } - - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("contains any of "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _compareTo.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof ContainsAnyOfSetMatcher)) return false; - - ContainsAnyOfSetMatcher other = (ContainsAnyOfSetMatcher) obj; - - return _compareTo.equals(other._compareTo); - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/collections/EqualToSetMatcher.java b/client/src/main/java/io/split/engine/matchers/collections/EqualToSetMatcher.java deleted file mode 100644 index 4a09c9efc..000000000 --- a/client/src/main/java/io/split/engine/matchers/collections/EqualToSetMatcher.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.split.engine.matchers.collections; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.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; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class EqualToSetMatcher implements Matcher { - - private final Set _compareTo = new HashSet<>(); - - public EqualToSetMatcher(Collection compareTo) { - if (compareTo == null) { - throw new IllegalArgumentException("Null whitelist"); - } - _compareTo.addAll(compareTo); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof Collection)) { - return false; - } - - Set keyAsSet = toSetOfStrings((Collection) matchValue); - - return keyAsSet.equals(_compareTo); - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("is equal to "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _compareTo.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof EqualToSetMatcher)) return false; - - EqualToSetMatcher other = (EqualToSetMatcher) obj; - - return _compareTo.equals(other._compareTo); - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/collections/PartOfSetMatcher.java b/client/src/main/java/io/split/engine/matchers/collections/PartOfSetMatcher.java deleted file mode 100644 index 8bb5f1399..000000000 --- a/client/src/main/java/io/split/engine/matchers/collections/PartOfSetMatcher.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.split.engine.matchers.collections; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.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; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class PartOfSetMatcher implements Matcher { - - private final Set _compareTo = new HashSet<>(); - - public PartOfSetMatcher(Collection compareTo) { - if (compareTo == null) { - throw new IllegalArgumentException("Null whitelist"); - } - _compareTo.addAll(compareTo); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof Collection)) { - return false; - } - - Set keyAsSet = toSetOfStrings((Collection) matchValue); - - if (keyAsSet.isEmpty()) { - return false; - } - - return _compareTo.containsAll(keyAsSet); - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("is part of "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _compareTo.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof PartOfSetMatcher)) return false; - - PartOfSetMatcher other = (PartOfSetMatcher) obj; - - return _compareTo.equals(other._compareTo); - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/strings/ContainsAnyOfMatcher.java b/client/src/main/java/io/split/engine/matchers/strings/ContainsAnyOfMatcher.java deleted file mode 100644 index b8cbe8fca..000000000 --- a/client/src/main/java/io/split/engine/matchers/strings/ContainsAnyOfMatcher.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.split.engine.matchers.strings; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class ContainsAnyOfMatcher implements Matcher { - - private final Set _compareTo = new HashSet<>(); - - public ContainsAnyOfMatcher(Collection compareTo) { - if (compareTo == null) { - throw new IllegalArgumentException("Null whitelist"); - } - _compareTo.addAll(compareTo); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof String) ) { - return false; - } - - if (_compareTo.isEmpty()) { - return false; - } - - String keyAsString = (String) matchValue; - - for (String s : _compareTo) { - if (s.isEmpty()) { - // ignore empty strings. - continue; - } - if (keyAsString.contains(s)) { - return true; - } - } - - return false; - } - - - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("contains "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _compareTo.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof ContainsAnyOfMatcher)) return false; - - ContainsAnyOfMatcher other = (ContainsAnyOfMatcher) obj; - - return _compareTo.equals(other._compareTo); - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcher.java b/client/src/main/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcher.java deleted file mode 100644 index 32ac9f7f3..000000000 --- a/client/src/main/java/io/split/engine/matchers/strings/EndsWithAnyOfMatcher.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.split.engine.matchers.strings; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class EndsWithAnyOfMatcher implements Matcher { - - private final Set _compareTo = new HashSet<>(); - - public EndsWithAnyOfMatcher(Collection compareTo) { - if (compareTo == null) { - throw new IllegalArgumentException("Null whitelist"); - } - _compareTo.addAll(compareTo); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof String) ) { - return false; - } - - if (_compareTo.isEmpty()) { - return false; - } - - String keyAsString = (String) matchValue; - - for (String s : _compareTo) { - if (s.isEmpty()) { - // ignore empty strings. - continue; - } - if (keyAsString.endsWith(s)) { - return true; - } - } - - return false; - } - - - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("ends with "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _compareTo.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof EndsWithAnyOfMatcher)) return false; - - EndsWithAnyOfMatcher other = (EndsWithAnyOfMatcher) obj; - - return _compareTo.equals(other._compareTo); - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/strings/RegularExpressionMatcher.java b/client/src/main/java/io/split/engine/matchers/strings/RegularExpressionMatcher.java deleted file mode 100644 index f64b3264b..000000000 --- a/client/src/main/java/io/split/engine/matchers/strings/RegularExpressionMatcher.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.split.engine.matchers.strings; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; - -import java.util.Map; -import java.util.regex.Pattern; - -public class RegularExpressionMatcher implements Matcher { - private String _stringMatcher; - private Pattern _pattern; - - public RegularExpressionMatcher(String matcherValue) { - _stringMatcher = matcherValue; - _pattern = Pattern.compile(matcherValue); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - if (matchValue instanceof String) { - java.util.regex.Matcher matcher = _pattern.matcher((String) matchValue); - return matcher.find(); - } - - return false; - } - - @Override - public String toString() { - return "matches " + _stringMatcher; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - RegularExpressionMatcher that = (RegularExpressionMatcher) o; - - return _stringMatcher != null ? _stringMatcher.equals(that._stringMatcher) : that._stringMatcher == null; - } - - @Override - public int hashCode() { - return _stringMatcher != null ? _stringMatcher.hashCode() : 0; - } -} diff --git a/client/src/main/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcher.java b/client/src/main/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcher.java deleted file mode 100644 index 7f1ed2cad..000000000 --- a/client/src/main/java/io/split/engine/matchers/strings/StartsWithAnyOfMatcher.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.split.engine.matchers.strings; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Created by adilaijaz on 3/7/16. - */ -public class StartsWithAnyOfMatcher implements Matcher { - - private final Set _compareTo = new HashSet<>(); - - public StartsWithAnyOfMatcher(Collection compareTo) { - if (compareTo == null) { - throw new IllegalArgumentException("Null whitelist"); - } - _compareTo.addAll(compareTo); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - if (matchValue == null) { - return false; - } - - if (!(matchValue instanceof String) ) { - return false; - } - - if (_compareTo.isEmpty()) { - return false; - } - - String keyAsString = (String) matchValue; - - for (String s : _compareTo) { - if (s.isEmpty()) { - // ignore empty strings. - continue; - } - if (keyAsString.startsWith(s)) { - return true; - } - - } - - return false; - } - - - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("starts with "); - bldr.append(_compareTo); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _compareTo.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof StartsWithAnyOfMatcher)) return false; - - StartsWithAnyOfMatcher other = (StartsWithAnyOfMatcher) obj; - - return _compareTo.equals(other._compareTo); - } - -} diff --git a/client/src/main/java/io/split/engine/matchers/strings/WhitelistMatcher.java b/client/src/main/java/io/split/engine/matchers/strings/WhitelistMatcher.java deleted file mode 100644 index 5068c1437..000000000 --- a/client/src/main/java/io/split/engine/matchers/strings/WhitelistMatcher.java +++ /dev/null @@ -1,67 +0,0 @@ -package io.split.engine.matchers.strings; - -import io.split.engine.evaluator.EvaluationContext; -import io.split.engine.matchers.Matcher; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Created by adilaijaz on 5/4/15. - */ -public class WhitelistMatcher implements Matcher { - private final Set _whitelist = new HashSet<>(); - - public WhitelistMatcher(Collection whitelist) { - if (whitelist == null) { - throw new IllegalArgumentException("Null whitelist parameter"); - } - _whitelist.addAll(whitelist); - } - - @Override - public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { - return _whitelist.contains(matchValue); - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - bldr.append("in segment ["); - boolean first = true; - - for (String item : _whitelist) { - if (!first) { - bldr.append(','); - } - bldr.append('"'); - bldr.append(item); - bldr.append('"'); - first = false; - } - - bldr.append("]"); - return bldr.toString(); - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _whitelist.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof WhitelistMatcher)) return false; - - WhitelistMatcher other = (WhitelistMatcher) obj; - - return _whitelist.equals(other._whitelist); - } - -} 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/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 From 0a24b5649f93a52d8dafafe6136d939bf500f672 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 1 Apr 2026 15:43:47 -0300 Subject: [PATCH 2/9] Targeting rule evaluator AI-Session-Id: 095253e8-7e1c-4578-9779-bf96395021cf AI-Tool: claude-code AI-Model: unknown --- targeting-engine/pom.xml | 50 +++ .../io/split/rules/bucketing/Bucketer.java | 78 +++++ .../io/split/rules/bucketing/MurmurHash3.java | 302 ++++++++++++++++++ .../split/rules/engine/EvaluationContext.java | 25 ++ .../split/rules/engine/EvaluationLabels.java | 14 + .../split/rules/engine/EvaluationResult.java | 25 ++ .../split/rules/engine/TargetingEngine.java | 16 + .../rules/engine/TargetingEngineImpl.java | 74 +++++ .../exceptions/VersionedExceptionWrapper.java | 19 ++ .../java/io/split/rules/logging/Logger.java | 8 + .../split/rules/matchers/AllKeysMatcher.java | 39 +++ .../rules/matchers/AttributeMatcher.java | 105 ++++++ .../split/rules/matchers/BetweenMatcher.java | 84 +++++ .../rules/matchers/BetweenSemverMatcher.java | 58 ++++ .../split/rules/matchers/BooleanMatcher.java | 46 +++ .../rules/matchers/CombiningMatcher.java | 73 +++++ .../rules/matchers/DependencyMatcher.java | 63 ++++ .../split/rules/matchers/EqualToMatcher.java | 71 ++++ .../rules/matchers/EqualToSemverMatcher.java | 54 ++++ .../matchers/GreaterThanOrEqualToMatcher.java | 74 +++++ .../GreaterThanOrEqualToSemverMatcher.java | 54 ++++ .../rules/matchers/InListSemverMatcher.java | 77 +++++ .../matchers/LessThanOrEqualToMatcher.java | 73 +++++ .../LessThanOrEqualToSemverMatcher.java | 54 ++++ .../java/io/split/rules/matchers/Matcher.java | 9 + .../rules/matchers/PrerequisitesMatcher.java | 71 ++++ .../matchers/RuleBasedSegmentMatcher.java | 41 +++ .../java/io/split/rules/matchers/Semver.java | 171 ++++++++++ .../io/split/rules/matchers/Transformers.java | 103 ++++++ .../matchers/UserDefinedSegmentMatcher.java | 40 +++ .../rules/matchers/WhitelistMatcher.java | 66 ++++ .../collections/ContainsAllOfSetMatcher.java | 70 ++++ .../collections/ContainsAnyOfSetMatcher.java | 75 +++++ .../collections/EqualToSetMatcher.java | 68 ++++ .../collections/PartOfSetMatcher.java | 72 +++++ .../strings/ContainsAnyOfMatcher.java | 83 +++++ .../strings/EndsWithAnyOfMatcher.java | 83 +++++ .../strings/RegularExpressionMatcher.java | 51 +++ .../strings/StartsWithAnyOfMatcher.java | 83 +++++ .../java/io/split/rules/model/Condition.java | 36 +++ .../io/split/rules/model/ConditionType.java | 6 + .../java/io/split/rules/model/DataType.java | 7 + .../java/io/split/rules/model/Partition.java | 11 + .../io/split/rules/model/Prerequisite.java | 37 +++ .../io/split/rules/model/TargetingRule.java | 73 +++++ .../split/rules/bucketing/BucketerTest.java | 88 +++++ .../rules/engine/TargetingEngineImplTest.java | 161 ++++++++++ .../rules/matchers/AllKeysMatcherTest.java | 26 ++ .../rules/matchers/BetweenMatcherTest.java | 37 +++ .../rules/matchers/BooleanMatcherTest.java | 34 ++ .../io/split/rules/matchers/SemverTest.java | 52 +++ 51 files changed, 3190 insertions(+) create mode 100644 targeting-engine/pom.xml create mode 100644 targeting-engine/src/main/java/io/split/rules/bucketing/Bucketer.java create mode 100644 targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/EvaluationContext.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/EvaluationLabels.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/TargetingEngine.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java create mode 100644 targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java create mode 100644 targeting-engine/src/main/java/io/split/rules/logging/Logger.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/CombiningMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/Matcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/RuleBasedSegmentMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/Semver.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/UserDefinedSegmentMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/Condition.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/ConditionType.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/DataType.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/Partition.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/Prerequisite.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java create mode 100644 targeting-engine/src/test/java/io/split/rules/bucketing/BucketerTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/matchers/AllKeysMatcherTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/matchers/BetweenMatcherTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/matchers/BooleanMatcherTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/matchers/SemverTest.java 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/targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java b/targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java new file mode 100644 index 000000000..da0376d8e --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java @@ -0,0 +1,302 @@ +package io.split.rules.bucketing; + +/** + * The MurmurHash3 algorithm was created by Austin Appleby and placed in the public domain. + * This java port was authored by Yonik Seeley and also placed into the public domain. + * The author hereby disclaims copyright to this source code. + *

+ * This produces exactly the same hash values as the final C++ + * version of MurmurHash3 and is thus suitable for producing the same hash values across + * platforms. + *

+ * The 32 bit x86 version of this hash should be the fastest variant for relatively short keys like ids. + * murmurhash3_x64_128 is a good choice for longer strings or if you need more than 32 bits of hash. + *

+ * Note - The x86 and x64 versions do _not_ produce the same results, as the + * algorithms are optimized for their respective platforms. + *

+ * See http://github.com/yonik/java_util for future updates to this file. + */ +public final class MurmurHash3 { + + /** + * 128 bits of state + */ + public static final class LongPair { + public long val1; + public long val2; + } + + public static final int fmix32(int h) { + h ^= h >>> 16; + h *= 0x85ebca6b; + h ^= h >>> 13; + h *= 0xc2b2ae35; + h ^= h >>> 16; + return h; + } + + public static final long fmix64(long k) { + k ^= k >>> 33; + k *= 0xff51afd7ed558ccdL; + k ^= k >>> 33; + k *= 0xc4ceb9fe1a85ec53L; + k ^= k >>> 33; + return k; + } + + /** + * Gets a long from a byte buffer in little endian byte order. + */ + public static final long getLongLittleEndian(byte[] buf, int offset) { + return ((long) buf[offset + 7] << 56) // no mask needed + | ((buf[offset + 6] & 0xffL) << 48) + | ((buf[offset + 5] & 0xffL) << 40) + | ((buf[offset + 4] & 0xffL) << 32) + | ((buf[offset + 3] & 0xffL) << 24) + | ((buf[offset + 2] & 0xffL) << 16) + | ((buf[offset + 1] & 0xffL) << 8) + | ((buf[offset] & 0xffL)); // no shift needed + } + + + /** + * Returns the MurmurHash3_x86_32 hash of the UTF-8 bytes of the String without actually encoding + * the string to a temporary buffer. This is more than 2x faster than hashing the result + * of String.getBytes(). + */ + public static long murmurhash3_x86_32(CharSequence data, int offset, int len, int seed) { + + final int c1 = 0xcc9e2d51; + final int c2 = 0x1b873593; + + int h1 = seed; + + int pos = offset; + int end = offset + len; + int k1 = 0; + int k2 = 0; + int shift = 0; + int bits = 0; + int nBytes = 0; // length in UTF8 bytes + + + while (pos < end) { + int code = data.charAt(pos++); + if (code < 0x80) { + k2 = code; + bits = 8; + + } else if (code < 0x800) { + k2 = (0xC0 | (code >> 6)) + | ((0x80 | (code & 0x3F)) << 8); + bits = 16; + } else if (code < 0xD800 || code > 0xDFFF || pos >= end) { + // we check for pos>=end to encode an unpaired surrogate as 3 bytes. + k2 = (0xE0 | (code >> 12)) + | ((0x80 | ((code >> 6) & 0x3F)) << 8) + | ((0x80 | (code & 0x3F)) << 16); + bits = 24; + } else { + // surrogate pair + // int utf32 = pos < end ? (int) data.charAt(pos++) : 0; + int utf32 = (int) data.charAt(pos++); + utf32 = ((code - 0xD7C0) << 10) + (utf32 & 0x3FF); + k2 = (0xff & (0xF0 | (utf32 >> 18))) + | ((0x80 | ((utf32 >> 12) & 0x3F))) << 8 + | ((0x80 | ((utf32 >> 6) & 0x3F))) << 16 + | (0x80 | (utf32 & 0x3F)) << 24; + bits = 32; + } + + + k1 |= k2 << shift; + + // int used_bits = 32 - shift; // how many bits of k2 were used in k1. + // int unused_bits = bits - used_bits; // (bits-(32-shift)) == bits+shift-32 == bits-newshift + + shift += bits; + if (shift >= 32) { + // mix after we have a complete word + + k1 *= c1; + k1 = (k1 << 15) | (k1 >>> 17); // ROTL32(k1,15); + k1 *= c2; + + h1 ^= k1; + h1 = (h1 << 13) | (h1 >>> 19); // ROTL32(h1,13); + h1 = h1 * 5 + 0xe6546b64; + + shift -= 32; + // unfortunately, java won't let you shift 32 bits off, so we need to check for 0 + if (shift != 0) { + k1 = k2 >>> (bits - shift); // bits used == bits - newshift + } else { + k1 = 0; + } + nBytes += 4; + } + + } // inner + + // handle tail + if (shift > 0) { + nBytes += shift >> 3; + k1 *= c1; + k1 = (k1 << 15) | (k1 >>> 17); // ROTL32(k1,15); + k1 *= c2; + h1 ^= k1; + } + + // finalization + h1 ^= nBytes; + + // fmix(h1); + h1 ^= h1 >>> 16; + h1 *= 0x85ebca6b; + h1 ^= h1 >>> 13; + h1 *= 0xc2b2ae35; + h1 ^= h1 >>> 16; + + return h1 & 0xFFFFFFFFL; + } + + // The following set of methods and constants are borrowed from: + // `This method is borrowed from `org.apache.commons.codec.digest.MurmurHash3` + + // Constants for 128-bit variant + private static final long C1 = 0x87c37b91114253d5L; + private static final long C2 = 0x4cf5ad432745937fL; + private static final int R1 = 31; + private static final int R2 = 27; + private static final int R3 = 33; + private static final int M = 5; + private static final int N1 = 0x52dce729; + private static final int N2 = 0x38495ab5; + + /** + * Gets the little-endian long from 8 bytes starting at the specified index. + * + * @param data The data + * @param index The index + * @return The little-endian long + */ + private static long getLittleEndianLong(final byte[] data, final int index) { + return (((long) data[index ] & 0xff) ) | + (((long) data[index + 1] & 0xff) << 8) | + (((long) data[index + 2] & 0xff) << 16) | + (((long) data[index + 3] & 0xff) << 24) | + (((long) data[index + 4] & 0xff) << 32) | + (((long) data[index + 5] & 0xff) << 40) | + (((long) data[index + 6] & 0xff) << 48) | + (((long) data[index + 7] & 0xff) << 56); + } + + public static long[] hash128x64(final byte[] data) { + return hash128x64(data, 0, data.length, 0); + } + + /** + * Generates 128-bit hash from the byte array with the given offset, length and seed. + * + *

This is an implementation of the 128-bit hash function {@code MurmurHash3_x64_128} + * from from Austin Applyby's original MurmurHash3 {@code c++} code in SMHasher.

+ * + * @param data The input byte array + * @param offset The first element of array + * @param length The length of array + * @param seed The initial seed value + * @return The 128-bit hash (2 longs) + */ + public static long[] hash128x64(final byte[] data, final int offset, final int length, final long seed) { + long h1 = seed; + long h2 = seed; + final int nblocks = length >> 4; + + // body + for (int i = 0; i < nblocks; i++) { + final int index = offset + (i << 4); + long k1 = getLittleEndianLong(data, index); + long k2 = getLittleEndianLong(data, index + 8); + + // mix functions for k1 + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + h1 ^= k1; + h1 = Long.rotateLeft(h1, R2); + h1 += h2; + h1 = h1 * M + N1; + + // mix functions for k2 + k2 *= C2; + k2 = Long.rotateLeft(k2, R3); + k2 *= C1; + h2 ^= k2; + h2 = Long.rotateLeft(h2, R1); + h2 += h1; + h2 = h2 * M + N2; + } + + // tail + long k1 = 0; + long k2 = 0; + final int index = offset + (nblocks << 4); + switch (offset + length - index) { + case 15: + k2 ^= ((long) data[index + 14] & 0xff) << 48; + case 14: + k2 ^= ((long) data[index + 13] & 0xff) << 40; + case 13: + k2 ^= ((long) data[index + 12] & 0xff) << 32; + case 12: + k2 ^= ((long) data[index + 11] & 0xff) << 24; + case 11: + k2 ^= ((long) data[index + 10] & 0xff) << 16; + case 10: + k2 ^= ((long) data[index + 9] & 0xff) << 8; + case 9: + k2 ^= data[index + 8] & 0xff; + k2 *= C2; + k2 = Long.rotateLeft(k2, R3); + k2 *= C1; + h2 ^= k2; + + case 8: + k1 ^= ((long) data[index + 7] & 0xff) << 56; + case 7: + k1 ^= ((long) data[index + 6] & 0xff) << 48; + case 6: + k1 ^= ((long) data[index + 5] & 0xff) << 40; + case 5: + k1 ^= ((long) data[index + 4] & 0xff) << 32; + case 4: + k1 ^= ((long) data[index + 3] & 0xff) << 24; + case 3: + k1 ^= ((long) data[index + 2] & 0xff) << 16; + case 2: + k1 ^= ((long) data[index + 1] & 0xff) << 8; + case 1: + k1 ^= data[index] & 0xff; + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + h1 ^= k1; + } + + // finalization + h1 ^= length; + h2 ^= length; + + h1 += h2; + h2 += h1; + + h1 = fmix64(h1); + h2 = fmix64(h2); + + h1 += h2; + h2 += h1; + + return new long[] { h1, h2 }; + } +} \ No newline at end of file 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/targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java new file mode 100644 index 000000000..dd9d9df57 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java @@ -0,0 +1,39 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +/** + * A matcher that matches all keys. It returns true for everything. + * + * @author adil + */ +public final class AllKeysMatcher implements Matcher { + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + return true; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof AllKeysMatcher)) return false; + return true; + } + + @Override + public int hashCode() { + return 17; + } + + @Override + public String toString() { + return "in segment all"; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java new file mode 100644 index 000000000..6ea30f10f --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java @@ -0,0 +1,105 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; +import java.util.Objects; + +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"); + _matcher = new NegatableMatcher(matcher, negate); + } + + public boolean match(String key, String bucketingKey, Map attributes, EvaluationContext context) { + if (_attribute == null) { + return _matcher.match(key, bucketingKey, attributes, context); + } + if (attributes == null) return false; + Object value = attributes.get(_attribute); + if (value == null) return false; + return _matcher.match(value, bucketingKey, null, null); + } + + public String attribute() { return _attribute; } + public Matcher matcher() { return _matcher; } + + public boolean isUserDefinedSegmentMatcher() { + return ((NegatableMatcher) _matcher).delegate() instanceof UserDefinedSegmentMatcher; + } + + public UserDefinedSegmentMatcher asUserDefinedSegmentMatcher() { + return (UserDefinedSegmentMatcher) ((NegatableMatcher) _matcher).delegate(); + } + + public boolean isRuleBasedSegmentMatcher() { + return ((NegatableMatcher) _matcher).delegate() instanceof RuleBasedSegmentMatcher; + } + + 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); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder("key"); + if (_attribute != null) bldr.append(".").append(_attribute); + bldr.append(" is").append(_matcher); + return bldr.toString(); + } + + public static final class NegatableMatcher implements Matcher { + private final boolean _negate; + private final Matcher _delegate; + + public NegatableMatcher(Matcher matcher, boolean negate) { + _negate = negate; + _delegate = matcher; + } + + @Override + 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); } + + @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); + } + + @Override + public String toString() { + return (_negate ? " not " : " ") + _delegate; + } + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java new file mode 100644 index 000000000..14de73b23 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java @@ -0,0 +1,84 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +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 + * + * @author adil + */ +public class BetweenMatcher implements Matcher { + private final long _start; + private final long _end; + private final long _normalizedStart; + private final long _normalizedEnd; + + private final DataType _dataType; + + public BetweenMatcher(long start, long end, DataType dataType) { + _start = start; + _end = end; + _dataType = dataType; + + if (_dataType == DataType.DATETIME) { + _normalizedStart = asDateHourMinute(_start); + _normalizedEnd = asDateHourMinute(_end); + } else { + _normalizedStart = _start; + _normalizedEnd = _end; + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + Long keyAsLong; + + if (_dataType == DataType.DATETIME) { + keyAsLong = asDateHourMinute(matchValue); + } else { + keyAsLong = asLong(matchValue); + } + + if (keyAsLong == null) { + return false; + } + + return keyAsLong >= _normalizedStart && keyAsLong <= _normalizedEnd; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("between "); + bldr.append(_start); + bldr.append(" and "); + bldr.append(_end); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)(_start ^ (_start >>> 32)); + result = 31 * result + (int)(_end ^ (_end >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof BetweenMatcher)) return false; + + BetweenMatcher other = (BetweenMatcher) obj; + + return _start == other._start && _end == other._end; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java new file mode 100644 index 000000000..e393c2372 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java @@ -0,0 +1,58 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public class BetweenSemverMatcher implements Matcher { + + private final Semver _semverStart; + private final Semver _semverEnd; + + public BetweenSemverMatcher(String semverStart, String semverEnd) { + _semverStart = Semver.build(semverStart); + _semverEnd = Semver.build(semverEnd); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String) || _semverStart == null || _semverEnd == null) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + return matchSemver.compare(_semverStart) >= 0 && matchSemver.compare(_semverEnd) <= 0; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("between semver "); + bldr.append(_semverStart.version()); + bldr.append(" and "); + bldr.append(_semverEnd.version()); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semverStart.hashCode() + _semverEnd.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof BetweenSemverMatcher)) return false; + + BetweenSemverMatcher other = (BetweenSemverMatcher) obj; + + return _semverStart == other._semverStart && _semverEnd == other._semverEnd; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java new file mode 100644 index 000000000..59800896b --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java @@ -0,0 +1,46 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asBoolean; + +public class BooleanMatcher implements Matcher { + private boolean _booleanValue; + + public BooleanMatcher(boolean booleanValue) { + _booleanValue = booleanValue; + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + Boolean valueAsBoolean = asBoolean(matchValue); + + return valueAsBoolean != null && valueAsBoolean == _booleanValue; + } + + @Override + public String toString() { + return "is " + _booleanValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BooleanMatcher that = (BooleanMatcher) o; + + return _booleanValue == that._booleanValue; + } + + @Override + public int hashCode() { + return (_booleanValue ? 1 : 0); + } +} 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/targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java new file mode 100644 index 000000000..8d4b521c9 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java @@ -0,0 +1,63 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Supports the logic: if user is in split "feature" treatments ["on","off"] + */ +public class DependencyMatcher implements Matcher { + private String _featureFlag; + private List _treatments; + + public DependencyMatcher(String featureFlag, List treatments) { + _featureFlag = featureFlag; + _treatments = treatments; + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String)) { + return false; + } + + String result = evaluationContext.evaluate((String) matchValue, bucketingKey, _featureFlag, attributes).treatment; + + return _treatments.contains(result); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("in split \""); + bldr.append(this._featureFlag); + bldr.append("\" treatment "); + bldr.append(this._treatments); + return bldr.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DependencyMatcher that = (DependencyMatcher) o; + + if (!Objects.equals(_featureFlag, that._featureFlag)) return false; + return Objects.equals(_treatments, that._treatments); + } + + @Override + public int hashCode() { + int result = _featureFlag != null ? _featureFlag.hashCode() : 0; + result = 31 * result + (_treatments != null ? _treatments.hashCode() : 0); + return result; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java new file mode 100644 index 000000000..8792648bd --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java @@ -0,0 +1,71 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asDate; +import static io.split.rules.matchers.Transformers.asLong; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class EqualToMatcher implements Matcher { + + private final long _compareTo; + private final long _normalizedCompareTo; + private final DataType _dataType; + + public EqualToMatcher(long compareTo, DataType dataType) { + _compareTo = compareTo; + _dataType = dataType; + + if (_dataType == DataType.DATETIME) { + _normalizedCompareTo = asDate(_compareTo); + } else { + _normalizedCompareTo = _compareTo; + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + Long keyAsLong; + + if (_dataType == DataType.DATETIME) { + keyAsLong = asDate(matchValue); + } else { + keyAsLong = asLong(matchValue); + } + + return keyAsLong != null && keyAsLong == _normalizedCompareTo; + } + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("== "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof EqualToMatcher)) return false; + + EqualToMatcher other = (EqualToMatcher) obj; + + return _compareTo == other._compareTo; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java new file mode 100644 index 000000000..0af499ae3 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java @@ -0,0 +1,54 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public class EqualToSemverMatcher implements Matcher { + + private final Semver _semVer; + + public EqualToSemverMatcher(String semVer) { + _semVer = Semver.build(semVer); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String) || _semVer == null) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + return matchSemver.version().equals(_semVer.version()); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("== semver "); + bldr.append(_semVer.version()); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semVer.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof EqualToSemverMatcher)) return false; + + EqualToSemverMatcher other = (EqualToSemverMatcher) obj; + + return _semVer == other._semVer; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java new file mode 100644 index 000000000..3c1a0b864 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java @@ -0,0 +1,74 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class GreaterThanOrEqualToMatcher implements Matcher { + + private final long _compareTo; + private final long _normalizedCompareTo; + private final DataType _dataType; + + public GreaterThanOrEqualToMatcher(long compareTo, DataType dataType) { + _compareTo = compareTo; + _dataType = dataType; + + if (_dataType == DataType.DATETIME) { + _normalizedCompareTo = asDateHourMinute(_compareTo); + } else { + _normalizedCompareTo = _compareTo; + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + Long keyAsLong; + + if (_dataType == DataType.DATETIME) { + keyAsLong = asDateHourMinute(matchValue); + } else { + keyAsLong = asLong(matchValue); + } + + if (keyAsLong == null) { + return false; + } + + return keyAsLong >= _normalizedCompareTo; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append(">= "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof GreaterThanOrEqualToMatcher)) return false; + + GreaterThanOrEqualToMatcher other = (GreaterThanOrEqualToMatcher) obj; + + return _compareTo == other._compareTo; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java new file mode 100644 index 000000000..6d92594a0 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java @@ -0,0 +1,54 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public class GreaterThanOrEqualToSemverMatcher implements Matcher { + + private final Semver _semVer; + + public GreaterThanOrEqualToSemverMatcher(String semVer) { + _semVer = Semver.build(semVer); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String)|| _semVer == null) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + return matchSemver.compare(_semVer) >= 0; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append(">= semver "); + bldr.append(_semVer.version()); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semVer.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof GreaterThanOrEqualToSemverMatcher)) return false; + + GreaterThanOrEqualToSemverMatcher other = (GreaterThanOrEqualToSemverMatcher) obj; + + return _semVer == other._semVer; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java new file mode 100644 index 000000000..f1d1422e8 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java @@ -0,0 +1,77 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class InListSemverMatcher implements Matcher { + + private final Set _semverlist = new HashSet<>(); + + public InListSemverMatcher(Collection whitelist) { + for (String item : whitelist) { + Semver semver = Semver.build(item); + if (semver == null) continue; + + _semverlist.add(semver); + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String) || _semverlist.isEmpty()) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + for (Semver semverItem : _semverlist) { + if (semverItem.version().equals(matchSemver.version())) return true; + } + return false; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("in semver list ["); + boolean first = true; + + for (Semver item : _semverlist) { + if (!first) { + bldr.append(','); + } + bldr.append('"'); + bldr.append(item.version()); + bldr.append('"'); + first = false; + } + + bldr.append("]"); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semverlist.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof InListSemverMatcher)) return false; + + InListSemverMatcher other = (InListSemverMatcher) obj; + + return _semverlist == other._semverlist; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java new file mode 100644 index 000000000..432821651 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java @@ -0,0 +1,73 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class LessThanOrEqualToMatcher implements Matcher { + private final long _compareTo; + private final long _normalizedCompareTo; + private final DataType _dataType; + + public LessThanOrEqualToMatcher(long compareTo, DataType dataType) { + _compareTo = compareTo; + _dataType = dataType; + + if (_dataType == DataType.DATETIME) { + _normalizedCompareTo = asDateHourMinute(_compareTo); + } else { + _normalizedCompareTo = _compareTo; + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + Long keyAsLong; + + if (_dataType == DataType.DATETIME) { + keyAsLong = asDateHourMinute(matchValue); + } else { + keyAsLong = asLong(matchValue); + } + + if (keyAsLong == null) { + return false; + } + + return keyAsLong <= _normalizedCompareTo; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("<= "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof LessThanOrEqualToMatcher)) return false; + + LessThanOrEqualToMatcher other = (LessThanOrEqualToMatcher) obj; + + return _compareTo == other._compareTo; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java new file mode 100644 index 000000000..0c6b499ac --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java @@ -0,0 +1,54 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public class LessThanOrEqualToSemverMatcher implements Matcher { + + private final Semver _semVer; + + public LessThanOrEqualToSemverMatcher(String semVer) { + _semVer = Semver.build(semVer); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String) || _semVer == null) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + return matchSemver.compare(_semVer) <= 0; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("<= semver "); + bldr.append(_semVer.version()); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semVer.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof LessThanOrEqualToSemverMatcher)) return false; + + LessThanOrEqualToSemverMatcher other = (LessThanOrEqualToSemverMatcher) obj; + + return _semVer == other._semVer; + } + +} 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/targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java new file mode 100644 index 000000000..5951c9fb0 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java @@ -0,0 +1,71 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.model.Prerequisite; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class PrerequisitesMatcher implements Matcher { + private List _prerequisites; + + public PrerequisitesMatcher(List prerequisites) { + _prerequisites = prerequisites; + } + + public List getPrerequisites() { return _prerequisites; } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String)) { + return false; + } + + if (_prerequisites == null) { + return true; + } + + for (Prerequisite prerequisites : _prerequisites) { + String treatment = evaluationContext.evaluate((String) matchValue, bucketingKey, + prerequisites.featureFlagName(), attributes).treatment; + if (!prerequisites.treatments().contains(treatment)) { + return false; + } + } + return true; + } + + @Override + 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(", "))); + } + return bldr.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PrerequisitesMatcher that = (PrerequisitesMatcher) o; + + return Objects.equals(_prerequisites, that._prerequisites); + } + + @Override + public int hashCode() { + int result = _prerequisites != null ? _prerequisites.hashCode() : 0; + result = 31 * result + (_prerequisites != null ? _prerequisites.hashCode() : 0); + return result; + } +} 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/targeting-engine/src/main/java/io/split/rules/matchers/Semver.java b/targeting-engine/src/main/java/io/split/rules/matchers/Semver.java new file mode 100644 index 000000000..a9f7f217f --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/Semver.java @@ -0,0 +1,171 @@ +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_REGEX = "\\."; + private static final String VALUE_DELIMITER = "."; + + private Long _major; + private Long _minor; + private Long _patch; + private String[] _preRelease = new String[] {}; + private boolean _isStable; + private String _metadata; + private String _version; + + public static Semver build(String version) { + if (version == null || version.isEmpty()) return null; + try { + return new Semver(version); + } catch (Exception ex) { + return null; + } + } + + public String version() { + return _version; + } + + public Long major() { + return _major; + } + + public Long minor() { + return _minor; + } + + public Long patch() { + return _patch; + } + + public String[] prerelease() { + return _preRelease; + } + + public String metadata() { + return _metadata; + } + + public boolean isStable() { + return _isStable; + } + + /** + * Precedence comparision between 2 Semver objects. + * + * @return the value {@code 0} if {@code this == toCompare}; + * a value less than {@code 0} if {@code this < toCompare}; and + * a value greater than {@code 0} if {@code this > toCompare} + */ + public int compare(Semver toCompare) { + if (_version.equals(toCompare.version())) { + return 0; + } + // Compare major, minor, and patch versions numerically + int result = Long.compare(_major, toCompare.major()); + if (result != 0) { + return result; + } + result = Long.compare(_minor, toCompare.minor()); + if (result != 0) { + return result; + } + result = Long.compare(_patch, toCompare.patch()); + if (result != 0) { + return result; + } + if (!_isStable && toCompare.isStable()) { + return -1; + } else if (_isStable && !toCompare.isStable()) { + return 1; + } + // Compare pre-release versions lexically + int minLength = Math.min(_preRelease.length, toCompare.prerelease().length); + for (int i = 0; i < minLength; i++) { + if (_preRelease[i].equals(toCompare.prerelease()[i])) { + continue; + } + if ( isNumeric(_preRelease[i]) && isNumeric(toCompare._preRelease[i])) { + return Long.compare(Integer.parseInt(_preRelease[i]), Long.parseLong(toCompare._preRelease[i])); + } + return adjustNumber(_preRelease[i].compareTo(toCompare._preRelease[i])); + } + // Compare lengths of pre-release versions + return Integer.compare(_preRelease.length, toCompare._preRelease.length); + } + + private int adjustNumber(int number) { + if (number > 0) return 1; + if (number < 0) return -1; + return 0; + } + private Semver(String version) { + String vWithoutMetadata = setAndRemoveMetadataIfExists(version); + String vWithoutPreRelease = setAndRemovePreReleaseIfExists(vWithoutMetadata); + setMajorMinorAndPatch(vWithoutPreRelease); + _version = setVersion(); + } + 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 IllegalArgumentException("Unable to convert to Semver, incorrect pre release data"); + } + return version.substring(0, index); + } + 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_REGEX); + if (_preRelease == null || Arrays.stream(_preRelease).allMatch(pr -> pr == null || pr.isEmpty())) { + throw new IllegalArgumentException("Unable to convert to Semver, incorrect pre release data"); + } + return vWithoutMetadata.substring(0, index); + } + private void setMajorMinorAndPatch(String version) { + String[] vParts = version.split(VALUE_DELIMITER_REGEX); + if (vParts.length != 3) + 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]); + } + + private String setVersion() { + String toReturn = _major + VALUE_DELIMITER + _minor + VALUE_DELIMITER + _patch; + if (_preRelease != null && _preRelease.length != 0) + { + for (int i = 0; i < _preRelease.length; i++) + { + if (isNumeric(_preRelease[i])) + { + _preRelease[i] = Long.toString(Long.parseLong(_preRelease[i])); + } + } + toReturn = toReturn + PRERELEASE_DELIMITER + String.join(VALUE_DELIMITER, _preRelease); + } + if (_metadata != null && !_metadata.isEmpty()) { + toReturn = toReturn + METADATA_DELIMITER + _metadata; + } + return toReturn; + } + + private static boolean isNumeric(String str) { + try { + Double.parseDouble(str); + return true; + } catch(NumberFormatException e){ + return false; + } + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java b/targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java new file mode 100644 index 000000000..b34e60991 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java @@ -0,0 +1,103 @@ +package io.split.rules.matchers; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.TimeZone; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class Transformers { + 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) { + if (obj == null) { + return null; + } + + if (obj instanceof Integer) { + return ((Integer) obj).longValue(); + } + + if (obj instanceof Long) { + return ((Long) obj).longValue(); + } + + return null; + } + + public static Long asDate(Object obj) { + Calendar c = toCalendar(obj); + + if (c == null) { + return null; + } + + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + + return c.getTimeInMillis(); + } + + public static Long asDateHourMinute(Object obj) { + + Calendar c = toCalendar(obj); + + if (c == null) { + return null; + } + + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + + return c.getTimeInMillis(); + } + + public static Boolean asBoolean(Object obj) { + if (obj == null) { + return null; + } + + if (obj instanceof Boolean) { + return (Boolean) obj; + } + + if (obj instanceof String) { + if (VALID_BOOLEAN_STRINGS.contains(((String) obj).toLowerCase())) { + return Boolean.parseBoolean((String) obj); + } + } + + return null; + } + + private static Calendar toCalendar(Object obj) { + Long millisecondsSinceEpoch = asLong(obj); + + if (millisecondsSinceEpoch == null) { + return null; + } + + Calendar c = Calendar.getInstance(); + c.setTimeZone(UTC); + c.setTimeInMillis(millisecondsSinceEpoch.longValue()); + + return c; + } + + + public static Set toSetOfStrings(Collection key) { + Set result = new HashSet(key.size()); + for (Object o : key) { + result.add(o.toString()); + } + return result; + } + +} 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/targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java new file mode 100644 index 000000000..64fb4753c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java @@ -0,0 +1,66 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Created by adilaijaz on 5/4/15. + */ +public class WhitelistMatcher implements Matcher { + private final Set _whitelist = new HashSet<>(); + + public WhitelistMatcher(Collection whitelist) { + if (whitelist == null) { + throw new IllegalArgumentException("Null whitelist parameter"); + } + _whitelist.addAll(whitelist); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + return _whitelist.contains(matchValue); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("in segment ["); + boolean first = true; + + for (String item : _whitelist) { + if (!first) { + bldr.append(','); + } + bldr.append('"'); + bldr.append(item); + bldr.append('"'); + first = false; + } + + bldr.append("]"); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _whitelist.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof WhitelistMatcher)) return false; + + WhitelistMatcher other = (WhitelistMatcher) obj; + + return _whitelist.equals(other._whitelist); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java new file mode 100644 index 000000000..65814087c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java @@ -0,0 +1,70 @@ +package io.split.rules.matchers.collections; + +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.rules.matchers.Transformers.toSetOfStrings; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class ContainsAllOfSetMatcher implements Matcher { + private final Set _compareTo = new HashSet<>(); + + public ContainsAllOfSetMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof Collection)) { + return false; + } + + if (_compareTo.isEmpty()) { + return false; + } + + Set keyAsSet = toSetOfStrings((Collection) matchValue); + return keyAsSet.containsAll(_compareTo); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("contains all of "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ContainsAllOfSetMatcher)) return false; + + ContainsAllOfSetMatcher other = (ContainsAllOfSetMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java new file mode 100644 index 000000000..2288020e1 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java @@ -0,0 +1,75 @@ +package io.split.rules.matchers.collections; + +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.rules.matchers.Transformers.toSetOfStrings; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class ContainsAnyOfSetMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public ContainsAnyOfSetMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof Collection)) { + return false; + } + + Set keyAsSet = toSetOfStrings((Collection) matchValue); + + for (String s : _compareTo) { + if ((keyAsSet.contains(s))) { + return true; + } + } + + return false; + } + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("contains any of "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ContainsAnyOfSetMatcher)) return false; + + ContainsAnyOfSetMatcher other = (ContainsAnyOfSetMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java new file mode 100644 index 000000000..212b9f0c7 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java @@ -0,0 +1,68 @@ +package io.split.rules.matchers.collections; + +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.rules.matchers.Transformers.toSetOfStrings; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class EqualToSetMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public EqualToSetMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof Collection)) { + return false; + } + + Set keyAsSet = toSetOfStrings((Collection) matchValue); + + return keyAsSet.equals(_compareTo); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("is equal to "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof EqualToSetMatcher)) return false; + + EqualToSetMatcher other = (EqualToSetMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java new file mode 100644 index 000000000..54aa9730b --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java @@ -0,0 +1,72 @@ +package io.split.rules.matchers.collections; + +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.rules.matchers.Transformers.toSetOfStrings; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class PartOfSetMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public PartOfSetMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof Collection)) { + return false; + } + + Set keyAsSet = toSetOfStrings((Collection) matchValue); + + if (keyAsSet.isEmpty()) { + return false; + } + + return _compareTo.containsAll(keyAsSet); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("is part of "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof PartOfSetMatcher)) return false; + + PartOfSetMatcher other = (PartOfSetMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java new file mode 100644 index 000000000..755a33aad --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java @@ -0,0 +1,83 @@ +package io.split.rules.matchers.strings; + +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; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class ContainsAnyOfMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public ContainsAnyOfMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String) ) { + return false; + } + + if (_compareTo.isEmpty()) { + return false; + } + + String keyAsString = (String) matchValue; + + for (String s : _compareTo) { + if (s.isEmpty()) { + // ignore empty strings. + continue; + } + if (keyAsString.contains(s)) { + return true; + } + } + + return false; + } + + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("contains "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ContainsAnyOfMatcher)) return false; + + ContainsAnyOfMatcher other = (ContainsAnyOfMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java new file mode 100644 index 000000000..64b67881a --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java @@ -0,0 +1,83 @@ +package io.split.rules.matchers.strings; + +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; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class EndsWithAnyOfMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public EndsWithAnyOfMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String) ) { + return false; + } + + if (_compareTo.isEmpty()) { + return false; + } + + String keyAsString = (String) matchValue; + + for (String s : _compareTo) { + if (s.isEmpty()) { + // ignore empty strings. + continue; + } + if (keyAsString.endsWith(s)) { + return true; + } + } + + return false; + } + + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("ends with "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof EndsWithAnyOfMatcher)) return false; + + EndsWithAnyOfMatcher other = (EndsWithAnyOfMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java new file mode 100644 index 000000000..5ae33cee4 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java @@ -0,0 +1,51 @@ +package io.split.rules.matchers.strings; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Map; +import java.util.regex.Pattern; + +public class RegularExpressionMatcher implements Matcher { + private String _stringMatcher; + private Pattern _pattern; + + public RegularExpressionMatcher(String matcherValue) { + _stringMatcher = matcherValue; + _pattern = Pattern.compile(matcherValue); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (matchValue instanceof String) { + java.util.regex.Matcher matcher = _pattern.matcher((String) matchValue); + return matcher.find(); + } + + return false; + } + + @Override + public String toString() { + return "matches " + _stringMatcher; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RegularExpressionMatcher that = (RegularExpressionMatcher) o; + + return _stringMatcher != null ? _stringMatcher.equals(that._stringMatcher) : that._stringMatcher == null; + } + + @Override + public int hashCode() { + return _stringMatcher != null ? _stringMatcher.hashCode() : 0; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java new file mode 100644 index 000000000..fb85d8fbc --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java @@ -0,0 +1,83 @@ +package io.split.rules.matchers.strings; + +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; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class StartsWithAnyOfMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public StartsWithAnyOfMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String) ) { + return false; + } + + if (_compareTo.isEmpty()) { + return false; + } + + String keyAsString = (String) matchValue; + + for (String s : _compareTo) { + if (s.isEmpty()) { + // ignore empty strings. + continue; + } + if (keyAsString.startsWith(s)) { + return true; + } + + } + + return false; + } + + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("starts with "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof StartsWithAnyOfMatcher)) return false; + + StartsWithAnyOfMatcher other = (StartsWithAnyOfMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} 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); + } +} From 521e8e9b07a50584c41691af432ad8d91e35636c Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 12:08:29 -0300 Subject: [PATCH 3/9] Fix imports AI-Session-Id: 7d0b171a-a576-4317-965f-99fb98c11ba8 AI-Tool: claude-code AI-Model: unknown --- .../io/split/client/CacheUpdaterService.java | 11 +-- .../evaluator/EvaluationContextImp.java | 72 +++++++++++++++++++ .../experiments/ParsedRuleBasedSegment.java | 4 +- .../split/engine/experiments/ParsedSplit.java | 2 +- .../split/engine/experiments/ParserUtils.java | 4 +- .../experiments/RuleBasedSegmentParser.java | 3 +- .../split/engine/experiments/SplitParser.java | 2 +- 7 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java diff --git a/client/src/main/java/io/split/client/CacheUpdaterService.java b/client/src/main/java/io/split/client/CacheUpdaterService.java index db8db3b1e..f65a4aa2a 100644 --- a/client/src/main/java/io/split/client/CacheUpdaterService.java +++ b/client/src/main/java/io/split/client/CacheUpdaterService.java @@ -12,6 +12,7 @@ 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; @@ -77,9 +78,9 @@ private List getConditions(String splitKey, ParsedSplit split, private ParsedCondition createWhitelistCondition(String splitKey, Partition partition) { ParsedCondition parsedCondition = new ParsedCondition(ConditionType.WHITELIST, new CombiningMatcher(CombiningMatcher.Combiner.AND, - new java.util.ArrayList<>(java.util.Arrays.asList( - new AttributeMatcher(null, new WhitelistMatcher(java.util.Arrays.asList(splitKey)), false)))), - new java.util.ArrayList<>(java.util.Arrays.asList(partition)), splitKey); + new ArrayList<>(Arrays.asList( + new AttributeMatcher(null, new WhitelistMatcher(Arrays.asList(splitKey)), false)))), + new ArrayList<>(Arrays.asList(partition)), splitKey); return parsedCondition; } @@ -89,8 +90,8 @@ private ParsedCondition createRolloutCondition(Partition partition) { rolloutPartition.size = 0; ParsedCondition parsedCondition = new ParsedCondition(ConditionType.ROLLOUT, new CombiningMatcher(CombiningMatcher.Combiner.AND, - new java.util.ArrayList<>(java.util.Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))), - new java.util.ArrayList<>(java.util.Arrays.asList(partition, rolloutPartition)), "LOCAL"); + 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/engine/evaluator/EvaluationContextImp.java b/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java new file mode 100644 index 000000000..5d636a70f --- /dev/null +++ b/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java @@ -0,0 +1,72 @@ +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.storages.RuleBasedSegmentCacheConsumer; +import io.split.storages.SegmentCacheConsumer; + +import java.util.Map; +import java.util.Objects; + +public class EvaluationContextImp implements EvaluationContext { + private final Evaluator _evaluator; + private final SegmentCacheConsumer _segmentCacheConsumer; + private final RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; + + public EvaluationContextImp(Evaluator evaluator, SegmentCacheConsumer segmentCacheConsumer, + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { + _evaluator = Objects.requireNonNull(evaluator); + _segmentCacheConsumer = Objects.requireNonNull(segmentCacheConsumer); + _ruleBasedSegmentCacheConsumer = Objects.requireNonNull(ruleBasedSegmentCacheConsumer); + } + + public Evaluator getEvaluator() { + return _evaluator; + } + + public SegmentCacheConsumer getSegmentCache() { + return _segmentCacheConsumer; + } + + 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/experiments/ParsedRuleBasedSegment.java b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java index e58d1d762..f9f260e3d 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java @@ -4,6 +4,8 @@ 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; @@ -44,7 +46,7 @@ public ParsedRuleBasedSegment( List excludedSegments ) { _ruleBasedSegment = ruleBasedSegment; - _parsedCondition = java.util.Collections.unmodifiableList(new java.util.ArrayList<>(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 4e20ed98e..4413a5ec1 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java @@ -302,7 +302,7 @@ private static TargetingRule buildTargetingRule( : Collections.unmodifiableList(prerequisitesMatcher.getPrerequisites()); return new TargetingRule(feature, seed, killed, defaultTreatment, conditions, trafficTypeName, changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, - flagSets == null ? new java.util.HashSet<>() : flagSets, impressionsDisabled, prereqs); + flagSets == null ? new HashSet<>() : flagSets, impressionsDisabled, prereqs); } private static io.split.rules.model.Condition toTargetingCondition(ParsedCondition c) { 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 03cc4ec14..0ba45a5c2 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -83,8 +83,8 @@ public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { } - private static io.split.rules.model.DataType toRulesDataType(DataType dt) { - return io.split.rules.model.DataType.valueOf(dt.name()); + private static DataType toRulesDataType(io.split.client.dtos.DataType dt) { + return DataType.valueOf(dt.name()); } public static AttributeMatcher toMatcher(Matcher matcher) { 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 1bf62dba5..31e223098 100644 --- a/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java +++ b/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; import static io.split.engine.experiments.ParserUtils.checkUnsupportedMatcherExist; @@ -29,7 +30,7 @@ public ParsedRuleBasedSegment parse(RuleBasedSegment ruleBasedSegment) { } private ParsedRuleBasedSegment parseWithoutExceptionHandling(RuleBasedSegment ruleBasedSegment) { - List parsedConditionList = new java.util.ArrayList<>(); + 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 0cc589d34..6494c5287 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitParser.java +++ b/client/src/main/java/io/split/engine/experiments/SplitParser.java @@ -86,7 +86,7 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) { split.trafficAllocationSeed, split.algo, split.configurations, - split.sets == null ? new java.util.HashSet<>() : split.sets, + split.sets == null ? new HashSet<>() : split.sets, split.impressionsDisabled, prerequisites); From 7c06d4f6add01194eef3ae7cbd6e53009b8b80f7 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 12:12:11 -0300 Subject: [PATCH 4/9] Remove unused file AI-Session-Id: 7d0b171a-a576-4317-965f-99fb98c11ba8 AI-Tool: claude-code AI-Model: unknown --- .../evaluator/EvaluationContextImp.java | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java diff --git a/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java b/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java deleted file mode 100644 index 5d636a70f..000000000 --- a/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java +++ /dev/null @@ -1,72 +0,0 @@ -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.storages.RuleBasedSegmentCacheConsumer; -import io.split.storages.SegmentCacheConsumer; - -import java.util.Map; -import java.util.Objects; - -public class EvaluationContextImp implements EvaluationContext { - private final Evaluator _evaluator; - private final SegmentCacheConsumer _segmentCacheConsumer; - private final RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; - - public EvaluationContextImp(Evaluator evaluator, SegmentCacheConsumer segmentCacheConsumer, - RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { - _evaluator = Objects.requireNonNull(evaluator); - _segmentCacheConsumer = Objects.requireNonNull(segmentCacheConsumer); - _ruleBasedSegmentCacheConsumer = Objects.requireNonNull(ruleBasedSegmentCacheConsumer); - } - - public Evaluator getEvaluator() { - return _evaluator; - } - - public SegmentCacheConsumer getSegmentCache() { - return _segmentCacheConsumer; - } - - 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; - } -} From fb7b1ae66cde08fb677428b147a2e8a46dd7bb3f Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 12:53:14 -0300 Subject: [PATCH 5/9] refactor: extract TargetingRuleFactory from ParsedSplit - Create TargetingRuleFactory with static buildTargetingRule() method - Eliminate FQNs for io.split.rules.model types - Add comprehensive tests for factory methods - Update ParsedSplit to delegate to factory This improves testability and separation of concerns by isolating the mapping logic from SDK domain objects to targeting-engine objects. AI-Session-Id: 7d0b171a-a576-4317-965f-99fb98c11ba8 AI-Tool: claude-code AI-Model: unknown --- .../split/engine/experiments/ParsedSplit.java | 39 +-- .../experiments/TargetingRuleFactory.java | 59 ++++ .../experiments/TargetingRuleFactoryTest.java | 320 ++++++++++++++++++ 3 files changed, 382 insertions(+), 36 deletions(-) create mode 100644 client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java create mode 100644 client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java 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 4413a5ec1..c6538b9dd 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java @@ -68,7 +68,7 @@ public static ParsedSplit createParsedSplitForTests( flagSets, impressionsDisabled, prerequisitesMatcher, - buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, 100, seed, algo, null, flagSets, impressionsDisabled, prerequisitesMatcher) ); } @@ -102,7 +102,7 @@ public static ParsedSplit createParsedSplitForTests( flagSets, impressionsDisabled, prerequisitesMatcher, - buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, 100, seed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher) ); } @@ -126,7 +126,7 @@ public ParsedSplit( this(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher, - buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher)); } @@ -284,39 +284,6 @@ public String toString() { } - private 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(ParsedSplit::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 io.split.rules.model.Condition toTargetingCondition(ParsedCondition c) { - List partitions = c.partitions() == null - ? Collections.emptyList() - : c.partitions().stream() - .map(p -> new io.split.rules.model.Partition(p.treatment, p.size)) - .collect(Collectors.toList()); - io.split.rules.model.ConditionType condType = c.conditionType() == ConditionType.ROLLOUT - ? io.split.rules.model.ConditionType.ROLLOUT - : io.split.rules.model.ConditionType.WHITELIST; - return new io.split.rules.model.Condition(condType, c.matcher(), partitions, c.label()); - } - public Set getSegmentsNames() { return parsedConditions().stream() .flatMap(parsedCondition -> parsedCondition.matcher().attributeMatchers().stream()) 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/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()); + } +} From ed86b23a6cc22a2f221818edb7ac3b5f94325dd9 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 13:10:28 -0300 Subject: [PATCH 6/9] Parser utils change' AI-Session-Id: 490d81f4-6832-4178-9c38-e45460ab97de AI-Tool: claude-code AI-Model: unknown --- .../main/java/io/split/engine/experiments/ParserUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0ba45a5c2..6317a9f75 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -83,8 +83,8 @@ public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { } - private static DataType toRulesDataType(io.split.client.dtos.DataType dt) { - return DataType.valueOf(dt.name()); + private static io.split.rules.model.DataType toRulesDataType(io.split.client.dtos.DataType dt) { + return io.split.rules.model.DataType.valueOf(dt.name()); } public static AttributeMatcher toMatcher(Matcher matcher) { From 03910769acae486c434ce503e850213c00207763 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 13:18:37 -0300 Subject: [PATCH 7/9] Restore checkNotNull guards accidentally removed from ParserUtils Guards for userDefinedSegmentMatcherData, whitelistMatcherData, unaryNumericMatcherData, and betweenMatcherData were dropped as an unintended side effect of the DataType import refactor in ed86b23a. Co-Authored-By: Claude Sonnet 4.6 AI-Session-Id: 588a98fd-ba49-4318-8286-5fb79f9efaca AI-Tool: claude-code AI-Model: unknown --- .../io/split/engine/experiments/ParserUtils.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 6317a9f75..45e3cd8a6 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -36,6 +36,8 @@ import java.util.ArrayList; import java.util.List; +import static com.google.common.base.Preconditions.checkNotNull; + public final class ParserUtils { private ParserUtils() { @@ -94,37 +96,47 @@ public static AttributeMatcher toMatcher(Matcher matcher) { delegate = new AllKeysMatcher(); break; case IN_SEGMENT: + checkNotNull(matcher.userDefinedSegmentMatcherData); String segmentName = matcher.userDefinedSegmentMatcherData.segmentName; delegate = new UserDefinedSegmentMatcher(segmentName); break; case WHITELIST: + checkNotNull(matcher.whitelistMatcherData); delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); break; case EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); 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, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); break; case LESS_THAN_OR_EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); 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, toRulesDataType(matcher.betweenMatcherData.dataType)); break; case EQUAL_TO_SET: + checkNotNull(matcher.whitelistMatcherData); delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); break; case PART_OF_SET: + checkNotNull(matcher.whitelistMatcherData); delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case CONTAINS_ALL_OF_SET: + checkNotNull(matcher.whitelistMatcherData); delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case CONTAINS_ANY_OF_SET: + checkNotNull(matcher.whitelistMatcherData); delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case STARTS_WITH: From 5b8bc948bf2a501e9f45914e4c1dfa6769e48562 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 14:00:21 -0300 Subject: [PATCH 8/9] Map combiner from DTO instead of hardcoding AND in ParserUtils Restores original behavior: reads matcherGroup.combiner and maps it to CombiningMatcher.Combiner via valueOf, consistent with how DataType is already mapped across the DTO/domain boundary. Co-Authored-By: Claude Sonnet 4.6 AI-Session-Id: 588a98fd-ba49-4318-8286-5fb79f9efaca AI-Tool: claude-code AI-Model: unknown --- .../main/java/io/split/engine/experiments/ParserUtils.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 45e3cd8a6..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,6 +1,7 @@ package io.split.engine.experiments; 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; @@ -81,7 +82,7 @@ public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { toCombine.add(toMatcher(matcher)); } - return new CombiningMatcher(CombiningMatcher.Combiner.AND, toCombine); + return new CombiningMatcher(toCombiner(matcherGroup.combiner), toCombine); } @@ -89,6 +90,10 @@ private static io.split.rules.model.DataType toRulesDataType(io.split.client.dto 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.rules.matchers.Matcher delegate = null; switch (matcher.matcherType) { From 872a9ba5df05638a67c8f46140e7f9c8de70fb2e Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 9 Apr 2026 10:09:14 -0300 Subject: [PATCH 9/9] Restore remaining checkNotNull guards dropped during matchers migration Guards for STARTS_WITH, ENDS_WITH, CONTAINS_STRING, MATCHES_STRING, EQUAL_TO_SEMVER, GREATER_THAN_OR_EQUAL_TO_SEMVER, LESS_THAN_OR_EQUAL_TO_SEMVER, IN_LIST_SEMVER, BETWEEN_SEMVER, IN_RULE_BASED_SEGMENT, and the final delegate null check were lost when matchers were migrated from io.split.engine.matchers to io.split.rules.matchers. Also restores checkNotNull with message for IN_SPLIT_TREATMENT and EQUAL_TO_BOOLEAN (previously replaced with manual NPE throws). Co-Authored-By: Claude Sonnet 4.6 AI-Session-Id: 588a98fd-ba49-4318-8286-5fb79f9efaca AI-Tool: claude-code AI-Model: unknown --- .../split/engine/experiments/ParserUtils.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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 92a0ba788..9ada61159 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -145,43 +145,55 @@ 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: - if (matcher.dependencyMatcherData == null) throw new NullPointerException( - "MatcherType is " + matcher.matcherType + ". matcher.dependencyMatcherData() MUST NOT BE null"); + checkNotNull(matcher.dependencyMatcherData, + "MatcherType is " + matcher.matcherType + + ". matcher.dependencyMatcherData() MUST NOT BE null"); delegate = new DependencyMatcher(matcher.dependencyMatcherData.split, matcher.dependencyMatcherData.treatments); break; case EQUAL_TO_BOOLEAN: - if (matcher.booleanMatcherData == null) throw new NullPointerException( - "MatcherType is " + matcher.matcherType + ". matcher.booleanMatcherData() MUST NOT BE null"); + checkNotNull(matcher.booleanMatcherData, + "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,6 +201,8 @@ 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;