prefab-cloud-ruby 0.24.2 → 0.24.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/.rubocop.yml +13 -0
  4. data/CHANGELOG.md +76 -0
  5. data/Gemfile.lock +4 -4
  6. data/VERSION +1 -1
  7. data/bin/console +21 -0
  8. data/compile_protos.sh +6 -0
  9. data/lib/prefab/client.rb +25 -4
  10. data/lib/prefab/config_client.rb +16 -6
  11. data/lib/prefab/config_loader.rb +1 -1
  12. data/lib/prefab/config_resolver.rb +2 -4
  13. data/lib/prefab/config_value_wrapper.rb +18 -0
  14. data/lib/prefab/context.rb +22 -2
  15. data/lib/prefab/context_shape.rb +20 -0
  16. data/lib/prefab/context_shape_aggregator.rb +63 -0
  17. data/lib/prefab/criteria_evaluator.rb +61 -41
  18. data/lib/prefab/evaluated_configs_aggregator.rb +60 -0
  19. data/lib/prefab/evaluated_keys_aggregator.rb +41 -0
  20. data/lib/prefab/local_config_parser.rb +13 -13
  21. data/lib/prefab/log_path_aggregator.rb +64 -0
  22. data/lib/prefab/logger_client.rb +29 -16
  23. data/lib/prefab/options.rb +46 -1
  24. data/lib/prefab/periodic_sync.rb +51 -0
  25. data/lib/prefab/time_helpers.rb +7 -0
  26. data/lib/prefab-cloud-ruby.rb +8 -1
  27. data/lib/prefab_pb.rb +33 -220
  28. data/prefab-cloud-ruby.gemspec +21 -5
  29. data/test/test_config_loader.rb +15 -15
  30. data/test/test_config_resolver.rb +102 -102
  31. data/test/test_config_value_unwrapper.rb +13 -13
  32. data/test/test_context.rb +42 -0
  33. data/test/test_context_shape.rb +51 -0
  34. data/test/test_context_shape_aggregator.rb +137 -0
  35. data/test/test_criteria_evaluator.rb +253 -150
  36. data/test/test_evaluated_configs_aggregator.rb +254 -0
  37. data/test/test_evaluated_keys_aggregator.rb +54 -0
  38. data/test/test_helper.rb +34 -2
  39. data/test/test_log_path_aggregator.rb +57 -0
  40. data/test/test_logger.rb +61 -76
  41. data/test/test_weighted_value_resolver.rb +2 -2
  42. metadata +21 -5
  43. data/lib/prefab/log_path_collector.rb +0 -102
  44. data/test/test_log_path_collector.rb +0 -58
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'timecop'
5
+
6
+ class TestEvaluatedConfigsAggregator < Minitest::Test
7
+ MAX_WAIT = 2
8
+ SLEEP_TIME = 0.01
9
+
10
+ def test_push
11
+ aggregator = Prefab::EvaluatedConfigsAggregator.new(client: new_client, max_configs: 2, sync_interval: 1000)
12
+
13
+ aggregator.push([])
14
+ aggregator.push([])
15
+
16
+ assert_equal 2, aggregator.data.size
17
+
18
+ # we've reached the limit, so no more
19
+ aggregator.push([])
20
+ assert_equal 2, aggregator.data.size
21
+ end
22
+
23
+ def test_coerce_to_proto
24
+ aggregator = Prefab::EvaluatedConfigsAggregator.new(client: new_client, max_configs: 2, sync_interval: 2)
25
+
26
+ Timecop.freeze do
27
+ coerced = aggregator.coerce_to_proto([
28
+ CONFIG_1,
29
+ DESIRED_VALUE,
30
+ Prefab::Context.new(CONTEXT)
31
+ ])
32
+
33
+ assert_equal PrefabProto::EvaluatedConfig.new(
34
+ key: CONFIG_1.key,
35
+ config_version: CONFIG_1.id,
36
+ result: DESIRED_VALUE,
37
+ context: PrefabProto::ContextSet.new(
38
+ contexts: [
39
+ PrefabProto::Context.new(
40
+ type: "user",
41
+ values: {
42
+ "id" => PrefabProto::ConfigValue.new(int: 1),
43
+ "email_suffix" => PrefabProto::ConfigValue.new(string: "hotmail.com")
44
+ }
45
+ ),
46
+ PrefabProto::Context.new(
47
+ type: "team",
48
+ values: {
49
+ "id" => PrefabProto::ConfigValue.new(int: 2),
50
+ "name" => PrefabProto::ConfigValue.new(string: "team-name")
51
+ }
52
+ ),
53
+ PrefabProto::Context.new(
54
+ type: "prefab",
55
+ values: {
56
+ "current-time" => PrefabProto::ConfigValue.new(int: Prefab::TimeHelpers.now_in_ms),
57
+ }
58
+ ),
59
+ ]
60
+ ),
61
+ timestamp: Prefab::TimeHelpers.now_in_ms
62
+ ), coerced
63
+ end
64
+ end
65
+
66
+ def test_sync
67
+ client = new_client(namespace: 'this.is.a.namespace')
68
+
69
+ inject_config(client, CONFIG_1)
70
+ inject_config(client, CONFIG_2)
71
+ inject_project_env_id(client, PROJECT_ENV_ID)
72
+
73
+ client.get CONFIG_1.key, 'default', CONTEXT
74
+ client.get CONFIG_1.key, 'default', { user: { email_suffix: "example.com" }, device: { mobile: true } }
75
+ client.get CONFIG_2.key, 'default', CONTEXT
76
+
77
+ # logger items are not reported
78
+ client.get "#{Prefab::ConfigClient::LOGGING_KEY_PREFIX}something", 'default', CONTEXT
79
+ client.get "#{Prefab::LoggerClient::BASE_KEY}", 'default', CONTEXT
80
+
81
+ requests = wait_for_post_requests(client) do
82
+ client.evaluated_configs_aggregator.send(:sync)
83
+ end
84
+
85
+ assert_equal [[
86
+ '/api/v1/evaluated-configs',
87
+ PrefabProto::EvaluatedConfigs.new(
88
+ configs: [
89
+ PrefabProto::EvaluatedConfig.new(
90
+ key: CONFIG_1.key,
91
+ config_version: CONFIG_1.id,
92
+ result: DESIRED_VALUE,
93
+ context: PrefabProto::ContextSet.new(
94
+ contexts: [
95
+ PrefabProto::Context.new(
96
+ type: "user",
97
+ values: {
98
+ "id" => PrefabProto::ConfigValue.new(int: 1),
99
+ "email_suffix" => PrefabProto::ConfigValue.new(string: "hotmail.com")
100
+ }
101
+ ),
102
+ PrefabProto::Context.new(
103
+ type: "team",
104
+ values: {
105
+ "id" => PrefabProto::ConfigValue.new(int: 2),
106
+ "name" => PrefabProto::ConfigValue.new(string: "team-name")
107
+ }
108
+ ),
109
+ PrefabProto::Context.new(
110
+ type: "prefab",
111
+ values: {
112
+ "current-time" => PrefabProto::ConfigValue.new(int: Prefab::TimeHelpers.now_in_ms),
113
+ "namespace" => PrefabProto::ConfigValue.new(string: "this.is.a.namespace"),
114
+ }
115
+ ),
116
+ ]
117
+ ),
118
+ timestamp: Prefab::TimeHelpers.now_in_ms
119
+ ),
120
+ PrefabProto::EvaluatedConfig.new(
121
+ key: CONFIG_1.key,
122
+ config_version: CONFIG_1.id,
123
+ result: DEFAULT_VALUE,
124
+ context: PrefabProto::ContextSet.new(
125
+ contexts: [
126
+ PrefabProto::Context.new(
127
+ type: "user",
128
+ values: {
129
+ "email_suffix" => PrefabProto::ConfigValue.new(string: "example.com")
130
+ }
131
+ ),
132
+ PrefabProto::Context.new(
133
+ type: "device",
134
+ values: {
135
+ "mobile" => PrefabProto::ConfigValue.new(bool: true),
136
+ }
137
+ ),
138
+ PrefabProto::Context.new(
139
+ type: "prefab",
140
+ values: {
141
+ "current-time" => PrefabProto::ConfigValue.new(int: Prefab::TimeHelpers.now_in_ms),
142
+ "namespace" => PrefabProto::ConfigValue.new(string: "this.is.a.namespace"),
143
+ }
144
+ )
145
+
146
+ ]
147
+ ),
148
+ timestamp: Prefab::TimeHelpers.now_in_ms
149
+ ),
150
+
151
+ PrefabProto::EvaluatedConfig.new(
152
+ key: CONFIG_2.key,
153
+ config_version: CONFIG_2.id,
154
+ result: DEFAULT_VALUE,
155
+ context: PrefabProto::ContextSet.new(
156
+ contexts: [
157
+ PrefabProto::Context.new(
158
+ type: "user",
159
+ values: {
160
+ "id" => PrefabProto::ConfigValue.new(int: 1),
161
+ "email_suffix" => PrefabProto::ConfigValue.new(string: "hotmail.com")
162
+ }
163
+ ),
164
+ PrefabProto::Context.new(
165
+ type: "team",
166
+ values: {
167
+ "id" => PrefabProto::ConfigValue.new(int: 2),
168
+ "name" => PrefabProto::ConfigValue.new(string: "team-name")
169
+ }
170
+ ),
171
+ PrefabProto::Context.new(
172
+ type: "prefab",
173
+ values: {
174
+ "current-time" => PrefabProto::ConfigValue.new(int: Prefab::TimeHelpers.now_in_ms),
175
+ "namespace" => PrefabProto::ConfigValue.new(string: "this.is.a.namespace"),
176
+ }
177
+ )
178
+ ]
179
+ ),
180
+ timestamp: Prefab::TimeHelpers.now_in_ms
181
+ )
182
+
183
+ ]
184
+ )
185
+ ]], requests
186
+ end
187
+
188
+ private
189
+
190
+ def new_client(overrides = {})
191
+ super(**{
192
+ prefab_datasources: Prefab::Options::DATASOURCES::ALL,
193
+ initialization_timeout_sec: 0,
194
+ on_init_failure: Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN,
195
+ api_key: '123-development-yourapikey-SDK',
196
+ collect_sync_interval: 1000, # we'll trigger sync manually in our test
197
+ collect_evaluations: true
198
+ }.merge(overrides))
199
+ end
200
+
201
+ DEFAULT_VALUE = PrefabProto::ConfigValue.new(string: '❌')
202
+
203
+ DESIRED_VALUE = PrefabProto::ConfigValue.new(string: "✅")
204
+
205
+ DEFAULT_ROW = PrefabProto::ConfigRow.new(
206
+ values: [
207
+ PrefabProto::ConditionalValue.new(
208
+ value: DEFAULT_VALUE
209
+ )
210
+ ]
211
+ )
212
+
213
+ PROJECT_ENV_ID = 1
214
+
215
+ CONFIG_1 = PrefabProto::Config.new(
216
+ id: 1,
217
+ key: "key.1",
218
+ rows: [
219
+ PrefabProto::ConfigRow.new(
220
+ project_env_id: PROJECT_ENV_ID,
221
+ values: [
222
+ PrefabProto::ConditionalValue.new(
223
+ criteria: [
224
+ PrefabProto::Criterion.new(
225
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_IS_ONE_OF,
226
+ value_to_match: string_list(['hotmail.com', 'gmail.com']),
227
+ property_name: 'user.email_suffix'
228
+ )
229
+ ],
230
+ value: DESIRED_VALUE
231
+ )
232
+ ]
233
+ ),
234
+ DEFAULT_ROW
235
+ ]
236
+ )
237
+
238
+ CONFIG_2 = PrefabProto::Config.new(
239
+ id: 2,
240
+ key: "key.2",
241
+ rows: [DEFAULT_ROW,]
242
+ )
243
+
244
+ CONTEXT = {
245
+ user: {
246
+ id: 1,
247
+ email_suffix: 'hotmail.com'
248
+ },
249
+ team: {
250
+ id: 2,
251
+ name: 'team-name'
252
+ }
253
+ }
254
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestEvaluatedKeysAggregator < Minitest::Test
6
+ MAX_WAIT = 2
7
+ SLEEP_TIME = 0.01
8
+
9
+ def test_push
10
+ aggregator = Prefab::EvaluatedKeysAggregator.new(client: new_client, max_keys: 2, sync_interval: 1000)
11
+
12
+ aggregator.push('key.1')
13
+ aggregator.push('key.2')
14
+
15
+ assert_equal 2, aggregator.data.size
16
+
17
+ # we've reached the limit, so no more
18
+ aggregator.push('key.3')
19
+ assert_equal 2, aggregator.data.size
20
+ end
21
+
22
+ def test_sync
23
+ client = new_client(namespace: 'this.is.a.namespace')
24
+
25
+ client.get 'key.1', 'default', {}
26
+ client.get 'key.1', 'default', {}
27
+ client.get 'key.2', 'default', {}
28
+
29
+ requests = wait_for_post_requests(client) do
30
+ client.evaluated_keys_aggregator.send(:sync)
31
+ end
32
+
33
+ assert_equal [[
34
+ '/api/v1/evaluated-keys',
35
+ PrefabProto::EvaluatedKeys.new(
36
+ keys: ['key.1', 'key.2'],
37
+ namespace: 'this.is.a.namespace'
38
+ )
39
+ ]], requests
40
+ end
41
+
42
+ private
43
+
44
+ def new_client(overrides = {})
45
+ super(**{
46
+ prefab_datasources: Prefab::Options::DATASOURCES::ALL,
47
+ initialization_timeout_sec: 0,
48
+ on_init_failure: Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN,
49
+ api_key: '123-development-yourapikey-SDK',
50
+ collect_sync_interval: 1000, # we'll trigger sync manually in our test
51
+ collect_keys: true
52
+ }.merge(overrides))
53
+ end
54
+ end
data/test/test_helper.rb CHANGED
@@ -33,6 +33,12 @@ class MockBaseClient
33
33
 
34
34
  def log_internal(level, message); end
35
35
 
36
+ def context_shape_aggregator; end
37
+
38
+ def evaluated_keys_aggregator; end
39
+
40
+ def evaluated_configs_aggregator; end
41
+
36
42
  def config_value(key)
37
43
  @config_values[key]
38
44
  end
@@ -48,7 +54,7 @@ class MockConfigClient
48
54
  end
49
55
 
50
56
  def get_config(key)
51
- Prefab::Config.new(value: @config_values[key], key: key)
57
+ PrefabProto::Config.new(value: @config_values[key], key: key)
52
58
  end
53
59
 
54
60
  def mock_this_config(key, config_value)
@@ -95,7 +101,7 @@ def new_client(overrides = {})
95
101
  end
96
102
 
97
103
  def string_list(values)
98
- Prefab::ConfigValue.new(string_list: Prefab::StringList.new(values: values))
104
+ PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: values))
99
105
  end
100
106
 
101
107
  def inject_config(client, config)
@@ -109,3 +115,29 @@ def inject_project_env_id(client, project_env_id)
109
115
  resolver = client.config_client.instance_variable_get('@config_resolver')
110
116
  resolver.project_env_id = project_env_id
111
117
  end
118
+
119
+ def wait_for_post_requests(client)
120
+ max_wait = 2
121
+ sleep_time = 0.01
122
+
123
+ requests = []
124
+
125
+ client.define_singleton_method(:post) do |*params|
126
+ requests.push(params)
127
+
128
+ OpenStruct.new(status: 200)
129
+ end
130
+
131
+ yield
132
+
133
+ # let the flush thread run
134
+ wait_time = 0
135
+ while requests.empty?
136
+ wait_time += sleep_time
137
+ sleep sleep_time
138
+
139
+ raise "Waited #{max_wait} seconds for the flush thread to run, but it never did" if wait_time > max_wait
140
+ end
141
+
142
+ requests
143
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'timecop'
5
+
6
+ class TestLogPathAggregator < Minitest::Test
7
+ MAX_WAIT = 2
8
+ SLEEP_TIME = 0.01
9
+
10
+ def test_push
11
+ aggregator = Prefab::LogPathAggregator.new(client: new_client, max_paths: 2, sync_interval: 1000)
12
+
13
+ aggregator.push('test.test_log_path_aggregator.test_push.1', ::Logger::INFO)
14
+ aggregator.push('test.test_log_path_aggregator.test_push.2', ::Logger::DEBUG)
15
+
16
+ assert_equal 2, aggregator.data.size
17
+
18
+ # we've reached the limit, so no more
19
+ aggregator.push('test.test_log_path_aggregator.test_push.3', ::Logger::INFO)
20
+ assert_equal 2, aggregator.data.size
21
+ end
22
+
23
+ def test_sync
24
+ Timecop.freeze do
25
+ client = new_client(namespace: 'this.is.a.namespace')
26
+
27
+ 2.times { client.log.info('here is a message') }
28
+ 3.times { client.log.error('here is a message') }
29
+
30
+ requests = wait_for_post_requests(client) do
31
+ client.log_path_aggregator.send(:sync)
32
+ end
33
+
34
+ assert_equal [[
35
+ '/api/v1/known-loggers',
36
+ PrefabProto::Loggers.new(
37
+ loggers: [PrefabProto::Logger.new(logger_name: 'test.test_log_path_aggregator.test_sync',
38
+ infos: 2, errors: 3)],
39
+ start_at: Prefab::TimeHelpers.now_in_ms,
40
+ end_at: Prefab::TimeHelpers.now_in_ms,
41
+ instance_hash: client.instance_hash,
42
+ namespace: 'this.is.a.namespace'
43
+ )
44
+ ]], requests
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def new_client(overrides = {})
51
+ super(**{
52
+ prefab_datasources: Prefab::Options::DATASOURCES::ALL,
53
+ api_key: '123-development-yourapikey-SDK',
54
+ collect_sync_interval: 1000 # we'll trigger sync manually in our test
55
+ }.merge(overrides))
56
+ end
57
+ end