prefab-cloud-ruby 0

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