prefab-cloud-ruby 0.24.2 → 0.24.4

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 (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