prefab-cloud-ruby 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/.envrc.sample +3 -0
 - data/.github/workflows/ruby.yml +4 -0
 - data/.gitmodules +3 -0
 - data/Gemfile +12 -12
 - data/Gemfile.lock +16 -14
 - data/README.md +1 -1
 - data/Rakefile +13 -14
 - data/VERSION +1 -1
 - data/lib/prefab/auth_interceptor.rb +2 -1
 - data/lib/prefab/cancellable_interceptor.rb +8 -7
 - data/lib/prefab/client.rb +33 -24
 - data/lib/prefab/config_client.rb +55 -66
 - data/lib/prefab/config_loader.rb +7 -114
 - data/lib/prefab/config_resolver.rb +27 -57
 - data/lib/prefab/config_value_unwrapper.rb +23 -0
 - data/lib/prefab/criteria_evaluator.rb +96 -0
 - data/lib/prefab/errors/invalid_api_key_error.rb +1 -1
 - data/lib/prefab/feature_flag_client.rb +13 -145
 - data/lib/prefab/internal_logger.rb +6 -5
 - data/lib/prefab/local_config_parser.rb +110 -0
 - data/lib/prefab/logger_client.rb +26 -31
 - data/lib/prefab/murmer3.rb +3 -4
 - data/lib/prefab/noop_cache.rb +5 -7
 - data/lib/prefab/noop_stats.rb +2 -3
 - data/lib/prefab/options.rb +11 -9
 - data/lib/prefab/ratelimit_client.rb +11 -13
 - data/lib/prefab/sse_logger.rb +3 -2
 - data/lib/prefab/weighted_value_resolver.rb +42 -0
 - data/lib/prefab/yaml_config_parser.rb +32 -0
 - data/lib/prefab-cloud-ruby.rb +7 -2
 - data/lib/prefab_pb.rb +49 -43
 - data/lib/prefab_services_pb.rb +0 -1
 - data/prefab-cloud-ruby.gemspec +28 -19
 - data/test/.prefab.unit_tests.config.yaml +3 -2
 - data/test/integration_test.rb +98 -0
 - data/test/integration_test_helpers.rb +37 -0
 - data/test/test_client.rb +32 -31
 - data/test/test_config_client.rb +21 -20
 - data/test/test_config_loader.rb +48 -37
 - data/test/test_config_resolver.rb +312 -135
 - data/test/test_config_value_unwrapper.rb +83 -0
 - data/test/test_criteria_evaluator.rb +533 -0
 - data/test/test_feature_flag_client.rb +35 -347
 - data/test/test_helper.rb +18 -14
 - data/test/test_integration.rb +33 -0
 - data/test/test_local_config_parser.rb +78 -0
 - data/test/test_logger.rb +47 -46
 - data/test/test_weighted_value_resolver.rb +65 -0
 - metadata +24 -27
 - data/lib/prefab/config_helper.rb +0 -31
 - data/run_test_harness_server.sh +0 -8
 - data/test/harness_server.rb +0 -64
 
    
        data/lib/prefab/config_loader.rb
    CHANGED
    
    | 
         @@ -1,6 +1,5 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            require 'yaml'
         
     | 
| 
       4 
3 
     | 
    
         
             
            module Prefab
         
     | 
| 
       5 
4 
     | 
    
         
             
              class ConfigLoader
         
     | 
| 
       6 
5 
     | 
    
         
             
                attr_reader :highwater_mark
         
     | 
| 
         @@ -19,21 +18,19 @@ module Prefab 
     | 
|
| 
       19 
18 
     | 
    
         
             
                  @api_config.each_key do |k|
         
     | 
| 
       20 
19 
     | 
    
         
             
                    rtn[k] = @api_config[k]
         
     | 
| 
       21 
20 
     | 
    
         
             
                  end
         
     | 
| 
       22 
     | 
    
         
            -
                  rtn 
     | 
| 
       23 
     | 
    
         
            -
                  rtn
         
     | 
| 
      
 21 
     | 
    
         
            +
                  rtn.merge(@local_overrides)
         
     | 
| 
       24 
22 
     | 
    
         
             
                end
         
     | 
| 
       25 
23 
     | 
    
         | 
| 
       26 
24 
     | 
    
         
             
                def set(config, source)
         
     | 
| 
       27 
25 
     | 
    
         
             
                  # don't overwrite newer values
         
     | 
| 
       28 
     | 
    
         
            -
                  if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
         
     | 
| 
       29 
     | 
    
         
            -
                    return
         
     | 
| 
       30 
     | 
    
         
            -
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
                  return if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
         
     | 
| 
       31 
27 
     | 
    
         | 
| 
       32 
28 
     | 
    
         
             
                  if config.rows.empty?
         
     | 
| 
       33 
29 
     | 
    
         
             
                    @api_config.delete(config.key)
         
     | 
| 
       34 
30 
     | 
    
         
             
                  else
         
     | 
| 
       35 
31 
     | 
    
         
             
                    if @api_config[config.key]
         
     | 
| 
       36 
     | 
    
         
            -
                      @base_client.log_internal Logger::DEBUG, 
     | 
| 
      
 32 
     | 
    
         
            +
                      @base_client.log_internal Logger::DEBUG,
         
     | 
| 
      
 33 
     | 
    
         
            +
                                                "Replace #{config.key} with value from #{source} #{@api_config[config.key][:config].id} -> #{config.id}"
         
     | 
| 
       37 
34 
     | 
    
         
             
                    end
         
     | 
| 
       38 
35 
     | 
    
         
             
                    @api_config[config.key] = { source: source, config: config }
         
     | 
| 
       39 
36 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -56,7 +53,7 @@ module Prefab 
     | 
|
| 
       56 
53 
     | 
    
         | 
| 
       57 
54 
     | 
    
         
             
                def load_classpath_config
         
     | 
| 
       58 
55 
     | 
    
         
             
                  classpath_dir = @prefab_options.prefab_config_classpath_dir
         
     | 
| 
       59 
     | 
    
         
            -
                  rtn = load_glob(File.join(classpath_dir,  
     | 
| 
      
 56 
     | 
    
         
            +
                  rtn = load_glob(File.join(classpath_dir, '.prefab.default.config.yaml'))
         
     | 
| 
       60 
57 
     | 
    
         
             
                  @prefab_options.prefab_envs.each do |env|
         
     | 
| 
       61 
58 
     | 
    
         
             
                    rtn = rtn.merge load_glob(File.join(classpath_dir, ".prefab.#{env}.config.yaml"))
         
     | 
| 
       62 
59 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -65,7 +62,7 @@ module Prefab 
     | 
|
| 
       65 
62 
     | 
    
         | 
| 
       66 
63 
     | 
    
         
             
                def load_local_overrides
         
     | 
| 
       67 
64 
     | 
    
         
             
                  override_dir = @prefab_options.prefab_config_override_dir
         
     | 
| 
       68 
     | 
    
         
            -
                  rtn = load_glob(File.join(override_dir,  
     | 
| 
      
 65 
     | 
    
         
            +
                  rtn = load_glob(File.join(override_dir, '.prefab.default.config.yaml'))
         
     | 
| 
       69 
66 
     | 
    
         
             
                  @prefab_options.prefab_envs.each do |env|
         
     | 
| 
       70 
67 
     | 
    
         
             
                    rtn = rtn.merge load_glob(File.join(override_dir, ".prefab.#{env}.config.yaml"))
         
     | 
| 
       71 
68 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -75,113 +72,9 @@ module Prefab 
     | 
|
| 
       75 
72 
     | 
    
         
             
                def load_glob(glob)
         
     | 
| 
       76 
73 
     | 
    
         
             
                  rtn = {}
         
     | 
| 
       77 
74 
     | 
    
         
             
                  Dir.glob(glob).each do |file|
         
     | 
| 
       78 
     | 
    
         
            -
                    @base_client. 
     | 
| 
       79 
     | 
    
         
            -
                    yaml = load(file)
         
     | 
| 
       80 
     | 
    
         
            -
                    yaml.each do |k, v|
         
     | 
| 
       81 
     | 
    
         
            -
                      load_kv(k, v, rtn, file)
         
     | 
| 
       82 
     | 
    
         
            -
                    end
         
     | 
| 
      
 75 
     | 
    
         
            +
                    Prefab::YAMLConfigParser.new(file, @base_client).merge(rtn)
         
     | 
| 
       83 
76 
     | 
    
         
             
                  end
         
     | 
| 
       84 
77 
     | 
    
         
             
                  rtn
         
     | 
| 
       85 
78 
     | 
    
         
             
                end
         
     | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
       87 
     | 
    
         
            -
                def load_kv(k, v, rtn, file)
         
     | 
| 
       88 
     | 
    
         
            -
                  if v.class == Hash
         
     | 
| 
       89 
     | 
    
         
            -
                    if v['feature_flag']
         
     | 
| 
       90 
     | 
    
         
            -
                      rtn[k] = feature_flag_config(file, k, v)
         
     | 
| 
       91 
     | 
    
         
            -
                    else
         
     | 
| 
       92 
     | 
    
         
            -
                      v.each do |nest_k, nest_v|
         
     | 
| 
       93 
     | 
    
         
            -
                        nested_key = "#{k}.#{nest_k}"
         
     | 
| 
       94 
     | 
    
         
            -
                        nested_key = k if nest_k == "_"
         
     | 
| 
       95 
     | 
    
         
            -
                        load_kv(nested_key, nest_v, rtn, file)
         
     | 
| 
       96 
     | 
    
         
            -
                      end
         
     | 
| 
       97 
     | 
    
         
            -
                    end
         
     | 
| 
       98 
     | 
    
         
            -
                  else
         
     | 
| 
       99 
     | 
    
         
            -
                    rtn[k] = {
         
     | 
| 
       100 
     | 
    
         
            -
                      source: file,
         
     | 
| 
       101 
     | 
    
         
            -
                      match: "default",
         
     | 
| 
       102 
     | 
    
         
            -
                      config: Prefab::Config.new(
         
     | 
| 
       103 
     | 
    
         
            -
                        key: k,
         
     | 
| 
       104 
     | 
    
         
            -
                        rows: [
         
     | 
| 
       105 
     | 
    
         
            -
                          Prefab::ConfigRow.new(value: Prefab::ConfigValue.new(value_from(k, v)))
         
     | 
| 
       106 
     | 
    
         
            -
                        ]
         
     | 
| 
       107 
     | 
    
         
            -
                      )
         
     | 
| 
       108 
     | 
    
         
            -
                    }
         
     | 
| 
       109 
     | 
    
         
            -
                  end
         
     | 
| 
       110 
     | 
    
         
            -
                end
         
     | 
| 
       111 
     | 
    
         
            -
             
     | 
| 
       112 
     | 
    
         
            -
                def load(filename)
         
     | 
| 
       113 
     | 
    
         
            -
                  if File.exist? filename
         
     | 
| 
       114 
     | 
    
         
            -
                    @base_client.log_internal Logger::INFO, "Load #{filename}"
         
     | 
| 
       115 
     | 
    
         
            -
                    YAML.load_file(filename)
         
     | 
| 
       116 
     | 
    
         
            -
                  else
         
     | 
| 
       117 
     | 
    
         
            -
                    @base_client.log_internal Logger::INFO, "No file #{filename}"
         
     | 
| 
       118 
     | 
    
         
            -
                    {}
         
     | 
| 
       119 
     | 
    
         
            -
                  end
         
     | 
| 
       120 
     | 
    
         
            -
                end
         
     | 
| 
       121 
     | 
    
         
            -
             
     | 
| 
       122 
     | 
    
         
            -
                def value_from(key, raw)
         
     | 
| 
       123 
     | 
    
         
            -
                  case raw
         
     | 
| 
       124 
     | 
    
         
            -
                  when String
         
     | 
| 
       125 
     | 
    
         
            -
                    if key.start_with? Prefab::LoggerClient::BASE_KEY
         
     | 
| 
       126 
     | 
    
         
            -
                      prefab_log_level_resolve = Prefab::LogLevel.resolve(raw.upcase.to_sym) || Prefab::LogLevel::NOT_SET_LOG_LEVEL
         
     | 
| 
       127 
     | 
    
         
            -
                      { log_level: prefab_log_level_resolve }
         
     | 
| 
       128 
     | 
    
         
            -
                    else
         
     | 
| 
       129 
     | 
    
         
            -
                      { string: raw }
         
     | 
| 
       130 
     | 
    
         
            -
                    end
         
     | 
| 
       131 
     | 
    
         
            -
                  when Integer
         
     | 
| 
       132 
     | 
    
         
            -
                    { int: raw }
         
     | 
| 
       133 
     | 
    
         
            -
                  when TrueClass, FalseClass
         
     | 
| 
       134 
     | 
    
         
            -
                    { bool: raw }
         
     | 
| 
       135 
     | 
    
         
            -
                  when Float
         
     | 
| 
       136 
     | 
    
         
            -
                    { double: raw }
         
     | 
| 
       137 
     | 
    
         
            -
                  end
         
     | 
| 
       138 
     | 
    
         
            -
                end
         
     | 
| 
       139 
     | 
    
         
            -
             
     | 
| 
       140 
     | 
    
         
            -
                def feature_flag_config(file, key, value)
         
     | 
| 
       141 
     | 
    
         
            -
                  criteria = Prefab::Criteria.new(operator: 'ALWAYS_TRUE')
         
     | 
| 
       142 
     | 
    
         
            -
             
     | 
| 
       143 
     | 
    
         
            -
                  if value['criteria']
         
     | 
| 
       144 
     | 
    
         
            -
                    criteria = Prefab::Criteria.new(criteria_values(value['criteria']))
         
     | 
| 
       145 
     | 
    
         
            -
                  end
         
     | 
| 
       146 
     | 
    
         
            -
             
     | 
| 
       147 
     | 
    
         
            -
                  row = Prefab::ConfigRow.new(
         
     | 
| 
       148 
     | 
    
         
            -
                    value: Prefab::ConfigValue.new(
         
     | 
| 
       149 
     | 
    
         
            -
                      feature_flag: Prefab::FeatureFlag.new(
         
     | 
| 
       150 
     | 
    
         
            -
                        active: true,
         
     | 
| 
       151 
     | 
    
         
            -
                        inactive_variant_idx: -1, # not supported
         
     | 
| 
       152 
     | 
    
         
            -
                        rules: [
         
     | 
| 
       153 
     | 
    
         
            -
                          Prefab::Rule.new(
         
     | 
| 
       154 
     | 
    
         
            -
                            variant_weights: [
         
     | 
| 
       155 
     | 
    
         
            -
                              Prefab::VariantWeight.new(variant_idx: 0, weight: 1000)
         
     | 
| 
       156 
     | 
    
         
            -
                            ],
         
     | 
| 
       157 
     | 
    
         
            -
                            criteria: criteria
         
     | 
| 
       158 
     | 
    
         
            -
                          )
         
     | 
| 
       159 
     | 
    
         
            -
                        ]
         
     | 
| 
       160 
     | 
    
         
            -
                      )
         
     | 
| 
       161 
     | 
    
         
            -
                    )
         
     | 
| 
       162 
     | 
    
         
            -
                  )
         
     | 
| 
       163 
     | 
    
         
            -
             
     | 
| 
       164 
     | 
    
         
            -
                  unless value.has_key?('value')
         
     | 
| 
       165 
     | 
    
         
            -
                    raise Prefab::Error, "Feature flag config `#{key}` #{file} must have a `value`"
         
     | 
| 
       166 
     | 
    
         
            -
                  end
         
     | 
| 
       167 
     | 
    
         
            -
             
     | 
| 
       168 
     | 
    
         
            -
                  {
         
     | 
| 
       169 
     | 
    
         
            -
                    source: file,
         
     | 
| 
       170 
     | 
    
         
            -
                    match: key,
         
     | 
| 
       171 
     | 
    
         
            -
                    config: Prefab::Config.new(
         
     | 
| 
       172 
     | 
    
         
            -
                      key: key,
         
     | 
| 
       173 
     | 
    
         
            -
                      variants: [Prefab::FeatureFlagVariant.new(value_from(key, value['value']))],
         
     | 
| 
       174 
     | 
    
         
            -
                      rows: [row]
         
     | 
| 
       175 
     | 
    
         
            -
                    )
         
     | 
| 
       176 
     | 
    
         
            -
                  }
         
     | 
| 
       177 
     | 
    
         
            -
                end
         
     | 
| 
       178 
     | 
    
         
            -
             
     | 
| 
       179 
     | 
    
         
            -
                def criteria_values(criteria_hash)
         
     | 
| 
       180 
     | 
    
         
            -
                  if RUBY_VERSION < '2.7'
         
     | 
| 
       181 
     | 
    
         
            -
                    criteria_hash.transform_keys(&:to_sym)
         
     | 
| 
       182 
     | 
    
         
            -
                  else
         
     | 
| 
       183 
     | 
    
         
            -
                    criteria_hash
         
     | 
| 
       184 
     | 
    
         
            -
                  end
         
     | 
| 
       185 
     | 
    
         
            -
                end
         
     | 
| 
       186 
79 
     | 
    
         
             
              end
         
     | 
| 
       187 
80 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,17 +1,16 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
       2 
3 
     | 
    
         
             
            module Prefab
         
     | 
| 
       3 
4 
     | 
    
         
             
              class ConfigResolver
         
     | 
| 
       4 
     | 
    
         
            -
                include Prefab::ConfigHelper
         
     | 
| 
       5 
     | 
    
         
            -
                NAMESPACE_DELIMITER = "."
         
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
       7 
5 
     | 
    
         
             
                attr_accessor :project_env_id # this will be set by the config_client when it gets an API response
         
     | 
| 
       8 
6 
     | 
    
         | 
| 
       9 
7 
     | 
    
         
             
                def initialize(base_client, config_loader)
         
     | 
| 
       10 
8 
     | 
    
         
             
                  @lock = Concurrent::ReadWriteLock.new
         
     | 
| 
       11 
9 
     | 
    
         
             
                  @local_store = {}
         
     | 
| 
       12 
     | 
    
         
            -
                  @ 
     | 
| 
      
 10 
     | 
    
         
            +
                  @additional_properties = { Prefab::CriteriaEvaluator::NAMESPACE_KEY => base_client.options.namespace }
         
     | 
| 
       13 
11 
     | 
    
         
             
                  @config_loader = config_loader
         
     | 
| 
       14 
     | 
    
         
            -
                  @project_env_id = 0
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @project_env_id = 0 # we don't know this yet, it is set from the API results
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @base_client = base_client
         
     | 
| 
       15 
14 
     | 
    
         
             
                  make_local
         
     | 
| 
       16 
15 
     | 
    
         
             
                end
         
     | 
| 
       17 
16 
     | 
    
         | 
| 
         @@ -22,82 +21,53 @@ module Prefab 
     | 
|
| 
       22 
21 
     | 
    
         
             
                      v = @local_store[k]
         
     | 
| 
       23 
22 
     | 
    
         
             
                      elements = [k.slice(0..49).ljust(50)]
         
     | 
| 
       24 
23 
     | 
    
         
             
                      if v.nil?
         
     | 
| 
       25 
     | 
    
         
            -
                        elements <<  
     | 
| 
      
 24 
     | 
    
         
            +
                        elements << 'tombstone'
         
     | 
| 
       26 
25 
     | 
    
         
             
                      else
         
     | 
| 
       27 
     | 
    
         
            -
                         
     | 
| 
       28 
     | 
    
         
            -
                         
     | 
| 
       29 
     | 
    
         
            -
                        elements <<  
     | 
| 
      
 26 
     | 
    
         
            +
                        config = evaluate(v[:config], {})
         
     | 
| 
      
 27 
     | 
    
         
            +
                        value = Prefab::ConfigValueUnwrapper.unwrap(config, k, {})
         
     | 
| 
      
 28 
     | 
    
         
            +
                        elements << value.to_s.slice(0..34).ljust(35)
         
     | 
| 
      
 29 
     | 
    
         
            +
                        elements << value.class.to_s.slice(0..6).ljust(7)
         
     | 
| 
       30 
30 
     | 
    
         
             
                        elements << "Match: #{v[:match]}".slice(0..29).ljust(30)
         
     | 
| 
       31 
31 
     | 
    
         
             
                        elements << "Source: #{v[:source]}"
         
     | 
| 
       32 
32 
     | 
    
         
             
                      end
         
     | 
| 
       33 
     | 
    
         
            -
                      str += elements.join( 
     | 
| 
      
 33 
     | 
    
         
            +
                      str += elements.join(' | ') << "\n"
         
     | 
| 
       34 
34 
     | 
    
         
             
                    end
         
     | 
| 
       35 
35 
     | 
    
         
             
                  end
         
     | 
| 
       36 
36 
     | 
    
         
             
                  str
         
     | 
| 
       37 
37 
     | 
    
         
             
                end
         
     | 
| 
       38 
38 
     | 
    
         | 
| 
       39 
     | 
    
         
            -
                def  
     | 
| 
       40 
     | 
    
         
            -
                   
     | 
| 
       41 
     | 
    
         
            -
                  config ? value_of(config[:value]) : nil
         
     | 
| 
       42 
     | 
    
         
            -
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
                def raw(key)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  via_key = @local_store[key]
         
     | 
| 
       43 
41 
     | 
    
         | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
       45 
     | 
    
         
            -
                  config = _get(property)
         
     | 
| 
       46 
     | 
    
         
            -
                  config ? config[:config] : nil
         
     | 
| 
      
 42 
     | 
    
         
            +
                  via_key ? via_key[:config] : nil
         
     | 
| 
       47 
43 
     | 
    
         
             
                end
         
     | 
| 
       48 
44 
     | 
    
         | 
| 
       49 
     | 
    
         
            -
                def  
     | 
| 
      
 45 
     | 
    
         
            +
                def get(key, lookup_key, properties = {})
         
     | 
| 
       50 
46 
     | 
    
         
             
                  @lock.with_read_lock do
         
     | 
| 
       51 
     | 
    
         
            -
                     
     | 
| 
      
 47 
     | 
    
         
            +
                    raw_config = raw(key)
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                    return nil unless raw_config
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                    evaluate(raw(key), lookup_key, properties)
         
     | 
| 
       52 
52 
     | 
    
         
             
                  end
         
     | 
| 
       53 
53 
     | 
    
         
             
                end
         
     | 
| 
       54 
54 
     | 
    
         | 
| 
      
 55 
     | 
    
         
            +
                def evaluate(config, lookup_key, properties = {})
         
     | 
| 
      
 56 
     | 
    
         
            +
                  props = properties.merge(@additional_properties).merge(Prefab::CriteriaEvaluator::LOOKUP_KEY => lookup_key)
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                  Prefab::CriteriaEvaluator.new(config,
         
     | 
| 
      
 59 
     | 
    
         
            +
                                                project_env_id: @project_env_id, resolver: self, base_client: @base_client).evaluate(props)
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
       55 
62 
     | 
    
         
             
                def update
         
     | 
| 
       56 
63 
     | 
    
         
             
                  make_local
         
     | 
| 
       57 
64 
     | 
    
         
             
                end
         
     | 
| 
       58 
65 
     | 
    
         | 
| 
       59 
66 
     | 
    
         
             
                private
         
     | 
| 
       60 
67 
     | 
    
         | 
| 
       61 
     | 
    
         
            -
                # Should client a.b.c see key in namespace a.b? yes
         
     | 
| 
       62 
     | 
    
         
            -
                # Should client a.b.c see key in namespace a.b.c? yes
         
     | 
| 
       63 
     | 
    
         
            -
                # Should client a.b.c see key in namespace a.b.d? no
         
     | 
| 
       64 
     | 
    
         
            -
                # Should client a.b.c see key in namespace ""? yes
         
     | 
| 
       65 
     | 
    
         
            -
                #
         
     | 
| 
       66 
     | 
    
         
            -
                def starts_with_ns?(key_namespace, client_namespace)
         
     | 
| 
       67 
     | 
    
         
            -
                  zipped = key_namespace.split(NAMESPACE_DELIMITER).zip(client_namespace.split(NAMESPACE_DELIMITER))
         
     | 
| 
       68 
     | 
    
         
            -
                  mapped = zipped.map do |k, c|
         
     | 
| 
       69 
     | 
    
         
            -
                    (k.nil? || k.empty?) || k == c
         
     | 
| 
       70 
     | 
    
         
            -
                  end
         
     | 
| 
       71 
     | 
    
         
            -
                  [mapped.all?, mapped.size]
         
     | 
| 
       72 
     | 
    
         
            -
                end
         
     | 
| 
       73 
     | 
    
         
            -
             
     | 
| 
       74 
68 
     | 
    
         
             
                def make_local
         
     | 
| 
       75 
     | 
    
         
            -
                  store = {}
         
     | 
| 
       76 
     | 
    
         
            -
                  @config_loader.calc_config.each do |key, config_resolver_obj|
         
     | 
| 
       77 
     | 
    
         
            -
                    config = config_resolver_obj[:config]
         
     | 
| 
       78 
     | 
    
         
            -
                    sortable = config.rows.map do |row|
         
     | 
| 
       79 
     | 
    
         
            -
                      if row.project_env_id != 0
         
     | 
| 
       80 
     | 
    
         
            -
                        if row.project_env_id == @project_env_id
         
     | 
| 
       81 
     | 
    
         
            -
                          if !row.namespace.empty?
         
     | 
| 
       82 
     | 
    
         
            -
                            (starts_with, count) = starts_with_ns?(row.namespace, @namespace)
         
     | 
| 
       83 
     | 
    
         
            -
                            # rubocop:disable BlockNesting
         
     | 
| 
       84 
     | 
    
         
            -
                            { sortable: 2 + count, match: "nm:#{row.namespace}", value: row.value, config: config} if starts_with
         
     | 
| 
       85 
     | 
    
         
            -
                          else
         
     | 
| 
       86 
     | 
    
         
            -
                            { sortable: 1, match: "env:#{row.project_env_id}", value: row.value, config: config}
         
     | 
| 
       87 
     | 
    
         
            -
                          end
         
     | 
| 
       88 
     | 
    
         
            -
                        end
         
     | 
| 
       89 
     | 
    
         
            -
                      else
         
     | 
| 
       90 
     | 
    
         
            -
                        match = config_resolver_obj[:match] || "default"
         
     | 
| 
       91 
     | 
    
         
            -
                        { sortable: 0, match: match, value: row.value, config: config}
         
     | 
| 
       92 
     | 
    
         
            -
                      end
         
     | 
| 
       93 
     | 
    
         
            -
                    end.compact
         
     | 
| 
       94 
     | 
    
         
            -
                    to_store = sortable.sort_by { |h| h[:sortable] }.last
         
     | 
| 
       95 
     | 
    
         
            -
                    to_store[:source] = config_resolver_obj[:source]
         
     | 
| 
       96 
     | 
    
         
            -
                    store[key] = to_store
         
     | 
| 
       97 
     | 
    
         
            -
                  end
         
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
       99 
69 
     | 
    
         
             
                  @lock.with_write_lock do
         
     | 
| 
       100 
     | 
    
         
            -
                    @local_store =  
     | 
| 
      
 70 
     | 
    
         
            +
                    @local_store = @config_loader.calc_config
         
     | 
| 
       101 
71 
     | 
    
         
             
                  end
         
     | 
| 
       102 
72 
     | 
    
         
             
                end
         
     | 
| 
       103 
73 
     | 
    
         
             
              end
         
     | 
| 
         @@ -0,0 +1,23 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Prefab
         
     | 
| 
      
 4 
     | 
    
         
            +
              class ConfigValueUnwrapper
         
     | 
| 
      
 5 
     | 
    
         
            +
                def self.unwrap(config_value, config_key, properties)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  return nil unless config_value
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  case config_value.type
         
     | 
| 
      
 9 
     | 
    
         
            +
                  when :int, :string, :double, :bool, :log_level
         
     | 
| 
      
 10 
     | 
    
         
            +
                    config_value.public_send(config_value.type)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  when :string_list
         
     | 
| 
      
 12 
     | 
    
         
            +
                    config_value.string_list.values
         
     | 
| 
      
 13 
     | 
    
         
            +
                  when :weighted_values
         
     | 
| 
      
 14 
     | 
    
         
            +
                    lookup_key = properties[Prefab::CriteriaEvaluator::LOOKUP_KEY]
         
     | 
| 
      
 15 
     | 
    
         
            +
                    weights = config_value.weighted_values.weighted_values
         
     | 
| 
      
 16 
     | 
    
         
            +
                    value = Prefab::WeightedValueResolver.new(weights, config_key, lookup_key).resolve
         
     | 
| 
      
 17 
     | 
    
         
            +
                    unwrap(value.value, config_key, properties)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  else
         
     | 
| 
      
 19 
     | 
    
         
            +
                    raise "Unknown type: #{config_value.type}"
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
              end
         
     | 
| 
      
 23 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,96 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Prefab
         
     | 
| 
      
 4 
     | 
    
         
            +
              class CriteriaEvaluator
         
     | 
| 
      
 5 
     | 
    
         
            +
                LOOKUP_KEY = 'LOOKUP'
         
     | 
| 
      
 6 
     | 
    
         
            +
                NAMESPACE_KEY = 'NAMESPACE'
         
     | 
| 
      
 7 
     | 
    
         
            +
                NO_MATCHING_ROWS = [].freeze
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                def initialize(config, project_env_id:, resolver:, base_client:)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @config = config
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @project_env_id = project_env_id
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @resolver = resolver
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @base_client = base_client
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                def evaluate(properties)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # TODO: optimize this and perhaps do it elsewhere
         
     | 
| 
      
 18 
     | 
    
         
            +
                  props = properties.transform_keys(&:to_s)
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  matching_environment_row_values.each do |conditional_value|
         
     | 
| 
      
 21 
     | 
    
         
            +
                    return conditional_value.value if all_criteria_match?(conditional_value, props)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  default_row_values.each do |conditional_value|
         
     | 
| 
      
 25 
     | 
    
         
            +
                    return conditional_value.value if all_criteria_match?(conditional_value, props)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  nil
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def all_criteria_match?(conditional_value, props)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  conditional_value.criteria.all? do |criterion|
         
     | 
| 
      
 33 
     | 
    
         
            +
                    evaluate_criteron(criterion, props)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                def evaluate_criteron(criterion, properties)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  value_from_properties = properties[criterion.property_name]
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  case criterion.operator
         
     | 
| 
      
 41 
     | 
    
         
            +
                  when :LOOKUP_KEY_IN, :PROP_IS_ONE_OF
         
     | 
| 
      
 42 
     | 
    
         
            +
                    matches?(criterion, value_from_properties, properties)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  when :LOOKUP_KEY_NOT_IN, :PROP_IS_NOT_ONE_OF
         
     | 
| 
      
 44 
     | 
    
         
            +
                    !matches?(criterion, value_from_properties, properties)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  when :IN_SEG
         
     | 
| 
      
 46 
     | 
    
         
            +
                    in_segment?(criterion, properties)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  when :NOT_IN_SEG
         
     | 
| 
      
 48 
     | 
    
         
            +
                    !in_segment?(criterion, properties)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  when :PROP_ENDS_WITH_ONE_OF
         
     | 
| 
      
 50 
     | 
    
         
            +
                    return false unless value_from_properties
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    criterion.value_to_match.string_list.values.any? do |ending|
         
     | 
| 
      
 53 
     | 
    
         
            +
                      value_from_properties.end_with?(ending)
         
     | 
| 
      
 54 
     | 
    
         
            +
                    end
         
     | 
| 
      
 55 
     | 
    
         
            +
                  when :PROP_DOES_NOT_END_WITH_ONE_OF
         
     | 
| 
      
 56 
     | 
    
         
            +
                    return true unless value_from_properties
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    criterion.value_to_match.string_list.values.none? do |ending|
         
     | 
| 
      
 59 
     | 
    
         
            +
                      value_from_properties.end_with?(ending)
         
     | 
| 
      
 60 
     | 
    
         
            +
                    end
         
     | 
| 
      
 61 
     | 
    
         
            +
                  when :HIERARCHICAL_MATCH
         
     | 
| 
      
 62 
     | 
    
         
            +
                    value_from_properties.start_with?(criterion.value_to_match.string)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  when :ALWAYS_TRUE
         
     | 
| 
      
 64 
     | 
    
         
            +
                    true
         
     | 
| 
      
 65 
     | 
    
         
            +
                  else
         
     | 
| 
      
 66 
     | 
    
         
            +
                    @base_client.log.info("Unknown Operator: #{criterion.operator}")
         
     | 
| 
      
 67 
     | 
    
         
            +
                    false
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                private
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                def matching_environment_row_values
         
     | 
| 
      
 74 
     | 
    
         
            +
                  @config.rows.find { |row| row.project_env_id == @project_env_id }&.values || NO_MATCHING_ROWS
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                def default_row_values
         
     | 
| 
      
 78 
     | 
    
         
            +
                  @config.rows.find { |row| row.project_env_id != @project_env_id }&.values || NO_MATCHING_ROWS
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                def in_segment?(criterion, properties)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  @resolver.get(criterion.value_to_match.string, properties[LOOKUP_KEY], properties).bool
         
     | 
| 
      
 83 
     | 
    
         
            +
                end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                def matches?(criterion, value_from_properties, properties)
         
     | 
| 
      
 86 
     | 
    
         
            +
                  criterion_value_or_values = Prefab::ConfigValueUnwrapper.unwrap(criterion.value_to_match, @config.key, properties)
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                  case criterion_value_or_values
         
     | 
| 
      
 89 
     | 
    
         
            +
                  when Google::Protobuf::RepeatedField
         
     | 
| 
      
 90 
     | 
    
         
            +
                    criterion_value_or_values.include?(value_from_properties)
         
     | 
| 
      
 91 
     | 
    
         
            +
                  else
         
     | 
| 
      
 92 
     | 
    
         
            +
                    criterion_value_or_values == value_from_properties
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
              end
         
     | 
| 
      
 96 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -5,7 +5,7 @@ module Prefab 
     | 
|
| 
       5 
5 
     | 
    
         
             
                class InvalidApiKeyError < Prefab::Error
         
     | 
| 
       6 
6 
     | 
    
         
             
                  def initialize(key)
         
     | 
| 
       7 
7 
     | 
    
         
             
                    if key.nil? || key.empty?
         
     | 
| 
       8 
     | 
    
         
            -
                      message =  
     | 
| 
      
 8 
     | 
    
         
            +
                      message = 'No API key. Set PREFAB_API_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY'
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
       10 
10 
     | 
    
         
             
                      super(message)
         
     | 
| 
       11 
11 
     | 
    
         
             
                    else
         
     | 
| 
         @@ -1,176 +1,44 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
       2 
3 
     | 
    
         
             
            module Prefab
         
     | 
| 
       3 
4 
     | 
    
         
             
              class FeatureFlagClient
         
     | 
| 
       4 
     | 
    
         
            -
                include Prefab::ConfigHelper
         
     | 
| 
       5 
     | 
    
         
            -
                MAX_32_FLOAT = 4294967294.0
         
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
       7 
5 
     | 
    
         
             
                def initialize(base_client)
         
     | 
| 
       8 
6 
     | 
    
         
             
                  @base_client = base_client
         
     | 
| 
       9 
7 
     | 
    
         
             
                end
         
     | 
| 
       10 
8 
     | 
    
         | 
| 
       11 
     | 
    
         
            -
                def upsert(feature_name, feature_obj)
         
     | 
| 
       12 
     | 
    
         
            -
                  @base_client.config_client.upsert(feature_name, Prefab::ConfigValue.new(feature_flag: feature_obj))
         
     | 
| 
       13 
     | 
    
         
            -
                end
         
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
9 
     | 
    
         
             
                def feature_is_on?(feature_name)
         
     | 
| 
       16 
10 
     | 
    
         
             
                  feature_is_on_for?(feature_name, nil)
         
     | 
| 
       17 
11 
     | 
    
         
             
                end
         
     | 
| 
       18 
12 
     | 
    
         | 
| 
       19 
13 
     | 
    
         
             
                def feature_is_on_for?(feature_name, lookup_key, attributes: {})
         
     | 
| 
       20 
     | 
    
         
            -
                  @base_client.stats.increment( 
     | 
| 
      
 14 
     | 
    
         
            +
                  @base_client.stats.increment('prefab.featureflag.on', tags: ["feature:#{feature_name}"])
         
     | 
| 
       21 
15 
     | 
    
         | 
| 
       22 
     | 
    
         
            -
                   
     | 
| 
       23 
     | 
    
         
            -
                end
         
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
     | 
    
         
            -
                def get(feature_name, lookup_key=nil, attributes={}, default: false)
         
     | 
| 
       26 
     | 
    
         
            -
                  variant = _get(feature_name, lookup_key, attributes, default: default)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  variant = @base_client.config_client.get(feature_name, false, attributes, lookup_key)
         
     | 
| 
       27 
17 
     | 
    
         | 
| 
       28 
     | 
    
         
            -
                   
     | 
| 
      
 18 
     | 
    
         
            +
                  is_on?(variant)
         
     | 
| 
       29 
19 
     | 
    
         
             
                end
         
     | 
| 
       30 
20 
     | 
    
         | 
| 
       31 
     | 
    
         
            -
                 
     | 
| 
      
 21 
     | 
    
         
            +
                def get(feature_name, lookup_key = nil, attributes = {}, default: false)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  value = _get(feature_name, lookup_key, attributes)
         
     | 
| 
       32 
23 
     | 
    
         | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
                  if variant_maybe.nil?
         
     | 
| 
       35 
     | 
    
         
            -
                    default != Prefab::Client::NO_DEFAULT_PROVIDED ? default : nil
         
     | 
| 
       36 
     | 
    
         
            -
                  else
         
     | 
| 
       37 
     | 
    
         
            -
                    value_of_variant(variant_maybe)
         
     | 
| 
       38 
     | 
    
         
            -
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
                  value.nil? ? default : value
         
     | 
| 
       39 
25 
     | 
    
         
             
                end
         
     | 
| 
       40 
26 
     | 
    
         | 
| 
       41 
     | 
    
         
            -
                 
     | 
| 
       42 
     | 
    
         
            -
                  feature_obj = @base_client.config_client.get(feature_name, default)
         
     | 
| 
       43 
     | 
    
         
            -
                  config_obj = @base_client.config_client.get_config_obj(feature_name)
         
     | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
       45 
     | 
    
         
            -
                  return nil if feature_obj.nil? || config_obj.nil?
         
     | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
       47 
     | 
    
         
            -
                  if feature_obj == !!feature_obj
         
     | 
| 
       48 
     | 
    
         
            -
                    return feature_obj
         
     | 
| 
       49 
     | 
    
         
            -
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
                private
         
     | 
| 
       50 
28 
     | 
    
         | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
                   
     | 
| 
      
 29 
     | 
    
         
            +
                def _get(feature_name, lookup_key = nil, attributes = {})
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @base_client.config_client.get(feature_name, nil, attributes, lookup_key)
         
     | 
| 
       53 
31 
     | 
    
         
             
                end
         
     | 
| 
       54 
32 
     | 
    
         | 
| 
       55 
33 
     | 
    
         
             
                def is_on?(variant)
         
     | 
| 
       56 
     | 
    
         
            -
                  if variant.nil?
         
     | 
| 
       57 
     | 
    
         
            -
                    return false
         
     | 
| 
       58 
     | 
    
         
            -
                  end
         
     | 
| 
      
 34 
     | 
    
         
            +
                  return false if variant.nil?
         
     | 
| 
       59 
35 
     | 
    
         | 
| 
       60 
     | 
    
         
            -
                  if variant == !!variant
         
     | 
| 
       61 
     | 
    
         
            -
                    return variant
         
     | 
| 
       62 
     | 
    
         
            -
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
                  return variant if variant == !!variant
         
     | 
| 
       63 
37 
     | 
    
         | 
| 
       64 
38 
     | 
    
         
             
                  variant.bool
         
     | 
| 
       65 
     | 
    
         
            -
                rescue
         
     | 
| 
      
 39 
     | 
    
         
            +
                rescue StandardError
         
     | 
| 
       66 
40 
     | 
    
         
             
                  @base_client.log.info("is_on? methods only work for boolean feature flags variants. This feature flags variant is '#{variant}'. Returning false")
         
     | 
| 
       67 
41 
     | 
    
         
             
                  false
         
     | 
| 
       68 
42 
     | 
    
         
             
                end
         
     | 
| 
       69 
     | 
    
         
            -
             
     | 
| 
       70 
     | 
    
         
            -
                def get_variant(feature_name, lookup_key, attributes, feature_obj, variants)
         
     | 
| 
       71 
     | 
    
         
            -
                  if !feature_obj.active
         
     | 
| 
       72 
     | 
    
         
            -
                    return get_variant_obj(variants, feature_obj.inactive_variant_idx)
         
     | 
| 
       73 
     | 
    
         
            -
                  end
         
     | 
| 
       74 
     | 
    
         
            -
             
     | 
| 
       75 
     | 
    
         
            -
                  #default to inactive
         
     | 
| 
       76 
     | 
    
         
            -
                  variant_weights = [Prefab::VariantWeight.new(variant_idx: feature_obj.inactive_variant_idx, weight: 1)]
         
     | 
| 
       77 
     | 
    
         
            -
             
     | 
| 
       78 
     | 
    
         
            -
                  # if rules.match
         
     | 
| 
       79 
     | 
    
         
            -
                  feature_obj.rules.each do |rule|
         
     | 
| 
       80 
     | 
    
         
            -
                    if criteria_match?(rule.criteria, lookup_key, attributes)
         
     | 
| 
       81 
     | 
    
         
            -
                      variant_weights = rule.variant_weights
         
     | 
| 
       82 
     | 
    
         
            -
                      break
         
     | 
| 
       83 
     | 
    
         
            -
                    end
         
     | 
| 
       84 
     | 
    
         
            -
                  end
         
     | 
| 
       85 
     | 
    
         
            -
             
     | 
| 
       86 
     | 
    
         
            -
                  percent_through_distribution = rand()
         
     | 
| 
       87 
     | 
    
         
            -
                  if lookup_key
         
     | 
| 
       88 
     | 
    
         
            -
                    percent_through_distribution = get_user_pct(feature_name, lookup_key)
         
     | 
| 
       89 
     | 
    
         
            -
                  end
         
     | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
       91 
     | 
    
         
            -
                  variant_idx = get_variant_idx_from_weights(variant_weights, percent_through_distribution, feature_name)
         
     | 
| 
       92 
     | 
    
         
            -
             
     | 
| 
       93 
     | 
    
         
            -
                  return get_variant_obj(variants, variant_idx)
         
     | 
| 
       94 
     | 
    
         
            -
                end
         
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
                def get_variant_obj(variants, idx)
         
     | 
| 
       97 
     | 
    
         
            -
                  # our array is 0 based, but the idx are 1 based so the protos are clearly set
         
     | 
| 
       98 
     | 
    
         
            -
                  return variants[idx - 1] if variants.length >= idx
         
     | 
| 
       99 
     | 
    
         
            -
                  nil
         
     | 
| 
       100 
     | 
    
         
            -
                end
         
     | 
| 
       101 
     | 
    
         
            -
             
     | 
| 
       102 
     | 
    
         
            -
                def get_variant_idx_from_weights(variant_weights, percent_through_distribution, feature_name)
         
     | 
| 
       103 
     | 
    
         
            -
                  distrubution_space = variant_weights.inject(0) { |sum, v| sum + v.weight }
         
     | 
| 
       104 
     | 
    
         
            -
                  bucket = distrubution_space * percent_through_distribution
         
     | 
| 
       105 
     | 
    
         
            -
                  sum = 0
         
     | 
| 
       106 
     | 
    
         
            -
                  variant_weights.each do |variant_weight|
         
     | 
| 
       107 
     | 
    
         
            -
                    if bucket < sum + variant_weight.weight
         
     | 
| 
       108 
     | 
    
         
            -
                      return variant_weight.variant_idx
         
     | 
| 
       109 
     | 
    
         
            -
                    else
         
     | 
| 
       110 
     | 
    
         
            -
                      sum += variant_weight.weight
         
     | 
| 
       111 
     | 
    
         
            -
                    end
         
     | 
| 
       112 
     | 
    
         
            -
                  end
         
     | 
| 
       113 
     | 
    
         
            -
                  # variants didn't add up to 100%
         
     | 
| 
       114 
     | 
    
         
            -
                  @base_client.log.info("Variants of #{feature_name} did not add to 100%")
         
     | 
| 
       115 
     | 
    
         
            -
                  return variant_weights.last.variant_idx
         
     | 
| 
       116 
     | 
    
         
            -
                end
         
     | 
| 
       117 
     | 
    
         
            -
             
     | 
| 
       118 
     | 
    
         
            -
                def get_user_pct(feature, lookup_key)
         
     | 
| 
       119 
     | 
    
         
            -
                  to_hash = "#{feature}#{lookup_key}"
         
     | 
| 
       120 
     | 
    
         
            -
                  int_value = Murmur3.murmur3_32(to_hash)
         
     | 
| 
       121 
     | 
    
         
            -
                  int_value / MAX_32_FLOAT
         
     | 
| 
       122 
     | 
    
         
            -
                end
         
     | 
| 
       123 
     | 
    
         
            -
             
     | 
| 
       124 
     | 
    
         
            -
                def criteria_match?(criteria, lookup_key, attributes)
         
     | 
| 
       125 
     | 
    
         
            -
                  case criteria.operator
         
     | 
| 
       126 
     | 
    
         
            -
                  when :ALWAYS_TRUE
         
     | 
| 
       127 
     | 
    
         
            -
                    true
         
     | 
| 
       128 
     | 
    
         
            -
                  when :LOOKUP_KEY_IN
         
     | 
| 
       129 
     | 
    
         
            -
                    criteria.values.include?(lookup_key)
         
     | 
| 
       130 
     | 
    
         
            -
                  when :LOOKUP_KEY_NOT_IN
         
     | 
| 
       131 
     | 
    
         
            -
                    !criteria.values.include?(lookup_key)
         
     | 
| 
       132 
     | 
    
         
            -
                  when :IN_SEG
         
     | 
| 
       133 
     | 
    
         
            -
                    segment_matches?(criteria.values, lookup_key, attributes)
         
     | 
| 
       134 
     | 
    
         
            -
                  when :NOT_IN_SEG
         
     | 
| 
       135 
     | 
    
         
            -
                    !segment_matches?(criteria.values, lookup_key, attributes)
         
     | 
| 
       136 
     | 
    
         
            -
                  when :PROP_IS_ONE_OF
         
     | 
| 
       137 
     | 
    
         
            -
                    criteria.values.include?(attribute_value(attributes, criteria.property))
         
     | 
| 
       138 
     | 
    
         
            -
                  when :PROP_IS_NOT_ONE_OF
         
     | 
| 
       139 
     | 
    
         
            -
                    !criteria.values.include?(attribute_value(attributes, criteria.property))
         
     | 
| 
       140 
     | 
    
         
            -
                  when :PROP_ENDS_WITH_ONE_OF
         
     | 
| 
       141 
     | 
    
         
            -
                    criteria.values.any? { |value| attribute_value(attributes, criteria.property)&.end_with?(value) }
         
     | 
| 
       142 
     | 
    
         
            -
                  when :PROP_DOES_NOT_END_WITH_ONE_OF
         
     | 
| 
       143 
     | 
    
         
            -
                    criteria.values.none? { |value| attribute_value(attributes, criteria.property)&.end_with?(value) }
         
     | 
| 
       144 
     | 
    
         
            -
                  else
         
     | 
| 
       145 
     | 
    
         
            -
                    @base_client.log.info("Unknown Operator: #{criteria.operator}")
         
     | 
| 
       146 
     | 
    
         
            -
                    false
         
     | 
| 
       147 
     | 
    
         
            -
                  end
         
     | 
| 
       148 
     | 
    
         
            -
                end
         
     | 
| 
       149 
     | 
    
         
            -
             
     | 
| 
       150 
     | 
    
         
            -
                def attribute_value(attributes, property)
         
     | 
| 
       151 
     | 
    
         
            -
                  attributes[property] || attributes[property.to_sym]
         
     | 
| 
       152 
     | 
    
         
            -
                end
         
     | 
| 
       153 
     | 
    
         
            -
             
     | 
| 
       154 
     | 
    
         
            -
                # evaluate each segment key and return whether any match
         
     | 
| 
       155 
     | 
    
         
            -
                # there should be an associated segment available as a standard config obj
         
     | 
| 
       156 
     | 
    
         
            -
                def segment_matches?(segment_keys, lookup_key, attributes)
         
     | 
| 
       157 
     | 
    
         
            -
                  segment_keys.any? do |segment_key|
         
     | 
| 
       158 
     | 
    
         
            -
                    segment = @base_client.config_client.get(segment_key)
         
     | 
| 
       159 
     | 
    
         
            -
                    if segment.nil?
         
     | 
| 
       160 
     | 
    
         
            -
                      @base_client.log.info("Missing Segment")
         
     | 
| 
       161 
     | 
    
         
            -
                      false
         
     | 
| 
       162 
     | 
    
         
            -
                    else
         
     | 
| 
       163 
     | 
    
         
            -
                      segment_match?(segment, lookup_key, attributes)
         
     | 
| 
       164 
     | 
    
         
            -
                    end
         
     | 
| 
       165 
     | 
    
         
            -
                  end
         
     | 
| 
       166 
     | 
    
         
            -
                end
         
     | 
| 
       167 
     | 
    
         
            -
             
     | 
| 
       168 
     | 
    
         
            -
                # does a given segment match?
         
     | 
| 
       169 
     | 
    
         
            -
                def segment_match?(segment, lookup_key, attributes)
         
     | 
| 
       170 
     | 
    
         
            -
                  segment.criterion.any? do |criteria|
         
     | 
| 
       171 
     | 
    
         
            -
                    criteria_match?(criteria, lookup_key, attributes)
         
     | 
| 
       172 
     | 
    
         
            -
                  end
         
     | 
| 
       173 
     | 
    
         
            -
                end
         
     | 
| 
       174 
43 
     | 
    
         
             
              end
         
     | 
| 
       175 
44 
     | 
    
         
             
            end
         
     | 
| 
       176 
     | 
    
         
            -
             
     | 
| 
         @@ -1,4 +1,5 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
       2 
3 
     | 
    
         
             
            module Prefab
         
     | 
| 
       3 
4 
     | 
    
         
             
              class InternalLogger < Logger
         
     | 
| 
       4 
5 
     | 
    
         
             
                def initialize(path, logger)
         
     | 
| 
         @@ -6,23 +7,23 @@ module Prefab 
     | 
|
| 
       6 
7 
     | 
    
         
             
                  @logger = logger
         
     | 
| 
       7 
8 
     | 
    
         
             
                end
         
     | 
| 
       8 
9 
     | 
    
         | 
| 
       9 
     | 
    
         
            -
                def debug(progname = nil 
     | 
| 
      
 10 
     | 
    
         
            +
                def debug(progname = nil)
         
     | 
| 
       10 
11 
     | 
    
         
             
                  @logger.log_internal yield, @path, progname, DEBUG
         
     | 
| 
       11 
12 
     | 
    
         
             
                end
         
     | 
| 
       12 
13 
     | 
    
         | 
| 
       13 
     | 
    
         
            -
                def info(progname = nil 
     | 
| 
      
 14 
     | 
    
         
            +
                def info(progname = nil)
         
     | 
| 
       14 
15 
     | 
    
         
             
                  @logger.log_internal yield, @path, progname, INFO
         
     | 
| 
       15 
16 
     | 
    
         
             
                end
         
     | 
| 
       16 
17 
     | 
    
         | 
| 
       17 
     | 
    
         
            -
                def warn(progname = nil 
     | 
| 
      
 18 
     | 
    
         
            +
                def warn(progname = nil)
         
     | 
| 
       18 
19 
     | 
    
         
             
                  @logger.log_internal yield, @path, progname, WARN
         
     | 
| 
       19 
20 
     | 
    
         
             
                end
         
     | 
| 
       20 
21 
     | 
    
         | 
| 
       21 
     | 
    
         
            -
                def error(progname = nil 
     | 
| 
      
 22 
     | 
    
         
            +
                def error(progname = nil)
         
     | 
| 
       22 
23 
     | 
    
         
             
                  @logger.log_internal yield, @path, progname, ERROR
         
     | 
| 
       23 
24 
     | 
    
         
             
                end
         
     | 
| 
       24 
25 
     | 
    
         | 
| 
       25 
     | 
    
         
            -
                def fatal(progname = nil 
     | 
| 
      
 26 
     | 
    
         
            +
                def fatal(progname = nil)
         
     | 
| 
       26 
27 
     | 
    
         
             
                  @logger.log_internal yield, @path, progname, FATAL
         
     | 
| 
       27 
28 
     | 
    
         
             
                end
         
     | 
| 
       28 
29 
     | 
    
         
             
              end
         
     |