Skip to content

Commit 09829cf

Browse files
authored
Merge pull request #548 from splitio/rbs-matcher
Added RBS matcher
2 parents a7b618a + 80cd16a commit 09829cf

File tree

9 files changed

+290
-33
lines changed

9 files changed

+290
-33
lines changed

lib/splitclient-rb.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
require 'splitclient-rb/helpers/decryption_helper'
4949
require 'splitclient-rb/helpers/util'
5050
require 'splitclient-rb/helpers/repository_helper'
51+
require 'splitclient-rb/helpers/evaluator_helper'
5152
require 'splitclient-rb/split_factory'
5253
require 'splitclient-rb/split_factory_builder'
5354
require 'splitclient-rb/split_config'
@@ -97,6 +98,7 @@
9798
require 'splitclient-rb/engine/matchers/less_than_or_equal_to_semver_matcher'
9899
require 'splitclient-rb/engine/matchers/between_semver_matcher'
99100
require 'splitclient-rb/engine/matchers/in_list_semver_matcher'
101+
require 'splitclient-rb/engine/matchers/rule_based_segment_matcher'
100102
require 'splitclient-rb/engine/evaluator/splitter'
101103
require 'splitclient-rb/engine/impressions/noop_unique_keys_tracker'
102104
require 'splitclient-rb/engine/impressions/unique_keys_tracker'

lib/splitclient-rb/cache/repositories/segments_repository.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ def segment_keys_count
8383
0
8484
end
8585

86+
def contains?(segment_names)
87+
if segment_names.empty?
88+
return false
89+
end
90+
return segment_names.to_set.subset?(used_segment_names.to_set)
91+
end
92+
8693
private
8794

8895
def segment_data(name)

lib/splitclient-rb/engine/matchers/combining_matcher.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ def eval_and(args)
5656

5757
@matchers.all? do |matcher|
5858
if match_with_key?(matcher)
59-
matcher.match?(value: args[:matching_key])
59+
key = args[:value]
60+
if args[:matching_key] != nil
61+
key = args[:matching_key]
62+
end
63+
matcher.match?(value: key)
6064
else
6165
matcher.match?(args)
6266
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
module SplitIoClient
4+
#
5+
# class to implement the user defined matcher
6+
#
7+
class RuleBasedSegmentMatcher < Matcher
8+
MATCHER_TYPE = 'IN_RULE_BASED_SEGMENT'
9+
10+
def initialize(segments_repository, rule_based_segments_repository, segment_name, config)
11+
super(config.logger)
12+
@rule_based_segments_repository = rule_based_segments_repository
13+
@segments_repository = segments_repository
14+
@segment_name = segment_name
15+
@config = config
16+
end
17+
18+
#
19+
# evaluates if the key matches the matcher
20+
#
21+
# @param key [string] key value to be matched
22+
#
23+
# @return [boolean] evaluation of the key against the segment
24+
def match?(args)
25+
rule_based_segment = @rule_based_segments_repository.get_rule_based_segment(@segment_name)
26+
return false if rule_based_segment.nil?
27+
28+
return false if rule_based_segment[:excluded][:keys].include?([args[:value]])
29+
30+
rule_based_segment[:excluded][:segments].each do |segment|
31+
return false if segment[:type] == 'standard' and @segments_repository.in_segment?(segment[:name], args[:value])
32+
33+
return false if segment[:type] == 'rule-based' and SplitIoClient::RuleBasedSegmentMatcher.new(@segments_repository, @rule_based_segments_repository, segment[:name], @config).match?(args)
34+
end
35+
36+
matches = false
37+
rule_based_segment[:conditions].each do |c|
38+
condition = SplitIoClient::Condition.new(c, @config)
39+
next if condition.empty?
40+
matches = Helpers::EvaluatorHelper.matcher_type(condition, @segments_repository, @rule_based_segments_repository).match?(args)
41+
end
42+
@logger.debug("[InRuleSegmentMatcher] #{@segment_name} is in rule based segment -> #{matches}")
43+
matches
44+
end
45+
end
46+
end

lib/splitclient-rb/engine/parser/condition.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ def matcher_in_list_semver(params)
230230
)
231231
end
232232

233+
def matcher_in_rule_based_segment(params)
234+
matcher = params[:matcher]
235+
segment_name = matcher[:userDefinedSegmentMatcherData] && matcher[:userDefinedSegmentMatcherData][:segmentName]
236+
237+
RuleBasedSegmentMatcher.new(params[:segments_repository], params[:rule_based_segments_repository], segment_name, @config)
238+
end
239+
233240
#
234241
# @return [object] the negate value for this condition
235242
def negate
@@ -246,6 +253,8 @@ def negate
246253
# @return [void]
247254
def set_partitions
248255
partitions_list = []
256+
return partitions_list unless @data.key?('partitions')
257+
249258
@data[:partitions].each do |p|
250259
partition = SplitIoClient::Partition.new(p)
251260
partitions_list << partition

lib/splitclient-rb/engine/parser/evaluator.rb

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module SplitIoClient
22
module Engine
33
module Parser
44
class Evaluator
5-
def initialize(segments_repository, splits_repository, config)
5+
def initialize(segments_repository, splits_repository, rb_segment_repository, config)
66
@splits_repository = splits_repository
77
@segments_repository = segments_repository
88
@config = config
@@ -59,7 +59,7 @@ def match(split, keys, attributes)
5959
in_rollout = true
6060
end
6161

62-
condition_matched = matcher_type(condition).match?(
62+
condition_matched = Helpers::EvaluatorHelper::matcher_type(condition, @segments_repository, @rb_segment_repository).match?(
6363
matching_key: keys[:matching_key],
6464
bucketing_key: keys[:bucketing_key],
6565
evaluator: self,
@@ -80,38 +80,9 @@ def match(split, keys, attributes)
8080
treatment_hash(Models::Label::NO_RULE_MATCHED, split[:defaultTreatment], split[:changeNumber], split_configurations(split[:defaultTreatment], split))
8181
end
8282

83-
def matcher_type(condition)
84-
matchers = []
85-
86-
@segments_repository.adapter.pipelined do
87-
condition.matchers.each do |matcher|
88-
matchers << if matcher[:negate]
89-
condition.negation_matcher(matcher_instance(matcher[:matcherType], condition, matcher))
90-
else
91-
matcher_instance(matcher[:matcherType], condition, matcher)
92-
end
93-
end
94-
end
95-
96-
final_matcher = condition.create_condition_matcher(matchers)
97-
98-
if final_matcher.nil?
99-
@logger.error('Invalid matcher type')
100-
else
101-
final_matcher
102-
end
103-
end
104-
10583
def treatment_hash(label, treatment, change_number = nil, configurations = nil)
10684
{ label: label, treatment: treatment, change_number: change_number, config: configurations }
10785
end
108-
109-
def matcher_instance(type, condition, matcher)
110-
condition.send(
111-
"matcher_#{type.downcase}",
112-
matcher: matcher, segments_repository: @segments_repository
113-
)
114-
end
11586
end
11687
end
11788
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
module SplitIoClient
4+
module Helpers
5+
class EvaluatorHelper
6+
def self.matcher_type(condition, segments_repository, rb_segment_repository)
7+
matchers = []
8+
segments_repository.adapter.pipelined do
9+
condition.matchers.each do |matcher|
10+
matchers << if matcher[:negate]
11+
condition.negation_matcher(matcher_instance(matcher[:matcherType], condition, matcher))
12+
else
13+
matcher_instance(matcher[:matcherType], condition, matcher, segments_repository, rb_segment_repository)
14+
end
15+
end
16+
end
17+
final_matcher = condition.create_condition_matcher(matchers)
18+
19+
if final_matcher.nil?
20+
config.logger.error('Invalid matcher type')
21+
else
22+
final_matcher
23+
end
24+
final_matcher
25+
end
26+
27+
def self.matcher_instance(type, condition, matcher, segments_repository, rb_segment_repository)
28+
condition.send(
29+
"matcher_#{type.downcase}",
30+
matcher: matcher, segments_repository: segments_repository, rule_based_segments_repository: rb_segment_repository
31+
)
32+
end
33+
end
34+
end
35+
end
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe SplitIoClient::RuleBasedSegmentMatcher do
6+
let(:config) { SplitIoClient::SplitConfig.new(debug_enabled: true) }
7+
let(:segments_repository) { SplitIoClient::Cache::Repositories::SegmentsRepository.new(config) }
8+
let(:flag_sets_repository) {SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([])}
9+
let(:flag_set_filter) {SplitIoClient::Cache::Filter::FlagSetsFilter.new([])}
10+
let(:splits_repository) { SplitIoClient::Cache::Repositories::SplitsRepository.new(config, flag_sets_repository, flag_set_filter) }
11+
12+
context '#string_type' do
13+
it 'is not string type matcher' do
14+
expect(described_class.new(nil, nil, nil, config).string_type?).to be false
15+
end
16+
end
17+
18+
context 'test_matcher' do
19+
it 'return false if excluded key is passed' do
20+
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
21+
rbs_repositoy.update([{name: 'foo', trafficTypeName: 'tt_name_1', conditions: [], excluded: {keys: ['key1'], segments: []}}], [], -1)
22+
matcher = described_class.new(segments_repository, rbs_repositoy, 'foo', config)
23+
expect(matcher.match?(value: 'key1')).to be false
24+
end
25+
26+
it 'return false if excluded segment is passed' do
27+
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
28+
evaluator = SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rbs_repositoy, true)
29+
segments_repository.add_to_segment({:name => 'segment1', :added => [], :removed => []})
30+
rbs_repositoy.update([{:name => 'foo', :trafficTypeName => 'tt_name_1', :conditions => [], :excluded => {:keys => ['key1'], :segments => [{:name => 'segment1', :type => 'standard'}]}}], [], -1)
31+
matcher = described_class.new(segments_repository, rbs_repositoy, 'foo', config)
32+
expect(matcher.match?(value: 'key2')).to be false
33+
end
34+
35+
it 'return false if excluded rb segment is matched' do
36+
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
37+
rbs = {:name => 'sample_rule_based_segment', :trafficTypeName => 'tt_name_1', :conditions => [{
38+
:matcherGroup => {
39+
:combiner => "AND",
40+
:matchers => [
41+
{
42+
:matcherType => "WHITELIST",
43+
:negate => false,
44+
:userDefinedSegmentMatcherData => nil,
45+
:whitelistMatcherData => {
46+
:whitelist => [
47+
48+
"bilal"
49+
]
50+
},
51+
:unaryNumericMatcherData => nil,
52+
:betweenMatcherData => nil
53+
}
54+
]
55+
}
56+
}], :excluded => {:keys => [], :segments => [{:name => 'no_excludes', :type => 'rule-based'}]}}
57+
rbs2 = {:name => 'no_excludes', :trafficTypeName => 'tt_name_1',
58+
:conditions => [{
59+
:matcherGroup => {
60+
:combiner => "AND",
61+
:matchers => [
62+
{
63+
:keySelector => {
64+
:trafficType => "user",
65+
:attribute => "email"
66+
},
67+
:matcherType => "ENDS_WITH",
68+
:negate => false,
69+
:whitelistMatcherData => {
70+
:whitelist => [
71+
"@split.io"
72+
]
73+
}
74+
}
75+
]
76+
}
77+
}
78+
], :excluded => {:keys => [], :segments => []}}
79+
80+
rbs_repositoy.update([rbs, rbs2], [], -1)
81+
matcher = described_class.new(segments_repository, rbs_repositoy, 'sample_rule_based_segment', config)
82+
expect(matcher.match?(value: '[email protected]', attributes: {'email': '[email protected]'})).to be false
83+
expect(matcher.match?(value: 'bilal', attributes: {'email': 'bilal'})).to be true
84+
end
85+
86+
it 'return true if condition matches' do
87+
rule_based_segment = { :name => 'corge', :trafficTypeName => 'tt_name_5',
88+
:excluded => {:keys => [], :segments => []},
89+
:conditions => [
90+
{
91+
:contitionType => 'WHITELIST',
92+
:label => 'some_label',
93+
:matcherGroup => {
94+
:matchers => [
95+
{
96+
:keySelector => nil,
97+
:matcherType => 'WHITELIST',
98+
:whitelistMatcherData => {
99+
:whitelist => ['k1', 'k2', 'k3']
100+
},
101+
:negate => false,
102+
}
103+
],
104+
:combiner => 'AND'
105+
}
106+
}]
107+
}
108+
109+
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
110+
rbs_repositoy.update([rule_based_segment], [], -1)
111+
matcher = described_class.new(segments_repository, rbs_repositoy, 'corge', config)
112+
expect(matcher.match?({:matching_key => 'user', :attributes => {}})).to be false
113+
expect(matcher.match?({:matching_key => 'k1', :attributes => {}})).to be true
114+
end
115+
116+
it 'return true if dependent rb segment matches' do
117+
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
118+
rbs = {
119+
:changeNumber => 5,
120+
:name => "dependent_rbs",
121+
:status => "ACTIVE",
122+
:trafficTypeName => "user",
123+
:excluded =>{:keys =>["[email protected]","[email protected]"],:segments =>[]},
124+
:conditions => [
125+
{
126+
:matcherGroup => {
127+
:combiner => "AND",
128+
:matchers => [
129+
{
130+
:keySelector => {
131+
:trafficType => "user",
132+
:attribute => "email"
133+
},
134+
:matcherType => "ENDS_WITH",
135+
:negate => false,
136+
:whitelistMatcherData => {
137+
:whitelist => [
138+
"@split.io"
139+
]
140+
}
141+
}
142+
]
143+
}
144+
}
145+
]}
146+
rbs2 = {
147+
:changeNumber => 5,
148+
:name => "sample_rule_based_segment",
149+
:status => "ACTIVE",
150+
:trafficTypeName => "user",
151+
:excluded => {
152+
:keys => [],
153+
:segments => []
154+
},
155+
:conditions => [
156+
{
157+
:conditionType => "ROLLOUT",
158+
:matcherGroup => {
159+
:combiner => "AND",
160+
:matchers => [
161+
{
162+
:keySelector => {
163+
:trafficType => "user"
164+
},
165+
:matcherType => "IN_RULE_BASED_SEGMENT",
166+
:negate => false,
167+
:userDefinedSegmentMatcherData => {
168+
:segmentName => "dependent_rbs"
169+
}
170+
}
171+
]
172+
}
173+
}
174+
]
175+
}
176+
rbs_repositoy.update([rbs, rbs2], [], -1)
177+
matcher = described_class.new(segments_repository, rbs_repositoy, 'sample_rule_based_segment', config)
178+
expect(matcher.match?(value: '[email protected]', attributes: {'email': '[email protected]'})).to be true
179+
expect(matcher.match?(value: 'bilal', attributes: {'email': 'bilal'})).to be false
180+
end
181+
end
182+
end

0 commit comments

Comments
 (0)