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
1 change: 1 addition & 0 deletions lib/splitclient-rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
require 'splitclient-rb/cache/repositories/splits_repository'
require 'splitclient-rb/cache/repositories/events_repository'
require 'splitclient-rb/cache/repositories/impressions_repository'
require 'splitclient-rb/cache/repositories/rule_based_segments_repository'
require 'splitclient-rb/cache/repositories/events/memory_repository'
require 'splitclient-rb/cache/repositories/events/redis_repository'
require 'splitclient-rb/cache/repositories/flag_sets/memory_repository'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
require 'concurrent'

module SplitIoClient
module Cache
module Repositories
class RuleBasedSegmentsRepository < Repository
attr_reader :adapter
DEFAULT_CONDITIONS_TEMPLATE = [{
conditionType: "ROLLOUT",
matcherGroup: {
combiner: "AND",
matchers: [
{
keySelector: nil,
matcherType: "ALL_KEYS",
negate: false,
userDefinedSegmentMatcherData: nil,
whitelistMatcherData: nil,
unaryNumericMatcherData: nil,
betweenMatcherData: nil,
dependencyMatcherData: nil,
booleanMatcherData: nil,
stringMatcherData: nil
}]
}
}]

def initialize(config)
super(config)
@adapter = case @config.cache_adapter.class.to_s
when 'SplitIoClient::Cache::Adapters::RedisAdapter'
SplitIoClient::Cache::Adapters::CacheAdapter.new(@config)
else
@config.cache_adapter
end
unless @config.mode.equal?(:consumer)
@adapter.set_string(namespace_key('.rbsegments.till'), '-1')
@adapter.initialize_map(namespace_key('.segments.registered'))
end
end

def update(to_add, to_delete, new_change_number)
to_add.each{ |rule_based_segment| add_rule_based_segment(rule_based_segment) }
to_delete.each{ |rule_based_segment| remove_rule_based_segment(rule_based_segment) }
set_change_number(new_change_number)
end

def get_rule_based_segment(name)
rule_based_segment = @adapter.string(namespace_key(".rbsegment.#{name}"))

JSON.parse(rule_based_segment, symbolize_names: true) if rule_based_segment
end

def rule_based_segment_names
@adapter.find_strings_by_prefix(namespace_key('.rbsegment.'))
.map { |rule_based_segment_names| rule_based_segment_names.gsub(namespace_key('.rbsegment.'), '') }
end

def set_change_number(since)
@adapter.set_string(namespace_key('.rbsegments.till'), since)
end

def get_change_number
@adapter.string(namespace_key('.rbsegments.till'))
end

def set_segment_names(names)
return if names.nil? || names.empty?

names.each do |name|
@adapter.add_to_set(namespace_key('.segments.registered'), name)
end
end

def exists?(name)
@adapter.exists?(namespace_key(".rbsegment.#{name}"))
end

def clear
@adapter.clear(namespace_key)
end

def contains?(segment_names)
return set(segment_names).subset?(rule_based_segment_names)
end

private

def add_rule_based_segment(rule_based_segment)
return unless rule_based_segment[:name]
existing_rule_based_segment = get_rule_based_segment(rule_based_segment[:name])

if check_undefined_matcher(rule_based_segment)
@config.logger.warn("Rule based segment #{rule_based_segment[:name]} has undefined matcher, setting conditions to default template.")
rule_based_segment[:conditions] = RuleBasedSegmentsRepository::DEFAULT_CONDITIONS_TEMPLATE
end

@adapter.set_string(namespace_key(".rbsegment.#{rule_based_segment[:name]}"), rule_based_segment.to_json)
end

def check_undefined_matcher(rule_based_segment)
for condition in rule_based_segment[:conditions]
for matcher in condition[:matcherGroup][:matchers]
if !SplitIoClient::Condition.instance_methods(false).map(&:to_s).include?("matcher_#{matcher[:matcherType].downcase}")
@config.logger.error("Detected undefined matcher #{matcher[:matcherType].downcase} in feature flag #{rule_based_segment[:name]}")
return true
end
end
end
return false
end

def remove_rule_based_segment(rule_based_segment)
@adapter.delete(namespace_key(".rbsegment.#{rule_based_segment[:name]}"))
end
end
end
end
end
109 changes: 109 additions & 0 deletions spec/cache/repositories/rule_based_segments_repository_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

require 'spec_helper'
require 'set'

describe SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository do
RSpec.shared_examples 'RuleBasedSegments Repository' do |cache_adapter|
let(:config) { SplitIoClient::SplitConfig.new(cache_adapter: cache_adapter) }
let(:repository) { described_class.new(config) }

before :all do
redis = Redis.new
redis.flushall
end

before do
# in memory setup
repository.update([{name: 'foo', trafficTypeName: 'tt_name_1', conditions: []},
{name: 'bar', trafficTypeName: 'tt_name_2', conditions: []},
{name: 'baz', trafficTypeName: 'tt_name_1', conditions: []}], [], -1)
end

after do
repository.update([], [{name: 'foo', trafficTypeName: 'tt_name_1', conditions: []},
{name: 'bar', trafficTypeName: 'tt_name_2', conditions: []},
{name: 'bar', trafficTypeName: 'tt_name_2', conditions: []},
{name: 'qux', trafficTypeName: 'tt_name_3', conditions: []},
{name: 'quux', trafficTypeName: 'tt_name_4', conditions: []},
{name: 'corge', trafficTypeName: 'tt_name_5', conditions: []},
{name: 'corge', trafficTypeName: 'tt_name_6', conditions: []}], -1)
end

it 'returns rule_based_segments names' do
expect(Set.new(repository.rule_based_segment_names)).to eq(Set.new(%w[foo bar baz]))
end

it 'returns rule_based_segment data' do
expect(repository.get_rule_based_segment('foo')).to eq(
{ conditions: [] , name: 'foo', trafficTypeName: 'tt_name_1' },
)
end

it 'remove undefined matcher with template condition' do
rule_based_segment = { name: 'corge', trafficTypeName: 'tt_name_5', conditions: [
{
contitionType: 'WHITELIST',
label: 'some_label',
matcherGroup: {
matchers: [
{
matcherType: 'UNDEFINED',
whitelistMatcherData: {
whitelist: ['k1', 'k2', 'k3']
},
negate: false,
}
],
combiner: 'AND'
}
}]
}

repository.update([rule_based_segment], [], -1)
expect(repository.get_rule_based_segment('corge')[:conditions]).to eq SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository::DEFAULT_CONDITIONS_TEMPLATE

# test with multiple conditions
rule_based_segment2 = {
name: 'corge2',
trafficTypeName: 'tt_name_5',
conditions: [
{
contitionType: 'WHITELIST',
label: 'some_label',
matcherGroup: {
matchers: [
{
matcherType: 'UNDEFINED',
whitelistMatcherData: {
whitelist: ['k1', 'k2', 'k3']
},
negate: false,
}
],
combiner: 'AND'
}
},
{
contitionType: 'WHITELIST',
label: 'some_other_label',
matcherGroup: {
matchers: [{matcherType: 'ALL_KEYS', negate: false}],
combiner: 'AND'
}
}]
}

repository.update([rule_based_segment2], [], -1)
expect(repository.get_rule_based_segment('corge2')[:conditions]).to eq SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository::DEFAULT_CONDITIONS_TEMPLATE
end
end

describe 'with Memory Adapter' do
it_behaves_like 'RuleBasedSegments Repository', :memory
end

describe 'with Redis Adapter' do
it_behaves_like 'RuleBasedSegments Repository', :redis
end
end