prefab-cloud-ruby 0.20.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
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