prefab-cloud-ruby 0.20.0 → 0.22.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc.sample +3 -0
  3. data/.github/workflows/ruby.yml +5 -1
  4. data/.gitmodules +3 -0
  5. data/Gemfile +14 -12
  6. data/Gemfile.lock +24 -14
  7. data/README.md +12 -10
  8. data/Rakefile +13 -14
  9. data/VERSION +1 -1
  10. data/lib/prefab/auth_interceptor.rb +2 -1
  11. data/lib/prefab/cancellable_interceptor.rb +8 -7
  12. data/lib/prefab/client.rb +52 -27
  13. data/lib/prefab/config_client.rb +59 -70
  14. data/lib/prefab/config_loader.rb +7 -114
  15. data/lib/prefab/config_resolver.rb +27 -57
  16. data/lib/prefab/config_value_unwrapper.rb +23 -0
  17. data/lib/prefab/criteria_evaluator.rb +96 -0
  18. data/lib/prefab/errors/invalid_api_key_error.rb +1 -1
  19. data/lib/prefab/feature_flag_client.rb +13 -145
  20. data/lib/prefab/internal_logger.rb +7 -6
  21. data/lib/prefab/local_config_parser.rb +110 -0
  22. data/lib/prefab/log_path_collector.rb +98 -0
  23. data/lib/prefab/logger_client.rb +46 -44
  24. data/lib/prefab/murmer3.rb +3 -4
  25. data/lib/prefab/noop_cache.rb +5 -7
  26. data/lib/prefab/noop_stats.rb +2 -3
  27. data/lib/prefab/options.rb +32 -11
  28. data/lib/prefab/ratelimit_client.rb +11 -13
  29. data/lib/prefab/sse_logger.rb +3 -2
  30. data/lib/prefab/weighted_value_resolver.rb +42 -0
  31. data/lib/prefab/yaml_config_parser.rb +32 -0
  32. data/lib/prefab-cloud-ruby.rb +7 -2
  33. data/lib/prefab_pb.rb +70 -43
  34. data/lib/prefab_services_pb.rb +14 -1
  35. data/prefab-cloud-ruby.gemspec +33 -19
  36. data/test/.prefab.unit_tests.config.yaml +3 -2
  37. data/test/integration_test.rb +98 -0
  38. data/test/integration_test_helpers.rb +37 -0
  39. data/test/test_client.rb +56 -31
  40. data/test/test_config_client.rb +21 -20
  41. data/test/test_config_loader.rb +48 -37
  42. data/test/test_config_resolver.rb +312 -135
  43. data/test/test_config_value_unwrapper.rb +83 -0
  44. data/test/test_criteria_evaluator.rb +533 -0
  45. data/test/test_feature_flag_client.rb +35 -347
  46. data/test/test_helper.rb +18 -14
  47. data/test/test_integration.rb +33 -0
  48. data/test/test_local_config_parser.rb +78 -0
  49. data/test/test_log_path_collector.rb +56 -0
  50. data/test/test_logger.rb +52 -51
  51. data/test/test_options.rb +32 -0
  52. data/test/test_weighted_value_resolver.rb +65 -0
  53. metadata +30 -16
  54. data/lib/prefab/config_helper.rb +0 -31
  55. data/run_test_harness_server.sh +0 -8
  56. data/test/harness_server.rb +0 -64
@@ -1,17 +1,16 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Prefab
3
4
  class ConfigClient
4
- include Prefab::ConfigHelper
5
-
6
5
  RECONNECT_WAIT = 5
7
6
  DEFAULT_CHECKPOINT_FREQ_SEC = 60
8
7
  SSE_READ_TIMEOUT = 300
9
- AUTH_USER = "authuser"
8
+ AUTH_USER = 'authuser'
10
9
 
11
10
  def initialize(base_client, timeout)
12
11
  @base_client = base_client
13
12
  @options = base_client.options
14
- @base_client.log_internal Logger::DEBUG, "Initialize ConfigClient"
13
+ @base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient'
15
14
  @timeout = timeout
16
15
 
17
16
  @stream_lock = Concurrent::ReadWriteLock.new
@@ -22,9 +21,9 @@ module Prefab
22
21
  @config_resolver = Prefab::ConfigResolver.new(@base_client, @config_loader)
23
22
 
24
23
  @initialization_lock = Concurrent::ReadWriteLock.new
25
- @base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquireWriteLock"
24
+ @base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient: AcquireWriteLock'
26
25
  @initialization_lock.acquire_write_lock
27
- @base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquiredWriteLock"
26
+ @base_client.log_internal ::Logger::DEBUG, 'Initialize ConfigClient: AcquiredWriteLock'
28
27
  @initialized_future = Concurrent::Future.execute { @initialization_lock.acquire_read_lock }
29
28
 
30
29
  @cancellable_interceptor = Prefab::CancellableInterceptor.new(@base_client)
@@ -45,14 +44,15 @@ module Prefab
45
44
  end
46
45
 
47
46
  def upsert(key, config_value, namespace = nil, previous_key = nil)
48
- raise "Key must not contain ':' set namespaces separately" if key.include? ":"
49
- raise "Namespace must not contain ':'" if namespace&.include?(":")
47
+ raise "Key must not contain ':' set namespaces separately" if key.include? ':'
48
+ raise "Namespace must not contain ':'" if namespace&.include?(':')
49
+
50
50
  config_delta = Prefab::ConfigClient.value_to_delta(key, config_value, namespace)
51
51
  upsert_req = Prefab::UpsertRequest.new(config_delta: config_delta)
52
52
  upsert_req.previous_key = previous_key if previous_key&.present?
53
53
 
54
54
  @base_client.request Prefab::ConfigService, :upsert, req_options: { timeout: @timeout }, params: upsert_req
55
- @base_client.stats.increment("prefab.config.upsert")
55
+ @base_client.stats.increment('prefab.config.upsert')
56
56
  @config_loader.set(config_delta, :upsert)
57
57
  @config_loader.rm(previous_key) if previous_key&.present?
58
58
  @config_resolver.update
@@ -68,46 +68,43 @@ module Prefab
68
68
  end
69
69
 
70
70
  def self.value_to_delta(key, config_value, namespace = nil)
71
- Prefab::Config.new(key: [namespace, key].compact.join(":"),
71
+ Prefab::Config.new(key: [namespace, key].compact.join(':'),
72
72
  rows: [Prefab::ConfigRow.new(value: config_value)])
73
73
  end
74
74
 
75
- def get(key, default=Prefab::Client::NO_DEFAULT_PROVIDED)
76
- config = _get(key)
77
- config ? value_of(config[:value]) : handle_default(key, default)
78
- end
79
-
80
- def get_config_obj(key)
81
- config = _get(key)
82
- config ? config[:config] : nil
75
+ def get(key, default = Prefab::Client::NO_DEFAULT_PROVIDED, properties = {}, lookup_key = nil)
76
+ value = _get(key, lookup_key, properties)
77
+ value ? Prefab::ConfigValueUnwrapper.unwrap(value, key, properties) : handle_default(key, default)
83
78
  end
84
79
 
85
80
  private
86
81
 
82
+ def raw(key)
83
+ @config_resolver.raw(key)
84
+ end
85
+
87
86
  def handle_default(key, default)
88
- if default != Prefab::Client::NO_DEFAULT_PROVIDED
89
- return default
90
- end
87
+ return default if default != Prefab::Client::NO_DEFAULT_PROVIDED
91
88
 
92
- if @options.on_no_default == Prefab::Options::ON_NO_DEFAULT::RAISE
93
- raise Prefab::Errors::MissingDefaultError.new(key)
94
- end
89
+ raise Prefab::Errors::MissingDefaultError, key if @options.on_no_default == Prefab::Options::ON_NO_DEFAULT::RAISE
95
90
 
96
91
  nil
97
92
  end
98
93
 
99
- def _get(key)
94
+ def _get(key, lookup_key, properties)
100
95
  # wait timeout sec for the initalization to be complete
101
96
  @initialized_future.value(@options.initialization_timeout_sec)
102
97
  if @initialized_future.incomplete?
103
- if @options.on_init_failure == Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN
104
- @base_client.log_internal Logger::WARN, "Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have"
105
- @initialization_lock.release_write_lock
106
- else
98
+ unless @options.on_init_failure == Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN
107
99
  raise Prefab::Errors::InitializationTimeoutError.new(@options.initialization_timeout_sec, key)
108
100
  end
101
+
102
+ @base_client.log_internal ::Logger::WARN,
103
+ "Couldn't Initialize In #{@options.initialization_timeout_sec}. Key #{key}. Returning what we have"
104
+ @initialization_lock.release_write_lock
105
+
109
106
  end
110
- @config_resolver._get(key)
107
+ @config_resolver.get(key, lookup_key, properties)
111
108
  end
112
109
 
113
110
  def stub
@@ -120,19 +117,15 @@ module Prefab
120
117
  def load_checkpoint
121
118
  success = load_checkpoint_api_cdn
122
119
 
123
- if success
124
- return
125
- else
126
- @base_client.log_internal Logger::INFO, "LoadCheckpoint: Fallback to GRPC API"
127
- end
120
+ return if success
121
+
122
+ @base_client.log_internal ::Logger::INFO, 'LoadCheckpoint: Fallback to GRPC API'
128
123
 
129
124
  success = load_checkpoint_from_grpc_api
130
125
 
131
- if success
132
- return
133
- else
134
- @base_client.log_internal Logger::WARN, "No success loading checkpoints"
135
- end
126
+ return if success
127
+
128
+ @base_client.log_internal ::Logger::WARN, 'No success loading checkpoints'
136
129
  end
137
130
 
138
131
  def load_checkpoint_from_grpc_api
@@ -142,9 +135,9 @@ module Prefab
142
135
  load_configs(resp, :remote_api_grpc)
143
136
  true
144
137
  rescue GRPC::Unauthenticated
145
- @base_client.log_internal Logger::WARN, "Unauthenticated"
146
- rescue => e
147
- @base_client.log_internal Logger::WARN, "Unexpected grpc_api problem loading checkpoint #{e}"
138
+ @base_client.log_internal ::Logger::WARN, 'Unauthenticated'
139
+ rescue StandardError => e
140
+ @base_client.log_internal ::Logger::WARN, "Unexpected grpc_api problem loading checkpoint #{e}"
148
141
  false
149
142
  end
150
143
 
@@ -169,11 +162,11 @@ module Prefab
169
162
  load_configs(configs, source)
170
163
  true
171
164
  else
172
- @base_client.log_internal Logger::INFO, "Checkpoint #{source} failed to load. Response #{resp.status}"
165
+ @base_client.log_internal ::Logger::INFO, "Checkpoint #{source} failed to load. Response #{resp.status}"
173
166
  false
174
167
  end
175
- rescue => e
176
- @base_client.log_internal Logger::WARN, "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
168
+ rescue StandardError => e
169
+ @base_client.log_internal ::Logger::WARN, "Unexpected #{source} problem loading checkpoint #{e} #{conn}"
177
170
  false
178
171
  end
179
172
 
@@ -187,42 +180,39 @@ module Prefab
187
180
  @config_loader.set(config, source)
188
181
  end
189
182
  if @config_loader.highwater_mark > starting_highwater_mark
190
- @base_client.log_internal Logger::INFO, "Found new checkpoint with highwater id #{@config_loader.highwater_mark} from #{source} in project #{project_id} environment: #{project_env_id} and namespace: '#{@namespace}'"
183
+ @base_client.log_internal ::Logger::INFO,
184
+ "Found new checkpoint with highwater id #{@config_loader.highwater_mark} from #{source} in project #{project_id} environment: #{project_env_id} and namespace: '#{@namespace}'"
191
185
  else
192
- @base_client.log_internal Logger::DEBUG, "Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.", "load_configs"
186
+ @base_client.log_internal ::Logger::DEBUG,
187
+ "Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.", 'load_configs'
193
188
  end
194
- @base_client.stats.increment("prefab.config.checkpoint.load")
189
+ @base_client.stats.increment('prefab.config.checkpoint.load')
195
190
  @config_resolver.update
196
191
  finish_init!(source)
197
192
  end
198
193
 
199
194
  # A thread that checks for a checkpoint
200
195
  def start_checkpointing_thread
201
-
202
196
  Thread.new do
203
197
  loop do
204
- begin
205
- load_checkpoint
206
-
207
- started_at = Time.now
208
- delta = @checkpoint_freq_secs - (Time.now - started_at)
209
- if delta > 0
210
- sleep(delta)
211
- end
212
- rescue StandardError => exn
213
- @base_client.log_internal Logger::INFO, "Issue Checkpointing #{exn.message}"
214
- end
198
+ load_checkpoint
199
+
200
+ started_at = Time.now
201
+ delta = @checkpoint_freq_secs - (Time.now - started_at)
202
+ sleep(delta) if delta > 0
203
+ rescue StandardError => e
204
+ @base_client.log_internal ::Logger::INFO, "Issue Checkpointing #{e.message}"
215
205
  end
216
206
  end
217
207
  end
218
208
 
219
209
  def finish_init!(source)
220
- if @initialization_lock.write_locked?
221
- @base_client.log_internal Logger::INFO, "Unlocked Config via #{source}"
222
- @initialization_lock.release_write_lock
223
- @base_client.log.set_config_client(self)
224
- @base_client.log_internal Logger::INFO, to_s
225
- end
210
+ return unless @initialization_lock.write_locked?
211
+
212
+ @base_client.log_internal ::Logger::INFO, "Unlocked Config via #{source}"
213
+ @initialization_lock.release_write_lock
214
+ @base_client.log.set_config_client(self)
215
+ @base_client.log_internal ::Logger::INFO, to_s
226
216
  end
227
217
 
228
218
  def start_sse_streaming_connection_thread(start_at_id)
@@ -230,10 +220,10 @@ module Prefab
230
220
  auth_string = Base64.strict_encode64(auth)
231
221
  headers = {
232
222
  "x-prefab-start-at-id": start_at_id,
233
- "Authorization": "Basic #{auth_string}",
223
+ "Authorization": "Basic #{auth_string}"
234
224
  }
235
225
  url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
236
- @base_client.log_internal Logger::INFO, "SSE Streaming Connect to #{url} start_at #{start_at_id}"
226
+ @base_client.log_internal ::Logger::INFO, "SSE Streaming Connect to #{url} start_at #{start_at_id}"
237
227
  @streaming_thread = SSE::Client.new(url,
238
228
  headers: headers,
239
229
  read_timeout: SSE_READ_TIMEOUT,
@@ -246,4 +236,3 @@ module Prefab
246
236
  end
247
237
  end
248
238
  end
249
-
@@ -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 = rtn.merge(@local_overrides)
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, "Replace #{config.key} with value from #{source} #{ @api_config[config.key][:config].id} -> #{config.id}"
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, ".prefab.default.config.yaml"))
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, ".prefab.overrides.config.yaml"))
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.log_internal Logger::INFO, "Load #{file}"
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
- @namespace = base_client.options.namespace
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 << "tombstone"
24
+ elements << 'tombstone'
26
25
  else
27
- value = v[:value]
28
- elements << value_of(value).to_s.slice(0..34).ljust(35)
29
- elements << value_of(value).class.to_s.slice(0..6).ljust(7)
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(" | ") << "\n"
33
+ str += elements.join(' | ') << "\n"
34
34
  end
35
35
  end
36
36
  str
37
37
  end
38
38
 
39
- def get(property)
40
- config = _get(property)
41
- config ? value_of(config[:value]) : nil
42
- end
39
+ def raw(key)
40
+ via_key = @local_store[key]
43
41
 
44
- def get_config(property)
45
- config = _get(property)
46
- config ? config[:config] : nil
42
+ via_key ? via_key[:config] : nil
47
43
  end
48
44
 
49
- def _get(key)
45
+ def get(key, lookup_key, properties = {})
50
46
  @lock.with_read_lock do
51
- @local_store[key]
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 = 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 = "No API key. Set PREFAB_API_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY"
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