prefab-cloud-ruby 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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/workflows/ruby.yml +46 -0
  4. data/.gitmodules +3 -0
  5. data/.rubocop.yml +13 -0
  6. data/.tool-versions +1 -0
  7. data/CHANGELOG.md +169 -0
  8. data/CODEOWNERS +1 -0
  9. data/Gemfile +26 -0
  10. data/Gemfile.lock +188 -0
  11. data/LICENSE.txt +20 -0
  12. data/README.md +94 -0
  13. data/Rakefile +50 -0
  14. data/VERSION +1 -0
  15. data/bin/console +21 -0
  16. data/compile_protos.sh +18 -0
  17. data/lib/prefab/client.rb +153 -0
  18. data/lib/prefab/config_client.rb +292 -0
  19. data/lib/prefab/config_client_presenter.rb +18 -0
  20. data/lib/prefab/config_loader.rb +84 -0
  21. data/lib/prefab/config_resolver.rb +77 -0
  22. data/lib/prefab/config_value_unwrapper.rb +115 -0
  23. data/lib/prefab/config_value_wrapper.rb +18 -0
  24. data/lib/prefab/context.rb +179 -0
  25. data/lib/prefab/context_shape.rb +20 -0
  26. data/lib/prefab/context_shape_aggregator.rb +65 -0
  27. data/lib/prefab/criteria_evaluator.rb +136 -0
  28. data/lib/prefab/encryption.rb +65 -0
  29. data/lib/prefab/error.rb +6 -0
  30. data/lib/prefab/errors/env_var_parse_error.rb +11 -0
  31. data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
  32. data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
  33. data/lib/prefab/errors/missing_default_error.rb +13 -0
  34. data/lib/prefab/errors/missing_env_var_error.rb +11 -0
  35. data/lib/prefab/errors/uninitialized_error.rb +13 -0
  36. data/lib/prefab/evaluation.rb +52 -0
  37. data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
  38. data/lib/prefab/example_contexts_aggregator.rb +78 -0
  39. data/lib/prefab/exponential_backoff.rb +21 -0
  40. data/lib/prefab/feature_flag_client.rb +42 -0
  41. data/lib/prefab/http_connection.rb +41 -0
  42. data/lib/prefab/internal_logger.rb +16 -0
  43. data/lib/prefab/local_config_parser.rb +151 -0
  44. data/lib/prefab/log_path_aggregator.rb +69 -0
  45. data/lib/prefab/logger_client.rb +264 -0
  46. data/lib/prefab/murmer3.rb +50 -0
  47. data/lib/prefab/options.rb +208 -0
  48. data/lib/prefab/periodic_sync.rb +69 -0
  49. data/lib/prefab/prefab.rb +56 -0
  50. data/lib/prefab/rate_limit_cache.rb +41 -0
  51. data/lib/prefab/resolved_config_presenter.rb +86 -0
  52. data/lib/prefab/time_helpers.rb +7 -0
  53. data/lib/prefab/weighted_value_resolver.rb +42 -0
  54. data/lib/prefab/yaml_config_parser.rb +34 -0
  55. data/lib/prefab-cloud-ruby.rb +57 -0
  56. data/lib/prefab_pb.rb +93 -0
  57. data/prefab-cloud-ruby.gemspec +155 -0
  58. data/test/.prefab.default.config.yaml +2 -0
  59. data/test/.prefab.unit_tests.config.yaml +28 -0
  60. data/test/integration_test.rb +150 -0
  61. data/test/integration_test_helpers.rb +151 -0
  62. data/test/support/common_helpers.rb +180 -0
  63. data/test/support/mock_base_client.rb +42 -0
  64. data/test/support/mock_config_client.rb +19 -0
  65. data/test/support/mock_config_loader.rb +1 -0
  66. data/test/test_client.rb +444 -0
  67. data/test/test_config_client.rb +109 -0
  68. data/test/test_config_loader.rb +117 -0
  69. data/test/test_config_resolver.rb +430 -0
  70. data/test/test_config_value_unwrapper.rb +224 -0
  71. data/test/test_config_value_wrapper.rb +42 -0
  72. data/test/test_context.rb +203 -0
  73. data/test/test_context_shape.rb +50 -0
  74. data/test/test_context_shape_aggregator.rb +147 -0
  75. data/test/test_criteria_evaluator.rb +726 -0
  76. data/test/test_encryption.rb +16 -0
  77. data/test/test_evaluation_summary_aggregator.rb +162 -0
  78. data/test/test_example_contexts_aggregator.rb +238 -0
  79. data/test/test_exponential_backoff.rb +18 -0
  80. data/test/test_feature_flag_client.rb +48 -0
  81. data/test/test_helper.rb +17 -0
  82. data/test/test_integration.rb +58 -0
  83. data/test/test_local_config_parser.rb +147 -0
  84. data/test/test_log_path_aggregator.rb +62 -0
  85. data/test/test_logger.rb +621 -0
  86. data/test/test_logger_initialization.rb +12 -0
  87. data/test/test_options.rb +75 -0
  88. data/test/test_prefab.rb +12 -0
  89. data/test/test_rate_limit_cache.rb +44 -0
  90. data/test/test_weighted_value_resolver.rb +71 -0
  91. metadata +337 -0
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestContext < Minitest::Test
6
+ EXAMPLE_PROPERTIES = { user: { key: 'some-user-key', name: 'Ted' }, team: { key: 'abc', plan: 'pro' } }.freeze
7
+
8
+ def setup
9
+ super
10
+ Prefab::Context.current = nil
11
+ end
12
+
13
+ def test_initialize_with_empty_context
14
+ context = Prefab::Context.new({})
15
+ assert_empty context.contexts
16
+ end
17
+
18
+ def test_initialize_with_named_context
19
+ named_context = Prefab::Context::NamedContext.new('test', foo: 'bar')
20
+ context = Prefab::Context.new(named_context)
21
+ assert_equal 1, context.contexts.size
22
+ assert_equal named_context, context.contexts['test']
23
+ end
24
+
25
+ def test_initialize_with_hash
26
+ context = Prefab::Context.new(test: { foo: 'bar' })
27
+ assert_equal 1, context.contexts.size
28
+ assert_equal 'bar', context.contexts['test'].get('foo')
29
+ end
30
+
31
+ def test_initialize_with_multiple_hashes
32
+ context = Prefab::Context.new(test: { foo: 'bar' }, other: { foo: 'baz' })
33
+ assert_equal 2, context.contexts.size
34
+ assert_equal 'bar', context.contexts['test'].get('foo')
35
+ assert_equal 'baz', context.contexts['other'].get('foo')
36
+ end
37
+
38
+ def test_initialize_with_invalid_hash
39
+ _, err = capture_io do
40
+ Prefab::Context.new({ foo: 'bar', baz: 'qux' })
41
+ end
42
+
43
+ assert_match '[DEPRECATION] Prefab contexts should be a hash with a key of the context name and a value of a hash',
44
+ err
45
+ end
46
+
47
+ def test_initialize_with_invalid_argument
48
+ assert_raises(ArgumentError) { Prefab::Context.new([]) }
49
+ end
50
+
51
+ def test_current
52
+ context = Prefab::Context.current
53
+ assert_instance_of Prefab::Context, context
54
+ assert_empty context.to_h
55
+ end
56
+
57
+ def test_current_set
58
+ context = Prefab::Context.new(EXAMPLE_PROPERTIES)
59
+ Prefab::Context.current = context
60
+ assert_instance_of Prefab::Context, context
61
+ assert_equal stringify(EXAMPLE_PROPERTIES), context.to_h
62
+ end
63
+
64
+ def test_merge_with_current
65
+ context = Prefab::Context.new(EXAMPLE_PROPERTIES)
66
+ Prefab::Context.current = context
67
+ assert_equal stringify(EXAMPLE_PROPERTIES), context.to_h
68
+
69
+ new_context = Prefab::Context.merge_with_current({ user: { key: 'brand-new', other: 'different' },
70
+ address: { city: 'New York' } })
71
+ assert_equal stringify({
72
+ # Note that the user's `name` from the original
73
+ # context is not included. This is because we don't _merge_ the new
74
+ # properties if they collide with an existing context name. We _replace_
75
+ # them.
76
+ user: { key: 'brand-new', other: 'different' },
77
+ team: EXAMPLE_PROPERTIES[:team],
78
+ address: { city: 'New York' }
79
+ }),
80
+ new_context.to_h
81
+
82
+ # the original/current context is unchanged
83
+ assert_equal stringify(EXAMPLE_PROPERTIES), Prefab::Context.current.to_h
84
+ end
85
+
86
+ def test_with_context
87
+ Prefab::Context.with_context(EXAMPLE_PROPERTIES) do
88
+ context = Prefab::Context.current
89
+ assert_equal(stringify(EXAMPLE_PROPERTIES), context.to_h)
90
+ assert_equal('some-user-key', context.get('user.key'))
91
+ end
92
+ end
93
+
94
+ def test_with_context_nesting
95
+ Prefab::Context.with_context(EXAMPLE_PROPERTIES) do
96
+ Prefab::Context.with_context({ user: { key: 'abc', other: 'different' } }) do
97
+ context = Prefab::Context.current
98
+ assert_equal({ 'user' => { 'key' => 'abc', 'other' => 'different' } }, context.to_h)
99
+ end
100
+
101
+ context = Prefab::Context.current
102
+ assert_equal(stringify(EXAMPLE_PROPERTIES), context.to_h)
103
+ end
104
+ end
105
+
106
+ def test_with_context_merge_nesting
107
+ Prefab::Context.with_context(EXAMPLE_PROPERTIES) do
108
+ Prefab::Context.with_merged_context({ user: { key: 'abc', other: 'different' } }) do
109
+ context = Prefab::Context.current
110
+ assert_equal({ 'user' => { 'key' => 'abc', 'other' => 'different' }, "team"=>{"key"=>"abc", "plan"=>"pro"} }, context.to_h)
111
+ end
112
+
113
+ context = Prefab::Context.current
114
+ assert_equal(stringify(EXAMPLE_PROPERTIES), context.to_h)
115
+ end
116
+ end
117
+
118
+ def test_setting
119
+ context = Prefab::Context.new({})
120
+ context.set('user', { key: 'value' })
121
+ context.set(:other, { key: 'different', something: 'other' })
122
+ assert_equal(stringify({ user: { key: 'value' }, other: { key: 'different', something: 'other' } }), context.to_h)
123
+ end
124
+
125
+ def test_getting
126
+ context = Prefab::Context.new(EXAMPLE_PROPERTIES)
127
+ assert_equal('some-user-key', context.get('user.key'))
128
+ assert_equal('pro', context.get('team.plan'))
129
+ end
130
+
131
+ def test_dot_notation_getting
132
+ context = Prefab::Context.new({ 'user' => { 'key' => 'value' } })
133
+ assert_equal('value', context.get('user.key'))
134
+ end
135
+
136
+ def test_dot_notation_getting_with_symbols
137
+ context = Prefab::Context.new({ user: { key: 'value' } })
138
+ assert_equal('value', context.get('user.key'))
139
+ end
140
+
141
+ def test_clear
142
+ context = Prefab::Context.new(EXAMPLE_PROPERTIES)
143
+ context.clear
144
+
145
+ assert_empty context.to_h
146
+ end
147
+
148
+ def test_to_proto
149
+ namespace = "my.namespace"
150
+
151
+ contexts = Prefab::Context.new({
152
+ user: {
153
+ id: 1,
154
+ email: 'user-email'
155
+ },
156
+ team: {
157
+ id: 2,
158
+ name: 'team-name'
159
+ }
160
+ })
161
+
162
+ assert_equal PrefabProto::ContextSet.new(
163
+ contexts: [
164
+ PrefabProto::Context.new(
165
+ type: "user",
166
+ values: {
167
+ "id" => PrefabProto::ConfigValue.new(int: 1),
168
+ "email" => PrefabProto::ConfigValue.new(string: "user-email")
169
+ }
170
+ ),
171
+ PrefabProto::Context.new(
172
+ type: "team",
173
+ values: {
174
+ "id" => PrefabProto::ConfigValue.new(int: 2),
175
+ "name" => PrefabProto::ConfigValue.new(string: "team-name")
176
+ }
177
+ ),
178
+
179
+ PrefabProto::Context.new(
180
+ type: "prefab",
181
+ values: {
182
+ 'current-time' => PrefabProto::ConfigValue.new(int: Prefab::TimeHelpers.now_in_ms),
183
+ 'namespace' => PrefabProto::ConfigValue.new(string: namespace)
184
+ }
185
+ )
186
+ ]
187
+ ), contexts.to_proto(namespace)
188
+ end
189
+
190
+ private
191
+
192
+ def stringify(hash)
193
+ hash.map { |k, v| [k.to_s, stringify_keys(v)] }.to_h
194
+ end
195
+
196
+ def stringify_keys(value)
197
+ if value.is_a?(Hash)
198
+ value.transform_keys(&:to_s)
199
+ else
200
+ value
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestContextShape < Minitest::Test
6
+ class Email; end
7
+
8
+ def test_field_type_number
9
+ [
10
+ [1, 1],
11
+ [99999999999999999999999999999999999999999999, 1],
12
+ [-99999999999999999999999999999999999999999999, 1],
13
+
14
+ ['a', 2],
15
+ ['99999999999999999999999999999999999999999999', 2],
16
+
17
+ [1.0, 4],
18
+ [99999999999999999999999999999999999999999999.0, 4],
19
+ [-99999999999999999999999999999999999999999999.0, 4],
20
+
21
+ [true, 5],
22
+ [false, 5],
23
+
24
+ [[], 10],
25
+ [[1, 2, 3], 10],
26
+ [['a', 'b', 'c'], 10],
27
+
28
+ [Email.new, 2],
29
+ ].each do |value, expected|
30
+ actual = Prefab::ContextShape.field_type_number(value)
31
+
32
+ refute_nil actual, "Expected a value for input: #{value}"
33
+ assert_equal expected, actual, "Expected #{expected} for #{value}"
34
+ end
35
+ end
36
+
37
+ # If this test fails, it means that we've added a new type to the ConfigValue
38
+ def test_mapping_is_exhaustive
39
+ unsupported = [:bytes, :limit_definition, :log_level, :weighted_values, :int_range, :provided]
40
+ type_fields = PrefabProto::ConfigValue.descriptor.lookup_oneof("type").entries
41
+ supported = type_fields.entries.reject do |entry|
42
+ unsupported.include?(entry.name.to_sym)
43
+ end.map(&:number)
44
+ mapped = Prefab::ContextShape::MAPPING.values.uniq
45
+
46
+ unless mapped == supported
47
+ raise "ContextShape MAPPING needs update: #{mapped} != #{supported}"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'timecop'
5
+
6
+ class TestContextShapeAggregator < Minitest::Test
7
+ DOB = Date.new
8
+
9
+ CONTEXT_1 = Prefab::Context.new({
10
+ 'user' => {
11
+ 'name' => 'user-name',
12
+ 'email' => 'user.email',
13
+ 'age' => 42.5
14
+ },
15
+ 'subscription' => {
16
+ 'plan' => 'advanced',
17
+ 'free' => false
18
+ }
19
+ }).freeze
20
+
21
+ CONTEXT_2 = Prefab::Context.new({
22
+ 'user' => {
23
+ 'name' => 'other-user-name',
24
+ 'dob' => DOB
25
+ },
26
+ 'device' => {
27
+ 'name' => 'device-name',
28
+ 'os' => 'os-name',
29
+ 'version' => 3
30
+ }
31
+ }).freeze
32
+
33
+ CONTEXT_3 = Prefab::Context.new({
34
+ 'subscription' => {
35
+ 'plan' => 'pro',
36
+ 'trial' => true
37
+ }
38
+ }).freeze
39
+
40
+ def test_push
41
+ aggregator = new_aggregator(max_shapes: 9)
42
+
43
+ aggregator.push(CONTEXT_1)
44
+ aggregator.push(CONTEXT_2)
45
+ assert_equal 9, aggregator.data.size
46
+
47
+ # we've reached the limit so no more
48
+ aggregator.push(CONTEXT_3)
49
+ assert_equal 9, aggregator.data.size
50
+
51
+ assert_equal [['user', 'name', 2], ['user', 'email', 2], ['user', 'age', 4], ['subscription', 'plan', 2], ['subscription', 'free', 5], ['user', 'dob', 2], ['device', 'name', 2], ['device', 'os', 2], ['device', 'version', 1]],
52
+ aggregator.data.to_a
53
+
54
+ assert_only_expected_logs
55
+ end
56
+
57
+ def test_prepare_data
58
+ aggregator = new_aggregator
59
+
60
+ aggregator.push(CONTEXT_1)
61
+ aggregator.push(CONTEXT_2)
62
+ aggregator.push(CONTEXT_3)
63
+
64
+ data = aggregator.prepare_data
65
+
66
+ assert_equal %w[user subscription device], data.keys
67
+
68
+ assert_equal data['user'], {
69
+ 'name' => 2,
70
+ 'email' => 2,
71
+ 'dob' => 2,
72
+ 'age' => 4
73
+ }
74
+
75
+ assert_equal data['subscription'], {
76
+ 'plan' => 2,
77
+ 'trial' => 5,
78
+ 'free' => 5
79
+ }
80
+
81
+ assert_equal data['device'], {
82
+ 'name' => 2,
83
+ 'os' => 2,
84
+ 'version' => 1
85
+ }
86
+
87
+ assert_equal [], aggregator.data.to_a
88
+ assert_only_expected_logs
89
+ end
90
+
91
+ def test_sync
92
+ client = new_client
93
+
94
+ client.get 'some.key', 'default', CONTEXT_1
95
+ client.get 'some.key', 'default', CONTEXT_2
96
+ client.get 'some.key', 'default', CONTEXT_3
97
+
98
+ requests = wait_for_post_requests(client) do
99
+ client.context_shape_aggregator.send(:sync)
100
+ end
101
+
102
+ assert_equal [
103
+ [
104
+ '/api/v1/context-shapes',
105
+ PrefabProto::ContextShapes.new(shapes: [
106
+ PrefabProto::ContextShape.new(
107
+ name: 'user', field_types: {
108
+ 'age' => 4, 'dob' => 2, 'email' => 2, 'name' => 2
109
+ }
110
+ ),
111
+ PrefabProto::ContextShape.new(
112
+ name: 'subscription', field_types: {
113
+ 'plan' => 2, 'free' => 5, 'trial' => 5
114
+ }
115
+ ),
116
+ PrefabProto::ContextShape.new(
117
+ name: 'device', field_types: {
118
+ 'version' => 1, 'os' => 2, 'name' => 2
119
+ }
120
+ )
121
+ ])
122
+ ]
123
+ ], requests
124
+
125
+
126
+ assert_logged [
127
+ "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client.configclient No success loading checkpoints",
128
+ "WARN 2023-08-09 15:18:12 -0400: cloud.prefab.client.configclient Couldn't Initialize In 0. Key some.key. Returning what we have"
129
+ ]
130
+ end
131
+
132
+ private
133
+
134
+ def new_client(overrides = {})
135
+ super(**{
136
+ prefab_datasources: Prefab::Options::DATASOURCES::ALL,
137
+ initialization_timeout_sec: 0,
138
+ on_init_failure: Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN,
139
+ api_key: '123-development-yourapikey-SDK',
140
+ context_upload_mode: :shape_only
141
+ }.merge(overrides))
142
+ end
143
+
144
+ def new_aggregator(max_shapes: 1000)
145
+ Prefab::ContextShapeAggregator.new(client: new_client, sync_interval: 1000, max_shapes: max_shapes)
146
+ end
147
+ end