From 68731e95c82ea29a088beaa713d0ceea83d6c39f Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 1 Apr 2026 15:28:05 -0300 Subject: [PATCH 01/10] 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 02/10] 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 62ce66adc57df398d706dcea34c5b090a7119cad Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 2 Apr 2026 21:08:04 -0300 Subject: [PATCH 03/10] =?UTF-8?q?PR=201.1=20=E2=80=94=20Tidy:=20Make=20Exc?= =?UTF-8?q?ludedSegments=20constants=20public=20for=20cross-module=20acces?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make STANDARD_TYPE and RULE_BASED_TYPE public so they are accessible from parsing-commons once that module is extracted. --- .../src/main/java/io/split/client/dtos/ExcludedSegments.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/io/split/client/dtos/ExcludedSegments.java b/client/src/main/java/io/split/client/dtos/ExcludedSegments.java index 9e65fa60f..690ca677d 100644 --- a/client/src/main/java/io/split/client/dtos/ExcludedSegments.java +++ b/client/src/main/java/io/split/client/dtos/ExcludedSegments.java @@ -1,8 +1,8 @@ package io.split.client.dtos; public class ExcludedSegments { - static final String STANDARD_TYPE = "standard"; - static final String RULE_BASED_TYPE = "rule-based"; + public static final String STANDARD_TYPE = "standard"; + public static final String RULE_BASED_TYPE = "rule-based"; public ExcludedSegments() {} public ExcludedSegments(String type, String name) { From 0fd93059fe7ed3e6ac2bbac57e1caea2c7c91bab Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 2 Apr 2026 21:50:23 -0300 Subject: [PATCH 04/10] =?UTF-8?q?PR=201.2=20=E2=80=94=20Extract:=20Create?= =?UTF-8?q?=20parsing-commons=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move 18 DTOs from client/dtos, ParsedCondition + ParserUtils from engine/experiments, and Labels from engine/evaluator into a new parsing-commons module. Add targeting-engine as its only dependency. Add parsing-commons dep to client/pom.xml and register module in root pom.xml. --- client/pom.xml | 5 + parsing-commons/pom.xml | 36 ++++ .../split/client/dtos/BetweenMatcherData.java | 12 ++ .../client/dtos/BetweenStringMatcherData.java | 11 ++ .../java/io/split/client/dtos/Condition.java | 16 ++ .../io/split/client/dtos/ConditionType.java | 9 + .../java/io/split/client/dtos/DataType.java | 10 + .../client/dtos/DependencyMatcherData.java | 8 + .../java/io/split/client/dtos/Excluded.java | 8 + .../split/client/dtos/ExcludedSegments.java | 27 +++ .../io/split/client/dtos/KeySelector.java | 7 + .../java/io/split/client/dtos/Matcher.java | 20 ++ .../io/split/client/dtos/MatcherCombiner.java | 8 + .../io/split/client/dtos/MatcherGroup.java | 8 + .../io/split/client/dtos/MatcherType.java | 44 +++++ .../java/io/split/client/dtos/Partition.java | 6 + .../java/io/split/client/dtos/Status.java | 6 + .../client/dtos/UnaryNumericMatcherData.java | 6 + .../dtos/UserDefinedSegmentMatcherData.java | 5 + .../client/dtos/WhitelistMatcherData.java | 7 + .../io/split/engine/evaluator/Labels.java | 12 ++ .../engine/experiments/ParsedCondition.java | 120 ++++++++++++ .../split/engine/experiments/ParserUtils.java | 185 ++++++++++++++++++ pom.xml | 1 + 24 files changed, 577 insertions(+) create mode 100644 parsing-commons/pom.xml create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/BetweenMatcherData.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/BetweenStringMatcherData.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/Condition.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/ConditionType.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/DataType.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/DependencyMatcherData.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/Excluded.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/ExcludedSegments.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/KeySelector.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/Matcher.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/MatcherCombiner.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/MatcherGroup.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/MatcherType.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/Partition.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/Status.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/UnaryNumericMatcherData.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/UserDefinedSegmentMatcherData.java create mode 100644 parsing-commons/src/main/java/io/split/client/dtos/WhitelistMatcherData.java create mode 100644 parsing-commons/src/main/java/io/split/engine/evaluator/Labels.java create mode 100644 parsing-commons/src/main/java/io/split/engine/experiments/ParsedCondition.java create mode 100644 parsing-commons/src/main/java/io/split/engine/experiments/ParserUtils.java diff --git a/client/pom.xml b/client/pom.xml index efffe9272..bb4da0e03 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -172,6 +172,11 @@ targeting-engine ${project.version}
+ + io.split.client + parsing-commons + ${project.version} + io.split.client pluggable-storage diff --git a/parsing-commons/pom.xml b/parsing-commons/pom.xml new file mode 100644 index 000000000..f16bbb366 --- /dev/null +++ b/parsing-commons/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + io.split.client + java-client-parent + 4.18.3 + + + 4.18.3 + parsing-commons + jar + Parsing Commons + DTOs and parsing utilities shared between Split SDK modules + + + + io.split.client + targeting-engine + ${project.version} + + + junit + junit + 4.13.1 + test + + + org.mockito + mockito-core + 5.14.2 + test + + + diff --git a/parsing-commons/src/main/java/io/split/client/dtos/BetweenMatcherData.java b/parsing-commons/src/main/java/io/split/client/dtos/BetweenMatcherData.java new file mode 100644 index 000000000..36a4d7723 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/BetweenMatcherData.java @@ -0,0 +1,12 @@ +package io.split.client.dtos; + +/** + * Metadata to support the between matcher. + * + * @author adil + */ +public class BetweenMatcherData { + public DataType dataType; + public long start; + public long end; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/BetweenStringMatcherData.java b/parsing-commons/src/main/java/io/split/client/dtos/BetweenStringMatcherData.java new file mode 100644 index 000000000..3af1cc0c1 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/BetweenStringMatcherData.java @@ -0,0 +1,11 @@ +package io.split.client.dtos; + +/** + * Metadata to support the between matcher. + * + * @author adil + */ +public class BetweenStringMatcherData { + public String start; + public String end; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/Condition.java b/parsing-commons/src/main/java/io/split/client/dtos/Condition.java new file mode 100644 index 000000000..20b2a033c --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/Condition.java @@ -0,0 +1,16 @@ +package io.split.client.dtos; + +import java.util.List; + +/** + * A single condition in the the name. If the condition is fulfilled, + * the partitions are used. + * + * @author adil + */ +public class Condition { + public ConditionType conditionType; + public MatcherGroup matcherGroup; + public List partitions; + public String label; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/ConditionType.java b/parsing-commons/src/main/java/io/split/client/dtos/ConditionType.java new file mode 100644 index 000000000..bd96b5cc5 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/ConditionType.java @@ -0,0 +1,9 @@ +package io.split.client.dtos; + +/** + * Created by adilaijaz on 5/28/15. + */ +public enum ConditionType { + WHITELIST, + ROLLOUT +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/DataType.java b/parsing-commons/src/main/java/io/split/client/dtos/DataType.java new file mode 100644 index 000000000..4ce5f3154 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/DataType.java @@ -0,0 +1,10 @@ +package io.split.client.dtos; + +/** + * Created by adilaijaz on 5/28/15. + */ +public enum DataType { + NUMBER, + DATETIME, + STRING +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/DependencyMatcherData.java b/parsing-commons/src/main/java/io/split/client/dtos/DependencyMatcherData.java new file mode 100644 index 000000000..df602376e --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/DependencyMatcherData.java @@ -0,0 +1,8 @@ +package io.split.client.dtos; + +import java.util.List; + +public class DependencyMatcherData { + public String split; + public List treatments; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/Excluded.java b/parsing-commons/src/main/java/io/split/client/dtos/Excluded.java new file mode 100644 index 000000000..e23afa4b0 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/Excluded.java @@ -0,0 +1,8 @@ +package io.split.client.dtos; + +import java.util.List; + +public class Excluded { + public List keys; + public List segments; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/ExcludedSegments.java b/parsing-commons/src/main/java/io/split/client/dtos/ExcludedSegments.java new file mode 100644 index 000000000..690ca677d --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/ExcludedSegments.java @@ -0,0 +1,27 @@ +package io.split.client.dtos; + +public class ExcludedSegments { + public static final String STANDARD_TYPE = "standard"; + public static final String RULE_BASED_TYPE = "rule-based"; + + public ExcludedSegments() {} + public ExcludedSegments(String type, String name) { + this.type = type; + this.name = name; + } + + public String type; + public String name; + + public boolean isStandard() { + return STANDARD_TYPE.equals(type); + } + + public boolean isRuleBased() { + return RULE_BASED_TYPE.equals(type); + } + + public String getSegmentName(){ + return name; + } +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/KeySelector.java b/parsing-commons/src/main/java/io/split/client/dtos/KeySelector.java new file mode 100644 index 000000000..39cf789f1 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/KeySelector.java @@ -0,0 +1,7 @@ +package io.split.client.dtos; + +public class KeySelector { + public String trafficType; + public String attribute; +} + diff --git a/parsing-commons/src/main/java/io/split/client/dtos/Matcher.java b/parsing-commons/src/main/java/io/split/client/dtos/Matcher.java new file mode 100644 index 000000000..fc2c65155 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/Matcher.java @@ -0,0 +1,20 @@ +package io.split.client.dtos; + +/** + * A leaf class representing a matcher. + * + * @author adil + */ +public class Matcher { + public KeySelector keySelector; + public MatcherType matcherType; + public boolean negate; + public UserDefinedSegmentMatcherData userDefinedSegmentMatcherData; + public WhitelistMatcherData whitelistMatcherData; + public UnaryNumericMatcherData unaryNumericMatcherData; + public BetweenMatcherData betweenMatcherData; + public BetweenStringMatcherData betweenStringMatcherData; + public DependencyMatcherData dependencyMatcherData; + public Boolean booleanMatcherData; + public String stringMatcherData; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/MatcherCombiner.java b/parsing-commons/src/main/java/io/split/client/dtos/MatcherCombiner.java new file mode 100644 index 000000000..af269887d --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/MatcherCombiner.java @@ -0,0 +1,8 @@ +package io.split.client.dtos; + +/** + * Created by adilaijaz on 5/28/15. + */ +public enum MatcherCombiner { + AND +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/MatcherGroup.java b/parsing-commons/src/main/java/io/split/client/dtos/MatcherGroup.java new file mode 100644 index 000000000..dd69101c4 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/MatcherGroup.java @@ -0,0 +1,8 @@ +package io.split.client.dtos; + +import java.util.List; + +public class MatcherGroup { + public MatcherCombiner combiner; + public List matchers; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/MatcherType.java b/parsing-commons/src/main/java/io/split/client/dtos/MatcherType.java new file mode 100644 index 000000000..22f22adb3 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/MatcherType.java @@ -0,0 +1,44 @@ +package io.split.client.dtos; + +/** + * Created by adilaijaz on 5/28/15. + */ +public enum MatcherType { + ALL_KEYS, + IN_SEGMENT, + WHITELIST, + + /* Numeric Matcher */ + EQUAL_TO, + GREATER_THAN_OR_EQUAL_TO, + LESS_THAN_OR_EQUAL_TO, + BETWEEN, + + /* Set Matcher */ + EQUAL_TO_SET, + CONTAINS_ANY_OF_SET, + CONTAINS_ALL_OF_SET, + PART_OF_SET, + + /* String Matcher */ + STARTS_WITH, + ENDS_WITH, + CONTAINS_STRING, + MATCHES_STRING, + + /* Boolean Matcher */ + EQUAL_TO_BOOLEAN, + + /* Dependency Matcher */ + IN_SPLIT_TREATMENT, + + /* Semver matchers */ + EQUAL_TO_SEMVER, + GREATER_THAN_OR_EQUAL_TO_SEMVER, + LESS_THAN_OR_EQUAL_TO_SEMVER, + IN_LIST_SEMVER, + BETWEEN_SEMVER, + + /* Rule based segment */ + IN_RULE_BASED_SEGMENT +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/Partition.java b/parsing-commons/src/main/java/io/split/client/dtos/Partition.java new file mode 100644 index 000000000..b87871c11 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/Partition.java @@ -0,0 +1,6 @@ +package io.split.client.dtos; + +public class Partition { + public String treatment; + public int size; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/Status.java b/parsing-commons/src/main/java/io/split/client/dtos/Status.java new file mode 100644 index 000000000..348ec330d --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/Status.java @@ -0,0 +1,6 @@ +package io.split.client.dtos; + +public enum Status { + ACTIVE, + ARCHIVED, +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/UnaryNumericMatcherData.java b/parsing-commons/src/main/java/io/split/client/dtos/UnaryNumericMatcherData.java new file mode 100644 index 000000000..ebc9a6d84 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/UnaryNumericMatcherData.java @@ -0,0 +1,6 @@ +package io.split.client.dtos; + +public class UnaryNumericMatcherData { + public DataType dataType; + public long value; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/UserDefinedSegmentMatcherData.java b/parsing-commons/src/main/java/io/split/client/dtos/UserDefinedSegmentMatcherData.java new file mode 100644 index 000000000..3bffd2167 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/UserDefinedSegmentMatcherData.java @@ -0,0 +1,5 @@ +package io.split.client.dtos; + +public class UserDefinedSegmentMatcherData { + public String segmentName; +} diff --git a/parsing-commons/src/main/java/io/split/client/dtos/WhitelistMatcherData.java b/parsing-commons/src/main/java/io/split/client/dtos/WhitelistMatcherData.java new file mode 100644 index 000000000..7f80ea87d --- /dev/null +++ b/parsing-commons/src/main/java/io/split/client/dtos/WhitelistMatcherData.java @@ -0,0 +1,7 @@ +package io.split.client.dtos; + +import java.util.List; + +public class WhitelistMatcherData { + public List whitelist; +} diff --git a/parsing-commons/src/main/java/io/split/engine/evaluator/Labels.java b/parsing-commons/src/main/java/io/split/engine/evaluator/Labels.java new file mode 100644 index 000000000..28966d51e --- /dev/null +++ b/parsing-commons/src/main/java/io/split/engine/evaluator/Labels.java @@ -0,0 +1,12 @@ +package io.split.engine.evaluator; + +public class Labels { + 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"; +} \ No newline at end of file diff --git a/parsing-commons/src/main/java/io/split/engine/experiments/ParsedCondition.java b/parsing-commons/src/main/java/io/split/engine/experiments/ParsedCondition.java new file mode 100644 index 000000000..a99fcd7aa --- /dev/null +++ b/parsing-commons/src/main/java/io/split/engine/experiments/ParsedCondition.java @@ -0,0 +1,120 @@ +package io.split.engine.experiments; + +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.Partition; +import io.split.rules.matchers.CombiningMatcher; + +import java.util.List; + +/** + * A pair of matcher and partitions. + * + * @author adil + */ +public final class ParsedCondition { + + private final ConditionType _conditionType; + private final CombiningMatcher _matcher; + private final List _partitions; + private final String _label; + + public static ParsedCondition createParsedConditionForTests(CombiningMatcher matcher, List partitions) { + return new ParsedCondition(ConditionType.ROLLOUT, matcher, partitions, null); + } + + + public ParsedCondition(ConditionType conditionType, CombiningMatcher matcher, List partitions, String label) { + _conditionType = conditionType; + _matcher = matcher; + _partitions = partitions; + _label = label; + } + + + public ConditionType conditionType() { + return _conditionType; + } + + public CombiningMatcher matcher() { + return _matcher; + } + + public List partitions() { + return _partitions; + } + + public String label() { + return _label; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _matcher.hashCode(); + + int partitionsHashCode = 17; + if (_partitions != null) { + for (Partition p : _partitions) { + partitionsHashCode = 31 * partitionsHashCode + p.treatment.hashCode(); + partitionsHashCode = 31 * partitionsHashCode + p.size; + } + } + result = 31 * result + partitionsHashCode; + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ParsedCondition)) return false; + + ParsedCondition other = (ParsedCondition) obj; + + boolean result = _matcher.equals(other._matcher); + + if (!result) { + return result; + } + if (_partitions == null) { + return result & (_partitions == other._partitions); + } + if (_partitions.size() != other._partitions.size()) { + return result; + } + + for (int i = 0; i < _partitions.size(); i++) { + Partition first = _partitions.get(i); + Partition second = other._partitions.get(i); + + result &= (first.size == second.size && first.treatment.equals(second.treatment)); + } + + return result; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + + bldr.append(_matcher); + bldr.append(" then split "); + boolean first = true; + if (_partitions == null) { + return bldr.toString(); + } + for (Partition partition : _partitions) { + if (!first) { + bldr.append(','); + } + bldr.append(partition.size); + bldr.append(':'); + bldr.append(partition.treatment); + first = false; + } + + return bldr.toString(); + } + + +} diff --git a/parsing-commons/src/main/java/io/split/engine/experiments/ParserUtils.java b/parsing-commons/src/main/java/io/split/engine/experiments/ParserUtils.java new file mode 100644 index 000000000..03cc4ec14 --- /dev/null +++ b/parsing-commons/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -0,0 +1,185 @@ +package io.split.engine.experiments; + +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.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; + +public final class ParserUtils { + + private ParserUtils() { + throw new IllegalStateException("Utility class"); + } + + public static boolean checkUnsupportedMatcherExist(List matchers) { + MatcherType typeCheck = null; + for (Matcher matcher : matchers) { + typeCheck = null; + try { + typeCheck = matcher.matcherType; + } catch (NullPointerException e) { + // If the exception is caught, it means unsupported matcher + break; + } + } + return (typeCheck == null); + } + + public static ParsedCondition getTemplateCondition() { + List templatePartitions = new ArrayList<>(); + Partition partition = new Partition(); + partition.treatment = "control"; + partition.size = 100; + templatePartitions.add(partition); + return new ParsedCondition( + ConditionType.ROLLOUT, + CombiningMatcher.of(new AllKeysMatcher()), + templatePartitions, + Labels.UNSUPPORTED_MATCHER); + } + + public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { + List matchers = matcherGroup.matchers; + if (matchers.isEmpty()) throw new IllegalArgumentException(); + + List toCombine = new ArrayList<>(); + + for (Matcher matcher : matchers) { + toCombine.add(toMatcher(matcher)); + } + + 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.rules.matchers.Matcher delegate = null; + switch (matcher.matcherType) { + case ALL_KEYS: + delegate = new AllKeysMatcher(); + break; + case IN_SEGMENT: + String segmentName = matcher.userDefinedSegmentMatcherData.segmentName; + delegate = new UserDefinedSegmentMatcher(segmentName); + break; + case WHITELIST: + delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); + break; + case EQUAL_TO: + delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); + break; + case GREATER_THAN_OR_EQUAL_TO: + delegate = new GreaterThanOrEqualToMatcher( + matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); + break; + case LESS_THAN_OR_EQUAL_TO: + delegate = new LessThanOrEqualToMatcher( + matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); + break; + case BETWEEN: + delegate = new BetweenMatcher(matcher.betweenMatcherData.start, + matcher.betweenMatcherData.end, toRulesDataType(matcher.betweenMatcherData.dataType)); + break; + case EQUAL_TO_SET: + delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case PART_OF_SET: + delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_ALL_OF_SET: + delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_ANY_OF_SET: + delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case STARTS_WITH: + delegate = new StartsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case ENDS_WITH: + delegate = new EndsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_STRING: + delegate = new ContainsAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case MATCHES_STRING: + 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"); + 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"); + delegate = new BooleanMatcher(matcher.booleanMatcherData); + break; + case EQUAL_TO_SEMVER: + delegate = new EqualToSemverMatcher(matcher.stringMatcherData); + break; + case GREATER_THAN_OR_EQUAL_TO_SEMVER: + delegate = new GreaterThanOrEqualToSemverMatcher(matcher.stringMatcherData); + break; + case LESS_THAN_OR_EQUAL_TO_SEMVER: + delegate = new LessThanOrEqualToSemverMatcher(matcher.stringMatcherData); + break; + case IN_LIST_SEMVER: + delegate = new InListSemverMatcher(matcher.whitelistMatcherData.whitelist); + break; + case BETWEEN_SEMVER: + delegate = new BetweenSemverMatcher(matcher.betweenStringMatcherData.start, matcher.betweenStringMatcherData.end); + break; + case IN_RULE_BASED_SEGMENT: + String ruleBasedSegmentName = matcher.userDefinedSegmentMatcherData.segmentName; + delegate = new RuleBasedSegmentMatcher(ruleBasedSegmentName); + break; + default: + throw new IllegalArgumentException("Unknown matcher type: " + matcher.matcherType); + } + + String attribute = null; + if (matcher.keySelector != null && matcher.keySelector.attribute != null) { + attribute = matcher.keySelector.attribute; + } + + boolean negate = matcher.negate; + + + return new AttributeMatcher(attribute, delegate, negate); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8df4d7ef9..abc13c7f8 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ targeting-engine + parsing-commons pluggable-storage redis-wrapper testing From 066a22c0e38918f95d952bde24acbf6f5f88c79f Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 2 Apr 2026 21:51:04 -0300 Subject: [PATCH 05/10] =?UTF-8?q?PR=201.3=20=E2=80=94=20Wire:=20Verify=20t?= =?UTF-8?q?ransitive=20consumers=20for=20parsing-commons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited pluggable-storage, redis-wrapper, and testing modules. None import the moved DTOs, ParserUtils, ParsedCondition, or Labels directly — no additional dependency changes required. From 665f1e6d49a46896af559724cadc1f7f781268e5 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 2 Apr 2026 21:52:54 -0300 Subject: [PATCH 06/10] =?UTF-8?q?Fix=20PR=201.2=20=E2=80=94=20Remove=20mov?= =?UTF-8?q?ed=20DTOs=20from=20client=20after=20extraction=20to=20parsing-c?= =?UTF-8?q?ommons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The git mv deletions were not staged in the original PR 1.2 commit due to a stash/pop mishap. This commit removes the 21 files from client/ that were moved to parsing-commons. --- .../split/client/dtos/BetweenMatcherData.java | 12 -- .../client/dtos/BetweenStringMatcherData.java | 11 -- .../java/io/split/client/dtos/Condition.java | 16 -- .../io/split/client/dtos/ConditionType.java | 9 - .../java/io/split/client/dtos/DataType.java | 10 - .../client/dtos/DependencyMatcherData.java | 8 - .../java/io/split/client/dtos/Excluded.java | 8 - .../split/client/dtos/ExcludedSegments.java | 27 --- .../io/split/client/dtos/KeySelector.java | 7 - .../java/io/split/client/dtos/Matcher.java | 20 -- .../io/split/client/dtos/MatcherCombiner.java | 8 - .../io/split/client/dtos/MatcherGroup.java | 8 - .../io/split/client/dtos/MatcherType.java | 44 ----- .../java/io/split/client/dtos/Partition.java | 6 - .../java/io/split/client/dtos/Status.java | 6 - .../client/dtos/UnaryNumericMatcherData.java | 6 - .../dtos/UserDefinedSegmentMatcherData.java | 5 - .../client/dtos/WhitelistMatcherData.java | 7 - .../io/split/engine/evaluator/Labels.java | 12 -- .../engine/experiments/ParsedCondition.java | 120 ------------ .../split/engine/experiments/ParserUtils.java | 185 ------------------ 21 files changed, 535 deletions(-) delete mode 100644 client/src/main/java/io/split/client/dtos/BetweenMatcherData.java delete mode 100644 client/src/main/java/io/split/client/dtos/BetweenStringMatcherData.java delete mode 100644 client/src/main/java/io/split/client/dtos/Condition.java delete mode 100644 client/src/main/java/io/split/client/dtos/ConditionType.java delete mode 100644 client/src/main/java/io/split/client/dtos/DataType.java delete mode 100644 client/src/main/java/io/split/client/dtos/DependencyMatcherData.java delete mode 100644 client/src/main/java/io/split/client/dtos/Excluded.java delete mode 100644 client/src/main/java/io/split/client/dtos/ExcludedSegments.java delete mode 100644 client/src/main/java/io/split/client/dtos/KeySelector.java delete mode 100644 client/src/main/java/io/split/client/dtos/Matcher.java delete mode 100644 client/src/main/java/io/split/client/dtos/MatcherCombiner.java delete mode 100644 client/src/main/java/io/split/client/dtos/MatcherGroup.java delete mode 100644 client/src/main/java/io/split/client/dtos/MatcherType.java delete mode 100644 client/src/main/java/io/split/client/dtos/Partition.java delete mode 100644 client/src/main/java/io/split/client/dtos/Status.java delete mode 100644 client/src/main/java/io/split/client/dtos/UnaryNumericMatcherData.java delete mode 100644 client/src/main/java/io/split/client/dtos/UserDefinedSegmentMatcherData.java delete mode 100644 client/src/main/java/io/split/client/dtos/WhitelistMatcherData.java delete mode 100644 client/src/main/java/io/split/engine/evaluator/Labels.java delete mode 100644 client/src/main/java/io/split/engine/experiments/ParsedCondition.java delete mode 100644 client/src/main/java/io/split/engine/experiments/ParserUtils.java diff --git a/client/src/main/java/io/split/client/dtos/BetweenMatcherData.java b/client/src/main/java/io/split/client/dtos/BetweenMatcherData.java deleted file mode 100644 index 36a4d7723..000000000 --- a/client/src/main/java/io/split/client/dtos/BetweenMatcherData.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.split.client.dtos; - -/** - * Metadata to support the between matcher. - * - * @author adil - */ -public class BetweenMatcherData { - public DataType dataType; - public long start; - public long end; -} diff --git a/client/src/main/java/io/split/client/dtos/BetweenStringMatcherData.java b/client/src/main/java/io/split/client/dtos/BetweenStringMatcherData.java deleted file mode 100644 index 3af1cc0c1..000000000 --- a/client/src/main/java/io/split/client/dtos/BetweenStringMatcherData.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.split.client.dtos; - -/** - * Metadata to support the between matcher. - * - * @author adil - */ -public class BetweenStringMatcherData { - public String start; - public String end; -} diff --git a/client/src/main/java/io/split/client/dtos/Condition.java b/client/src/main/java/io/split/client/dtos/Condition.java deleted file mode 100644 index 20b2a033c..000000000 --- a/client/src/main/java/io/split/client/dtos/Condition.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.split.client.dtos; - -import java.util.List; - -/** - * A single condition in the the name. If the condition is fulfilled, - * the partitions are used. - * - * @author adil - */ -public class Condition { - public ConditionType conditionType; - public MatcherGroup matcherGroup; - public List partitions; - public String label; -} diff --git a/client/src/main/java/io/split/client/dtos/ConditionType.java b/client/src/main/java/io/split/client/dtos/ConditionType.java deleted file mode 100644 index bd96b5cc5..000000000 --- a/client/src/main/java/io/split/client/dtos/ConditionType.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.split.client.dtos; - -/** - * Created by adilaijaz on 5/28/15. - */ -public enum ConditionType { - WHITELIST, - ROLLOUT -} diff --git a/client/src/main/java/io/split/client/dtos/DataType.java b/client/src/main/java/io/split/client/dtos/DataType.java deleted file mode 100644 index 4ce5f3154..000000000 --- a/client/src/main/java/io/split/client/dtos/DataType.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.split.client.dtos; - -/** - * Created by adilaijaz on 5/28/15. - */ -public enum DataType { - NUMBER, - DATETIME, - STRING -} diff --git a/client/src/main/java/io/split/client/dtos/DependencyMatcherData.java b/client/src/main/java/io/split/client/dtos/DependencyMatcherData.java deleted file mode 100644 index df602376e..000000000 --- a/client/src/main/java/io/split/client/dtos/DependencyMatcherData.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.split.client.dtos; - -import java.util.List; - -public class DependencyMatcherData { - public String split; - public List treatments; -} diff --git a/client/src/main/java/io/split/client/dtos/Excluded.java b/client/src/main/java/io/split/client/dtos/Excluded.java deleted file mode 100644 index e23afa4b0..000000000 --- a/client/src/main/java/io/split/client/dtos/Excluded.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.split.client.dtos; - -import java.util.List; - -public class Excluded { - public List keys; - public List segments; -} diff --git a/client/src/main/java/io/split/client/dtos/ExcludedSegments.java b/client/src/main/java/io/split/client/dtos/ExcludedSegments.java deleted file mode 100644 index 690ca677d..000000000 --- a/client/src/main/java/io/split/client/dtos/ExcludedSegments.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.split.client.dtos; - -public class ExcludedSegments { - public static final String STANDARD_TYPE = "standard"; - public static final String RULE_BASED_TYPE = "rule-based"; - - public ExcludedSegments() {} - public ExcludedSegments(String type, String name) { - this.type = type; - this.name = name; - } - - public String type; - public String name; - - public boolean isStandard() { - return STANDARD_TYPE.equals(type); - } - - public boolean isRuleBased() { - return RULE_BASED_TYPE.equals(type); - } - - public String getSegmentName(){ - return name; - } -} diff --git a/client/src/main/java/io/split/client/dtos/KeySelector.java b/client/src/main/java/io/split/client/dtos/KeySelector.java deleted file mode 100644 index 39cf789f1..000000000 --- a/client/src/main/java/io/split/client/dtos/KeySelector.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.split.client.dtos; - -public class KeySelector { - public String trafficType; - public String attribute; -} - diff --git a/client/src/main/java/io/split/client/dtos/Matcher.java b/client/src/main/java/io/split/client/dtos/Matcher.java deleted file mode 100644 index fc2c65155..000000000 --- a/client/src/main/java/io/split/client/dtos/Matcher.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.split.client.dtos; - -/** - * A leaf class representing a matcher. - * - * @author adil - */ -public class Matcher { - public KeySelector keySelector; - public MatcherType matcherType; - public boolean negate; - public UserDefinedSegmentMatcherData userDefinedSegmentMatcherData; - public WhitelistMatcherData whitelistMatcherData; - public UnaryNumericMatcherData unaryNumericMatcherData; - public BetweenMatcherData betweenMatcherData; - public BetweenStringMatcherData betweenStringMatcherData; - public DependencyMatcherData dependencyMatcherData; - public Boolean booleanMatcherData; - public String stringMatcherData; -} diff --git a/client/src/main/java/io/split/client/dtos/MatcherCombiner.java b/client/src/main/java/io/split/client/dtos/MatcherCombiner.java deleted file mode 100644 index af269887d..000000000 --- a/client/src/main/java/io/split/client/dtos/MatcherCombiner.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.split.client.dtos; - -/** - * Created by adilaijaz on 5/28/15. - */ -public enum MatcherCombiner { - AND -} diff --git a/client/src/main/java/io/split/client/dtos/MatcherGroup.java b/client/src/main/java/io/split/client/dtos/MatcherGroup.java deleted file mode 100644 index dd69101c4..000000000 --- a/client/src/main/java/io/split/client/dtos/MatcherGroup.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.split.client.dtos; - -import java.util.List; - -public class MatcherGroup { - public MatcherCombiner combiner; - public List matchers; -} diff --git a/client/src/main/java/io/split/client/dtos/MatcherType.java b/client/src/main/java/io/split/client/dtos/MatcherType.java deleted file mode 100644 index 22f22adb3..000000000 --- a/client/src/main/java/io/split/client/dtos/MatcherType.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.split.client.dtos; - -/** - * Created by adilaijaz on 5/28/15. - */ -public enum MatcherType { - ALL_KEYS, - IN_SEGMENT, - WHITELIST, - - /* Numeric Matcher */ - EQUAL_TO, - GREATER_THAN_OR_EQUAL_TO, - LESS_THAN_OR_EQUAL_TO, - BETWEEN, - - /* Set Matcher */ - EQUAL_TO_SET, - CONTAINS_ANY_OF_SET, - CONTAINS_ALL_OF_SET, - PART_OF_SET, - - /* String Matcher */ - STARTS_WITH, - ENDS_WITH, - CONTAINS_STRING, - MATCHES_STRING, - - /* Boolean Matcher */ - EQUAL_TO_BOOLEAN, - - /* Dependency Matcher */ - IN_SPLIT_TREATMENT, - - /* Semver matchers */ - EQUAL_TO_SEMVER, - GREATER_THAN_OR_EQUAL_TO_SEMVER, - LESS_THAN_OR_EQUAL_TO_SEMVER, - IN_LIST_SEMVER, - BETWEEN_SEMVER, - - /* Rule based segment */ - IN_RULE_BASED_SEGMENT -} diff --git a/client/src/main/java/io/split/client/dtos/Partition.java b/client/src/main/java/io/split/client/dtos/Partition.java deleted file mode 100644 index b87871c11..000000000 --- a/client/src/main/java/io/split/client/dtos/Partition.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.split.client.dtos; - -public class Partition { - public String treatment; - public int size; -} diff --git a/client/src/main/java/io/split/client/dtos/Status.java b/client/src/main/java/io/split/client/dtos/Status.java deleted file mode 100644 index 348ec330d..000000000 --- a/client/src/main/java/io/split/client/dtos/Status.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.split.client.dtos; - -public enum Status { - ACTIVE, - ARCHIVED, -} diff --git a/client/src/main/java/io/split/client/dtos/UnaryNumericMatcherData.java b/client/src/main/java/io/split/client/dtos/UnaryNumericMatcherData.java deleted file mode 100644 index ebc9a6d84..000000000 --- a/client/src/main/java/io/split/client/dtos/UnaryNumericMatcherData.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.split.client.dtos; - -public class UnaryNumericMatcherData { - public DataType dataType; - public long value; -} diff --git a/client/src/main/java/io/split/client/dtos/UserDefinedSegmentMatcherData.java b/client/src/main/java/io/split/client/dtos/UserDefinedSegmentMatcherData.java deleted file mode 100644 index 3bffd2167..000000000 --- a/client/src/main/java/io/split/client/dtos/UserDefinedSegmentMatcherData.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.split.client.dtos; - -public class UserDefinedSegmentMatcherData { - public String segmentName; -} diff --git a/client/src/main/java/io/split/client/dtos/WhitelistMatcherData.java b/client/src/main/java/io/split/client/dtos/WhitelistMatcherData.java deleted file mode 100644 index 7f80ea87d..000000000 --- a/client/src/main/java/io/split/client/dtos/WhitelistMatcherData.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.split.client.dtos; - -import java.util.List; - -public class WhitelistMatcherData { - public List whitelist; -} diff --git a/client/src/main/java/io/split/engine/evaluator/Labels.java b/client/src/main/java/io/split/engine/evaluator/Labels.java deleted file mode 100644 index 28966d51e..000000000 --- a/client/src/main/java/io/split/engine/evaluator/Labels.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.split.engine.evaluator; - -public class Labels { - 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"; -} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java deleted file mode 100644 index a99fcd7aa..000000000 --- a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java +++ /dev/null @@ -1,120 +0,0 @@ -package io.split.engine.experiments; - -import io.split.client.dtos.ConditionType; -import io.split.client.dtos.Partition; -import io.split.rules.matchers.CombiningMatcher; - -import java.util.List; - -/** - * A pair of matcher and partitions. - * - * @author adil - */ -public final class ParsedCondition { - - private final ConditionType _conditionType; - private final CombiningMatcher _matcher; - private final List _partitions; - private final String _label; - - public static ParsedCondition createParsedConditionForTests(CombiningMatcher matcher, List partitions) { - return new ParsedCondition(ConditionType.ROLLOUT, matcher, partitions, null); - } - - - public ParsedCondition(ConditionType conditionType, CombiningMatcher matcher, List partitions, String label) { - _conditionType = conditionType; - _matcher = matcher; - _partitions = partitions; - _label = label; - } - - - public ConditionType conditionType() { - return _conditionType; - } - - public CombiningMatcher matcher() { - return _matcher; - } - - public List partitions() { - return _partitions; - } - - public String label() { - return _label; - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + _matcher.hashCode(); - - int partitionsHashCode = 17; - if (_partitions != null) { - for (Partition p : _partitions) { - partitionsHashCode = 31 * partitionsHashCode + p.treatment.hashCode(); - partitionsHashCode = 31 * partitionsHashCode + p.size; - } - } - result = 31 * result + partitionsHashCode; - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) return false; - if (this == obj) return true; - if (!(obj instanceof ParsedCondition)) return false; - - ParsedCondition other = (ParsedCondition) obj; - - boolean result = _matcher.equals(other._matcher); - - if (!result) { - return result; - } - if (_partitions == null) { - return result & (_partitions == other._partitions); - } - if (_partitions.size() != other._partitions.size()) { - return result; - } - - for (int i = 0; i < _partitions.size(); i++) { - Partition first = _partitions.get(i); - Partition second = other._partitions.get(i); - - result &= (first.size == second.size && first.treatment.equals(second.treatment)); - } - - return result; - } - - @Override - public String toString() { - StringBuilder bldr = new StringBuilder(); - - bldr.append(_matcher); - bldr.append(" then split "); - boolean first = true; - if (_partitions == null) { - return bldr.toString(); - } - for (Partition partition : _partitions) { - if (!first) { - bldr.append(','); - } - bldr.append(partition.size); - bldr.append(':'); - bldr.append(partition.treatment); - first = false; - } - - return bldr.toString(); - } - - -} diff --git a/client/src/main/java/io/split/engine/experiments/ParserUtils.java b/client/src/main/java/io/split/engine/experiments/ParserUtils.java deleted file mode 100644 index 03cc4ec14..000000000 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ /dev/null @@ -1,185 +0,0 @@ -package io.split.engine.experiments; - -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.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; - -public final class ParserUtils { - - private ParserUtils() { - throw new IllegalStateException("Utility class"); - } - - public static boolean checkUnsupportedMatcherExist(List matchers) { - MatcherType typeCheck = null; - for (Matcher matcher : matchers) { - typeCheck = null; - try { - typeCheck = matcher.matcherType; - } catch (NullPointerException e) { - // If the exception is caught, it means unsupported matcher - break; - } - } - return (typeCheck == null); - } - - public static ParsedCondition getTemplateCondition() { - List templatePartitions = new ArrayList<>(); - Partition partition = new Partition(); - partition.treatment = "control"; - partition.size = 100; - templatePartitions.add(partition); - return new ParsedCondition( - ConditionType.ROLLOUT, - CombiningMatcher.of(new AllKeysMatcher()), - templatePartitions, - Labels.UNSUPPORTED_MATCHER); - } - - public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { - List matchers = matcherGroup.matchers; - if (matchers.isEmpty()) throw new IllegalArgumentException(); - - List toCombine = new ArrayList<>(); - - for (Matcher matcher : matchers) { - toCombine.add(toMatcher(matcher)); - } - - 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.rules.matchers.Matcher delegate = null; - switch (matcher.matcherType) { - case ALL_KEYS: - delegate = new AllKeysMatcher(); - break; - case IN_SEGMENT: - String segmentName = matcher.userDefinedSegmentMatcherData.segmentName; - delegate = new UserDefinedSegmentMatcher(segmentName); - break; - case WHITELIST: - delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); - break; - case EQUAL_TO: - delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); - break; - case GREATER_THAN_OR_EQUAL_TO: - delegate = new GreaterThanOrEqualToMatcher( - matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); - break; - case LESS_THAN_OR_EQUAL_TO: - delegate = new LessThanOrEqualToMatcher( - matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); - break; - case BETWEEN: - delegate = new BetweenMatcher(matcher.betweenMatcherData.start, - matcher.betweenMatcherData.end, toRulesDataType(matcher.betweenMatcherData.dataType)); - break; - case EQUAL_TO_SET: - delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case PART_OF_SET: - delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_ALL_OF_SET: - delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_ANY_OF_SET: - delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case STARTS_WITH: - delegate = new StartsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case ENDS_WITH: - delegate = new EndsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_STRING: - delegate = new ContainsAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case MATCHES_STRING: - 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"); - 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"); - delegate = new BooleanMatcher(matcher.booleanMatcherData); - break; - case EQUAL_TO_SEMVER: - delegate = new EqualToSemverMatcher(matcher.stringMatcherData); - break; - case GREATER_THAN_OR_EQUAL_TO_SEMVER: - delegate = new GreaterThanOrEqualToSemverMatcher(matcher.stringMatcherData); - break; - case LESS_THAN_OR_EQUAL_TO_SEMVER: - delegate = new LessThanOrEqualToSemverMatcher(matcher.stringMatcherData); - break; - case IN_LIST_SEMVER: - delegate = new InListSemverMatcher(matcher.whitelistMatcherData.whitelist); - break; - case BETWEEN_SEMVER: - delegate = new BetweenSemverMatcher(matcher.betweenStringMatcherData.start, matcher.betweenStringMatcherData.end); - break; - case IN_RULE_BASED_SEGMENT: - String ruleBasedSegmentName = matcher.userDefinedSegmentMatcherData.segmentName; - delegate = new RuleBasedSegmentMatcher(ruleBasedSegmentName); - break; - default: - throw new IllegalArgumentException("Unknown matcher type: " + matcher.matcherType); - } - - String attribute = null; - if (matcher.keySelector != null && matcher.keySelector.attribute != null) { - attribute = matcher.keySelector.attribute; - } - - boolean negate = matcher.negate; - - - return new AttributeMatcher(attribute, delegate, negate); - } -} \ No newline at end of file From 55f5e74c4593b863569dc96001651f9a499305f9 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 2 Apr 2026 21:55:17 -0300 Subject: [PATCH 07/10] =?UTF-8?q?PR=202.1=20=E2=80=94=20Tidy:=20Replace=20?= =?UTF-8?q?Guava=20Maps.newConcurrentMap()=20in=20RuleBasedSegmentCacheInM?= =?UTF-8?q?emoryImp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace com.google.common.collect.Maps.newConcurrentMap() with new ConcurrentHashMap<>() so this class can be extracted to storage-commons without a Guava dependency. --- .../storages/memory/RuleBasedSegmentCacheInMemoryImp.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java b/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java index 660811ca8..e32eededb 100644 --- a/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java +++ b/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java @@ -1,6 +1,5 @@ package io.split.storages.memory; -import com.google.common.collect.Maps; import io.split.engine.experiments.ParsedRuleBasedSegment; import io.split.storages.RuleBasedSegmentCache; @@ -11,6 +10,7 @@ import java.util.List; import java.util.Set; import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @@ -29,7 +29,7 @@ public RuleBasedSegmentCacheInMemoryImp() { } public RuleBasedSegmentCacheInMemoryImp(long startingChangeNumber) { - _concurrentMap = Maps.newConcurrentMap(); + _concurrentMap = new ConcurrentHashMap<>(); _changeNumber = new AtomicLong(startingChangeNumber); } From 5d1f54820681894566a4ca70f441c0bdc8cc6e91 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 2 Apr 2026 22:02:43 -0300 Subject: [PATCH 08/10] =?UTF-8?q?PR=202.2=20=E2=80=94=20Extract:=20Create?= =?UTF-8?q?=20storage-commons=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move 3 RuleBasedSegment cache interfaces, 1 in-memory impl, RuleBasedSegmentParser, ParsedRuleBasedSegment, and RuleBasedSegment DTO into new storage-commons module. Depends on parsing-commons and slf4j. Add storage-commons dep to client/pom.xml. --- client/pom.xml | 5 +++ pom.xml | 1 + storage-commons/pom.xml | 41 +++++++++++++++++++ .../split/client/dtos/RuleBasedSegment.java | 0 .../experiments/ParsedRuleBasedSegment.java | 0 .../experiments/RuleBasedSegmentParser.java | 0 .../split/storages/RuleBasedSegmentCache.java | 0 .../RuleBasedSegmentCacheConsumer.java | 0 .../RuleBasedSegmentCacheProducer.java | 0 .../RuleBasedSegmentCacheInMemoryImp.java | 0 10 files changed, 47 insertions(+) create mode 100644 storage-commons/pom.xml rename {client => storage-commons}/src/main/java/io/split/client/dtos/RuleBasedSegment.java (100%) rename {client => storage-commons}/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java (100%) rename {client => storage-commons}/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java (100%) rename {client => storage-commons}/src/main/java/io/split/storages/RuleBasedSegmentCache.java (100%) rename {client => storage-commons}/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java (100%) rename {client => storage-commons}/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java (100%) rename {client => storage-commons}/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java (100%) diff --git a/client/pom.xml b/client/pom.xml index bb4da0e03..d5ec59bef 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -177,6 +177,11 @@ parsing-commons ${project.version} + + io.split.client + storage-commons + ${project.version} + io.split.client pluggable-storage diff --git a/pom.xml b/pom.xml index abc13c7f8..685a89269 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,7 @@ targeting-engine parsing-commons + storage-commons pluggable-storage redis-wrapper testing diff --git a/storage-commons/pom.xml b/storage-commons/pom.xml new file mode 100644 index 000000000..afef20b28 --- /dev/null +++ b/storage-commons/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + io.split.client + java-client-parent + 4.18.3 + + + 4.18.3 + storage-commons + jar + Storage Commons + RuleBasedSegment cache interfaces, in-memory impl, parser, and domain objects + + + + io.split.client + parsing-commons + ${project.version} + + + org.slf4j + slf4j-api + 1.7.36 + + + junit + junit + 4.13.1 + test + + + org.mockito + mockito-core + 5.14.2 + test + + + diff --git a/client/src/main/java/io/split/client/dtos/RuleBasedSegment.java b/storage-commons/src/main/java/io/split/client/dtos/RuleBasedSegment.java similarity index 100% rename from client/src/main/java/io/split/client/dtos/RuleBasedSegment.java rename to storage-commons/src/main/java/io/split/client/dtos/RuleBasedSegment.java diff --git a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java b/storage-commons/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java similarity index 100% rename from client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java rename to storage-commons/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java diff --git a/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java b/storage-commons/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java similarity index 100% rename from client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java rename to storage-commons/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCache.java b/storage-commons/src/main/java/io/split/storages/RuleBasedSegmentCache.java similarity index 100% rename from client/src/main/java/io/split/storages/RuleBasedSegmentCache.java rename to storage-commons/src/main/java/io/split/storages/RuleBasedSegmentCache.java diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java b/storage-commons/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java similarity index 100% rename from client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java rename to storage-commons/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java b/storage-commons/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java similarity index 100% rename from client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java rename to storage-commons/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java diff --git a/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java b/storage-commons/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java similarity index 100% rename from client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java rename to storage-commons/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java From c06660f45c0607f42a39322890a540e9bf2fe54a Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 2 Apr 2026 22:03:14 -0300 Subject: [PATCH 09/10] =?UTF-8?q?PR=202.3=20=E2=80=94=20Wire:=20Verify=20t?= =?UTF-8?q?ransitive=20consumers=20for=20storage-commons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited pluggable-storage, redis-wrapper, and testing modules. None import RuleBasedSegment cache interfaces, ParsedRuleBasedSegment, RuleBasedSegmentParser, or RuleBasedSegment DTO directly. No additional dependency changes required. From 45065b05c6bd6c97100aa0ca60b7491aca6a3352 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 2 Apr 2026 22:25:12 -0300 Subject: [PATCH 10/10] =?UTF-8?q?PR=203.1=20=E2=80=94=20Tidy:=20Extract=20?= =?UTF-8?q?narrow=20interfaces=20for=20segment-commons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ExecutorFactory, TelemetryListener, DefinitionsCacheConsumer interfaces - Refactor SegmentFetcherImp and SegmentSynchronizationTaskImp to use them - Remove Guava dependencies from segment classes - Update SplitFactoryImpl and all affected tests --- .../io/split/client/SplitFactoryImpl.java | 18 +++++++- .../engine/segments/ExecutorFactory.java | 8 ++++ .../engine/segments/SegmentFetcherImp.java | 19 ++++---- .../SegmentSynchronizationTaskImp.java | 43 +++++++++---------- .../engine/segments/TelemetryListener.java | 5 +++ .../storages/DefinitionsCacheConsumer.java | 7 +++ .../io/split/storages/SplitCacheCommons.java | 5 +-- .../common/LocalhostSynchronizerTest.java | 8 +++- .../split/engine/common/SynchronizerTest.java | 8 +++- .../engine/experiments/SplitFetcherTest.java | 10 +++-- .../segments/SegmentFetcherImpTest.java | 16 +++---- .../SegmentSynchronizationTaskImpTest.java | 26 ++++++----- 12 files changed, 104 insertions(+), 69 deletions(-) create mode 100644 client/src/main/java/io/split/engine/segments/ExecutorFactory.java create mode 100644 client/src/main/java/io/split/engine/segments/TelemetryListener.java create mode 100644 client/src/main/java/io/split/storages/DefinitionsCacheConsumer.java diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index 9ad38ef1b..e71a78ecb 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -57,8 +57,10 @@ import io.split.engine.experiments.SplitParser; import io.split.engine.experiments.SplitSynchronizationTask; import io.split.engine.experiments.RuleBasedSegmentParser; +import io.split.engine.segments.ExecutorFactory; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTaskImp; +import io.split.engine.segments.TelemetryListener; import io.split.integrations.IntegrationsConfig; import io.split.service.SplitHttpClientImpl; import io.split.service.SplitHttpClient; @@ -85,6 +87,7 @@ import io.split.storages.pluggable.adapters.UserCustomRuleBasedSegmentAdapterConsumer; import io.split.storages.pluggable.domain.UserStorageWrapper; import io.split.storages.pluggable.synchronizer.TelemetryConsumerSubmitter; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.NoopTelemetryStorage; import io.split.telemetry.storage.TelemetryStorage; @@ -129,6 +132,7 @@ import java.util.List; import java.util.ArrayList; +import io.split.client.utils.SplitExecutorFactory; import static io.split.client.utils.SplitExecutorFactory.buildExecutorService; public class SplitFactoryImpl implements SplitFactory { @@ -427,12 +431,17 @@ protected SplitFactoryImpl(SplitClientConfig config) { segmentChangeFetcher = new LocalhostSegmentChangeFetcher(config.segmentDirectory()); } + TelemetryListener segmentTelemetryListener = + t -> _telemetryStorageProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.SEGMENTS, t); + ExecutorFactory segmentExecutorFactory = + (tf, name, n) -> SplitExecutorFactory.buildScheduledExecutorService(tf, name, n); _segmentSynchronizationTaskImp = new SegmentSynchronizationTaskImp(segmentChangeFetcher, config.segmentsRefreshRate(), config.numThreadsForSegmentFetch(), segmentCache, - _telemetryStorageProducer, + segmentTelemetryListener, _splitCache, + segmentExecutorFactory, config.getThreadFactory(), ruleBasedSegmentCache); @@ -696,12 +705,17 @@ private SegmentSynchronizationTaskImp buildSegments(SplitClientConfig config, SegmentChangeFetcher segmentChangeFetcher = HttpSegmentChangeFetcher.create(_splitHttpClient, _rootTarget, _telemetryStorageProducer); + TelemetryListener segTelemetryListener = + t -> _telemetryStorageProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.SEGMENTS, t); + ExecutorFactory segExecutorFactory = + (tf, name, n) -> SplitExecutorFactory.buildScheduledExecutorService(tf, name, n); return new SegmentSynchronizationTaskImp(segmentChangeFetcher, config.segmentsRefreshRate(), config.numThreadsForSegmentFetch(), segmentCacheProducer, - _telemetryStorageProducer, + segTelemetryListener, splitCacheConsumer, + segExecutorFactory, config.getThreadFactory(), ruleBasedSegmentCache); } diff --git a/client/src/main/java/io/split/engine/segments/ExecutorFactory.java b/client/src/main/java/io/split/engine/segments/ExecutorFactory.java new file mode 100644 index 000000000..233ade6a6 --- /dev/null +++ b/client/src/main/java/io/split/engine/segments/ExecutorFactory.java @@ -0,0 +1,8 @@ +package io.split.engine.segments; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; + +public interface ExecutorFactory { + ScheduledExecutorService build(ThreadFactory threadFactory, String nameFormat, int numThreads); +} diff --git a/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java b/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java index 526ca02d1..a72df364c 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java +++ b/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java @@ -2,16 +2,13 @@ import io.split.client.dtos.SegmentChange; import io.split.storages.SegmentCacheProducer; -import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; -import io.split.telemetry.storage.TelemetryRuntimeProducer; import io.split.engine.common.FetchOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; +import java.util.Objects; public class SegmentFetcherImp implements SegmentFetcher { private static final Logger _log = LoggerFactory.getLogger(SegmentFetcherImp.class); @@ -19,16 +16,16 @@ public class SegmentFetcherImp implements SegmentFetcher { private final String _segmentName; private final SegmentChangeFetcher _segmentChangeFetcher; private final SegmentCacheProducer _segmentCacheProducer; - private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + private final TelemetryListener _telemetryListener; private final Object _lock = new Object(); public SegmentFetcherImp(String segmentName, SegmentChangeFetcher segmentChangeFetcher, SegmentCacheProducer segmentCacheProducer, - TelemetryRuntimeProducer telemetryRuntimeProducer) { - _segmentName = checkNotNull(segmentName); - _segmentChangeFetcher = checkNotNull(segmentChangeFetcher); - _segmentCacheProducer = checkNotNull(segmentCacheProducer); - _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + TelemetryListener telemetryListener) { + _segmentName = Objects.requireNonNull(segmentName); + _segmentChangeFetcher = Objects.requireNonNull(segmentChangeFetcher); + _segmentCacheProducer = Objects.requireNonNull(segmentCacheProducer); + _telemetryListener = Objects.requireNonNull(telemetryListener); _segmentCacheProducer.updateSegment(segmentName, new ArrayList<>(), new ArrayList<>(), -1L); } @@ -97,7 +94,7 @@ private void runWithoutExceptionHandling(FetchOptions options) { _log.info(_segmentName + " removed keys: " + summarize(change.removed)); } - _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.SEGMENTS, System.currentTimeMillis()); + _telemetryListener.recordSuccessfulSync(System.currentTimeMillis()); } } diff --git a/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java b/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java index 857493087..616b41c19 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java +++ b/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java @@ -1,12 +1,9 @@ package io.split.engine.segments; -import com.google.common.collect.Maps; -import io.split.client.utils.SplitExecutorFactory; import io.split.engine.common.FetchOptions; +import io.split.storages.DefinitionsCacheConsumer; import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheProducer; -import io.split.storages.SplitCacheConsumer; -import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,7 +11,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -26,9 +25,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - public class SegmentSynchronizationTaskImp implements SegmentSynchronizationTask, Closeable { private static final Logger _log = LoggerFactory.getLogger(SegmentSynchronizationTaskImp.class); @@ -36,30 +32,33 @@ public class SegmentSynchronizationTaskImp implements SegmentSynchronizationTask private final AtomicLong _refreshEveryNSeconds; private final AtomicBoolean _running; private final Object _lock = new Object(); - private final ConcurrentMap _segmentFetchers = Maps.newConcurrentMap(); + private final ConcurrentMap _segmentFetchers = new ConcurrentHashMap<>(); private final SegmentCacheProducer _segmentCacheProducer; private final ScheduledExecutorService _scheduledExecutorService; - private final TelemetryRuntimeProducer _telemetryRuntimeProducer; - private final SplitCacheConsumer _splitCacheConsumer; + private final TelemetryListener _telemetryListener; + private final DefinitionsCacheConsumer _definitionsCacheConsumer; private final RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; private ScheduledFuture _scheduledFuture; public SegmentSynchronizationTaskImp(SegmentChangeFetcher segmentChangeFetcher, long refreshEveryNSeconds, int numThreads, - SegmentCacheProducer segmentCacheProducer, TelemetryRuntimeProducer telemetryRuntimeProducer, - SplitCacheConsumer splitCacheConsumer, ThreadFactory threadFactory, + SegmentCacheProducer segmentCacheProducer, TelemetryListener telemetryListener, + DefinitionsCacheConsumer definitionsCacheConsumer, ExecutorFactory executorFactory, + ThreadFactory threadFactory, RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { - _segmentChangeFetcher = checkNotNull(segmentChangeFetcher); + _segmentChangeFetcher = Objects.requireNonNull(segmentChangeFetcher); - checkArgument(refreshEveryNSeconds >= 0L); + if (refreshEveryNSeconds < 0L) { + throw new IllegalArgumentException("refreshEveryNSeconds must be non-negative"); + } _refreshEveryNSeconds = new AtomicLong(refreshEveryNSeconds); - _scheduledExecutorService = SplitExecutorFactory.buildScheduledExecutorService(threadFactory, "split-segmentFetcher-" + "%d", numThreads); + _scheduledExecutorService = executorFactory.build(threadFactory, "split-segmentFetcher-%d", numThreads); _running = new AtomicBoolean(false); - _segmentCacheProducer = checkNotNull(segmentCacheProducer); - _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); - _splitCacheConsumer = checkNotNull(splitCacheConsumer); - _ruleBasedSegmentCacheConsumer = checkNotNull(ruleBasedSegmentCacheConsumer); + _segmentCacheProducer = Objects.requireNonNull(segmentCacheProducer); + _telemetryListener = Objects.requireNonNull(telemetryListener); + _definitionsCacheConsumer = Objects.requireNonNull(definitionsCacheConsumer); + _ruleBasedSegmentCacheConsumer = Objects.requireNonNull(ruleBasedSegmentCacheConsumer); } public void initializeSegment(String segmentName) { @@ -77,7 +76,7 @@ public void initializeSegment(String segmentName) { return; } - SegmentFetcher newSegment = new SegmentFetcherImp(segmentName, _segmentChangeFetcher, _segmentCacheProducer, _telemetryRuntimeProducer); + SegmentFetcher newSegment = new SegmentFetcherImp(segmentName, _segmentChangeFetcher, _segmentCacheProducer, _telemetryListener); if (_running.get()) { _scheduledExecutorService.submit(() -> newSegment.fetch(new FetchOptions.Builder().build())); @@ -195,14 +194,14 @@ private void initialize(String segmentName) { return; } - segment = new SegmentFetcherImp(segmentName, _segmentChangeFetcher, _segmentCacheProducer, _telemetryRuntimeProducer); + segment = new SegmentFetcherImp(segmentName, _segmentChangeFetcher, _segmentCacheProducer, _telemetryListener); _segmentFetchers.putIfAbsent(segmentName, segment); } } private Set getSegmentNames() { - Set names = new HashSet<>(_splitCacheConsumer.getSegments()); + Set names = new HashSet<>(_definitionsCacheConsumer.getSegments()); names.addAll(_ruleBasedSegmentCacheConsumer.getSegments()); return names; diff --git a/client/src/main/java/io/split/engine/segments/TelemetryListener.java b/client/src/main/java/io/split/engine/segments/TelemetryListener.java new file mode 100644 index 000000000..78b8da527 --- /dev/null +++ b/client/src/main/java/io/split/engine/segments/TelemetryListener.java @@ -0,0 +1,5 @@ +package io.split.engine.segments; + +public interface TelemetryListener { + void recordSuccessfulSync(long time); +} diff --git a/client/src/main/java/io/split/storages/DefinitionsCacheConsumer.java b/client/src/main/java/io/split/storages/DefinitionsCacheConsumer.java new file mode 100644 index 000000000..8200dec08 --- /dev/null +++ b/client/src/main/java/io/split/storages/DefinitionsCacheConsumer.java @@ -0,0 +1,7 @@ +package io.split.storages; + +import java.util.Set; + +public interface DefinitionsCacheConsumer { + Set getSegments(); +} diff --git a/client/src/main/java/io/split/storages/SplitCacheCommons.java b/client/src/main/java/io/split/storages/SplitCacheCommons.java index 539e3144e..49bbf3adb 100644 --- a/client/src/main/java/io/split/storages/SplitCacheCommons.java +++ b/client/src/main/java/io/split/storages/SplitCacheCommons.java @@ -1,8 +1,5 @@ package io.split.storages; -import java.util.Set; - -public interface SplitCacheCommons { +public interface SplitCacheCommons extends DefinitionsCacheConsumer { long getChangeNumber(); - Set getSegments(); } diff --git a/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java b/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java index 04163aedd..a46d3176e 100644 --- a/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java +++ b/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java @@ -7,8 +7,10 @@ import io.split.client.utils.FileInputStreamProvider; import io.split.client.utils.InputStreamProvider; import io.split.engine.experiments.*; +import io.split.engine.segments.ExecutorFactory; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTaskImp; +import io.split.engine.segments.TelemetryListener; import io.split.storages.*; import io.split.storages.memory.InMemoryCacheImp; import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; @@ -24,6 +26,8 @@ public class LocalhostSynchronizerTest { private static final TelemetryStorage TELEMETRY_STORAGE_NOOP = Mockito.mock(NoopTelemetryStorage.class); + private static final TelemetryListener TELEMETRY_LISTENER = t -> {}; + private static final ExecutorFactory EXECUTOR_FACTORY = (tf, name, n) -> java.util.concurrent.Executors.newScheduledThreadPool(n); private static final FlagSetsFilter FLAG_SETS_FILTER = new FlagSetsFilterImpl(new HashSet<>()); @Test @@ -45,7 +49,7 @@ public void testSyncAll(){ SegmentCacheProducer segmentCacheProducer = new SegmentCacheInMemoryImpl(); SegmentSynchronizationTaskImp segmentSynchronizationTaskImp = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1000, 1, segmentCacheProducer, - TELEMETRY_STORAGE_NOOP, splitCacheProducer, null, ruleBasedSegmentCache); + TELEMETRY_LISTENER, splitCacheProducer, EXECUTOR_FACTORY, null, ruleBasedSegmentCache); SplitTasks splitTasks = SplitTasks.build(splitSynchronizationTask, segmentSynchronizationTaskImp, null, null, null, null); LocalhostSynchronizer localhostSynchronizer = new LocalhostSynchronizer(splitTasks, splitFetcher, false); @@ -72,7 +76,7 @@ public void testPeriodicFetching() throws InterruptedException { SegmentCacheProducer segmentCacheProducer = new SegmentCacheInMemoryImpl(); SegmentSynchronizationTaskImp segmentSynchronizationTaskImp = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1000, 1, segmentCacheProducer, - TELEMETRY_STORAGE_NOOP, splitCacheProducer, null, ruleBasedSegmentCache); + TELEMETRY_LISTENER, splitCacheProducer, EXECUTOR_FACTORY, null, ruleBasedSegmentCache); SplitTasks splitTasks = SplitTasks.build(splitSynchronizationTask, segmentSynchronizationTaskImp, null, null, null, null); LocalhostSynchronizer localhostSynchronizer = new LocalhostSynchronizer(splitTasks, splitFetcher, true); diff --git a/client/src/test/java/io/split/engine/common/SynchronizerTest.java b/client/src/test/java/io/split/engine/common/SynchronizerTest.java index 0ce439c7f..a0150d52e 100644 --- a/client/src/test/java/io/split/engine/common/SynchronizerTest.java +++ b/client/src/test/java/io/split/engine/common/SynchronizerTest.java @@ -5,8 +5,10 @@ import io.split.client.impressions.UniqueKeysTracker; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; +import io.split.engine.segments.ExecutorFactory; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTaskImp; +import io.split.engine.segments.TelemetryListener; import io.split.storages.*; import io.split.storages.memory.InMemoryCacheImp; import io.split.engine.experiments.FetchResult; @@ -81,9 +83,11 @@ public void syncAll() throws InterruptedException { @Test public void testSyncAllSegments() throws InterruptedException, NoSuchFieldException, IllegalAccessException { + ExecutorFactory executorFactory = (tf, name, n) -> java.util.concurrent.Executors.newScheduledThreadPool(n); + TelemetryListener telemetryListener = t -> {}; SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(Mockito.mock(SegmentChangeFetcher.class), - 20L, 1, _segmentCacheProducer, Mockito.mock(TelemetryRuntimeProducer.class), - Mockito.mock(SplitCacheConsumer.class), null, Mockito.mock(RuleBasedSegmentCache.class)); + 20L, 1, _segmentCacheProducer, telemetryListener, + Mockito.mock(SplitCacheConsumer.class), executorFactory, null, Mockito.mock(RuleBasedSegmentCache.class)); Field synchronizerSegmentFetcher = SynchronizerImp.class.getDeclaredField("_segmentSynchronizationTaskImp"); synchronizerSegmentFetcher.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); 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 d388d7b6d..e58b4a40f 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java @@ -12,9 +12,11 @@ import io.split.engine.common.FetchOptions; import io.split.rules.matchers.AllKeysMatcher; import io.split.rules.matchers.CombiningMatcher; +import io.split.engine.segments.ExecutorFactory; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTask; import io.split.engine.segments.SegmentSynchronizationTaskImp; +import io.split.engine.segments.TelemetryListener; import io.split.grammar.Treatments; import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.TelemetryRuntimeProducer; @@ -52,6 +54,8 @@ public class SplitFetcherTest { private static final Logger _log = LoggerFactory.getLogger(SplitFetcherTest.class); private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + private static final TelemetryListener TELEMETRY_LISTENER = t -> {}; + private static final ExecutorFactory EXECUTOR_FACTORY = (tf, name, n) -> java.util.concurrent.Executors.newScheduledThreadPool(n); private static final FlagSetsFilter FLAG_SETS_FILTER = new FlagSetsFilterImpl(new HashSet<>()); @Test @@ -159,7 +163,7 @@ public void whenParserFailsWeRemoveTheExperiment() throws InterruptedException { RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, TELEMETRY_STORAGE, cache, null, ruleBasedSegmentCache); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1, 10, segmentCache, TELEMETRY_LISTENER, cache, EXECUTOR_FACTORY, null, ruleBasedSegmentCache); segmentSynchronizationTask.start(); SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER, ruleBasedSegmentParser, ruleBasedSegmentCache); @@ -184,7 +188,7 @@ public void ifThereIsAProblemTalkingToSplitChangeCountDownLatchIsNotDecremented( RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, TELEMETRY_STORAGE, cache, null, ruleBasedSegmentCache); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1, 10, segmentCache, TELEMETRY_LISTENER, cache, EXECUTOR_FACTORY, null, ruleBasedSegmentCache); segmentSynchronizationTask.start(); SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER, ruleBasedSegmentParser, ruleBasedSegmentCache); @@ -286,7 +290,7 @@ public void worksWithUserDefinedSegments() throws Exception { SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); SegmentChange segmentChange = getSegmentChange(0L, 0L, segmentName); when(segmentChangeFetcher.fetch(anyString(), anyLong(), any())).thenReturn(segmentChange); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, Mockito.mock(TelemetryStorage.class), cache, null, ruleBasedSegmentCache); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1, 10, segmentCache, TELEMETRY_LISTENER, cache, EXECUTOR_FACTORY, null, ruleBasedSegmentCache); segmentSynchronizationTask.start(); SplitFetcherImp fetcher = new SplitFetcherImp(experimentChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER, ruleBasedSegmentParser, ruleBasedSegmentCache); diff --git a/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java b/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java index 0872fb45b..f4bb44adc 100644 --- a/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java +++ b/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java @@ -4,9 +4,7 @@ import io.split.storages.SegmentCacheProducer; import io.split.storages.memory.SegmentCacheInMemoryImpl; import io.split.client.dtos.SegmentChange; -import io.split.telemetry.storage.InMemoryTelemetryStorage; -import io.split.telemetry.storage.TelemetryRuntimeProducer; -import io.split.telemetry.storage.TelemetryStorage; + import io.split.engine.common.FetchOptions; import org.junit.Assert; import org.junit.Test; @@ -33,7 +31,7 @@ public class SegmentFetcherImpTest { private static final Logger _log = LoggerFactory.getLogger(SegmentFetcherImpTest.class); private static final String SEGMENT_NAME = "foo"; - private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + private static final TelemetryListener TELEMETRY_LISTENER = t -> {}; @Test public void worksWhenWeStartWithoutState() throws InterruptedException { @@ -53,7 +51,7 @@ public void worksWhenThereAreNoChanges() throws InterruptedException { SegmentChange segmentChange = getSegmentChange(-1L, 10L); Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChange); - SegmentFetcherImp fetcher = new SegmentFetcherImp(SEGMENT_NAME, segmentChangeFetcher, segmentCache, TELEMETRY_STORAGE); + SegmentFetcherImp fetcher = new SegmentFetcherImp(SEGMENT_NAME, segmentChangeFetcher, segmentCache, TELEMETRY_LISTENER); // execute the fetcher for a little bit. ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); @@ -88,7 +86,7 @@ private void works(long startingChangeNumber) throws InterruptedException { Mockito.when(segmentChangeFetcher.fetch(Mockito.eq(SEGMENT_NAME),Mockito.eq( -1L), Mockito.any())).thenReturn(segmentChange); Mockito.when(segmentChangeFetcher.fetch(Mockito.eq(SEGMENT_NAME),Mockito.eq( 0L), Mockito.any())).thenReturn(segmentChange); - SegmentFetcher fetcher = new SegmentFetcherImp(segmentName, segmentChangeFetcher, segmentCacheProducer, Mockito.mock(TelemetryRuntimeProducer.class)); + SegmentFetcher fetcher = new SegmentFetcherImp(segmentName, segmentChangeFetcher, segmentCacheProducer, t -> {}); // execute the fetcher for a little bit. ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); @@ -113,21 +111,21 @@ private void works(long startingChangeNumber) throws InterruptedException { @Test(expected = NullPointerException.class) public void doesNotWorkIfSegmentChangeFetcherIsNull() { SegmentCacheProducer segmentCacheProducer = Mockito.mock(SegmentCacheProducer.class); - SegmentFetcher fetcher = new SegmentFetcherImp(SEGMENT_NAME, null, segmentCacheProducer, TELEMETRY_STORAGE); + SegmentFetcher fetcher = new SegmentFetcherImp(SEGMENT_NAME, null, segmentCacheProducer, TELEMETRY_LISTENER); } @Test(expected = NullPointerException.class) public void doesNotWorkIfSegmentNameIsNull() { SegmentCacheProducer segmentCacheProducer = Mockito.mock(SegmentCacheProducer.class); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); - SegmentFetcher fetcher = new SegmentFetcherImp(null, segmentChangeFetcher, segmentCacheProducer, TELEMETRY_STORAGE); + SegmentFetcher fetcher = new SegmentFetcherImp(null, segmentChangeFetcher, segmentCacheProducer, TELEMETRY_LISTENER); } @Test public void testBypassCdnClearedAfterFirstHit() { SegmentChangeFetcher mockFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentCache segmentCacheMock = new SegmentCacheInMemoryImpl(); - SegmentFetcher fetcher = new SegmentFetcherImp("someSegment", mockFetcher, segmentCacheMock, Mockito.mock(TelemetryRuntimeProducer.class)); + SegmentFetcher fetcher = new SegmentFetcherImp("someSegment", mockFetcher, segmentCacheMock, t -> {}); SegmentChange response1 = new SegmentChange(); diff --git a/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java b/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java index f6f7f04f4..c5099e429 100644 --- a/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java +++ b/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java @@ -1,7 +1,5 @@ package io.split.engine.segments; -import com.google.common.collect.Maps; -import io.split.Spec; import io.split.client.LocalhostSegmentChangeFetcher; import io.split.client.JsonLocalhostSplitChangeFetcher; import io.split.client.interceptors.FlagSetsFilter; @@ -11,12 +9,10 @@ import io.split.engine.common.FetchOptions; import io.split.engine.experiments.*; import io.split.storages.*; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import io.split.storages.memory.InMemoryCacheImp; import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; -import io.split.telemetry.storage.InMemoryTelemetryStorage; -import io.split.telemetry.storage.NoopTelemetryStorage; -import io.split.telemetry.storage.TelemetryStorage; import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; @@ -32,6 +28,7 @@ import java.lang.reflect.Modifier; import java.util.HashSet; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -47,8 +44,9 @@ */ public class SegmentSynchronizationTaskImpTest { private static final Logger _log = LoggerFactory.getLogger(SegmentSynchronizationTaskImpTest.class); - private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); - private static final TelemetryStorage TELEMETRY_STORAGE_NOOP = Mockito.mock(NoopTelemetryStorage.class); + private static final TelemetryListener TELEMETRY_STORAGE = t -> {}; + private static final TelemetryListener TELEMETRY_STORAGE_NOOP = t -> {}; + private static final ExecutorFactory EXECUTOR_FACTORY = (tf, name, n) -> java.util.concurrent.Executors.newScheduledThreadPool(n); private AtomicReference fetcher1 = null; private AtomicReference fetcher2 = null; @@ -65,7 +63,7 @@ public void works() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, segmentCacheProducer, - TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null, Mockito.mock(RuleBasedSegmentCache.class)); + TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), EXECUTOR_FACTORY, null, Mockito.mock(RuleBasedSegmentCache.class)); // create two tasks that will separately call segment and make sure @@ -104,13 +102,13 @@ public void run() { @Test public void testFetchAllAsynchronousAndGetFalse() throws NoSuchFieldException, IllegalAccessException { SegmentCacheProducer segmentCacheProducer = Mockito.mock(SegmentCacheProducer.class); - ConcurrentMap _segmentFetchers = Maps.newConcurrentMap(); + ConcurrentMap _segmentFetchers = new ConcurrentHashMap<>(); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentFetcherImp segmentFetcher = Mockito.mock(SegmentFetcherImp.class); _segmentFetchers.put("SF", segmentFetcher); final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, - segmentCacheProducer, TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null, Mockito.mock(RuleBasedSegmentCache.class)); + segmentCacheProducer, TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), EXECUTOR_FACTORY, null, Mockito.mock(RuleBasedSegmentCache.class)); Mockito.when(segmentFetcher.runWhitCacheHeader()).thenReturn(false); Mockito.when(segmentFetcher.fetch(Mockito.anyObject())).thenReturn(false); @@ -130,11 +128,11 @@ public void testFetchAllAsynchronousAndGetFalse() throws NoSuchFieldException, I public void testFetchAllAsynchronousAndGetTrue() throws NoSuchFieldException, IllegalAccessException { SegmentCacheProducer segmentCacheProducer = Mockito.mock(SegmentCacheProducer.class); - ConcurrentMap _segmentFetchers = Maps.newConcurrentMap(); + ConcurrentMap _segmentFetchers = new ConcurrentHashMap<>(); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentFetcherImp segmentFetcher = Mockito.mock(SegmentFetcherImp.class); final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, segmentCacheProducer, - TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null, Mockito.mock(RuleBasedSegmentCache.class)); + TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), EXECUTOR_FACTORY, null, Mockito.mock(RuleBasedSegmentCache.class)); // Before executing, we'll update the map of segmentFecthers via reflection. Field segmentFetchersForced = SegmentSynchronizationTaskImp.class.getDeclaredField("_segmentFetchers"); @@ -162,7 +160,7 @@ public void testLocalhostSegmentChangeFetcher() throws InterruptedException, Fil RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter, + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, Mockito.mock(TelemetryRuntimeProducer.class), flagSetsFilter, ruleBasedSegmentParser, ruleBasedSegmentCache); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000, null); @@ -175,7 +173,7 @@ public void testLocalhostSegmentChangeFetcher() throws InterruptedException, Fil SegmentCacheProducer segmentCacheProducer = new SegmentCacheInMemoryImpl(); SegmentSynchronizationTaskImp segmentSynchronizationTaskImp = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1000, 1, segmentCacheProducer, - TELEMETRY_STORAGE_NOOP, splitCacheProducer, null, ruleBasedSegmentCache); + TELEMETRY_STORAGE_NOOP, splitCacheProducer, EXECUTOR_FACTORY, null, ruleBasedSegmentCache); segmentSynchronizationTaskImp.start();