diff --git a/lib/splitclient-rb.rb b/lib/splitclient-rb.rb index aebc4126..b6d04f55 100644 --- a/lib/splitclient-rb.rb +++ b/lib/splitclient-rb.rb @@ -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' @@ -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' diff --git a/lib/splitclient-rb/cache/repositories/segments_repository.rb b/lib/splitclient-rb/cache/repositories/segments_repository.rb index 953c18c7..a45ee167 100644 --- a/lib/splitclient-rb/cache/repositories/segments_repository.rb +++ b/lib/splitclient-rb/cache/repositories/segments_repository.rb @@ -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) diff --git a/lib/splitclient-rb/engine/matchers/combining_matcher.rb b/lib/splitclient-rb/engine/matchers/combining_matcher.rb index 9a54b75a..b3012011 100644 --- a/lib/splitclient-rb/engine/matchers/combining_matcher.rb +++ b/lib/splitclient-rb/engine/matchers/combining_matcher.rb @@ -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 diff --git a/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb b/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb new file mode 100644 index 00000000..c3a5f9f8 --- /dev/null +++ b/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb @@ -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 diff --git a/lib/splitclient-rb/engine/parser/condition.rb b/lib/splitclient-rb/engine/parser/condition.rb index b5d7567b..926dd206 100644 --- a/lib/splitclient-rb/engine/parser/condition.rb +++ b/lib/splitclient-rb/engine/parser/condition.rb @@ -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 @@ -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 diff --git a/lib/splitclient-rb/engine/parser/evaluator.rb b/lib/splitclient-rb/engine/parser/evaluator.rb index afefd364..b9508e38 100644 --- a/lib/splitclient-rb/engine/parser/evaluator.rb +++ b/lib/splitclient-rb/engine/parser/evaluator.rb @@ -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 @@ -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, @@ -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 diff --git a/lib/splitclient-rb/helpers/evaluator_helper.rb b/lib/splitclient-rb/helpers/evaluator_helper.rb new file mode 100644 index 00000000..e9c55cfc --- /dev/null +++ b/lib/splitclient-rb/helpers/evaluator_helper.rb @@ -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 diff --git a/spec/engine/matchers/rule_based_segment_matcher_spec.rb b/spec/engine/matchers/rule_based_segment_matcher_spec.rb new file mode 100644 index 00000000..4f7786e4 --- /dev/null +++ b/spec/engine/matchers/rule_based_segment_matcher_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/engine/parser/evaluator_spec.rb b/spec/engine/parser/evaluator_spec.rb index b3af7d57..99f5cdfb 100644 --- a/spec/engine/parser/evaluator_spec.rb +++ b/spec/engine/parser/evaluator_spec.rb @@ -4,10 +4,11 @@ describe SplitIoClient::Engine::Parser::Evaluator do let(:segments_repository) { SplitIoClient::Cache::Repositories::SegmentsRepository.new(@default_config) } + let(:rule_based_segments_repository) { SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(@default_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(@default_config, flag_sets_repository, flag_set_filter) } - let(:evaluator) { described_class.new(segments_repository, splits_repository, true) } + let(:evaluator) { described_class.new(segments_repository, splits_repository, rule_based_segments_repository, true) } let(:killed_split) { { killed: true, defaultTreatment: 'default' } } let(:archived_split) { { status: 'ARCHIVED' } }