diff --git a/lib/splitclient-rb.rb b/lib/splitclient-rb.rb index d76f8218..a916dabb 100644 --- a/lib/splitclient-rb.rb +++ b/lib/splitclient-rb.rb @@ -95,6 +95,7 @@ require 'splitclient-rb/engine/matchers/greater_than_or_equal_to_semver_matcher' 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/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/engine/matchers/in_list_semver_matcher.rb b/lib/splitclient-rb/engine/matchers/in_list_semver_matcher.rb new file mode 100644 index 00000000..7a424483 --- /dev/null +++ b/lib/splitclient-rb/engine/matchers/in_list_semver_matcher.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module SplitIoClient + class InListSemverMatcher < Matcher + MATCHER_TYPE = 'IN_LIST_SEMVER' + + attr_reader :attribute + + def initialize(attribute, list_value, logger, validator) + super(logger) + @validator = validator + @attribute = attribute + @semver_list = list_value.map { |item| SplitIoClient::Semver.new(item) } + @logger = logger + end + + def match?(args) + @logger.log_if_debug('[InListSemverMatcher] evaluating value and attributes.') + return false unless @validator.valid_matcher_arguments(args) + + value_to_match = args[:attributes][@attribute.to_sym] + unless value_to_match.is_a?(String) + @logger.log_if_debug('whitelistMatcherData is required for IN_LIST_SEMVER matcher type') + return false + end + matches = (@semver_list.map { |item| item.compare(SplitIoClient::Semver.new(value_to_match)) }).any?(&:zero?) + @logger.log_if_debug("[InListSemverMatcher] #{value_to_match} matches -> #{matches}") + matches + end + end +end diff --git a/lib/splitclient-rb/engine/parser/condition.rb b/lib/splitclient-rb/engine/parser/condition.rb index cefec1da..b5d7567b 100644 --- a/lib/splitclient-rb/engine/parser/condition.rb +++ b/lib/splitclient-rb/engine/parser/condition.rb @@ -222,6 +222,14 @@ def matcher_between_semver(params) ) end + def matcher_in_list_semver(params) + InListSemverMatcher.new( + params[:matcher][:keySelector][:attribute], + params[:matcher][:whitelistMatcherData][:whitelist], + @config.split_logger, @config.split_validator + ) + end + # # @return [object] the negate value for this condition def negate diff --git a/spec/engine/matchers/matches_in_list_semver_matcher_spec.rb b/spec/engine/matchers/matches_in_list_semver_matcher_spec.rb new file mode 100644 index 00000000..dad00078 --- /dev/null +++ b/spec/engine/matchers/matches_in_list_semver_matcher_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SplitIoClient::InListSemverMatcher do + let(:raw) { { + 'negate': false, + 'matcherType': 'INLIST_SEMVER', + 'whitelistMatcherData': {"whitelist": ["2.1.8", "2.1.11"]} +} } + let(:config) { SplitIoClient::SplitConfig.new } + + it 'initilized params' do + matcher = described_class.new("version", raw[:whitelistMatcherData][:whitelist], config.split_logger, config.split_validator) + expect(matcher.attribute).to eq("version") + semver_list = matcher.instance_variable_get(:@semver_list) + expect(semver_list[0].instance_variable_get(:@old_version)).to eq("2.1.8") + expect(semver_list[1].instance_variable_get(:@old_version)).to eq("2.1.11") + end + + it 'matches' do + matcher = described_class.new("version", raw[:whitelistMatcherData][:whitelist], config.split_logger, config.split_validator) + expect(matcher.match?(:attributes=>{"version": "2.1.8+rc"})).to eq(true) + expect(matcher.match?(:attributes=>{"version": "2.1.11"})).to eq(true) + end + + it 'does not match' do + matcher = described_class.new("version", raw[:whitelistMatcherData][:whitelist], config.split_logger, config.split_validator) + expect(matcher.match?(:attributes=>{"version": "2.1.7"})).to eq(false) + expect(matcher.match?(:attributes=>{"version": "2.1.11-rc12"})).to eq(false) + expect(matcher.match?(:attributes=>{"version": "2.1.8-rc1"})).to eq(false) + end + + it 'invalid attribute' do + matcher = described_class.new("version", raw[:whitelistMatcherData][:whitelist], config.split_logger, config.split_validator) + expect(matcher.match?(:attributes=>{"version": 2.1})).to eq(false) + expect(matcher.match?(:attributes=>{"version": nil})).to eq(false) + end + +end diff --git a/spec/engine/matchers/semver_matchers_integration_spec.rb b/spec/engine/matchers/semver_matchers_integration_spec.rb new file mode 100644 index 00000000..bedb07aa --- /dev/null +++ b/spec/engine/matchers/semver_matchers_integration_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Semver matchers integration' do + subject do + SplitIoClient::SplitFactory.new('test_api_key', { + logger: Logger.new(log), + streaming_enabled: false, + impressions_refresh_rate: 9999, + impressions_mode: :none, + features_refresh_rate: 9999, + telemetry_refresh_rate: 99999}).client + end + + let(:log) { StringIO.new } + + let(:semver_between_matcher_splits) do + File.read(File.expand_path(File.join(File.dirname(__FILE__), + '../../test_data/splits/semver_matchers/semver_between.json'))) + end + + let(:semver_equalto_matcher_splits) do + File.read(File.expand_path(File.join(File.dirname(__FILE__), + '../../test_data/splits/semver_matchers/semver_equalto.json'))) + end + + let(:semver_greater_or_equalto_matcher_splits) do + File.read(File.expand_path(File.join(File.dirname(__FILE__), + '../../test_data/splits/semver_matchers/semver_greater_or_equalto.json'))) + end + + let(:semver_less_or_equalto_matcher_splits) do + File.read(File.expand_path(File.join(File.dirname(__FILE__), + '../../test_data/splits/semver_matchers/semver_less_or_equalto.json'))) + end + + let(:semver_inlist_matcher_splits) do + File.read(File.expand_path(File.join(File.dirname(__FILE__), + '../../test_data/splits/semver_matchers/semver_inlist.json'))) + end + + let(:user) { 'fake_user_id_1' } + + before do + stub_request(:any, /https:\/\/telemetry.*/).to_return(status: 200, body: 'ok') + stub_request(:any, /https:\/\/events.*/).to_return(status: 200, body: "", headers: {}) + end + + context 'equal to matcher' do + before do + stub_request(:get, /https:\/\/sdk\.split\.io\/api\/splitChanges\?since/) + .to_return(status: 200, body: semver_equalto_matcher_splits) + stub_request(:get, 'https://sdk.split.io/api/splitChanges?since=-1') + .to_return(status: 200, body: semver_equalto_matcher_splits) + sleep 1 + subject.block_until_ready + end + + it 'validates the treatment is ON for correct attribute value' do + expect(subject.get_treatment(user, 'semver_equalto', {:version => "1.22.9"})).to eq 'on' + end + + it 'validates the treatment is the default treatment for incorrect attributes hash and nil' do + expect(subject.get_treatment(user, 'semver_equalto')).to eq 'off' + expect(subject.get_treatment(user, 'semver_equalto', {:version => "1.22.10"})).to eq 'off' + subject.destroy() + end + end + + context 'greater than or equal to matcher' do + before do + stub_request(:get, /https:\/\/sdk\.split\.io\/api\/splitChanges\?since/) + .to_return(status: 200, body: semver_greater_or_equalto_matcher_splits) + stub_request(:get, 'https://sdk.split.io/api/splitChanges?since=-1') + .to_return(status: 200, body: semver_greater_or_equalto_matcher_splits) + sleep 1 + subject.block_until_ready + end + + it 'validates the treatment is ON for correct attribute value' do + expect(subject.get_treatment(user, 'semver_greater_or_equalto', {:version => "1.22.9"})).to eq 'on' + expect(subject.get_treatment(user, 'semver_greater_or_equalto', {:version => "1.22.10"})).to eq 'on' + end + + it 'validates the treatment is the default treatment for incorrect attributes hash and nil' do + expect(subject.get_treatment(user, 'semver_greater_or_equalto')).to eq 'off' + expect(subject.get_treatment(user, 'semver_greater_or_equalto', {:version => "1.22.8"})).to eq 'off' + subject.destroy() + end + end + + context 'less than or equal to matcher' do + before do + stub_request(:get, /https:\/\/sdk\.split\.io\/api\/splitChanges\?since/) + .to_return(status: 200, body: semver_less_or_equalto_matcher_splits) + stub_request(:get, 'https://sdk.split.io/api/splitChanges?since=-1') + .to_return(status: 200, body: semver_less_or_equalto_matcher_splits) + sleep 1 + subject.block_until_ready + end + + it 'validates the treatment is ON for correct attribute value' do + expect(subject.get_treatment(user, 'semver_less_or_equalto', {:version => "1.22.9"})).to eq 'on' + expect(subject.get_treatment(user, 'semver_less_or_equalto', {:version => "1.22.8"})).to eq 'on' + end + + it 'validates the treatment is the default treatment for incorrect attributes hash and nil' do + expect(subject.get_treatment(user, 'semver_less_or_equalto')).to eq 'off' + expect(subject.get_treatment(user, 'semver_less_or_equalto', {:version => "1.22.10"})).to eq 'off' + subject.destroy() + end + end + + context 'in list matcher' do + before do + stub_request(:get, /https:\/\/sdk\.split\.io\/api\/splitChanges\?since/) + .to_return(status: 200, body: semver_inlist_matcher_splits) + stub_request(:get, 'https://sdk.split.io/api/splitChanges?since=-1') + .to_return(status: 200, body: semver_inlist_matcher_splits) + sleep 1 + subject.block_until_ready + end + + it 'validates the treatment is ON for correct attribute value' do + expect(subject.get_treatment(user, 'semver_inlist', {:version => "1.22.9"})).to eq 'on' + expect(subject.get_treatment(user, 'semver_inlist', {:version => "2.1.0"})).to eq 'on' + end + + it 'validates the treatment is the default treatment for incorrect attributes hash and nil' do + expect(subject.get_treatment(user, 'semver_inlist')).to eq 'off' + expect(subject.get_treatment(user, 'semver_inlist', {:version => "1.22.10"})).to eq 'off' + subject.destroy() + end + end + + context 'between matcher' do + before do + stub_request(:get, /https:\/\/sdk\.split\.io\/api\/splitChanges\?since/) + .to_return(status: 200, body: semver_between_matcher_splits) + stub_request(:get, 'https://sdk.split.io/api/splitChanges?since=-1') + .to_return(status: 200, body: semver_between_matcher_splits) + sleep 1 + subject.block_until_ready + end + + it 'validates the treatment is ON for correct attribute value' do + expect(subject.get_treatment(user, 'semver_between', {:version => "1.22.9"})).to eq 'on' + expect(subject.get_treatment(user, 'semver_between', {:version => "2.0.10"})).to eq 'on' + end + + it 'validates the treatment is the default treatment for incorrect attributes hash and nil' do + expect(subject.get_treatment(user, 'semver_between')).to eq 'off' + expect(subject.get_treatment(user, 'semver_between', {:version => "1.22.9-rc1"})).to eq 'off' + expect(subject.get_treatment(user, 'semver_between', {:version => "2.1.1"})).to eq 'off' + subject.destroy() + end + end +end diff --git a/spec/test_data/splits/semver_matchers/semver_between.json b/spec/test_data/splits/semver_matchers/semver_between.json new file mode 100644 index 00000000..44edc2b6 --- /dev/null +++ b/spec/test_data/splits/semver_matchers/semver_between.json @@ -0,0 +1,86 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "semver_between", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": + [ + {"keySelector": {"trafficType": "user", "attribute": "version"}, + "matcherType": "BETWEEN_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": null, + "betweenStringMatcherData": {"start": "1.22.9", "end": "2.1.0"}} + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "between semver" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1675259356568 +} diff --git a/spec/test_data/splits/semver_matchers/semver_equalto.json b/spec/test_data/splits/semver_matchers/semver_equalto.json new file mode 100644 index 00000000..c3daa9ea --- /dev/null +++ b/spec/test_data/splits/semver_matchers/semver_equalto.json @@ -0,0 +1,85 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "semver_equalto", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": + [ + {"keySelector": {"trafficType": "user", "attribute": "version"}, + "matcherType": "EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": "1.22.9"} + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "equal to semver" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1675259356568 +} diff --git a/spec/test_data/splits/semver_matchers/semver_greater_or_equalto.json b/spec/test_data/splits/semver_matchers/semver_greater_or_equalto.json new file mode 100644 index 00000000..40f0f036 --- /dev/null +++ b/spec/test_data/splits/semver_matchers/semver_greater_or_equalto.json @@ -0,0 +1,85 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "semver_greater_or_equalto", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": + [ + {"keySelector": {"trafficType": "user", "attribute": "version"}, + "matcherType": "GREATER_THAN_OR_EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": "1.22.9"} + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "greater than or equal to semver" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1675259356568 +} diff --git a/spec/test_data/splits/semver_matchers/semver_inlist.json b/spec/test_data/splits/semver_matchers/semver_inlist.json new file mode 100644 index 00000000..9f1e6246 --- /dev/null +++ b/spec/test_data/splits/semver_matchers/semver_inlist.json @@ -0,0 +1,86 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "semver_inlist", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": + [ + {"keySelector": {"trafficType": "user", "attribute": "version"}, + "matcherType": "IN_LIST_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": {"whitelist": ["1.22.9", "2.1.0"]}, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": null, + "betweenStringMatcherData": null} + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "between semver" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1675259356568 +} diff --git a/spec/test_data/splits/semver_matchers/semver_less_or_equalto.json b/spec/test_data/splits/semver_matchers/semver_less_or_equalto.json new file mode 100644 index 00000000..9a46807f --- /dev/null +++ b/spec/test_data/splits/semver_matchers/semver_less_or_equalto.json @@ -0,0 +1,85 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "semver_less_or_equalto", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": + [ + {"keySelector": {"trafficType": "user", "attribute": "version"}, + "matcherType": "LESS_THAN_OR_EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": "1.22.9"} + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "less than or equal to semver" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1675259356568 +}