Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/splitclient-rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
require 'splitclient-rb/helpers/decryption_helper'
require 'splitclient-rb/helpers/util'
require 'splitclient-rb/helpers/repository_helper'
require 'splitclient-rb/helpers/evaluator_helper'
require 'splitclient-rb/split_factory'
require 'splitclient-rb/split_factory_builder'
require 'splitclient-rb/split_config'
Expand Down Expand Up @@ -97,6 +98,7 @@
require 'splitclient-rb/engine/matchers/less_than_or_equal_to_semver_matcher'
require 'splitclient-rb/engine/matchers/between_semver_matcher'
require 'splitclient-rb/engine/matchers/in_list_semver_matcher'
require 'splitclient-rb/engine/matchers/rule_based_segment_matcher'
require 'splitclient-rb/engine/evaluator/splitter'
require 'splitclient-rb/engine/impressions/noop_unique_keys_tracker'
require 'splitclient-rb/engine/impressions/unique_keys_tracker'
Expand Down
7 changes: 7 additions & 0 deletions lib/splitclient-rb/cache/repositories/segments_repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ def segment_keys_count
0
end

def contains?(segment_names)
if segment_names.empty?
return false
end
return segment_names.to_set.subset?(used_segment_names.to_set)
end

private

def segment_data(name)
Expand Down
6 changes: 5 additions & 1 deletion lib/splitclient-rb/engine/matchers/combining_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ def eval_and(args)

@matchers.all? do |matcher|
if match_with_key?(matcher)
matcher.match?(value: args[:matching_key])
key = args[:value]
if args[:matching_key] != nil
key = args[:matching_key]
end
matcher.match?(value: key)
else
matcher.match?(args)
end
Expand Down
46 changes: 46 additions & 0 deletions lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module SplitIoClient
#
# class to implement the user defined matcher
#
class RuleBasedSegmentMatcher < Matcher
MATCHER_TYPE = 'IN_RULE_BASED_SEGMENT'

def initialize(segments_repository, rule_based_segments_repository, segment_name, config)
super(config.logger)
@rule_based_segments_repository = rule_based_segments_repository
@segments_repository = segments_repository
@segment_name = segment_name
@config = config
end

#
# evaluates if the key matches the matcher
#
# @param key [string] key value to be matched
#
# @return [boolean] evaluation of the key against the segment
def match?(args)
rule_based_segment = @rule_based_segments_repository.get_rule_based_segment(@segment_name)
return false if rule_based_segment.nil?

return false if rule_based_segment[:excluded][:keys].include?([args[:value]])

rule_based_segment[:excluded][:segments].each do |segment|
return false if segment[:type] == 'standard' and @segments_repository.in_segment?(segment[:name], args[:value])

return false if segment[:type] == 'rule-based' and SplitIoClient::RuleBasedSegmentMatcher.new(@segments_repository, @rule_based_segments_repository, segment[:name], @config).match?(args)
end

matches = false
rule_based_segment[:conditions].each do |c|
condition = SplitIoClient::Condition.new(c, @config)
next if condition.empty?
matches = Helpers::EvaluatorHelper.matcher_type(condition, @segments_repository, @rule_based_segments_repository).match?(args)
end
@logger.debug("[InRuleSegmentMatcher] #{@segment_name} is in rule based segment -> #{matches}")
matches
end
end
end
9 changes: 9 additions & 0 deletions lib/splitclient-rb/engine/parser/condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ def matcher_in_list_semver(params)
)
end

def matcher_in_rule_based_segment(params)
matcher = params[:matcher]
segment_name = matcher[:userDefinedSegmentMatcherData] && matcher[:userDefinedSegmentMatcherData][:segmentName]

RuleBasedSegmentMatcher.new(params[:segments_repository], params[:rule_based_segments_repository], segment_name, @config)
end

#
# @return [object] the negate value for this condition
def negate
Expand All @@ -246,6 +253,8 @@ def negate
# @return [void]
def set_partitions
partitions_list = []
return partitions_list unless @data.key?('partitions')

@data[:partitions].each do |p|
partition = SplitIoClient::Partition.new(p)
partitions_list << partition
Expand Down
33 changes: 2 additions & 31 deletions lib/splitclient-rb/engine/parser/evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module SplitIoClient
module Engine
module Parser
class Evaluator
def initialize(segments_repository, splits_repository, config)
def initialize(segments_repository, splits_repository, rb_segment_repository, config)
@splits_repository = splits_repository
@segments_repository = segments_repository
@config = config
Expand Down Expand Up @@ -59,7 +59,7 @@ def match(split, keys, attributes)
in_rollout = true
end

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

def matcher_type(condition)
matchers = []

@segments_repository.adapter.pipelined do
condition.matchers.each do |matcher|
matchers << if matcher[:negate]
condition.negation_matcher(matcher_instance(matcher[:matcherType], condition, matcher))
else
matcher_instance(matcher[:matcherType], condition, matcher)
end
end
end

final_matcher = condition.create_condition_matcher(matchers)

if final_matcher.nil?
@logger.error('Invalid matcher type')
else
final_matcher
end
end

def treatment_hash(label, treatment, change_number = nil, configurations = nil)
{ label: label, treatment: treatment, change_number: change_number, config: configurations }
end

def matcher_instance(type, condition, matcher)
condition.send(
"matcher_#{type.downcase}",
matcher: matcher, segments_repository: @segments_repository
)
end
end
end
end
Expand Down
35 changes: 35 additions & 0 deletions lib/splitclient-rb/helpers/evaluator_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module SplitIoClient
module Helpers
class EvaluatorHelper
def self.matcher_type(condition, segments_repository, rb_segment_repository)
matchers = []
segments_repository.adapter.pipelined do
condition.matchers.each do |matcher|
matchers << if matcher[:negate]
condition.negation_matcher(matcher_instance(matcher[:matcherType], condition, matcher))
else
matcher_instance(matcher[:matcherType], condition, matcher, segments_repository, rb_segment_repository)
end
end
end
final_matcher = condition.create_condition_matcher(matchers)

if final_matcher.nil?
config.logger.error('Invalid matcher type')
else
final_matcher
end
final_matcher
end

def self.matcher_instance(type, condition, matcher, segments_repository, rb_segment_repository)
condition.send(
"matcher_#{type.downcase}",
matcher: matcher, segments_repository: segments_repository, rule_based_segments_repository: rb_segment_repository
)
end
end
end
end
182 changes: 182 additions & 0 deletions spec/engine/matchers/rule_based_segment_matcher_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# frozen_string_literal: true

require 'spec_helper'

describe SplitIoClient::RuleBasedSegmentMatcher do
let(:config) { SplitIoClient::SplitConfig.new(debug_enabled: true) }
let(:segments_repository) { SplitIoClient::Cache::Repositories::SegmentsRepository.new(config) }
let(:flag_sets_repository) {SplitIoClient::Cache::Repositories::MemoryFlagSetsRepository.new([])}
let(:flag_set_filter) {SplitIoClient::Cache::Filter::FlagSetsFilter.new([])}
let(:splits_repository) { SplitIoClient::Cache::Repositories::SplitsRepository.new(config, flag_sets_repository, flag_set_filter) }

context '#string_type' do
it 'is not string type matcher' do
expect(described_class.new(nil, nil, nil, config).string_type?).to be false
end
end

context 'test_matcher' do
it 'return false if excluded key is passed' do
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
rbs_repositoy.update([{name: 'foo', trafficTypeName: 'tt_name_1', conditions: [], excluded: {keys: ['key1'], segments: []}}], [], -1)
matcher = described_class.new(segments_repository, rbs_repositoy, 'foo', config)
expect(matcher.match?(value: 'key1')).to be false
end

it 'return false if excluded segment is passed' do
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
evaluator = SplitIoClient::Engine::Parser::Evaluator.new(segments_repository, splits_repository, rbs_repositoy, true)
segments_repository.add_to_segment({:name => 'segment1', :added => [], :removed => []})
rbs_repositoy.update([{:name => 'foo', :trafficTypeName => 'tt_name_1', :conditions => [], :excluded => {:keys => ['key1'], :segments => [{:name => 'segment1', :type => 'standard'}]}}], [], -1)
matcher = described_class.new(segments_repository, rbs_repositoy, 'foo', config)
expect(matcher.match?(value: 'key2')).to be false
end

it 'return false if excluded rb segment is matched' do
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
rbs = {:name => 'sample_rule_based_segment', :trafficTypeName => 'tt_name_1', :conditions => [{
:matcherGroup => {
:combiner => "AND",
:matchers => [
{
:matcherType => "WHITELIST",
:negate => false,
:userDefinedSegmentMatcherData => nil,
:whitelistMatcherData => {
:whitelist => [
"bilal@split.io",
"bilal"
]
},
:unaryNumericMatcherData => nil,
:betweenMatcherData => nil
}
]
}
}], :excluded => {:keys => [], :segments => [{:name => 'no_excludes', :type => 'rule-based'}]}}
rbs2 = {:name => 'no_excludes', :trafficTypeName => 'tt_name_1',
:conditions => [{
:matcherGroup => {
:combiner => "AND",
:matchers => [
{
:keySelector => {
:trafficType => "user",
:attribute => "email"
},
:matcherType => "ENDS_WITH",
:negate => false,
:whitelistMatcherData => {
:whitelist => [
"@split.io"
]
}
}
]
}
}
], :excluded => {:keys => [], :segments => []}}

rbs_repositoy.update([rbs, rbs2], [], -1)
matcher = described_class.new(segments_repository, rbs_repositoy, 'sample_rule_based_segment', config)
expect(matcher.match?(value: 'bilal@split.io', attributes: {'email': 'bilal@split.io'})).to be false
expect(matcher.match?(value: 'bilal', attributes: {'email': 'bilal'})).to be true
end

it 'return true if condition matches' do
rule_based_segment = { :name => 'corge', :trafficTypeName => 'tt_name_5',
:excluded => {:keys => [], :segments => []},
:conditions => [
{
:contitionType => 'WHITELIST',
:label => 'some_label',
:matcherGroup => {
:matchers => [
{
:keySelector => nil,
:matcherType => 'WHITELIST',
:whitelistMatcherData => {
:whitelist => ['k1', 'k2', 'k3']
},
:negate => false,
}
],
:combiner => 'AND'
}
}]
}

rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
rbs_repositoy.update([rule_based_segment], [], -1)
matcher = described_class.new(segments_repository, rbs_repositoy, 'corge', config)
expect(matcher.match?({:matching_key => 'user', :attributes => {}})).to be false
expect(matcher.match?({:matching_key => 'k1', :attributes => {}})).to be true
end

it 'return true if dependent rb segment matches' do
rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config)
rbs = {
:changeNumber => 5,
:name => "dependent_rbs",
:status => "ACTIVE",
:trafficTypeName => "user",
:excluded =>{:keys =>["mauro@split.io","gaston@split.io"],:segments =>[]},
:conditions => [
{
:matcherGroup => {
:combiner => "AND",
:matchers => [
{
:keySelector => {
:trafficType => "user",
:attribute => "email"
},
:matcherType => "ENDS_WITH",
:negate => false,
:whitelistMatcherData => {
:whitelist => [
"@split.io"
]
}
}
]
}
}
]}
rbs2 = {
:changeNumber => 5,
:name => "sample_rule_based_segment",
:status => "ACTIVE",
:trafficTypeName => "user",
:excluded => {
:keys => [],
:segments => []
},
:conditions => [
{
:conditionType => "ROLLOUT",
:matcherGroup => {
:combiner => "AND",
:matchers => [
{
:keySelector => {
:trafficType => "user"
},
:matcherType => "IN_RULE_BASED_SEGMENT",
:negate => false,
:userDefinedSegmentMatcherData => {
:segmentName => "dependent_rbs"
}
}
]
}
}
]
}
rbs_repositoy.update([rbs, rbs2], [], -1)
matcher = described_class.new(segments_repository, rbs_repositoy, 'sample_rule_based_segment', config)
expect(matcher.match?(value: 'bilal@split.io', attributes: {'email': 'bilal@split.io'})).to be true
expect(matcher.match?(value: 'bilal', attributes: {'email': 'bilal'})).to be false
end
end
end
Loading