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



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
              }]
          }
        }]
        TILL_PREFIX = '.rbsegments.till'
        RB_SEGMENTS_PREFIX = '.rbsegment.'
        REGISTERED_PREFIX = '.segments.registered'

        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(TILL_PREFIX), '-1')
            @adapter.initialize_map(namespace_key(REGISTERED_PREFIX))
          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("#{RB_SEGMENTS_PREFIX}#{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(RB_SEGMENTS_PREFIX))
            .map { |rule_based_segment_names| rule_based_segment_names.gsub(namespace_key(RB_SEGMENTS_PREFIX), '') }
        end

        def set_change_number(since)
          @adapter.set_string(namespace_key(TILL_PREFIX), since)
        end

        def get_change_number
          @adapter.string(namespace_key(TILL_PREFIX))
        end

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

          names.each do |name|
            @adapter.add_to_set(namespace_key(REGISTERED_PREFIX), name)
          end
        end

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

        def clear
          @adapter.clear(namespace_key)
        end

        def contains?(segment_names)
          return false if rule_based_segment_names.empty?

          return segment_names.to_set.subset?(rule_based_segment_names.to_set)
        end

        private

        def add_rule_based_segment(rule_based_segment)
          return unless 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("#{RB_SEGMENTS_PREFIX}#{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("#{RB_SEGMENTS_PREFIX}#{rule_based_segment[:name]}"))
        end
      end
    end
  end
end