From 68731e95c82ea29a088beaa713d0ceea83d6c39f Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 1 Apr 2026 15:28:05 -0300 Subject: [PATCH 01/11] 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/11] Targeting rule evaluator AI-Session-Id: 095253e8-7e1c-4578-9779-bf96395021cf AI-Tool: claude-code AI-Model: unknown --- targeting-engine/pom.xml | 50 +++ .../io/split/rules/bucketing/Bucketer.java | 78 +++++ .../io/split/rules/bucketing/MurmurHash3.java | 302 ++++++++++++++++++ .../split/rules/engine/EvaluationContext.java | 25 ++ .../split/rules/engine/EvaluationLabels.java | 14 + .../split/rules/engine/EvaluationResult.java | 25 ++ .../split/rules/engine/TargetingEngine.java | 16 + .../rules/engine/TargetingEngineImpl.java | 74 +++++ .../exceptions/VersionedExceptionWrapper.java | 19 ++ .../java/io/split/rules/logging/Logger.java | 8 + .../split/rules/matchers/AllKeysMatcher.java | 39 +++ .../rules/matchers/AttributeMatcher.java | 105 ++++++ .../split/rules/matchers/BetweenMatcher.java | 84 +++++ .../rules/matchers/BetweenSemverMatcher.java | 58 ++++ .../split/rules/matchers/BooleanMatcher.java | 46 +++ .../rules/matchers/CombiningMatcher.java | 73 +++++ .../rules/matchers/DependencyMatcher.java | 63 ++++ .../split/rules/matchers/EqualToMatcher.java | 71 ++++ .../rules/matchers/EqualToSemverMatcher.java | 54 ++++ .../matchers/GreaterThanOrEqualToMatcher.java | 74 +++++ .../GreaterThanOrEqualToSemverMatcher.java | 54 ++++ .../rules/matchers/InListSemverMatcher.java | 77 +++++ .../matchers/LessThanOrEqualToMatcher.java | 73 +++++ .../LessThanOrEqualToSemverMatcher.java | 54 ++++ .../java/io/split/rules/matchers/Matcher.java | 9 + .../rules/matchers/PrerequisitesMatcher.java | 71 ++++ .../matchers/RuleBasedSegmentMatcher.java | 41 +++ .../java/io/split/rules/matchers/Semver.java | 171 ++++++++++ .../io/split/rules/matchers/Transformers.java | 103 ++++++ .../matchers/UserDefinedSegmentMatcher.java | 40 +++ .../rules/matchers/WhitelistMatcher.java | 66 ++++ .../collections/ContainsAllOfSetMatcher.java | 70 ++++ .../collections/ContainsAnyOfSetMatcher.java | 75 +++++ .../collections/EqualToSetMatcher.java | 68 ++++ .../collections/PartOfSetMatcher.java | 72 +++++ .../strings/ContainsAnyOfMatcher.java | 83 +++++ .../strings/EndsWithAnyOfMatcher.java | 83 +++++ .../strings/RegularExpressionMatcher.java | 51 +++ .../strings/StartsWithAnyOfMatcher.java | 83 +++++ .../java/io/split/rules/model/Condition.java | 36 +++ .../io/split/rules/model/ConditionType.java | 6 + .../java/io/split/rules/model/DataType.java | 7 + .../java/io/split/rules/model/Partition.java | 11 + .../io/split/rules/model/Prerequisite.java | 37 +++ .../io/split/rules/model/TargetingRule.java | 73 +++++ .../split/rules/bucketing/BucketerTest.java | 88 +++++ .../rules/engine/TargetingEngineImplTest.java | 161 ++++++++++ .../rules/matchers/AllKeysMatcherTest.java | 26 ++ .../rules/matchers/BetweenMatcherTest.java | 37 +++ .../rules/matchers/BooleanMatcherTest.java | 34 ++ .../io/split/rules/matchers/SemverTest.java | 52 +++ 51 files changed, 3190 insertions(+) create mode 100644 targeting-engine/pom.xml create mode 100644 targeting-engine/src/main/java/io/split/rules/bucketing/Bucketer.java create mode 100644 targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/EvaluationContext.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/EvaluationLabels.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/TargetingEngine.java create mode 100644 targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java create mode 100644 targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java create mode 100644 targeting-engine/src/main/java/io/split/rules/logging/Logger.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/CombiningMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/Matcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/RuleBasedSegmentMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/Semver.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/UserDefinedSegmentMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/Condition.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/ConditionType.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/DataType.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/Partition.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/Prerequisite.java create mode 100644 targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java create mode 100644 targeting-engine/src/test/java/io/split/rules/bucketing/BucketerTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/matchers/AllKeysMatcherTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/matchers/BetweenMatcherTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/matchers/BooleanMatcherTest.java create mode 100644 targeting-engine/src/test/java/io/split/rules/matchers/SemverTest.java diff --git a/targeting-engine/pom.xml b/targeting-engine/pom.xml new file mode 100644 index 000000000..d362412df --- /dev/null +++ b/targeting-engine/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + io.split.client + java-client-parent + 4.18.3 + + + 4.18.3 + targeting-engine + jar + Targeting Engine + A generic, zero-dependency targeting rules engine extracted from the Split Java SDK + + + + junit + junit + 4.13.1 + test + + + org.mockito + mockito-core + 5.14.2 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/targeting-engine/src/main/java/io/split/rules/bucketing/Bucketer.java b/targeting-engine/src/main/java/io/split/rules/bucketing/Bucketer.java new file mode 100644 index 000000000..23594924f --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/bucketing/Bucketer.java @@ -0,0 +1,78 @@ +package io.split.rules.bucketing; + +import io.split.rules.model.Partition; + +import java.util.List; + +/** + * Hashes keys into buckets and selects treatments from partition lists. + */ +public final class Bucketer { + private static final int ALGO_LEGACY = 1; + private static final int ALGO_MURMUR = 2; + private static final String CONTROL = "control"; + + /** + * Returns the treatment for the given key, seed, partitions, and algorithm. + * Returns "control" if no partition matches. + */ + public static String getTreatment(String key, int seed, List partitions, int algo) { + if (partitions.isEmpty()) { + return CONTROL; + } + if (hundredPercentOneTreatment(partitions)) { + return partitions.get(0).treatment; + } + return selectTreatment(bucket(hash(key, seed, algo)), partitions); + } + + /** + * Returns a bucket between 1 and 100, inclusive. + */ + public static int getBucket(String key, int seed, int algo) { + return bucket(hash(key, seed, algo)); + } + + static long hash(String key, int seed, int algo) { + switch (algo) { + case ALGO_MURMUR: + return murmurHash(key, seed); + case ALGO_LEGACY: + default: + return legacyHash(key, seed); + } + } + + static long murmurHash(String key, int seed) { + return MurmurHash3.murmurhash3_x86_32(key, 0, key.length(), seed); + } + + static int legacyHash(String key, int seed) { + int h = 0; + for (int i = 0; i < key.length(); i++) { + h = 31 * h + key.charAt(i); + } + return h ^ seed; + } + + static int bucket(long hash) { + return (int) (Math.abs(hash % 100) + 1); + } + + private static String selectTreatment(int bucket, List partitions) { + int covered = 0; + for (Partition partition : partitions) { + covered += partition.size; + if (covered >= bucket) { + return partition.treatment; + } + } + return CONTROL; + } + + private static boolean hundredPercentOneTreatment(List partitions) { + return partitions.size() == 1 && partitions.get(0).size == 100; + } + + private Bucketer() {} +} diff --git a/targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java b/targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java new file mode 100644 index 000000000..da0376d8e --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/bucketing/MurmurHash3.java @@ -0,0 +1,302 @@ +package io.split.rules.bucketing; + +/** + * The MurmurHash3 algorithm was created by Austin Appleby and placed in the public domain. + * This java port was authored by Yonik Seeley and also placed into the public domain. + * The author hereby disclaims copyright to this source code. + *

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

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

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

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

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

+ * + * @param data The input byte array + * @param offset The first element of array + * @param length The length of array + * @param seed The initial seed value + * @return The 128-bit hash (2 longs) + */ + public static long[] hash128x64(final byte[] data, final int offset, final int length, final long seed) { + long h1 = seed; + long h2 = seed; + final int nblocks = length >> 4; + + // body + for (int i = 0; i < nblocks; i++) { + final int index = offset + (i << 4); + long k1 = getLittleEndianLong(data, index); + long k2 = getLittleEndianLong(data, index + 8); + + // mix functions for k1 + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + h1 ^= k1; + h1 = Long.rotateLeft(h1, R2); + h1 += h2; + h1 = h1 * M + N1; + + // mix functions for k2 + k2 *= C2; + k2 = Long.rotateLeft(k2, R3); + k2 *= C1; + h2 ^= k2; + h2 = Long.rotateLeft(h2, R1); + h2 += h1; + h2 = h2 * M + N2; + } + + // tail + long k1 = 0; + long k2 = 0; + final int index = offset + (nblocks << 4); + switch (offset + length - index) { + case 15: + k2 ^= ((long) data[index + 14] & 0xff) << 48; + case 14: + k2 ^= ((long) data[index + 13] & 0xff) << 40; + case 13: + k2 ^= ((long) data[index + 12] & 0xff) << 32; + case 12: + k2 ^= ((long) data[index + 11] & 0xff) << 24; + case 11: + k2 ^= ((long) data[index + 10] & 0xff) << 16; + case 10: + k2 ^= ((long) data[index + 9] & 0xff) << 8; + case 9: + k2 ^= data[index + 8] & 0xff; + k2 *= C2; + k2 = Long.rotateLeft(k2, R3); + k2 *= C1; + h2 ^= k2; + + case 8: + k1 ^= ((long) data[index + 7] & 0xff) << 56; + case 7: + k1 ^= ((long) data[index + 6] & 0xff) << 48; + case 6: + k1 ^= ((long) data[index + 5] & 0xff) << 40; + case 5: + k1 ^= ((long) data[index + 4] & 0xff) << 32; + case 4: + k1 ^= ((long) data[index + 3] & 0xff) << 24; + case 3: + k1 ^= ((long) data[index + 2] & 0xff) << 16; + case 2: + k1 ^= ((long) data[index + 1] & 0xff) << 8; + case 1: + k1 ^= data[index] & 0xff; + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + h1 ^= k1; + } + + // finalization + h1 ^= length; + h2 ^= length; + + h1 += h2; + h2 += h1; + + h1 = fmix64(h1); + h2 = fmix64(h2); + + h1 += h2; + h2 += h1; + + return new long[] { h1, h2 }; + } +} \ No newline at end of file diff --git a/targeting-engine/src/main/java/io/split/rules/engine/EvaluationContext.java b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationContext.java new file mode 100644 index 000000000..4cfae4f47 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationContext.java @@ -0,0 +1,25 @@ +package io.split.rules.engine; + +import java.util.Map; + +/** + * Provides recursive evaluation and segment membership checks to matchers. + * Each SDK implements this interface to bridge to its own storage and evaluator. + */ +public interface EvaluationContext { + + /** + * Evaluates a targeting rule by name. Used by DependencyMatcher and PrerequisitesMatcher. + */ + EvaluationResult evaluate(String matchingKey, String bucketingKey, String ruleName, Map attributes); + + /** + * Checks if the given key is a member of a standard segment. + */ + boolean isInSegment(String segmentName, String key); + + /** + * Checks if the given key is a member of a rule-based segment. + */ + boolean isInRuleBasedSegment(String segmentName, String key, String bucketingKey, Map attributes); +} diff --git a/targeting-engine/src/main/java/io/split/rules/engine/EvaluationLabels.java b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationLabels.java new file mode 100644 index 000000000..5d89abc9d --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationLabels.java @@ -0,0 +1,14 @@ +package io.split.rules.engine; + +public final class EvaluationLabels { + public static final String NOT_IN_SPLIT = "not in split"; + public static final String DEFAULT_RULE = "default rule"; + public static final String KILLED = "killed"; + public static final String DEFINITION_NOT_FOUND = "definition not found"; + public static final String EXCEPTION = "exception"; + public static final String UNSUPPORTED_MATCHER = "targeting rule type unsupported by sdk"; + public static final String PREREQUISITES_NOT_MET = "prerequisites not met"; + public static final String NOT_READY = "not ready"; + + private EvaluationLabels() {} +} diff --git a/targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java new file mode 100644 index 000000000..0d9481f6c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java @@ -0,0 +1,25 @@ +package io.split.rules.engine; + +public final class EvaluationResult { + public final String treatment; + public final String label; + public final Long version; + public final String config; + public final boolean impressionsDisabled; + + public EvaluationResult(String treatment, String label) { + this(treatment, label, null, null, false); + } + + public EvaluationResult(String treatment, String label, Long version) { + this(treatment, label, version, null, false); + } + + public EvaluationResult(String treatment, String label, Long version, String config, boolean impressionsDisabled) { + this.treatment = treatment; + this.label = label; + this.version = version; + this.config = config; + this.impressionsDisabled = impressionsDisabled; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngine.java b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngine.java new file mode 100644 index 000000000..6664650ba --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngine.java @@ -0,0 +1,16 @@ +package io.split.rules.engine; + +import io.split.rules.exceptions.VersionedExceptionWrapper; +import io.split.rules.model.TargetingRule; + +import java.util.Map; + +/** + * Evaluates a targeting rule against a key and attributes. + * This is the core contract of the targeting engine. + */ +public interface TargetingEngine { + EvaluationResult evaluate(String matchingKey, String bucketingKey, + TargetingRule rule, Map attributes, + EvaluationContext context) throws VersionedExceptionWrapper; +} diff --git a/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java new file mode 100644 index 000000000..590d31789 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java @@ -0,0 +1,74 @@ +package io.split.rules.engine; + +import io.split.rules.bucketing.Bucketer; +import io.split.rules.exceptions.VersionedExceptionWrapper; +import io.split.rules.model.Condition; +import io.split.rules.model.ConditionType; +import io.split.rules.model.TargetingRule; + +import java.util.Map; + +public final class TargetingEngineImpl implements TargetingEngine { + + @Override + public EvaluationResult evaluate(String matchingKey, String bucketingKey, + TargetingRule rule, Map attributes, + EvaluationContext context) throws VersionedExceptionWrapper { + try { + String config = getConfig(rule, rule.defaultTreatment()); + + // 1. Killed rule → return default treatment + if (rule.killed()) { + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.KILLED, + rule.changeNumber(), config, rule.impressionsDisabled()); + } + + // 2. Bucketing key resolution + String bk = bucketingKey != null ? bucketingKey : matchingKey; + + // 3. Prerequisites check + if (!rule.prerequisitesMatcher().match(matchingKey, bk, attributes, context)) { + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.PREREQUISITES_NOT_MET, + rule.changeNumber(), config, rule.impressionsDisabled()); + } + + // 4. Iterate conditions + boolean inRollout = false; + for (Condition condition : rule.conditions()) { + + // 4a. Traffic allocation check (once, before first ROLLOUT condition) + if (!inRollout && condition.conditionType() == ConditionType.ROLLOUT) { + if (rule.trafficAllocation() < 100) { + int bucket = Bucketer.getBucket(bk, rule.trafficAllocationSeed(), rule.algo()); + if (bucket > rule.trafficAllocation()) { + config = getConfig(rule, rule.defaultTreatment()); + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.NOT_IN_SPLIT, + rule.changeNumber(), config, rule.impressionsDisabled()); + } + } + inRollout = true; + } + + // 4b. Condition match → select treatment + if (condition.matcher().match(matchingKey, bucketingKey, attributes, context)) { + String treatment = Bucketer.getTreatment(bk, rule.seed(), condition.partitions(), rule.algo()); + config = getConfig(rule, treatment); + return new EvaluationResult(treatment, condition.label(), + rule.changeNumber(), config, rule.impressionsDisabled()); + } + } + + // 5. No condition matched → default rule + config = getConfig(rule, rule.defaultTreatment()); + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.DEFAULT_RULE, + rule.changeNumber(), config, rule.impressionsDisabled()); + + } catch (Exception e) { + throw new VersionedExceptionWrapper(e, rule.changeNumber()); + } + } + + private String getConfig(TargetingRule rule, String treatment) { + return rule.configurations() != null ? rule.configurations().get(treatment) : null; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java b/targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java new file mode 100644 index 000000000..0501f7f78 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java @@ -0,0 +1,19 @@ +package io.split.rules.exceptions; + +public class VersionedExceptionWrapper extends Exception { + private final Exception _wrappedException; + private final Long _version; + + public VersionedExceptionWrapper(Exception wrappedException, Long version) { + _wrappedException = wrappedException; + _version = version; + } + + public Exception wrappedException() { + return _wrappedException; + } + + public Long version() { + return _version; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/logging/Logger.java b/targeting-engine/src/main/java/io/split/rules/logging/Logger.java new file mode 100644 index 000000000..89b822a41 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/logging/Logger.java @@ -0,0 +1,8 @@ +package io.split.rules.logging; + +public interface Logger { + void debug(String message); + void warn(String message); + void error(String message); + void error(String message, Throwable t); +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java new file mode 100644 index 000000000..dd9d9df57 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/AllKeysMatcher.java @@ -0,0 +1,39 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +/** + * A matcher that matches all keys. It returns true for everything. + * + * @author adil + */ +public final class AllKeysMatcher implements Matcher { + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + return true; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof AllKeysMatcher)) return false; + return true; + } + + @Override + public int hashCode() { + return 17; + } + + @Override + public String toString() { + return "in segment all"; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java new file mode 100644 index 000000000..6ea30f10f --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/AttributeMatcher.java @@ -0,0 +1,105 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; +import java.util.Objects; + +public final class AttributeMatcher { + private final String _attribute; + private final Matcher _matcher; + + public static AttributeMatcher vanilla(Matcher matcher) { + return new AttributeMatcher(null, matcher, false); + } + + public AttributeMatcher(String attribute, Matcher matcher, boolean negate) { + _attribute = attribute; + if (matcher == null) throw new IllegalArgumentException("Null matcher"); + _matcher = new NegatableMatcher(matcher, negate); + } + + public boolean match(String key, String bucketingKey, Map attributes, EvaluationContext context) { + if (_attribute == null) { + return _matcher.match(key, bucketingKey, attributes, context); + } + if (attributes == null) return false; + Object value = attributes.get(_attribute); + if (value == null) return false; + return _matcher.match(value, bucketingKey, null, null); + } + + public String attribute() { return _attribute; } + public Matcher matcher() { return _matcher; } + + public boolean isUserDefinedSegmentMatcher() { + return ((NegatableMatcher) _matcher).delegate() instanceof UserDefinedSegmentMatcher; + } + + public UserDefinedSegmentMatcher asUserDefinedSegmentMatcher() { + return (UserDefinedSegmentMatcher) ((NegatableMatcher) _matcher).delegate(); + } + + public boolean isRuleBasedSegmentMatcher() { + return ((NegatableMatcher) _matcher).delegate() instanceof RuleBasedSegmentMatcher; + } + + public RuleBasedSegmentMatcher asRuleBasedSegmentMatcher() { + return (RuleBasedSegmentMatcher) ((NegatableMatcher) _matcher).delegate(); + } + + @Override + public int hashCode() { return Objects.hash(_attribute, _matcher); } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof AttributeMatcher)) return false; + AttributeMatcher other = (AttributeMatcher) obj; + return Objects.equals(_attribute, other._attribute) && _matcher.equals(other._matcher); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder("key"); + if (_attribute != null) bldr.append(".").append(_attribute); + bldr.append(" is").append(_matcher); + return bldr.toString(); + } + + public static final class NegatableMatcher implements Matcher { + private final boolean _negate; + private final Matcher _delegate; + + public NegatableMatcher(Matcher matcher, boolean negate) { + _negate = negate; + _delegate = matcher; + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext context) { + boolean result = _delegate.match(matchValue, bucketingKey, attributes, context); + return _negate ? !result : result; + } + + public Matcher delegate() { return _delegate; } + + @Override + public int hashCode() { return Objects.hash(_negate, _delegate); } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof NegatableMatcher)) return false; + NegatableMatcher other = (NegatableMatcher) obj; + return _negate == other._negate && _delegate.equals(other._delegate); + } + + @Override + public String toString() { + return (_negate ? " not " : " ") + _delegate; + } + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java new file mode 100644 index 000000000..14de73b23 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenMatcher.java @@ -0,0 +1,84 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; + +/** + * Supports the logic: if user.age is between x and y + * + * @author adil + */ +public class BetweenMatcher implements Matcher { + private final long _start; + private final long _end; + private final long _normalizedStart; + private final long _normalizedEnd; + + private final DataType _dataType; + + public BetweenMatcher(long start, long end, DataType dataType) { + _start = start; + _end = end; + _dataType = dataType; + + if (_dataType == DataType.DATETIME) { + _normalizedStart = asDateHourMinute(_start); + _normalizedEnd = asDateHourMinute(_end); + } else { + _normalizedStart = _start; + _normalizedEnd = _end; + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + Long keyAsLong; + + if (_dataType == DataType.DATETIME) { + keyAsLong = asDateHourMinute(matchValue); + } else { + keyAsLong = asLong(matchValue); + } + + if (keyAsLong == null) { + return false; + } + + return keyAsLong >= _normalizedStart && keyAsLong <= _normalizedEnd; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("between "); + bldr.append(_start); + bldr.append(" and "); + bldr.append(_end); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)(_start ^ (_start >>> 32)); + result = 31 * result + (int)(_end ^ (_end >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof BetweenMatcher)) return false; + + BetweenMatcher other = (BetweenMatcher) obj; + + return _start == other._start && _end == other._end; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java new file mode 100644 index 000000000..e393c2372 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BetweenSemverMatcher.java @@ -0,0 +1,58 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public class BetweenSemverMatcher implements Matcher { + + private final Semver _semverStart; + private final Semver _semverEnd; + + public BetweenSemverMatcher(String semverStart, String semverEnd) { + _semverStart = Semver.build(semverStart); + _semverEnd = Semver.build(semverEnd); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String) || _semverStart == null || _semverEnd == null) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + return matchSemver.compare(_semverStart) >= 0 && matchSemver.compare(_semverEnd) <= 0; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("between semver "); + bldr.append(_semverStart.version()); + bldr.append(" and "); + bldr.append(_semverEnd.version()); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semverStart.hashCode() + _semverEnd.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof BetweenSemverMatcher)) return false; + + BetweenSemverMatcher other = (BetweenSemverMatcher) obj; + + return _semverStart == other._semverStart && _semverEnd == other._semverEnd; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java new file mode 100644 index 000000000..59800896b --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/BooleanMatcher.java @@ -0,0 +1,46 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asBoolean; + +public class BooleanMatcher implements Matcher { + private boolean _booleanValue; + + public BooleanMatcher(boolean booleanValue) { + _booleanValue = booleanValue; + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + Boolean valueAsBoolean = asBoolean(matchValue); + + return valueAsBoolean != null && valueAsBoolean == _booleanValue; + } + + @Override + public String toString() { + return "is " + _booleanValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BooleanMatcher that = (BooleanMatcher) o; + + return _booleanValue == that._booleanValue; + } + + @Override + public int hashCode() { + return (_booleanValue ? 1 : 0); + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/CombiningMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/CombiningMatcher.java new file mode 100644 index 000000000..be1730e6f --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/CombiningMatcher.java @@ -0,0 +1,73 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class CombiningMatcher { + + public enum Combiner { AND } + + private final List _delegates; + private final Combiner _combiner; + + public static CombiningMatcher of(Matcher matcher) { + return new CombiningMatcher(Combiner.AND, + new ArrayList<>(Arrays.asList(AttributeMatcher.vanilla(matcher)))); + } + + public static CombiningMatcher of(String attribute, Matcher matcher) { + return new CombiningMatcher(Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(attribute, matcher, false)))); + } + + public CombiningMatcher(Combiner combiner, List delegates) { + if (delegates == null || delegates.isEmpty()) throw new IllegalArgumentException("Delegates must not be empty"); + _delegates = Collections.unmodifiableList(new ArrayList<>(delegates)); + _combiner = combiner; + } + + public boolean match(String key, String bucketingKey, Map attributes, EvaluationContext context) { + if (_delegates.isEmpty()) return false; + switch (_combiner) { + case AND: + for (AttributeMatcher d : _delegates) { + if (!d.match(key, bucketingKey, attributes, context)) return false; + } + return true; + default: + throw new IllegalArgumentException("Unknown combiner: " + _combiner); + } + } + + public List attributeMatchers() { return _delegates; } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder("if"); + boolean first = true; + for (AttributeMatcher m : _delegates) { + if (!first) bldr.append(" ").append(_combiner); + bldr.append(" ").append(m); + first = false; + } + return bldr.toString(); + } + + @Override + public int hashCode() { return Objects.hash(_combiner, _delegates); } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof CombiningMatcher)) return false; + CombiningMatcher other = (CombiningMatcher) obj; + return _combiner.equals(other._combiner) && _delegates.equals(other._delegates); + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java new file mode 100644 index 000000000..8d4b521c9 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/DependencyMatcher.java @@ -0,0 +1,63 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Supports the logic: if user is in split "feature" treatments ["on","off"] + */ +public class DependencyMatcher implements Matcher { + private String _featureFlag; + private List _treatments; + + public DependencyMatcher(String featureFlag, List treatments) { + _featureFlag = featureFlag; + _treatments = treatments; + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String)) { + return false; + } + + String result = evaluationContext.evaluate((String) matchValue, bucketingKey, _featureFlag, attributes).treatment; + + return _treatments.contains(result); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("in split \""); + bldr.append(this._featureFlag); + bldr.append("\" treatment "); + bldr.append(this._treatments); + return bldr.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DependencyMatcher that = (DependencyMatcher) o; + + if (!Objects.equals(_featureFlag, that._featureFlag)) return false; + return Objects.equals(_treatments, that._treatments); + } + + @Override + public int hashCode() { + int result = _featureFlag != null ? _featureFlag.hashCode() : 0; + result = 31 * result + (_treatments != null ? _treatments.hashCode() : 0); + return result; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java new file mode 100644 index 000000000..8792648bd --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToMatcher.java @@ -0,0 +1,71 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asDate; +import static io.split.rules.matchers.Transformers.asLong; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class EqualToMatcher implements Matcher { + + private final long _compareTo; + private final long _normalizedCompareTo; + private final DataType _dataType; + + public EqualToMatcher(long compareTo, DataType dataType) { + _compareTo = compareTo; + _dataType = dataType; + + if (_dataType == DataType.DATETIME) { + _normalizedCompareTo = asDate(_compareTo); + } else { + _normalizedCompareTo = _compareTo; + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + Long keyAsLong; + + if (_dataType == DataType.DATETIME) { + keyAsLong = asDate(matchValue); + } else { + keyAsLong = asLong(matchValue); + } + + return keyAsLong != null && keyAsLong == _normalizedCompareTo; + } + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("== "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof EqualToMatcher)) return false; + + EqualToMatcher other = (EqualToMatcher) obj; + + return _compareTo == other._compareTo; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java new file mode 100644 index 000000000..0af499ae3 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/EqualToSemverMatcher.java @@ -0,0 +1,54 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public class EqualToSemverMatcher implements Matcher { + + private final Semver _semVer; + + public EqualToSemverMatcher(String semVer) { + _semVer = Semver.build(semVer); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String) || _semVer == null) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + return matchSemver.version().equals(_semVer.version()); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("== semver "); + bldr.append(_semVer.version()); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semVer.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof EqualToSemverMatcher)) return false; + + EqualToSemverMatcher other = (EqualToSemverMatcher) obj; + + return _semVer == other._semVer; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java new file mode 100644 index 000000000..3c1a0b864 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToMatcher.java @@ -0,0 +1,74 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class GreaterThanOrEqualToMatcher implements Matcher { + + private final long _compareTo; + private final long _normalizedCompareTo; + private final DataType _dataType; + + public GreaterThanOrEqualToMatcher(long compareTo, DataType dataType) { + _compareTo = compareTo; + _dataType = dataType; + + if (_dataType == DataType.DATETIME) { + _normalizedCompareTo = asDateHourMinute(_compareTo); + } else { + _normalizedCompareTo = _compareTo; + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + Long keyAsLong; + + if (_dataType == DataType.DATETIME) { + keyAsLong = asDateHourMinute(matchValue); + } else { + keyAsLong = asLong(matchValue); + } + + if (keyAsLong == null) { + return false; + } + + return keyAsLong >= _normalizedCompareTo; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append(">= "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof GreaterThanOrEqualToMatcher)) return false; + + GreaterThanOrEqualToMatcher other = (GreaterThanOrEqualToMatcher) obj; + + return _compareTo == other._compareTo; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java new file mode 100644 index 000000000..6d92594a0 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/GreaterThanOrEqualToSemverMatcher.java @@ -0,0 +1,54 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public class GreaterThanOrEqualToSemverMatcher implements Matcher { + + private final Semver _semVer; + + public GreaterThanOrEqualToSemverMatcher(String semVer) { + _semVer = Semver.build(semVer); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String)|| _semVer == null) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + return matchSemver.compare(_semVer) >= 0; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append(">= semver "); + bldr.append(_semVer.version()); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semVer.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof GreaterThanOrEqualToSemverMatcher)) return false; + + GreaterThanOrEqualToSemverMatcher other = (GreaterThanOrEqualToSemverMatcher) obj; + + return _semVer == other._semVer; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java new file mode 100644 index 000000000..f1d1422e8 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/InListSemverMatcher.java @@ -0,0 +1,77 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class InListSemverMatcher implements Matcher { + + private final Set _semverlist = new HashSet<>(); + + public InListSemverMatcher(Collection whitelist) { + for (String item : whitelist) { + Semver semver = Semver.build(item); + if (semver == null) continue; + + _semverlist.add(semver); + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String) || _semverlist.isEmpty()) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + for (Semver semverItem : _semverlist) { + if (semverItem.version().equals(matchSemver.version())) return true; + } + return false; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("in semver list ["); + boolean first = true; + + for (Semver item : _semverlist) { + if (!first) { + bldr.append(','); + } + bldr.append('"'); + bldr.append(item.version()); + bldr.append('"'); + first = false; + } + + bldr.append("]"); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semverlist.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof InListSemverMatcher)) return false; + + InListSemverMatcher other = (InListSemverMatcher) obj; + + return _semverlist == other._semverlist; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java new file mode 100644 index 000000000..432821651 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToMatcher.java @@ -0,0 +1,73 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +import static io.split.rules.matchers.Transformers.asDateHourMinute; +import static io.split.rules.matchers.Transformers.asLong; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class LessThanOrEqualToMatcher implements Matcher { + private final long _compareTo; + private final long _normalizedCompareTo; + private final DataType _dataType; + + public LessThanOrEqualToMatcher(long compareTo, DataType dataType) { + _compareTo = compareTo; + _dataType = dataType; + + if (_dataType == DataType.DATETIME) { + _normalizedCompareTo = asDateHourMinute(_compareTo); + } else { + _normalizedCompareTo = _compareTo; + } + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + Long keyAsLong; + + if (_dataType == DataType.DATETIME) { + keyAsLong = asDateHourMinute(matchValue); + } else { + keyAsLong = asLong(matchValue); + } + + if (keyAsLong == null) { + return false; + } + + return keyAsLong <= _normalizedCompareTo; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("<= "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)(_compareTo ^ (_compareTo >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof LessThanOrEqualToMatcher)) return false; + + LessThanOrEqualToMatcher other = (LessThanOrEqualToMatcher) obj; + + return _compareTo == other._compareTo; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java new file mode 100644 index 000000000..0c6b499ac --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/LessThanOrEqualToSemverMatcher.java @@ -0,0 +1,54 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public class LessThanOrEqualToSemverMatcher implements Matcher { + + private final Semver _semVer; + + public LessThanOrEqualToSemverMatcher(String semVer) { + _semVer = Semver.build(semVer); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String) || _semVer == null) { + return false; + } + Semver matchSemver = Semver.build(matchValue.toString()); + if (matchSemver == null) { + return false; + } + + return matchSemver.compare(_semVer) <= 0; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("<= semver "); + bldr.append(_semVer.version()); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _semVer.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof LessThanOrEqualToSemverMatcher)) return false; + + LessThanOrEqualToSemverMatcher other = (LessThanOrEqualToSemverMatcher) obj; + + return _semVer == other._semVer; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/Matcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/Matcher.java new file mode 100644 index 000000000..347270073 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/Matcher.java @@ -0,0 +1,9 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; + +public interface Matcher { + boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext context); +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java new file mode 100644 index 000000000..5951c9fb0 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/PrerequisitesMatcher.java @@ -0,0 +1,71 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.model.Prerequisite; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class PrerequisitesMatcher implements Matcher { + private List _prerequisites; + + public PrerequisitesMatcher(List prerequisites) { + _prerequisites = prerequisites; + } + + public List getPrerequisites() { return _prerequisites; } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String)) { + return false; + } + + if (_prerequisites == null) { + return true; + } + + for (Prerequisite prerequisites : _prerequisites) { + String treatment = evaluationContext.evaluate((String) matchValue, bucketingKey, + prerequisites.featureFlagName(), attributes).treatment; + if (!prerequisites.treatments().contains(treatment)) { + return false; + } + } + return true; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("prerequisites: "); + if (this._prerequisites != null) { + bldr.append(this._prerequisites.stream().map(pr -> pr.featureFlagName() + " " + + pr.treatments().toString()).map(Object::toString).collect(Collectors.joining(", "))); + } + return bldr.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PrerequisitesMatcher that = (PrerequisitesMatcher) o; + + return Objects.equals(_prerequisites, that._prerequisites); + } + + @Override + public int hashCode() { + int result = _prerequisites != null ? _prerequisites.hashCode() : 0; + result = 31 * result + (_prerequisites != null ? _prerequisites.hashCode() : 0); + return result; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/RuleBasedSegmentMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/RuleBasedSegmentMatcher.java new file mode 100644 index 000000000..66347ce1d --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/RuleBasedSegmentMatcher.java @@ -0,0 +1,41 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; +import java.util.Objects; + +/** + * Checks if the key is a member of a rule-based segment. + * Delegates to EvaluationContext.isInRuleBasedSegment() — the SDK provides the actual evaluation logic + * (excluded keys, excluded segments, condition matching). + */ +public final class RuleBasedSegmentMatcher implements Matcher { + private final String _segmentName; + + public RuleBasedSegmentMatcher(String segmentName) { + _segmentName = Objects.requireNonNull(segmentName); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext context) { + if (!(matchValue instanceof String)) return false; + return context.isInRuleBasedSegment(_segmentName, (String) matchValue, bucketingKey, attributes); + } + + public String getSegmentName() { return _segmentName; } + + @Override + public int hashCode() { return 31 * 17 + _segmentName.hashCode(); } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof RuleBasedSegmentMatcher)) return false; + return _segmentName.equals(((RuleBasedSegmentMatcher) obj)._segmentName); + } + + @Override + public String toString() { return "in rule-based segment " + _segmentName; } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/Semver.java b/targeting-engine/src/main/java/io/split/rules/matchers/Semver.java new file mode 100644 index 000000000..a9f7f217f --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/Semver.java @@ -0,0 +1,171 @@ +package io.split.rules.matchers; + +import java.util.Arrays; + +public class Semver { + private static final String METADATA_DELIMITER = "+"; + private static final String PRERELEASE_DELIMITER = "-"; + private static final String VALUE_DELIMITER_REGEX = "\\."; + private static final String VALUE_DELIMITER = "."; + + private Long _major; + private Long _minor; + private Long _patch; + private String[] _preRelease = new String[] {}; + private boolean _isStable; + private String _metadata; + private String _version; + + public static Semver build(String version) { + if (version == null || version.isEmpty()) return null; + try { + return new Semver(version); + } catch (Exception ex) { + return null; + } + } + + public String version() { + return _version; + } + + public Long major() { + return _major; + } + + public Long minor() { + return _minor; + } + + public Long patch() { + return _patch; + } + + public String[] prerelease() { + return _preRelease; + } + + public String metadata() { + return _metadata; + } + + public boolean isStable() { + return _isStable; + } + + /** + * Precedence comparision between 2 Semver objects. + * + * @return the value {@code 0} if {@code this == toCompare}; + * a value less than {@code 0} if {@code this < toCompare}; and + * a value greater than {@code 0} if {@code this > toCompare} + */ + public int compare(Semver toCompare) { + if (_version.equals(toCompare.version())) { + return 0; + } + // Compare major, minor, and patch versions numerically + int result = Long.compare(_major, toCompare.major()); + if (result != 0) { + return result; + } + result = Long.compare(_minor, toCompare.minor()); + if (result != 0) { + return result; + } + result = Long.compare(_patch, toCompare.patch()); + if (result != 0) { + return result; + } + if (!_isStable && toCompare.isStable()) { + return -1; + } else if (_isStable && !toCompare.isStable()) { + return 1; + } + // Compare pre-release versions lexically + int minLength = Math.min(_preRelease.length, toCompare.prerelease().length); + for (int i = 0; i < minLength; i++) { + if (_preRelease[i].equals(toCompare.prerelease()[i])) { + continue; + } + if ( isNumeric(_preRelease[i]) && isNumeric(toCompare._preRelease[i])) { + return Long.compare(Integer.parseInt(_preRelease[i]), Long.parseLong(toCompare._preRelease[i])); + } + return adjustNumber(_preRelease[i].compareTo(toCompare._preRelease[i])); + } + // Compare lengths of pre-release versions + return Integer.compare(_preRelease.length, toCompare._preRelease.length); + } + + private int adjustNumber(int number) { + if (number > 0) return 1; + if (number < 0) return -1; + return 0; + } + private Semver(String version) { + String vWithoutMetadata = setAndRemoveMetadataIfExists(version); + String vWithoutPreRelease = setAndRemovePreReleaseIfExists(vWithoutMetadata); + setMajorMinorAndPatch(vWithoutPreRelease); + _version = setVersion(); + } + private String setAndRemoveMetadataIfExists(String version) { + int index = version.indexOf(METADATA_DELIMITER); + if (index == -1) { + return version; + } + _metadata = version.substring(index+1); + if (_metadata == null || _metadata.isEmpty()) { + throw new IllegalArgumentException("Unable to convert to Semver, incorrect pre release data"); + } + return version.substring(0, index); + } + private String setAndRemovePreReleaseIfExists(String vWithoutMetadata) { + int index = vWithoutMetadata.indexOf(PRERELEASE_DELIMITER); + if (index == -1) { + _isStable = true; + return vWithoutMetadata; + } + String preReleaseData = vWithoutMetadata.substring(index+1); + _preRelease = preReleaseData.split(VALUE_DELIMITER_REGEX); + if (_preRelease == null || Arrays.stream(_preRelease).allMatch(pr -> pr == null || pr.isEmpty())) { + throw new IllegalArgumentException("Unable to convert to Semver, incorrect pre release data"); + } + return vWithoutMetadata.substring(0, index); + } + private void setMajorMinorAndPatch(String version) { + String[] vParts = version.split(VALUE_DELIMITER_REGEX); + if (vParts.length != 3) + throw new IllegalArgumentException("Unable to convert to Semver, incorrect format: " + version); + _major = Long.parseLong(vParts[0]); + _minor = Long.parseLong(vParts[1]); + _patch = Long.parseLong(vParts[2]); + } + + private String setVersion() { + String toReturn = _major + VALUE_DELIMITER + _minor + VALUE_DELIMITER + _patch; + if (_preRelease != null && _preRelease.length != 0) + { + for (int i = 0; i < _preRelease.length; i++) + { + if (isNumeric(_preRelease[i])) + { + _preRelease[i] = Long.toString(Long.parseLong(_preRelease[i])); + } + } + toReturn = toReturn + PRERELEASE_DELIMITER + String.join(VALUE_DELIMITER, _preRelease); + } + if (_metadata != null && !_metadata.isEmpty()) { + toReturn = toReturn + METADATA_DELIMITER + _metadata; + } + return toReturn; + } + + private static boolean isNumeric(String str) { + try { + Double.parseDouble(str); + return true; + } catch(NumberFormatException e){ + return false; + } + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java b/targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java new file mode 100644 index 000000000..b34e60991 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/Transformers.java @@ -0,0 +1,103 @@ +package io.split.rules.matchers; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.TimeZone; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class Transformers { + private static Set VALID_BOOLEAN_STRINGS = new HashSet<>(Arrays.asList("true", "false")); + private static TimeZone UTC = TimeZone.getTimeZone("UTC"); + + public static Long asLong(Object obj) { + if (obj == null) { + return null; + } + + if (obj instanceof Integer) { + return ((Integer) obj).longValue(); + } + + if (obj instanceof Long) { + return ((Long) obj).longValue(); + } + + return null; + } + + public static Long asDate(Object obj) { + Calendar c = toCalendar(obj); + + if (c == null) { + return null; + } + + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + + return c.getTimeInMillis(); + } + + public static Long asDateHourMinute(Object obj) { + + Calendar c = toCalendar(obj); + + if (c == null) { + return null; + } + + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + + return c.getTimeInMillis(); + } + + public static Boolean asBoolean(Object obj) { + if (obj == null) { + return null; + } + + if (obj instanceof Boolean) { + return (Boolean) obj; + } + + if (obj instanceof String) { + if (VALID_BOOLEAN_STRINGS.contains(((String) obj).toLowerCase())) { + return Boolean.parseBoolean((String) obj); + } + } + + return null; + } + + private static Calendar toCalendar(Object obj) { + Long millisecondsSinceEpoch = asLong(obj); + + if (millisecondsSinceEpoch == null) { + return null; + } + + Calendar c = Calendar.getInstance(); + c.setTimeZone(UTC); + c.setTimeInMillis(millisecondsSinceEpoch.longValue()); + + return c; + } + + + public static Set toSetOfStrings(Collection key) { + Set result = new HashSet(key.size()); + for (Object o : key) { + result.add(o.toString()); + } + return result; + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/UserDefinedSegmentMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/UserDefinedSegmentMatcher.java new file mode 100644 index 000000000..fc95318a4 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/UserDefinedSegmentMatcher.java @@ -0,0 +1,40 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Map; +import java.util.Objects; + +/** + * Checks if the key is a member of a standard (user-defined) segment. + * Delegates to EvaluationContext.isInSegment() — the SDK provides the actual storage lookup. + */ +public final class UserDefinedSegmentMatcher implements Matcher { + private final String _segmentName; + + public UserDefinedSegmentMatcher(String segmentName) { + _segmentName = Objects.requireNonNull(segmentName); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext context) { + if (!(matchValue instanceof String)) return false; + return context.isInSegment(_segmentName, (String) matchValue); + } + + public String getSegmentName() { return _segmentName; } + + @Override + public int hashCode() { return 31 * 17 + _segmentName.hashCode(); } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof UserDefinedSegmentMatcher)) return false; + return _segmentName.equals(((UserDefinedSegmentMatcher) obj)._segmentName); + } + + @Override + public String toString() { return "in segment " + _segmentName; } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java new file mode 100644 index 000000000..64fb4753c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/WhitelistMatcher.java @@ -0,0 +1,66 @@ +package io.split.rules.matchers; + +import io.split.rules.engine.EvaluationContext; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Created by adilaijaz on 5/4/15. + */ +public class WhitelistMatcher implements Matcher { + private final Set _whitelist = new HashSet<>(); + + public WhitelistMatcher(Collection whitelist) { + if (whitelist == null) { + throw new IllegalArgumentException("Null whitelist parameter"); + } + _whitelist.addAll(whitelist); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + return _whitelist.contains(matchValue); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("in segment ["); + boolean first = true; + + for (String item : _whitelist) { + if (!first) { + bldr.append(','); + } + bldr.append('"'); + bldr.append(item); + bldr.append('"'); + first = false; + } + + bldr.append("]"); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _whitelist.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof WhitelistMatcher)) return false; + + WhitelistMatcher other = (WhitelistMatcher) obj; + + return _whitelist.equals(other._whitelist); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java new file mode 100644 index 000000000..65814087c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAllOfSetMatcher.java @@ -0,0 +1,70 @@ +package io.split.rules.matchers.collections; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static io.split.rules.matchers.Transformers.toSetOfStrings; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class ContainsAllOfSetMatcher implements Matcher { + private final Set _compareTo = new HashSet<>(); + + public ContainsAllOfSetMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof Collection)) { + return false; + } + + if (_compareTo.isEmpty()) { + return false; + } + + Set keyAsSet = toSetOfStrings((Collection) matchValue); + return keyAsSet.containsAll(_compareTo); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("contains all of "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ContainsAllOfSetMatcher)) return false; + + ContainsAllOfSetMatcher other = (ContainsAllOfSetMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java new file mode 100644 index 000000000..2288020e1 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/ContainsAnyOfSetMatcher.java @@ -0,0 +1,75 @@ +package io.split.rules.matchers.collections; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static io.split.rules.matchers.Transformers.toSetOfStrings; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class ContainsAnyOfSetMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public ContainsAnyOfSetMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof Collection)) { + return false; + } + + Set keyAsSet = toSetOfStrings((Collection) matchValue); + + for (String s : _compareTo) { + if ((keyAsSet.contains(s))) { + return true; + } + } + + return false; + } + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("contains any of "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ContainsAnyOfSetMatcher)) return false; + + ContainsAnyOfSetMatcher other = (ContainsAnyOfSetMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java new file mode 100644 index 000000000..212b9f0c7 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/EqualToSetMatcher.java @@ -0,0 +1,68 @@ +package io.split.rules.matchers.collections; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static io.split.rules.matchers.Transformers.toSetOfStrings; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class EqualToSetMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public EqualToSetMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof Collection)) { + return false; + } + + Set keyAsSet = toSetOfStrings((Collection) matchValue); + + return keyAsSet.equals(_compareTo); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("is equal to "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof EqualToSetMatcher)) return false; + + EqualToSetMatcher other = (EqualToSetMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java new file mode 100644 index 000000000..54aa9730b --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/collections/PartOfSetMatcher.java @@ -0,0 +1,72 @@ +package io.split.rules.matchers.collections; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static io.split.rules.matchers.Transformers.toSetOfStrings; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class PartOfSetMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public PartOfSetMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof Collection)) { + return false; + } + + Set keyAsSet = toSetOfStrings((Collection) matchValue); + + if (keyAsSet.isEmpty()) { + return false; + } + + return _compareTo.containsAll(keyAsSet); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("is part of "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof PartOfSetMatcher)) return false; + + PartOfSetMatcher other = (PartOfSetMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java new file mode 100644 index 000000000..755a33aad --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/ContainsAnyOfMatcher.java @@ -0,0 +1,83 @@ +package io.split.rules.matchers.strings; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class ContainsAnyOfMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public ContainsAnyOfMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String) ) { + return false; + } + + if (_compareTo.isEmpty()) { + return false; + } + + String keyAsString = (String) matchValue; + + for (String s : _compareTo) { + if (s.isEmpty()) { + // ignore empty strings. + continue; + } + if (keyAsString.contains(s)) { + return true; + } + } + + return false; + } + + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("contains "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ContainsAnyOfMatcher)) return false; + + ContainsAnyOfMatcher other = (ContainsAnyOfMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java new file mode 100644 index 000000000..64b67881a --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/EndsWithAnyOfMatcher.java @@ -0,0 +1,83 @@ +package io.split.rules.matchers.strings; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class EndsWithAnyOfMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public EndsWithAnyOfMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String) ) { + return false; + } + + if (_compareTo.isEmpty()) { + return false; + } + + String keyAsString = (String) matchValue; + + for (String s : _compareTo) { + if (s.isEmpty()) { + // ignore empty strings. + continue; + } + if (keyAsString.endsWith(s)) { + return true; + } + } + + return false; + } + + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("ends with "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof EndsWithAnyOfMatcher)) return false; + + EndsWithAnyOfMatcher other = (EndsWithAnyOfMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java new file mode 100644 index 000000000..5ae33cee4 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/RegularExpressionMatcher.java @@ -0,0 +1,51 @@ +package io.split.rules.matchers.strings; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Map; +import java.util.regex.Pattern; + +public class RegularExpressionMatcher implements Matcher { + private String _stringMatcher; + private Pattern _pattern; + + public RegularExpressionMatcher(String matcherValue) { + _stringMatcher = matcherValue; + _pattern = Pattern.compile(matcherValue); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (matchValue instanceof String) { + java.util.regex.Matcher matcher = _pattern.matcher((String) matchValue); + return matcher.find(); + } + + return false; + } + + @Override + public String toString() { + return "matches " + _stringMatcher; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RegularExpressionMatcher that = (RegularExpressionMatcher) o; + + return _stringMatcher != null ? _stringMatcher.equals(that._stringMatcher) : that._stringMatcher == null; + } + + @Override + public int hashCode() { + return _stringMatcher != null ? _stringMatcher.hashCode() : 0; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java b/targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java new file mode 100644 index 000000000..fb85d8fbc --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/matchers/strings/StartsWithAnyOfMatcher.java @@ -0,0 +1,83 @@ +package io.split.rules.matchers.strings; + +import io.split.rules.engine.EvaluationContext; +import io.split.rules.matchers.Matcher; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Created by adilaijaz on 3/7/16. + */ +public class StartsWithAnyOfMatcher implements Matcher { + + private final Set _compareTo = new HashSet<>(); + + public StartsWithAnyOfMatcher(Collection compareTo) { + if (compareTo == null) { + throw new IllegalArgumentException("Null whitelist"); + } + _compareTo.addAll(compareTo); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (matchValue == null) { + return false; + } + + if (!(matchValue instanceof String) ) { + return false; + } + + if (_compareTo.isEmpty()) { + return false; + } + + String keyAsString = (String) matchValue; + + for (String s : _compareTo) { + if (s.isEmpty()) { + // ignore empty strings. + continue; + } + if (keyAsString.startsWith(s)) { + return true; + } + + } + + return false; + } + + + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("starts with "); + bldr.append(_compareTo); + return bldr.toString(); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _compareTo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof StartsWithAnyOfMatcher)) return false; + + StartsWithAnyOfMatcher other = (StartsWithAnyOfMatcher) obj; + + return _compareTo.equals(other._compareTo); + } + +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/Condition.java b/targeting-engine/src/main/java/io/split/rules/model/Condition.java new file mode 100644 index 000000000..5dd868a99 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/Condition.java @@ -0,0 +1,36 @@ +package io.split.rules.model; + +import io.split.rules.matchers.CombiningMatcher; + +import java.util.Collections; +import java.util.List; + +public final class Condition { + private final ConditionType _conditionType; + private final CombiningMatcher _matcher; + private final List _partitions; + private final String _label; + + public Condition(ConditionType conditionType, CombiningMatcher matcher, List partitions, String label) { + _conditionType = conditionType; + _matcher = matcher; + _partitions = partitions != null ? Collections.unmodifiableList(partitions) : Collections.emptyList(); + _label = label; + } + + public ConditionType conditionType() { + return _conditionType; + } + + public CombiningMatcher matcher() { + return _matcher; + } + + public List partitions() { + return _partitions; + } + + public String label() { + return _label; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/ConditionType.java b/targeting-engine/src/main/java/io/split/rules/model/ConditionType.java new file mode 100644 index 000000000..96ad57f6c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/ConditionType.java @@ -0,0 +1,6 @@ +package io.split.rules.model; + +public enum ConditionType { + WHITELIST, + ROLLOUT +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/DataType.java b/targeting-engine/src/main/java/io/split/rules/model/DataType.java new file mode 100644 index 000000000..a7ffbad06 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/DataType.java @@ -0,0 +1,7 @@ +package io.split.rules.model; + +public enum DataType { + NUMBER, + DATETIME, + STRING +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/Partition.java b/targeting-engine/src/main/java/io/split/rules/model/Partition.java new file mode 100644 index 000000000..45d9e8d5c --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/Partition.java @@ -0,0 +1,11 @@ +package io.split.rules.model; + +public final class Partition { + public final String treatment; + public final int size; + + public Partition(String treatment, int size) { + this.treatment = treatment; + this.size = size; + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/Prerequisite.java b/targeting-engine/src/main/java/io/split/rules/model/Prerequisite.java new file mode 100644 index 000000000..3b4367d91 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/Prerequisite.java @@ -0,0 +1,37 @@ +package io.split.rules.model; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public final class Prerequisite { + private final String _featureFlagName; + private final List _treatments; + + public Prerequisite(String featureFlagName, List treatments) { + _featureFlagName = Objects.requireNonNull(featureFlagName); + _treatments = Collections.unmodifiableList(treatments); + } + + public String featureFlagName() { + return _featureFlagName; + } + + public List treatments() { + return _treatments; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Prerequisite that = (Prerequisite) o; + return Objects.equals(_featureFlagName, that._featureFlagName) + && Objects.equals(_treatments, that._treatments); + } + + @Override + public int hashCode() { + return Objects.hash(_featureFlagName, _treatments); + } +} diff --git a/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java b/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java new file mode 100644 index 000000000..922948b85 --- /dev/null +++ b/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java @@ -0,0 +1,73 @@ +package io.split.rules.model; + +import io.split.rules.matchers.PrerequisitesMatcher; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A fully-parsed targeting rule (analogous to ParsedSplit). + * Contains all the information needed to evaluate feature flag targeting. + */ +public final class TargetingRule { + private final String _name; + private final int _seed; + private final boolean _killed; + private final String _defaultTreatment; + private final List _conditions; + private final String _trafficTypeName; + private final long _changeNumber; + private final int _trafficAllocation; + private final int _trafficAllocationSeed; + private final int _algo; + private final Map _configurations; + private final Set _flagSets; + private final boolean _impressionsDisabled; + private final List _prerequisites; + private final PrerequisitesMatcher _prerequisitesMatcher; + + public TargetingRule(String name, int seed, boolean killed, String defaultTreatment, + List conditions, String trafficTypeName, long changeNumber, + int trafficAllocation, int trafficAllocationSeed, int algo, + Map configurations, Set flagSets, + boolean impressionsDisabled, List prerequisites) { + _name = Objects.requireNonNull(name); + _seed = seed; + _killed = killed; + _defaultTreatment = Objects.requireNonNull(defaultTreatment); + _conditions = conditions != null + ? Collections.unmodifiableList(conditions) + : Collections.emptyList(); + _trafficTypeName = trafficTypeName; + _changeNumber = changeNumber; + _trafficAllocation = trafficAllocation; + _trafficAllocationSeed = trafficAllocationSeed; + _algo = algo; + _configurations = configurations; + _flagSets = flagSets; + _impressionsDisabled = impressionsDisabled; + _prerequisites = prerequisites != null + ? Collections.unmodifiableList(prerequisites) + : Collections.emptyList(); + _prerequisitesMatcher = new PrerequisitesMatcher(_prerequisites); + } + + public String name() { return _name; } + public int seed() { return _seed; } + public boolean killed() { return _killed; } + public String defaultTreatment() { return _defaultTreatment; } + public List conditions() { return _conditions; } + public String trafficTypeName() { return _trafficTypeName; } + public long changeNumber() { return _changeNumber; } + public int trafficAllocation() { return _trafficAllocation; } + public int trafficAllocationSeed() { return _trafficAllocationSeed; } + public int algo() { return _algo; } + public Map configurations() { return _configurations; } + public Set flagSets() { return _flagSets; } + public boolean impressionsDisabled() { return _impressionsDisabled; } + public List prerequisites() { return _prerequisites; } + public PrerequisitesMatcher prerequisitesMatcher() { return _prerequisitesMatcher; } +} diff --git a/targeting-engine/src/test/java/io/split/rules/bucketing/BucketerTest.java b/targeting-engine/src/test/java/io/split/rules/bucketing/BucketerTest.java new file mode 100644 index 000000000..edda8d386 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/bucketing/BucketerTest.java @@ -0,0 +1,88 @@ +package io.split.rules.bucketing; + +import io.split.rules.model.Partition; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class BucketerTest { + + @Test + public void getBucketIsBetween1And100Inclusive() { + int bucket = Bucketer.getBucket("somekey", 12345, 2); + assertTrue("bucket should be >= 1", bucket >= 1); + assertTrue("bucket should be <= 100", bucket <= 100); + } + + @Test + public void getBucketLegacyAlgoReturnsSameResultForSameInput() { + int b1 = Bucketer.getBucket("user1", 123, 1); + int b2 = Bucketer.getBucket("user1", 123, 1); + assertEquals(b1, b2); + } + + @Test + public void getBucketMurmurAlgoReturnsSameResultForSameInput() { + int b1 = Bucketer.getBucket("user1", 123, 2); + int b2 = Bucketer.getBucket("user1", 123, 2); + assertEquals(b1, b2); + } + + @Test + public void getTreatmentReturnsControlForEmptyPartitions() { + List empty = Collections.emptyList(); + assertEquals("control", Bucketer.getTreatment("key", 123, empty, 2)); + } + + @Test + public void getTreatmentReturnsSingleTreatmentWhen100Percent() { + List partitions = Collections.singletonList(new Partition("on", 100)); + assertEquals("on", Bucketer.getTreatment("key", 123, partitions, 2)); + } + + @Test + public void getTreatmentSelectsFromPartitionsBasedOnBucket() { + List partitions = Arrays.asList( + new Partition("on", 50), + new Partition("off", 50) + ); + // Verify it returns one of the two treatments + String t = Bucketer.getTreatment("user1", 12345, partitions, 2); + assertTrue("on".equals(t) || "off".equals(t)); + } + + @Test + public void getTreatmentReturnsControlWhenBucketExceedsAllPartitions() { + // partitions that don't sum to 100 — bucket could exceed them + List partitions = Collections.singletonList(new Partition("on", 1)); + // Most keys should get "control" with 1% partition + long controlCount = 0; + for (int i = 0; i < 200; i++) { + if ("control".equals(Bucketer.getTreatment("user" + i, 12345, partitions, 2))) { + controlCount++; + } + } + assertTrue("Most keys should get control with 1% partition", controlCount > 150); + } + + @Test + public void bucketMathIsCorrect() { + // bucket() returns (Math.abs(hash % 100) + 1) + assertEquals(1, Bucketer.bucket(0)); + assertEquals(1, Bucketer.bucket(100)); + assertEquals(50, Bucketer.bucket(49)); + assertEquals(100, Bucketer.bucket(99)); + } + + @Test + public void knownMurmurHashValue() { + // Verify consistent murmur hash across platforms + long hash = Bucketer.murmurHash("testKey", 12345); + assertEquals(hash, Bucketer.murmurHash("testKey", 12345)); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java b/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java new file mode 100644 index 000000000..4e76fd644 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java @@ -0,0 +1,161 @@ +package io.split.rules.engine; + +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.model.Condition; +import io.split.rules.model.ConditionType; +import io.split.rules.model.Partition; +import io.split.rules.model.Prerequisite; +import io.split.rules.model.TargetingRule; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class TargetingEngineImplTest { + + private TargetingEngine _engine; + private EvaluationContext _context; + + @Before + public void setUp() { + _engine = new TargetingEngineImpl(); + _context = Mockito.mock(EvaluationContext.class); + } + + private TargetingRule buildRule(boolean killed, List conditions, List prerequisites, + int trafficAllocation) { + return new TargetingRule( + "test_flag", 12345, killed, "off", + conditions, "user", 1L, + trafficAllocation, 12345, 2, + null, new HashSet<>(), false, + prerequisites + ); + } + + private Condition rolloutCondition(String treatment) { + return new Condition( + ConditionType.ROLLOUT, + CombiningMatcher.of(new AllKeysMatcher()), + Collections.singletonList(new Partition(treatment, 100)), + "test label" + ); + } + + private Condition whitelistCondition(String treatment) { + return new Condition( + ConditionType.WHITELIST, + CombiningMatcher.of(new AllKeysMatcher()), + Collections.singletonList(new Partition(treatment, 100)), + "whitelist label" + ); + } + + @Test + public void killedRuleReturnsDefaultTreatment() throws Exception { + TargetingRule rule = buildRule(true, Collections.singletonList(rolloutCondition("on")), null, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("off", result.treatment); + assertEquals(EvaluationLabels.KILLED, result.label); + assertEquals(Long.valueOf(1L), result.version); + } + + @Test + public void emptyConditionsReturnsDefaultRule() throws Exception { + TargetingRule rule = buildRule(false, Collections.emptyList(), null, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("off", result.treatment); + assertEquals(EvaluationLabels.DEFAULT_RULE, result.label); + } + + @Test + public void matchingConditionReturnsTreatment() throws Exception { + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), null, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("on", result.treatment); + assertEquals("test label", result.label); + } + + @Test + public void whitelistConditionMatchesBeforeTrafficAllocation() throws Exception { + TargetingRule rule = buildRule(false, Collections.singletonList(whitelistCondition("on")), null, 0); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("on", result.treatment); + assertEquals("whitelist label", result.label); + } + + @Test + public void trafficAllocationZeroReturnsNotInSplit() throws Exception { + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), null, 0); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("off", result.treatment); + assertEquals(EvaluationLabels.NOT_IN_SPLIT, result.label); + } + + @Test + public void prerequisitesNotMetReturnsDefaultTreatment() throws Exception { + Mockito.when(_context.evaluate("user1", "user1", "prereq_flag", null)) + .thenReturn(new EvaluationResult("off", EvaluationLabels.DEFAULT_RULE)); + List prereqs = Collections.singletonList( + new Prerequisite("prereq_flag", Collections.singletonList("on")) + ); + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), prereqs, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("off", result.treatment); + assertEquals(EvaluationLabels.PREREQUISITES_NOT_MET, result.label); + } + + @Test + public void prerequisitesMetProceedsToEvaluation() throws Exception { + Mockito.when(_context.evaluate("user1", "user1", "prereq_flag", null)) + .thenReturn(new EvaluationResult("on", EvaluationLabels.DEFAULT_RULE)); + List prereqs = Collections.singletonList( + new Prerequisite("prereq_flag", Collections.singletonList("on")) + ); + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), prereqs, 100); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("on", result.treatment); + } + + @Test + public void bucketingKeyUsedWhenProvided() throws Exception { + TargetingRule rule = buildRule(false, Collections.singletonList(rolloutCondition("on")), null, 100); + EvaluationResult result = _engine.evaluate("user1", "bucket_user", rule, null, _context); + assertEquals("on", result.treatment); + } + + @Test + public void configReturnedForTreatment() throws Exception { + Map configs = new HashMap<>(); + configs.put("on", "{\"color\":\"red\"}"); + TargetingRule rule = new TargetingRule( + "test_flag", 12345, false, "off", + Collections.singletonList(rolloutCondition("on")), "user", 1L, + 100, 12345, 2, configs, new HashSet<>(), false, null + ); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals("on", result.treatment); + assertEquals("{\"color\":\"red\"}", result.config); + } + + @Test + public void impressionsDisabledPreserved() throws Exception { + TargetingRule rule = new TargetingRule( + "test_flag", 12345, false, "off", + Collections.singletonList(rolloutCondition("on")), "user", 1L, + 100, 12345, 2, null, new HashSet<>(), true, null + ); + EvaluationResult result = _engine.evaluate("user1", null, rule, null, _context); + assertEquals(true, result.impressionsDisabled); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/matchers/AllKeysMatcherTest.java b/targeting-engine/src/test/java/io/split/rules/matchers/AllKeysMatcherTest.java new file mode 100644 index 000000000..e7704d2a2 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/matchers/AllKeysMatcherTest.java @@ -0,0 +1,26 @@ +package io.split.rules.matchers; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AllKeysMatcherTest { + + private final AllKeysMatcher _matcher = new AllKeysMatcher(); + + @Test + public void matchesNonNullValue() { + assertTrue(_matcher.match("anything", null, null, null)); + } + + @Test + public void doesNotMatchNull() { + assertFalse(_matcher.match(null, null, null, null)); + } + + @Test + public void equalityHolds() { + assertTrue(_matcher.equals(new AllKeysMatcher())); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/matchers/BetweenMatcherTest.java b/targeting-engine/src/test/java/io/split/rules/matchers/BetweenMatcherTest.java new file mode 100644 index 000000000..68aeae594 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/matchers/BetweenMatcherTest.java @@ -0,0 +1,37 @@ +package io.split.rules.matchers; + +import io.split.rules.model.DataType; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class BetweenMatcherTest { + + @Test + public void matchesValueInRange() { + BetweenMatcher m = new BetweenMatcher(10L, 20L, DataType.NUMBER); + assertTrue(m.match(15L, null, null, null)); + assertTrue(m.match(10L, null, null, null)); + assertTrue(m.match(20L, null, null, null)); + } + + @Test + public void doesNotMatchValueOutOfRange() { + BetweenMatcher m = new BetweenMatcher(10L, 20L, DataType.NUMBER); + assertFalse(m.match(9L, null, null, null)); + assertFalse(m.match(21L, null, null, null)); + } + + @Test + public void doesNotMatchNull() { + BetweenMatcher m = new BetweenMatcher(10L, 20L, DataType.NUMBER); + assertFalse(m.match(null, null, null, null)); + } + + @Test + public void matchesIntegerInRange() { + BetweenMatcher m = new BetweenMatcher(10L, 20L, DataType.NUMBER); + assertTrue(m.match(15, null, null, null)); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/matchers/BooleanMatcherTest.java b/targeting-engine/src/test/java/io/split/rules/matchers/BooleanMatcherTest.java new file mode 100644 index 000000000..75e6efd81 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/matchers/BooleanMatcherTest.java @@ -0,0 +1,34 @@ +package io.split.rules.matchers; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class BooleanMatcherTest { + + @Test + public void matchesTrueWhenValueIsTrue() { + BooleanMatcher m = new BooleanMatcher(true); + assertTrue(m.match(true, null, null, null)); + assertTrue(m.match("true", null, null, null)); + } + + @Test + public void matchesFalseWhenValueIsFalse() { + BooleanMatcher m = new BooleanMatcher(false); + assertTrue(m.match(false, null, null, null)); + assertTrue(m.match("false", null, null, null)); + } + + @Test + public void doesNotMatchOpposite() { + assertTrue(new BooleanMatcher(true).match(true, null, null, null)); + assertFalse(new BooleanMatcher(true).match(false, null, null, null)); + } + + @Test + public void doesNotMatchNull() { + assertFalse(new BooleanMatcher(true).match(null, null, null, null)); + } +} diff --git a/targeting-engine/src/test/java/io/split/rules/matchers/SemverTest.java b/targeting-engine/src/test/java/io/split/rules/matchers/SemverTest.java new file mode 100644 index 000000000..5ab49d374 --- /dev/null +++ b/targeting-engine/src/test/java/io/split/rules/matchers/SemverTest.java @@ -0,0 +1,52 @@ +package io.split.rules.matchers; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SemverTest { + + @Test + public void buildsValidSemver() { + Semver s = Semver.build("1.2.3"); + assertNotNull(s); + assertEquals("1.2.3", s.version()); + assertEquals(Long.valueOf(1), s.major()); + assertEquals(Long.valueOf(2), s.minor()); + assertEquals(Long.valueOf(3), s.patch()); + } + + @Test + public void buildsWithPreRelease() { + Semver s = Semver.build("1.0.0-alpha.1"); + assertNotNull(s); + assertFalse(s.isStable()); + } + + @Test + public void returnsNullForEmpty() { + assertNull(Semver.build("")); + } + + @Test + public void returnsNullForInvalidFormat() { + assertNull(Semver.build("notasemver")); + } + + @Test + public void compareReturnsZeroForEqual() { + assertEquals(0, Semver.build("1.2.3").compare(Semver.build("1.2.3"))); + } + + @Test + public void compareOrdersCorrectly() { + assertTrue(Semver.build("2.0.0").compare(Semver.build("1.0.0")) > 0); + assertTrue(Semver.build("1.0.0").compare(Semver.build("2.0.0")) < 0); + assertTrue(Semver.build("1.1.0").compare(Semver.build("1.0.0")) > 0); + } + + @Test + public void stableIsGreaterThanPreRelease() { + assertTrue(Semver.build("1.0.0").compare(Semver.build("1.0.0-alpha")) > 0); + } +} From 521e8e9b07a50584c41691af432ad8d91e35636c Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 12:08:29 -0300 Subject: [PATCH 03/11] Fix imports AI-Session-Id: 7d0b171a-a576-4317-965f-99fb98c11ba8 AI-Tool: claude-code AI-Model: unknown --- .../io/split/client/CacheUpdaterService.java | 11 +-- .../evaluator/EvaluationContextImp.java | 72 +++++++++++++++++++ .../experiments/ParsedRuleBasedSegment.java | 4 +- .../split/engine/experiments/ParsedSplit.java | 2 +- .../split/engine/experiments/ParserUtils.java | 4 +- .../experiments/RuleBasedSegmentParser.java | 3 +- .../split/engine/experiments/SplitParser.java | 2 +- 7 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java diff --git a/client/src/main/java/io/split/client/CacheUpdaterService.java b/client/src/main/java/io/split/client/CacheUpdaterService.java index db8db3b1e..f65a4aa2a 100644 --- a/client/src/main/java/io/split/client/CacheUpdaterService.java +++ b/client/src/main/java/io/split/client/CacheUpdaterService.java @@ -12,6 +12,7 @@ import io.split.storages.SplitCacheProducer; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -77,9 +78,9 @@ private List getConditions(String splitKey, ParsedSplit split, private ParsedCondition createWhitelistCondition(String splitKey, Partition partition) { ParsedCondition parsedCondition = new ParsedCondition(ConditionType.WHITELIST, new CombiningMatcher(CombiningMatcher.Combiner.AND, - new java.util.ArrayList<>(java.util.Arrays.asList( - new AttributeMatcher(null, new WhitelistMatcher(java.util.Arrays.asList(splitKey)), false)))), - new java.util.ArrayList<>(java.util.Arrays.asList(partition)), splitKey); + new ArrayList<>(Arrays.asList( + new AttributeMatcher(null, new WhitelistMatcher(Arrays.asList(splitKey)), false)))), + new ArrayList<>(Arrays.asList(partition)), splitKey); return parsedCondition; } @@ -89,8 +90,8 @@ private ParsedCondition createRolloutCondition(Partition partition) { rolloutPartition.size = 0; ParsedCondition parsedCondition = new ParsedCondition(ConditionType.ROLLOUT, new CombiningMatcher(CombiningMatcher.Combiner.AND, - new java.util.ArrayList<>(java.util.Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))), - new java.util.ArrayList<>(java.util.Arrays.asList(partition, rolloutPartition)), "LOCAL"); + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))), + new ArrayList<>(Arrays.asList(partition, rolloutPartition)), "LOCAL"); return parsedCondition; } diff --git a/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java b/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java new file mode 100644 index 000000000..5d636a70f --- /dev/null +++ b/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java @@ -0,0 +1,72 @@ +package io.split.engine.evaluator; + +import io.split.client.dtos.ExcludedSegments; +import io.split.engine.experiments.ParsedCondition; +import io.split.engine.experiments.ParsedRuleBasedSegment; + +import io.split.storages.RuleBasedSegmentCacheConsumer; +import io.split.storages.SegmentCacheConsumer; + +import java.util.Map; +import java.util.Objects; + +public class EvaluationContextImp implements EvaluationContext { + private final Evaluator _evaluator; + private final SegmentCacheConsumer _segmentCacheConsumer; + private final RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; + + public EvaluationContextImp(Evaluator evaluator, SegmentCacheConsumer segmentCacheConsumer, + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { + _evaluator = Objects.requireNonNull(evaluator); + _segmentCacheConsumer = Objects.requireNonNull(segmentCacheConsumer); + _ruleBasedSegmentCacheConsumer = Objects.requireNonNull(ruleBasedSegmentCacheConsumer); + } + + public Evaluator getEvaluator() { + return _evaluator; + } + + public SegmentCacheConsumer getSegmentCache() { + return _segmentCacheConsumer; + } + + public RuleBasedSegmentCacheConsumer getRuleBasedSegmentCache() { + return _ruleBasedSegmentCacheConsumer; + } + + @Override + public EvaluationResult evaluate(String matchingKey, String bucketingKey, String ruleName, Map attributes) { + EvaluatorImp.TreatmentLabelAndChangeNumber r = _evaluator.evaluateFeature(matchingKey, bucketingKey, ruleName, attributes); + return new EvaluationResult(r.treatment, r.label, r.changeNumber, r.configurations, r.track); + } + + @Override + public boolean isInSegment(String segmentName, String key) { + return _segmentCacheConsumer.isInSegment(segmentName, key); + } + + @Override + public boolean isInRuleBasedSegment(String segmentName, String key, String bucketingKey, Map attributes) { + ParsedRuleBasedSegment parsedRuleBasedSegment = _ruleBasedSegmentCacheConsumer.get(segmentName); + if (parsedRuleBasedSegment == null) { + return false; + } + if (parsedRuleBasedSegment.excludedKeys().contains(key)) { + return false; + } + for (ExcludedSegments excludedSegment : parsedRuleBasedSegment.excludedSegments()) { + if (excludedSegment.isStandard() && _segmentCacheConsumer.isInSegment(excludedSegment.name, key)) { + return false; + } + if (excludedSegment.isRuleBased() && isInRuleBasedSegment(excludedSegment.name, key, bucketingKey, attributes)) { + return false; + } + } + for (ParsedCondition condition : parsedRuleBasedSegment.parsedConditions()) { + if (condition.matcher().match(key, bucketingKey, attributes, this)) { + return true; + } + } + return false; + } +} diff --git a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java index e58d1d762..f9f260e3d 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java @@ -4,6 +4,8 @@ import io.split.rules.matchers.AttributeMatcher; import io.split.rules.matchers.UserDefinedSegmentMatcher; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -44,7 +46,7 @@ public ParsedRuleBasedSegment( List excludedSegments ) { _ruleBasedSegment = ruleBasedSegment; - _parsedCondition = java.util.Collections.unmodifiableList(new java.util.ArrayList<>(matcherAndSplits)); + _parsedCondition = Collections.unmodifiableList(new ArrayList<>(matcherAndSplits)); _trafficTypeName = trafficTypeName; _changeNumber = changeNumber; _excludedKeys = excludedKeys; diff --git a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java index 4e20ed98e..4413a5ec1 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java @@ -302,7 +302,7 @@ private static TargetingRule buildTargetingRule( : Collections.unmodifiableList(prerequisitesMatcher.getPrerequisites()); return new TargetingRule(feature, seed, killed, defaultTreatment, conditions, trafficTypeName, changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, - flagSets == null ? new java.util.HashSet<>() : flagSets, impressionsDisabled, prereqs); + flagSets == null ? new HashSet<>() : flagSets, impressionsDisabled, prereqs); } private static io.split.rules.model.Condition toTargetingCondition(ParsedCondition c) { diff --git a/client/src/main/java/io/split/engine/experiments/ParserUtils.java b/client/src/main/java/io/split/engine/experiments/ParserUtils.java index 03cc4ec14..0ba45a5c2 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -83,8 +83,8 @@ public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { } - private static io.split.rules.model.DataType toRulesDataType(DataType dt) { - return io.split.rules.model.DataType.valueOf(dt.name()); + private static DataType toRulesDataType(io.split.client.dtos.DataType dt) { + return DataType.valueOf(dt.name()); } public static AttributeMatcher toMatcher(Matcher matcher) { diff --git a/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java b/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java index 1bf62dba5..31e223098 100644 --- a/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java +++ b/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; import static io.split.engine.experiments.ParserUtils.checkUnsupportedMatcherExist; @@ -29,7 +30,7 @@ public ParsedRuleBasedSegment parse(RuleBasedSegment ruleBasedSegment) { } private ParsedRuleBasedSegment parseWithoutExceptionHandling(RuleBasedSegment ruleBasedSegment) { - List parsedConditionList = new java.util.ArrayList<>(); + List parsedConditionList = new ArrayList<>(); for (Condition condition : ruleBasedSegment.conditions) { if (checkUnsupportedMatcherExist(condition.matcherGroup.matchers)) { _log.error("Unsupported matcher type found for rule based segment: " + ruleBasedSegment.name + diff --git a/client/src/main/java/io/split/engine/experiments/SplitParser.java b/client/src/main/java/io/split/engine/experiments/SplitParser.java index 0cc589d34..6494c5287 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitParser.java +++ b/client/src/main/java/io/split/engine/experiments/SplitParser.java @@ -86,7 +86,7 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) { split.trafficAllocationSeed, split.algo, split.configurations, - split.sets == null ? new java.util.HashSet<>() : split.sets, + split.sets == null ? new HashSet<>() : split.sets, split.impressionsDisabled, prerequisites); From 7c06d4f6add01194eef3ae7cbd6e53009b8b80f7 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 12:12:11 -0300 Subject: [PATCH 04/11] Remove unused file AI-Session-Id: 7d0b171a-a576-4317-965f-99fb98c11ba8 AI-Tool: claude-code AI-Model: unknown --- .../evaluator/EvaluationContextImp.java | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java diff --git a/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java b/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java deleted file mode 100644 index 5d636a70f..000000000 --- a/client/src/main/java/io/split/engine/evaluator/EvaluationContextImp.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.split.engine.evaluator; - -import io.split.client.dtos.ExcludedSegments; -import io.split.engine.experiments.ParsedCondition; -import io.split.engine.experiments.ParsedRuleBasedSegment; - -import io.split.storages.RuleBasedSegmentCacheConsumer; -import io.split.storages.SegmentCacheConsumer; - -import java.util.Map; -import java.util.Objects; - -public class EvaluationContextImp implements EvaluationContext { - private final Evaluator _evaluator; - private final SegmentCacheConsumer _segmentCacheConsumer; - private final RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; - - public EvaluationContextImp(Evaluator evaluator, SegmentCacheConsumer segmentCacheConsumer, - RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { - _evaluator = Objects.requireNonNull(evaluator); - _segmentCacheConsumer = Objects.requireNonNull(segmentCacheConsumer); - _ruleBasedSegmentCacheConsumer = Objects.requireNonNull(ruleBasedSegmentCacheConsumer); - } - - public Evaluator getEvaluator() { - return _evaluator; - } - - public SegmentCacheConsumer getSegmentCache() { - return _segmentCacheConsumer; - } - - public RuleBasedSegmentCacheConsumer getRuleBasedSegmentCache() { - return _ruleBasedSegmentCacheConsumer; - } - - @Override - public EvaluationResult evaluate(String matchingKey, String bucketingKey, String ruleName, Map attributes) { - EvaluatorImp.TreatmentLabelAndChangeNumber r = _evaluator.evaluateFeature(matchingKey, bucketingKey, ruleName, attributes); - return new EvaluationResult(r.treatment, r.label, r.changeNumber, r.configurations, r.track); - } - - @Override - public boolean isInSegment(String segmentName, String key) { - return _segmentCacheConsumer.isInSegment(segmentName, key); - } - - @Override - public boolean isInRuleBasedSegment(String segmentName, String key, String bucketingKey, Map attributes) { - ParsedRuleBasedSegment parsedRuleBasedSegment = _ruleBasedSegmentCacheConsumer.get(segmentName); - if (parsedRuleBasedSegment == null) { - return false; - } - if (parsedRuleBasedSegment.excludedKeys().contains(key)) { - return false; - } - for (ExcludedSegments excludedSegment : parsedRuleBasedSegment.excludedSegments()) { - if (excludedSegment.isStandard() && _segmentCacheConsumer.isInSegment(excludedSegment.name, key)) { - return false; - } - if (excludedSegment.isRuleBased() && isInRuleBasedSegment(excludedSegment.name, key, bucketingKey, attributes)) { - return false; - } - } - for (ParsedCondition condition : parsedRuleBasedSegment.parsedConditions()) { - if (condition.matcher().match(key, bucketingKey, attributes, this)) { - return true; - } - } - return false; - } -} From fb7b1ae66cde08fb677428b147a2e8a46dd7bb3f Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 12:53:14 -0300 Subject: [PATCH 05/11] refactor: extract TargetingRuleFactory from ParsedSplit - Create TargetingRuleFactory with static buildTargetingRule() method - Eliminate FQNs for io.split.rules.model types - Add comprehensive tests for factory methods - Update ParsedSplit to delegate to factory This improves testability and separation of concerns by isolating the mapping logic from SDK domain objects to targeting-engine objects. AI-Session-Id: 7d0b171a-a576-4317-965f-99fb98c11ba8 AI-Tool: claude-code AI-Model: unknown --- .../split/engine/experiments/ParsedSplit.java | 39 +-- .../experiments/TargetingRuleFactory.java | 59 ++++ .../experiments/TargetingRuleFactoryTest.java | 320 ++++++++++++++++++ 3 files changed, 382 insertions(+), 36 deletions(-) create mode 100644 client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java create mode 100644 client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java diff --git a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java index 4413a5ec1..c6538b9dd 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java @@ -68,7 +68,7 @@ public static ParsedSplit createParsedSplitForTests( flagSets, impressionsDisabled, prerequisitesMatcher, - buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, 100, seed, algo, null, flagSets, impressionsDisabled, prerequisitesMatcher) ); } @@ -102,7 +102,7 @@ public static ParsedSplit createParsedSplitForTests( flagSets, impressionsDisabled, prerequisitesMatcher, - buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, 100, seed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher) ); } @@ -126,7 +126,7 @@ public ParsedSplit( this(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher, - buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, + TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher)); } @@ -284,39 +284,6 @@ public String toString() { } - private static TargetingRule buildTargetingRule( - String feature, int seed, boolean killed, String defaultTreatment, - List matcherAndSplits, String trafficTypeName, long changeNumber, - int trafficAllocation, int trafficAllocationSeed, int algo, - Map configurations, HashSet flagSets, - boolean impressionsDisabled, PrerequisitesMatcher prerequisitesMatcher) { - List conditions = matcherAndSplits == null - ? Collections.emptyList() - : matcherAndSplits.stream() - .map(ParsedSplit::toTargetingCondition) - .collect(Collectors.toList()); - List prereqs = prerequisitesMatcher == null - ? Collections.emptyList() - : prerequisitesMatcher.getPrerequisites() == null - ? Collections.emptyList() - : Collections.unmodifiableList(prerequisitesMatcher.getPrerequisites()); - return new TargetingRule(feature, seed, killed, defaultTreatment, conditions, trafficTypeName, - changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, - flagSets == null ? new HashSet<>() : flagSets, impressionsDisabled, prereqs); - } - - private static io.split.rules.model.Condition toTargetingCondition(ParsedCondition c) { - List partitions = c.partitions() == null - ? Collections.emptyList() - : c.partitions().stream() - .map(p -> new io.split.rules.model.Partition(p.treatment, p.size)) - .collect(Collectors.toList()); - io.split.rules.model.ConditionType condType = c.conditionType() == ConditionType.ROLLOUT - ? io.split.rules.model.ConditionType.ROLLOUT - : io.split.rules.model.ConditionType.WHITELIST; - return new io.split.rules.model.Condition(condType, c.matcher(), partitions, c.label()); - } - public Set getSegmentsNames() { return parsedConditions().stream() .flatMap(parsedCondition -> parsedCondition.matcher().attributeMatchers().stream()) diff --git a/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java b/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java new file mode 100644 index 000000000..daea047a4 --- /dev/null +++ b/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java @@ -0,0 +1,59 @@ +package io.split.engine.experiments; + +import io.split.rules.matchers.PrerequisitesMatcher; +import io.split.rules.model.Condition; +import io.split.rules.model.ConditionType; +import io.split.rules.model.Partition; +import io.split.rules.model.Prerequisite; +import io.split.rules.model.TargetingRule; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public final class TargetingRuleFactory { + + private TargetingRuleFactory() { + throw new IllegalStateException("Utility class"); + } + + public static TargetingRule buildTargetingRule( + String feature, int seed, boolean killed, String defaultTreatment, + List matcherAndSplits, String trafficTypeName, long changeNumber, + int trafficAllocation, int trafficAllocationSeed, int algo, + Map configurations, HashSet flagSets, + boolean impressionsDisabled, PrerequisitesMatcher prerequisitesMatcher) { + + List conditions = matcherAndSplits == null + ? Collections.emptyList() + : matcherAndSplits.stream() + .map(TargetingRuleFactory::toTargetingCondition) + .collect(Collectors.toList()); + + List prereqs = prerequisitesMatcher == null + ? Collections.emptyList() + : prerequisitesMatcher.getPrerequisites() == null + ? Collections.emptyList() + : Collections.unmodifiableList(prerequisitesMatcher.getPrerequisites()); + + return new TargetingRule(feature, seed, killed, defaultTreatment, conditions, trafficTypeName, + changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, + flagSets == null ? new HashSet<>() : flagSets, impressionsDisabled, prereqs); + } + + private static Condition toTargetingCondition(ParsedCondition c) { + List partitions = c.partitions() == null + ? Collections.emptyList() + : c.partitions().stream() + .map(p -> new Partition(p.treatment, p.size)) + .collect(Collectors.toList()); + + ConditionType condType = c.conditionType() == io.split.client.dtos.ConditionType.ROLLOUT + ? ConditionType.ROLLOUT + : ConditionType.WHITELIST; + + return new Condition(condType, c.matcher(), partitions, c.label()); + } +} diff --git a/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java b/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java new file mode 100644 index 000000000..cc1da5220 --- /dev/null +++ b/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java @@ -0,0 +1,320 @@ +package io.split.engine.experiments; + +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.Partition; +import io.split.rules.matchers.AllKeysMatcher; +import io.split.rules.matchers.AttributeMatcher; +import io.split.rules.matchers.CombiningMatcher; +import io.split.rules.model.Condition; +import io.split.rules.model.TargetingRule; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + +public class TargetingRuleFactoryTest { + + private Map _configurations; + private HashSet _flagSets; + + @Before + public void setUp() { + _configurations = new HashMap<>(); + _configurations.put("on", "{\"color\": \"blue\"}"); + _flagSets = new HashSet<>(Arrays.asList("set1", "set2")); + } + + @Test + public void testBuildTargetingRule_withNullConditions_returnsEmptyList() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + null, + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + _flagSets, + false, + null + ); + + assertNotNull(rule); + assertEquals("feature1", rule.name()); + assertTrue(rule.conditions().isEmpty()); + } + + @Test + public void testBuildTargetingRule_withValidConditions_mapsCorrectly() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + Partition dtoPartition = new Partition(); + dtoPartition.treatment = "on"; + dtoPartition.size = 100; + + ParsedCondition parsedCondition = new ParsedCondition( + ConditionType.ROLLOUT, + matcher, + Arrays.asList(dtoPartition), + "label1" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + Arrays.asList(parsedCondition), + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + _flagSets, + false, + null + ); + + assertNotNull(rule); + assertEquals("feature1", rule.name()); + assertEquals(1, rule.conditions().size()); + + Condition condition = rule.conditions().get(0); + assertEquals(io.split.rules.model.ConditionType.ROLLOUT, condition.conditionType()); + assertEquals("label1", condition.label()); + assertEquals(1, condition.partitions().size()); + + io.split.rules.model.Partition partition = condition.partitions().get(0); + assertEquals("on", partition.treatment); + assertEquals(100, partition.size); + } + + @Test + public void testBuildTargetingRule_withNullPrerequisites_returnsEmptyPrerequisiteList() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + new ArrayList<>(), + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + _flagSets, + false, + null + ); + + assertNotNull(rule); + assertTrue(rule.prerequisites().isEmpty()); + } + + @Test + public void testBuildTargetingRule_withNullFlagSets_createsEmptySet() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + new ArrayList<>(), + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + null, + false, + null + ); + + assertNotNull(rule); + assertNotNull(rule.flagSets()); + assertTrue(rule.flagSets().isEmpty()); + } + + @Test + public void testBuildTargetingRule_withFlagSets_preservesSet() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", + 12345, + false, + "control", + new ArrayList<>(), + "user_type", + 999L, + 100, + 456, + 1, + _configurations, + _flagSets, + false, + null + ); + + assertNotNull(rule); + assertEquals(_flagSets, rule.flagSets()); + } + + @Test + public void testBuildTargetingRule_withRolloutCondition_mapsConditionTypeCorrectly() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + Partition dtoPartition = new Partition(); + dtoPartition.treatment = "on"; + dtoPartition.size = 50; + + ParsedCondition rolloutCondition = new ParsedCondition( + ConditionType.ROLLOUT, + matcher, + Arrays.asList(dtoPartition), + "rollout_label" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", 12345, false, "control", + Arrays.asList(rolloutCondition), + "user_type", 999L, 100, 456, 1, + _configurations, _flagSets, false, null + ); + + Condition condition = rule.conditions().get(0); + assertEquals(io.split.rules.model.ConditionType.ROLLOUT, condition.conditionType()); + } + + @Test + public void testBuildTargetingRule_withWhitelistCondition_mapsConditionTypeCorrectly() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + Partition dtoPartition = new Partition(); + dtoPartition.treatment = "special"; + dtoPartition.size = 100; + + ParsedCondition whitelistCondition = new ParsedCondition( + ConditionType.WHITELIST, + matcher, + Arrays.asList(dtoPartition), + "whitelist_label" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", 12345, false, "control", + Arrays.asList(whitelistCondition), + "user_type", 999L, 100, 456, 1, + _configurations, _flagSets, false, null + ); + + Condition condition = rule.conditions().get(0); + assertEquals(io.split.rules.model.ConditionType.WHITELIST, condition.conditionType()); + } + + @Test + public void testBuildTargetingRule_withMultiplePartitions_mapsAllPartitions() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + Partition partition1 = new Partition(); + partition1.treatment = "on"; + partition1.size = 50; + + Partition partition2 = new Partition(); + partition2.treatment = "off"; + partition2.size = 50; + + ParsedCondition parsedCondition = new ParsedCondition( + ConditionType.ROLLOUT, + matcher, + Arrays.asList(partition1, partition2), + "multi_partition_label" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", 12345, false, "control", + Arrays.asList(parsedCondition), + "user_type", 999L, 100, 456, 1, + _configurations, _flagSets, false, null + ); + + Condition condition = rule.conditions().get(0); + assertEquals(2, condition.partitions().size()); + assertEquals("on", condition.partitions().get(0).treatment); + assertEquals(50, condition.partitions().get(0).size); + assertEquals("off", condition.partitions().get(1).treatment); + assertEquals(50, condition.partitions().get(1).size); + } + + @Test + public void testBuildTargetingRule_withNullPartitions_returnsEmptyPartitionList() { + CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, + new ArrayList<>(Arrays.asList(new AttributeMatcher(null, new AllKeysMatcher(), false)))); + + ParsedCondition parsedCondition = new ParsedCondition( + ConditionType.ROLLOUT, + matcher, + null, + "null_partitions_label" + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "feature1", 12345, false, "control", + Arrays.asList(parsedCondition), + "user_type", 999L, 100, 456, 1, + _configurations, _flagSets, false, null + ); + + Condition condition = rule.conditions().get(0); + assertTrue(condition.partitions().isEmpty()); + } + + @Test + public void testBuildTargetingRule_preservesAllNonMappedFields() { + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + "my_feature", + 98765, + true, + "killed", + new ArrayList<>(), + "account", + 555L, + 75, + 321, + 2, + _configurations, + _flagSets, + true, + null + ); + + assertEquals("my_feature", rule.name()); + assertEquals(98765, rule.seed()); + assertTrue(rule.killed()); + assertEquals("killed", rule.defaultTreatment()); + assertEquals("account", rule.trafficTypeName()); + assertEquals(555L, rule.changeNumber()); + assertEquals(75, rule.trafficAllocation()); + assertEquals(321, rule.trafficAllocationSeed()); + assertEquals(2, rule.algo()); + assertEquals(_configurations, rule.configurations()); + assertTrue(rule.impressionsDisabled()); + } +} From ed86b23a6cc22a2f221818edb7ac3b5f94325dd9 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 13:10:28 -0300 Subject: [PATCH 06/11] Parser utils change' AI-Session-Id: 490d81f4-6832-4178-9c38-e45460ab97de AI-Tool: claude-code AI-Model: unknown --- .../main/java/io/split/engine/experiments/ParserUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/io/split/engine/experiments/ParserUtils.java b/client/src/main/java/io/split/engine/experiments/ParserUtils.java index 0ba45a5c2..6317a9f75 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -83,8 +83,8 @@ public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { } - private static DataType toRulesDataType(io.split.client.dtos.DataType dt) { - return DataType.valueOf(dt.name()); + private static io.split.rules.model.DataType toRulesDataType(io.split.client.dtos.DataType dt) { + return io.split.rules.model.DataType.valueOf(dt.name()); } public static AttributeMatcher toMatcher(Matcher matcher) { From 03910769acae486c434ce503e850213c00207763 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 13:18:37 -0300 Subject: [PATCH 07/11] Restore checkNotNull guards accidentally removed from ParserUtils Guards for userDefinedSegmentMatcherData, whitelistMatcherData, unaryNumericMatcherData, and betweenMatcherData were dropped as an unintended side effect of the DataType import refactor in ed86b23a. Co-Authored-By: Claude Sonnet 4.6 AI-Session-Id: 588a98fd-ba49-4318-8286-5fb79f9efaca AI-Tool: claude-code AI-Model: unknown --- .../io/split/engine/experiments/ParserUtils.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/src/main/java/io/split/engine/experiments/ParserUtils.java b/client/src/main/java/io/split/engine/experiments/ParserUtils.java index 6317a9f75..45e3cd8a6 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -36,6 +36,8 @@ import java.util.ArrayList; import java.util.List; +import static com.google.common.base.Preconditions.checkNotNull; + public final class ParserUtils { private ParserUtils() { @@ -94,37 +96,47 @@ public static AttributeMatcher toMatcher(Matcher matcher) { delegate = new AllKeysMatcher(); break; case IN_SEGMENT: + checkNotNull(matcher.userDefinedSegmentMatcherData); String segmentName = matcher.userDefinedSegmentMatcherData.segmentName; delegate = new UserDefinedSegmentMatcher(segmentName); break; case WHITELIST: + checkNotNull(matcher.whitelistMatcherData); delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); break; case EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); break; case GREATER_THAN_OR_EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); delegate = new GreaterThanOrEqualToMatcher( matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); break; case LESS_THAN_OR_EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); delegate = new LessThanOrEqualToMatcher( matcher.unaryNumericMatcherData.value, toRulesDataType(matcher.unaryNumericMatcherData.dataType)); break; case BETWEEN: + checkNotNull(matcher.betweenMatcherData); delegate = new BetweenMatcher(matcher.betweenMatcherData.start, matcher.betweenMatcherData.end, toRulesDataType(matcher.betweenMatcherData.dataType)); break; case EQUAL_TO_SET: + checkNotNull(matcher.whitelistMatcherData); delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); break; case PART_OF_SET: + checkNotNull(matcher.whitelistMatcherData); delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case CONTAINS_ALL_OF_SET: + checkNotNull(matcher.whitelistMatcherData); delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case CONTAINS_ANY_OF_SET: + checkNotNull(matcher.whitelistMatcherData); delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case STARTS_WITH: From 5b8bc948bf2a501e9f45914e4c1dfa6769e48562 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 8 Apr 2026 14:00:21 -0300 Subject: [PATCH 08/11] Map combiner from DTO instead of hardcoding AND in ParserUtils Restores original behavior: reads matcherGroup.combiner and maps it to CombiningMatcher.Combiner via valueOf, consistent with how DataType is already mapped across the DTO/domain boundary. Co-Authored-By: Claude Sonnet 4.6 AI-Session-Id: 588a98fd-ba49-4318-8286-5fb79f9efaca AI-Tool: claude-code AI-Model: unknown --- .../main/java/io/split/engine/experiments/ParserUtils.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/io/split/engine/experiments/ParserUtils.java b/client/src/main/java/io/split/engine/experiments/ParserUtils.java index 45e3cd8a6..92a0ba788 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -1,6 +1,7 @@ package io.split.engine.experiments; import io.split.client.dtos.DataType; +import io.split.client.dtos.MatcherCombiner; import io.split.client.dtos.MatcherType; import io.split.client.dtos.Partition; import io.split.client.dtos.MatcherGroup; @@ -81,7 +82,7 @@ public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { toCombine.add(toMatcher(matcher)); } - return new CombiningMatcher(CombiningMatcher.Combiner.AND, toCombine); + return new CombiningMatcher(toCombiner(matcherGroup.combiner), toCombine); } @@ -89,6 +90,10 @@ private static io.split.rules.model.DataType toRulesDataType(io.split.client.dto return io.split.rules.model.DataType.valueOf(dt.name()); } + private static CombiningMatcher.Combiner toCombiner(MatcherCombiner combiner) { + return CombiningMatcher.Combiner.valueOf(combiner.name()); + } + public static AttributeMatcher toMatcher(Matcher matcher) { io.split.rules.matchers.Matcher delegate = null; switch (matcher.matcherType) { From 872a9ba5df05638a67c8f46140e7f9c8de70fb2e Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 9 Apr 2026 10:09:14 -0300 Subject: [PATCH 09/11] Restore remaining checkNotNull guards dropped during matchers migration Guards for STARTS_WITH, ENDS_WITH, CONTAINS_STRING, MATCHES_STRING, EQUAL_TO_SEMVER, GREATER_THAN_OR_EQUAL_TO_SEMVER, LESS_THAN_OR_EQUAL_TO_SEMVER, IN_LIST_SEMVER, BETWEEN_SEMVER, IN_RULE_BASED_SEGMENT, and the final delegate null check were lost when matchers were migrated from io.split.engine.matchers to io.split.rules.matchers. Also restores checkNotNull with message for IN_SPLIT_TREATMENT and EQUAL_TO_BOOLEAN (previously replaced with manual NPE throws). Co-Authored-By: Claude Sonnet 4.6 AI-Session-Id: 588a98fd-ba49-4318-8286-5fb79f9efaca AI-Tool: claude-code AI-Model: unknown --- .../split/engine/experiments/ParserUtils.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/io/split/engine/experiments/ParserUtils.java b/client/src/main/java/io/split/engine/experiments/ParserUtils.java index 92a0ba788..9ada61159 100644 --- a/client/src/main/java/io/split/engine/experiments/ParserUtils.java +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -145,43 +145,55 @@ public static AttributeMatcher toMatcher(Matcher matcher) { delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); break; case STARTS_WITH: + checkNotNull(matcher.whitelistMatcherData); delegate = new StartsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); break; case ENDS_WITH: + checkNotNull(matcher.whitelistMatcherData); delegate = new EndsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); break; case CONTAINS_STRING: + checkNotNull(matcher.whitelistMatcherData); delegate = new ContainsAnyOfMatcher(matcher.whitelistMatcherData.whitelist); break; case MATCHES_STRING: + checkNotNull(matcher.stringMatcherData); delegate = new RegularExpressionMatcher(matcher.stringMatcherData); break; case IN_SPLIT_TREATMENT: - if (matcher.dependencyMatcherData == null) throw new NullPointerException( - "MatcherType is " + matcher.matcherType + ". matcher.dependencyMatcherData() MUST NOT BE null"); + checkNotNull(matcher.dependencyMatcherData, + "MatcherType is " + matcher.matcherType + + ". matcher.dependencyMatcherData() MUST NOT BE null"); delegate = new DependencyMatcher(matcher.dependencyMatcherData.split, matcher.dependencyMatcherData.treatments); break; case EQUAL_TO_BOOLEAN: - if (matcher.booleanMatcherData == null) throw new NullPointerException( - "MatcherType is " + matcher.matcherType + ". matcher.booleanMatcherData() MUST NOT BE null"); + checkNotNull(matcher.booleanMatcherData, + "MatcherType is " + matcher.matcherType + + ". matcher.booleanMatcherData() MUST NOT BE null"); delegate = new BooleanMatcher(matcher.booleanMatcherData); break; case EQUAL_TO_SEMVER: + checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for EQUAL_TO_SEMVER matcher type"); delegate = new EqualToSemverMatcher(matcher.stringMatcherData); break; case GREATER_THAN_OR_EQUAL_TO_SEMVER: + checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for GREATER_THAN_OR_EQUAL_TO_SEMVER matcher type"); delegate = new GreaterThanOrEqualToSemverMatcher(matcher.stringMatcherData); break; case LESS_THAN_OR_EQUAL_TO_SEMVER: + checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for LESS_THAN_OR_EQUAL_SEMVER matcher type"); delegate = new LessThanOrEqualToSemverMatcher(matcher.stringMatcherData); break; case IN_LIST_SEMVER: + checkNotNull(matcher.whitelistMatcherData, "whitelistMatcherData is required for IN_LIST_SEMVER matcher type"); delegate = new InListSemverMatcher(matcher.whitelistMatcherData.whitelist); break; case BETWEEN_SEMVER: + checkNotNull(matcher.betweenStringMatcherData, "betweenStringMatcherData is required for BETWEEN_SEMVER matcher type"); delegate = new BetweenSemverMatcher(matcher.betweenStringMatcherData.start, matcher.betweenStringMatcherData.end); break; case IN_RULE_BASED_SEGMENT: + checkNotNull(matcher.userDefinedSegmentMatcherData); String ruleBasedSegmentName = matcher.userDefinedSegmentMatcherData.segmentName; delegate = new RuleBasedSegmentMatcher(ruleBasedSegmentName); break; @@ -189,6 +201,8 @@ public static AttributeMatcher toMatcher(Matcher matcher) { throw new IllegalArgumentException("Unknown matcher type: " + matcher.matcherType); } + checkNotNull(delegate, "We were not able to create a matcher for: " + matcher.matcherType); + String attribute = null; if (matcher.keySelector != null && matcher.keySelector.attribute != null) { attribute = matcher.keySelector.attribute; From c12432ad2dd98cfad0827906da1993e14ae8b8bb Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 9 Apr 2026 14:54:23 -0300 Subject: [PATCH 10/11] refactor: strip metadata from TargetingRule and EvaluationResult TargetingRule now holds only evaluation fields (seed, killed, defaultTreatment, conditions, trafficAllocation, trafficAllocationSeed, algo, prerequisites). Metadata (name, changeNumber, trafficTypeName, configurations, flagSets, impressionsDisabled) lives exclusively in ParsedSplit. EvaluationResult carries only (treatment, label). EvaluatorImp.getTreatment() enriches changeNumber, config, and impressionsDisabled from ParsedSplit after the engine returns. VersionedExceptionWrapper no longer carries a version field; the caller uses parsedSplit.changeNumber() in the catch block instead. Co-Authored-By: Claude Sonnet 4.6 AI-Session-Id: 724b08af-bcdd-4f98-b354-b4a62fbb2489 AI-Tool: claude-code AI-Model: unknown --- .../engine/evaluator/EvaluationContext.java | 2 +- .../split/engine/evaluator/EvaluatorImp.java | 7 +- .../split/engine/experiments/ParsedSplit.java | 16 +- .../split/engine/experiments/SplitParser.java | 7 - .../experiments/TargetingRuleFactory.java | 21 +-- .../experiments/TargetingRuleFactoryTest.java | 172 +++++------------- .../split/rules/engine/EvaluationResult.java | 14 -- .../rules/engine/TargetingEngineImpl.java | 26 +-- .../exceptions/VersionedExceptionWrapper.java | 8 +- .../io/split/rules/model/TargetingRule.java | 34 +--- .../rules/engine/TargetingEngineImplTest.java | 37 +--- 11 files changed, 77 insertions(+), 267 deletions(-) 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 b7de256b4..f6f7c2987 100644 --- a/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java +++ b/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java @@ -38,7 +38,7 @@ public RuleBasedSegmentCacheConsumer getRuleBasedSegmentCache() { @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); + return new EvaluationResult(r.treatment, r.label); } @Override 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 603d4bddf..2aab42e4c 100644 --- a/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java +++ b/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java @@ -116,9 +116,12 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu try { EvaluationResult r = _targetingEngine.evaluate(matchingKey, bucketingKey, parsedSplit.targetingRule(), attributes, _evaluationContext); - return new TreatmentLabelAndChangeNumber(r.treatment, r.label, r.version, r.config, r.impressionsDisabled); + String config = parsedSplit.configurations() != null + ? parsedSplit.configurations().get(r.treatment) : null; + return new TreatmentLabelAndChangeNumber(r.treatment, r.label, + parsedSplit.changeNumber(), config, parsedSplit.impressionsDisabled()); } catch (VersionedExceptionWrapper e) { - throw new ChangeNumberExceptionWrapper(e.wrappedException(), e.version()); + throw new ChangeNumberExceptionWrapper(e.wrappedException(), parsedSplit.changeNumber()); } } 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 c6538b9dd..d692d7e07 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java @@ -68,8 +68,9 @@ public static ParsedSplit createParsedSplitForTests( flagSets, impressionsDisabled, prerequisitesMatcher, - TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, - changeNumber, 100, seed, algo, null, flagSets, impressionsDisabled, prerequisitesMatcher) + TargetingRuleFactory.buildTargetingRule(seed, killed, defaultTreatment, matcherAndSplits, + 100, seed, algo, + prerequisitesMatcher == null ? Collections.emptyList() : prerequisitesMatcher.getPrerequisites()) ); } @@ -102,8 +103,9 @@ public static ParsedSplit createParsedSplitForTests( flagSets, impressionsDisabled, prerequisitesMatcher, - TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, - changeNumber, 100, seed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher) + TargetingRuleFactory.buildTargetingRule(seed, killed, defaultTreatment, matcherAndSplits, + 100, seed, algo, + prerequisitesMatcher == null ? Collections.emptyList() : prerequisitesMatcher.getPrerequisites()) ); } @@ -126,9 +128,9 @@ public ParsedSplit( this(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, flagSets, impressionsDisabled, prerequisitesMatcher, - TargetingRuleFactory.buildTargetingRule(feature, seed, killed, defaultTreatment, matcherAndSplits, trafficTypeName, - changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, - flagSets, impressionsDisabled, prerequisitesMatcher)); + TargetingRuleFactory.buildTargetingRule(seed, killed, defaultTreatment, matcherAndSplits, + trafficAllocation, trafficAllocationSeed, algo, + prerequisitesMatcher == null ? Collections.emptyList() : prerequisitesMatcher.getPrerequisites())); } public ParsedSplit( 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 6494c5287..bc8996915 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitParser.java +++ b/client/src/main/java/io/split/engine/experiments/SplitParser.java @@ -2,7 +2,6 @@ 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; @@ -75,19 +74,13 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) { .collect(Collectors.toList()); TargetingRule targetingRule = new TargetingRule( - split.name, split.seed, split.killed, split.defaultTreatment, targetingConditionList, - split.trafficTypeName, - split.changeNumber, split.trafficAllocation, split.trafficAllocationSeed, split.algo, - split.configurations, - split.sets == null ? new HashSet<>() : split.sets, - split.impressionsDisabled, prerequisites); return new ParsedSplit( diff --git a/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java b/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java index daea047a4..9ceee75e3 100644 --- a/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java +++ b/client/src/main/java/io/split/engine/experiments/TargetingRuleFactory.java @@ -1,6 +1,5 @@ package io.split.engine.experiments; -import io.split.rules.matchers.PrerequisitesMatcher; import io.split.rules.model.Condition; import io.split.rules.model.ConditionType; import io.split.rules.model.Partition; @@ -8,9 +7,7 @@ import io.split.rules.model.TargetingRule; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; public final class TargetingRuleFactory { @@ -20,11 +17,10 @@ private TargetingRuleFactory() { } public static TargetingRule buildTargetingRule( - String feature, int seed, boolean killed, String defaultTreatment, - List matcherAndSplits, String trafficTypeName, long changeNumber, + int seed, boolean killed, String defaultTreatment, + List matcherAndSplits, int trafficAllocation, int trafficAllocationSeed, int algo, - Map configurations, HashSet flagSets, - boolean impressionsDisabled, PrerequisitesMatcher prerequisitesMatcher) { + List prerequisites) { List conditions = matcherAndSplits == null ? Collections.emptyList() @@ -32,15 +28,8 @@ public static TargetingRule buildTargetingRule( .map(TargetingRuleFactory::toTargetingCondition) .collect(Collectors.toList()); - List prereqs = prerequisitesMatcher == null - ? Collections.emptyList() - : prerequisitesMatcher.getPrerequisites() == null - ? Collections.emptyList() - : Collections.unmodifiableList(prerequisitesMatcher.getPrerequisites()); - - return new TargetingRule(feature, seed, killed, defaultTreatment, conditions, trafficTypeName, - changeNumber, trafficAllocation, trafficAllocationSeed, algo, configurations, - flagSets == null ? new HashSet<>() : flagSets, impressionsDisabled, prereqs); + return new TargetingRule(seed, killed, defaultTreatment, conditions, + trafficAllocation, trafficAllocationSeed, algo, prerequisites); } private static Condition toTargetingCondition(ParsedCondition c) { diff --git a/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java b/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java index cc1da5220..aca1b0977 100644 --- a/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java +++ b/client/src/test/java/io/split/engine/experiments/TargetingRuleFactoryTest.java @@ -8,52 +8,27 @@ import io.split.rules.model.Condition; import io.split.rules.model.TargetingRule; -import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; import static org.junit.Assert.*; public class TargetingRuleFactoryTest { - private Map _configurations; - private HashSet _flagSets; - - @Before - public void setUp() { - _configurations = new HashMap<>(); - _configurations.put("on", "{\"color\": \"blue\"}"); - _flagSets = new HashSet<>(Arrays.asList("set1", "set2")); - } - @Test public void testBuildTargetingRule_withNullConditions_returnsEmptyList() { TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", - 12345, - false, - "control", + 12345, false, "control", null, - "user_type", - 999L, - 100, - 456, - 1, - _configurations, - _flagSets, - false, + 100, 456, 1, null ); assertNotNull(rule); - assertEquals("feature1", rule.name()); assertTrue(rule.conditions().isEmpty()); } @@ -74,24 +49,13 @@ public void testBuildTargetingRule_withValidConditions_mapsCorrectly() { ); TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", - 12345, - false, - "control", + 12345, false, "control", Arrays.asList(parsedCondition), - "user_type", - 999L, - 100, - 456, - 1, - _configurations, - _flagSets, - false, + 100, 456, 1, null ); assertNotNull(rule); - assertEquals("feature1", rule.name()); assertEquals(1, rule.conditions().size()); Condition condition = rule.conditions().get(0); @@ -107,19 +71,9 @@ public void testBuildTargetingRule_withValidConditions_mapsCorrectly() { @Test public void testBuildTargetingRule_withNullPrerequisites_returnsEmptyPrerequisiteList() { TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", - 12345, - false, - "control", - new ArrayList<>(), - "user_type", - 999L, - 100, - 456, - 1, - _configurations, - _flagSets, - false, + 12345, false, "control", + new ArrayList(), + 100, 456, 1, null ); @@ -127,53 +81,6 @@ public void testBuildTargetingRule_withNullPrerequisites_returnsEmptyPrerequisit assertTrue(rule.prerequisites().isEmpty()); } - @Test - public void testBuildTargetingRule_withNullFlagSets_createsEmptySet() { - TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", - 12345, - false, - "control", - new ArrayList<>(), - "user_type", - 999L, - 100, - 456, - 1, - _configurations, - null, - false, - null - ); - - assertNotNull(rule); - assertNotNull(rule.flagSets()); - assertTrue(rule.flagSets().isEmpty()); - } - - @Test - public void testBuildTargetingRule_withFlagSets_preservesSet() { - TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", - 12345, - false, - "control", - new ArrayList<>(), - "user_type", - 999L, - 100, - 456, - 1, - _configurations, - _flagSets, - false, - null - ); - - assertNotNull(rule); - assertEquals(_flagSets, rule.flagSets()); - } - @Test public void testBuildTargetingRule_withRolloutCondition_mapsConditionTypeCorrectly() { CombiningMatcher matcher = new CombiningMatcher(CombiningMatcher.Combiner.AND, @@ -191,10 +98,10 @@ public void testBuildTargetingRule_withRolloutCondition_mapsConditionTypeCorrect ); TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", 12345, false, "control", + 12345, false, "control", Arrays.asList(rolloutCondition), - "user_type", 999L, 100, 456, 1, - _configurations, _flagSets, false, null + 100, 456, 1, + null ); Condition condition = rule.conditions().get(0); @@ -218,10 +125,10 @@ public void testBuildTargetingRule_withWhitelistCondition_mapsConditionTypeCorre ); TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", 12345, false, "control", + 12345, false, "control", Arrays.asList(whitelistCondition), - "user_type", 999L, 100, 456, 1, - _configurations, _flagSets, false, null + 100, 456, 1, + null ); Condition condition = rule.conditions().get(0); @@ -249,10 +156,10 @@ public void testBuildTargetingRule_withMultiplePartitions_mapsAllPartitions() { ); TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", 12345, false, "control", + 12345, false, "control", Arrays.asList(parsedCondition), - "user_type", 999L, 100, 456, 1, - _configurations, _flagSets, false, null + 100, 456, 1, + null ); Condition condition = rule.conditions().get(0); @@ -276,10 +183,10 @@ public void testBuildTargetingRule_withNullPartitions_returnsEmptyPartitionList( ); TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "feature1", 12345, false, "control", + 12345, false, "control", Arrays.asList(parsedCondition), - "user_type", 999L, 100, 456, 1, - _configurations, _flagSets, false, null + 100, 456, 1, + null ); Condition condition = rule.conditions().get(0); @@ -287,34 +194,37 @@ public void testBuildTargetingRule_withNullPartitions_returnsEmptyPartitionList( } @Test - public void testBuildTargetingRule_preservesAllNonMappedFields() { + public void testBuildTargetingRule_preservesEvaluationFields() { TargetingRule rule = TargetingRuleFactory.buildTargetingRule( - "my_feature", - 98765, - true, - "killed", - new ArrayList<>(), - "account", - 555L, - 75, - 321, - 2, - _configurations, - _flagSets, - true, + 98765, true, "killed", + new ArrayList(), + 75, 321, 2, null ); - assertEquals("my_feature", rule.name()); assertEquals(98765, rule.seed()); assertTrue(rule.killed()); assertEquals("killed", rule.defaultTreatment()); - assertEquals("account", rule.trafficTypeName()); - assertEquals(555L, rule.changeNumber()); assertEquals(75, rule.trafficAllocation()); assertEquals(321, rule.trafficAllocationSeed()); assertEquals(2, rule.algo()); - assertEquals(_configurations, rule.configurations()); - assertTrue(rule.impressionsDisabled()); + assertTrue(rule.prerequisites().isEmpty()); + } + + @Test + public void testBuildTargetingRule_withPrerequisites_preservesList() { + List prereqs = Collections.singletonList( + new io.split.rules.model.Prerequisite("flag1", Collections.singletonList("on")) + ); + + TargetingRule rule = TargetingRuleFactory.buildTargetingRule( + 12345, false, "control", + new ArrayList(), + 100, 456, 1, + prereqs + ); + + assertEquals(1, rule.prerequisites().size()); + assertEquals("flag1", rule.prerequisites().get(0).featureFlagName()); } } 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 index 0d9481f6c..bf7812743 100644 --- a/targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java +++ b/targeting-engine/src/main/java/io/split/rules/engine/EvaluationResult.java @@ -3,23 +3,9 @@ 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/TargetingEngineImpl.java b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java index 590d31789..3ef30a39e 100644 --- a/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java +++ b/targeting-engine/src/main/java/io/split/rules/engine/TargetingEngineImpl.java @@ -15,12 +15,9 @@ 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()); + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.KILLED); } // 2. Bucketing key resolution @@ -28,8 +25,7 @@ public EvaluationResult evaluate(String matchingKey, String bucketingKey, // 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()); + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.PREREQUISITES_NOT_MET); } // 4. Iterate conditions @@ -41,9 +37,7 @@ public EvaluationResult evaluate(String matchingKey, String bucketingKey, 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()); + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.NOT_IN_SPLIT); } } inRollout = true; @@ -52,23 +46,15 @@ public EvaluationResult evaluate(String matchingKey, String bucketingKey, // 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()); + return new EvaluationResult(treatment, condition.label()); } } // 5. No condition matched → default rule - config = getConfig(rule, rule.defaultTreatment()); - return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.DEFAULT_RULE, - rule.changeNumber(), config, rule.impressionsDisabled()); + return new EvaluationResult(rule.defaultTreatment(), EvaluationLabels.DEFAULT_RULE); } catch (Exception e) { - throw new VersionedExceptionWrapper(e, rule.changeNumber()); + throw new VersionedExceptionWrapper(e); } } - - 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 index 0501f7f78..53ca94c0e 100644 --- a/targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java +++ b/targeting-engine/src/main/java/io/split/rules/exceptions/VersionedExceptionWrapper.java @@ -2,18 +2,12 @@ public class VersionedExceptionWrapper extends Exception { private final Exception _wrappedException; - private final Long _version; - public VersionedExceptionWrapper(Exception wrappedException, Long version) { + public VersionedExceptionWrapper(Exception wrappedException) { _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/model/TargetingRule.java b/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java index 922948b85..6298158ad 100644 --- a/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java +++ b/targeting-engine/src/main/java/io/split/rules/model/TargetingRule.java @@ -4,70 +4,50 @@ 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. + * A fully-parsed targeting rule containing only the fields needed for evaluation. + * Metadata (name, changeNumber, trafficTypeName, configurations, flagSets, impressionsDisabled) + * belongs in ParsedSplit (client module), not here. */ 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); + public TargetingRule(int seed, boolean killed, String defaultTreatment, + List conditions, int trafficAllocation, + int trafficAllocationSeed, int algo, + List prerequisites) { _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/engine/TargetingEngineImplTest.java b/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java index 4e76fd644..11fc1995f 100644 --- a/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java +++ b/targeting-engine/src/test/java/io/split/rules/engine/TargetingEngineImplTest.java @@ -11,15 +11,10 @@ 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 { @@ -35,10 +30,8 @@ public void setUp() { 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, + 12345, killed, "off", + conditions, trafficAllocation, 12345, 2, prerequisites ); } @@ -67,7 +60,6 @@ public void killedRuleReturnsDefaultTreatment() throws Exception { 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 @@ -133,29 +125,4 @@ public void bucketingKeyUsedWhenProvided() throws Exception { 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); - } } From f13d8af89a3b26df3d5ac131e61dc98f0f2b344a Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 17 Apr 2026 12:26:36 -0300 Subject: [PATCH 11/11] Targeting readme --- targeting-engine/README.md | 129 +++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 targeting-engine/README.md diff --git a/targeting-engine/README.md b/targeting-engine/README.md new file mode 100644 index 000000000..ea6987498 --- /dev/null +++ b/targeting-engine/README.md @@ -0,0 +1,129 @@ +# targeting-engine + +A generic, zero-dependency rule evaluation engine extracted from the Split Java SDK. Given a parsed `TargetingRule` and a user key, it deterministically returns a treatment. + +## Maven Dependency + +```xml + + io.split.client + targeting-engine + 4.18.3 + +``` + +Requires Java 8+. No runtime dependencies. + +## Usage + +### 1. Implement `EvaluationContext` + +The engine delegates two concerns back to the host application via `EvaluationContext`: + +```java +public class MyEvaluationContext implements EvaluationContext { + + // Called for DependencyMatcher and PrerequisitesMatcher + @Override + public EvaluationResult evaluate(String matchingKey, String bucketingKey, + String ruleName, Map attributes) { + return myEngine.getTreatment(matchingKey, bucketingKey, ruleName, attributes); + } + + // Called for UserDefinedSegmentMatcher + @Override + public boolean isInSegment(String segmentName, String key) { + return mySegmentStorage.isInSegment(segmentName, key); + } + + // Called for RuleBasedSegmentMatcher + @Override + public boolean isInRuleBasedSegment(String segmentName, String key, + String bucketingKey, + Map attributes) { + return myRuleBasedSegmentEvaluator.isIn(segmentName, key, bucketingKey, attributes); + } +} +``` + +### 2. Build a `TargetingRule` + +`TargetingRule` contains only the fields needed for evaluation. Metadata (name, configurations, flag sets, etc.) belongs in the calling SDK's domain model. + +```java +List conditions = List.of( + new Condition( + ConditionType.ROLLOUT, + CombiningMatcher.of(new AllKeysMatcher()), // match everyone + List.of(new Partition("on", 50), new Partition("off", 50)), + "default rule" + ) +); + +TargetingRule rule = new TargetingRule( + 123456, // seed + false, // killed + "off", // defaultTreatment + conditions, + 100, // trafficAllocation (0–100) + 654321, // trafficAllocationSeed + 2, // algo: 2 = MurmurHash3 (recommended), 1 = legacy + null // prerequisites (List or null) +); +``` + +### 3. Evaluate + +`TargetingEngineImpl` is stateless — create once, reuse across threads. + +```java +TargetingEngine engine = new TargetingEngineImpl(); + +EvaluationResult result = engine.evaluate( + "user-123", // matchingKey + null, // bucketingKey — null means use matchingKey + rule, + Map.of("plan", "pro"), // attributes — may be null + context +); + +result.treatment // "on" | "off" | ... +result.label // EvaluationLabels constant (e.g. "default rule") +``` + +## Evaluation Labels + +| Label constant | When returned | +|---|---| +| `EvaluationLabels.KILLED` | Rule is killed; `defaultTreatment` is returned | +| `EvaluationLabels.PREREQUISITES_NOT_MET` | A prerequisite flag returned the wrong treatment | +| `EvaluationLabels.NOT_IN_SPLIT` | User fell outside traffic allocation | +| `EvaluationLabels.DEFAULT_RULE` | No condition matched; last condition applied | + +Additional labels (`DEFINITION_NOT_FOUND`, `EXCEPTION`, `UNSUPPORTED_MATCHER`, `NOT_READY`) are set by the calling SDK, not by the engine. + +## Data Model + +| Class | Role | +|---|---| +| `TargetingRule` | Full rule definition (seed, conditions, traffic allocation, prerequisites) | +| `Condition` | One targeting condition (`WHITELIST` or `ROLLOUT`) | +| `Partition` | `(treatment, size%)` pair within a condition | +| `Prerequisite` | Dependency on another rule's treatment | +| `CombiningMatcher` | AND-combines a list of `AttributeMatcher`s | +| `AttributeMatcher` | Extracts one attribute then delegates to a `Matcher` | + +## Available Matchers + +`AllKeysMatcher`, `WhitelistMatcher`, `EqualToMatcher`, `BetweenMatcher`, `BooleanMatcher`, +`ContainsAnyOfMatcher`, `StartsWithAnyOfMatcher`, `EndsWithAnyOfMatcher`, `RegularExpressionMatcher`, +`EqualToSemverMatcher`, `BetweenSemverMatcher`, `GreaterThanOrEqualToSemverMatcher`, +`LessThanOrEqualToSemverMatcher`, `InListSemverMatcher`, +`ContainsAllOfSetMatcher`, `ContainsAnyOfSetMatcher`, `EqualToSetMatcher`, `PartOfSetMatcher`, +`UserDefinedSegmentMatcher`, `RuleBasedSegmentMatcher`, `DependencyMatcher` + +## Building + +```bash +mvn -pl :targeting-engine clean install +```