sdk-reforge 1.9.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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/pull_request_template.md +8 -0
  5. data/.github/workflows/ruby.yml +48 -0
  6. data/.gitmodules +3 -0
  7. data/.rubocop.yml +13 -0
  8. data/.tool-versions +1 -0
  9. data/CHANGELOG.md +257 -0
  10. data/CODEOWNERS +1 -0
  11. data/Gemfile +29 -0
  12. data/Gemfile.lock +182 -0
  13. data/LICENSE.txt +20 -0
  14. data/README.md +105 -0
  15. data/Rakefile +63 -0
  16. data/VERSION +1 -0
  17. data/compile_protos.sh +20 -0
  18. data/dev/allocation_stats +60 -0
  19. data/dev/benchmark +40 -0
  20. data/dev/console +12 -0
  21. data/dev/script_setup.rb +18 -0
  22. data/lib/prefab_pb.rb +77 -0
  23. data/lib/reforge/caching_http_connection.rb +95 -0
  24. data/lib/reforge/client.rb +133 -0
  25. data/lib/reforge/config_client.rb +275 -0
  26. data/lib/reforge/config_client_presenter.rb +18 -0
  27. data/lib/reforge/config_loader.rb +67 -0
  28. data/lib/reforge/config_resolver.rb +84 -0
  29. data/lib/reforge/config_value_unwrapper.rb +123 -0
  30. data/lib/reforge/config_value_wrapper.rb +18 -0
  31. data/lib/reforge/context.rb +241 -0
  32. data/lib/reforge/context_shape.rb +20 -0
  33. data/lib/reforge/context_shape_aggregator.rb +70 -0
  34. data/lib/reforge/criteria_evaluator.rb +345 -0
  35. data/lib/reforge/duration.rb +58 -0
  36. data/lib/reforge/encryption.rb +65 -0
  37. data/lib/reforge/error.rb +6 -0
  38. data/lib/reforge/errors/env_var_parse_error.rb +11 -0
  39. data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
  40. data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
  41. data/lib/reforge/errors/missing_default_error.rb +13 -0
  42. data/lib/reforge/errors/missing_env_var_error.rb +11 -0
  43. data/lib/reforge/errors/uninitialized_error.rb +13 -0
  44. data/lib/reforge/evaluation.rb +53 -0
  45. data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
  46. data/lib/reforge/example_contexts_aggregator.rb +77 -0
  47. data/lib/reforge/exponential_backoff.rb +21 -0
  48. data/lib/reforge/feature_flag_client.rb +43 -0
  49. data/lib/reforge/fixed_size_hash.rb +14 -0
  50. data/lib/reforge/http_connection.rb +45 -0
  51. data/lib/reforge/internal_logger.rb +43 -0
  52. data/lib/reforge/javascript_stub.rb +99 -0
  53. data/lib/reforge/local_config_parser.rb +151 -0
  54. data/lib/reforge/murmer3.rb +50 -0
  55. data/lib/reforge/options.rb +191 -0
  56. data/lib/reforge/periodic_sync.rb +74 -0
  57. data/lib/reforge/prefab.rb +120 -0
  58. data/lib/reforge/rate_limit_cache.rb +41 -0
  59. data/lib/reforge/resolved_config_presenter.rb +86 -0
  60. data/lib/reforge/semver.rb +132 -0
  61. data/lib/reforge/sse_config_client.rb +112 -0
  62. data/lib/reforge/time_helpers.rb +7 -0
  63. data/lib/reforge/weighted_value_resolver.rb +42 -0
  64. data/lib/reforge/yaml_config_parser.rb +34 -0
  65. data/lib/reforge-sdk.rb +57 -0
  66. data/test/fixtures/datafile.json +87 -0
  67. data/test/integration_test.rb +171 -0
  68. data/test/integration_test_helpers.rb +114 -0
  69. data/test/support/common_helpers.rb +201 -0
  70. data/test/support/mock_base_client.rb +41 -0
  71. data/test/support/mock_config_client.rb +19 -0
  72. data/test/support/mock_config_loader.rb +1 -0
  73. data/test/test_caching_http_connection.rb +218 -0
  74. data/test/test_client.rb +351 -0
  75. data/test/test_config_client.rb +84 -0
  76. data/test/test_config_loader.rb +82 -0
  77. data/test/test_config_resolver.rb +502 -0
  78. data/test/test_config_value_unwrapper.rb +270 -0
  79. data/test/test_config_value_wrapper.rb +42 -0
  80. data/test/test_context.rb +271 -0
  81. data/test/test_context_shape.rb +50 -0
  82. data/test/test_context_shape_aggregator.rb +150 -0
  83. data/test/test_criteria_evaluator.rb +1180 -0
  84. data/test/test_duration.rb +37 -0
  85. data/test/test_encryption.rb +16 -0
  86. data/test/test_evaluation_summary_aggregator.rb +162 -0
  87. data/test/test_example_contexts_aggregator.rb +233 -0
  88. data/test/test_exponential_backoff.rb +18 -0
  89. data/test/test_feature_flag_client.rb +16 -0
  90. data/test/test_fixed_size_hash.rb +119 -0
  91. data/test/test_helper.rb +17 -0
  92. data/test/test_integration.rb +75 -0
  93. data/test/test_internal_logger.rb +25 -0
  94. data/test/test_javascript_stub.rb +176 -0
  95. data/test/test_local_config_parser.rb +147 -0
  96. data/test/test_logger_initialization.rb +12 -0
  97. data/test/test_options.rb +93 -0
  98. data/test/test_prefab.rb +16 -0
  99. data/test/test_rate_limit_cache.rb +44 -0
  100. data/test/test_semver.rb +108 -0
  101. data/test/test_sse_config_client.rb +211 -0
  102. data/test/test_weighted_value_resolver.rb +71 -0
  103. metadata +345 -0
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigValueUnwrapper < Minitest::Test
6
+ CONFIG = PrefabProto::Config.new(
7
+ key: 'config_key'
8
+ )
9
+ EMPTY_CONTEXT = Reforge::Context.new()
10
+ DECRYPTION_KEY_NAME = "decryption.key"
11
+ DECRYPTION_KEY_VALUE = Reforge::Encryption.generate_new_hex_key
12
+
13
+ def setup
14
+ super
15
+ @mock_resolver = MockResolver.new
16
+ end
17
+
18
+ def test_unwrapping_int
19
+ config_value = PrefabProto::ConfigValue.new(int: 123)
20
+ assert_equal 123, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
21
+ end
22
+
23
+ def test_unwrapping_string
24
+ config_value = PrefabProto::ConfigValue.new(string: 'abc')
25
+ assert_equal 'abc', unwrap(config_value, CONFIG, EMPTY_CONTEXT)
26
+ assert_equal 'abc', reportable_value(config_value, CONFIG, EMPTY_CONTEXT)
27
+ end
28
+
29
+ def test_unwrapping_double
30
+ config_value = PrefabProto::ConfigValue.new(double: 1.23)
31
+ assert_equal 1.23, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
32
+ end
33
+
34
+ def test_unwrapping_bool
35
+ config_value = PrefabProto::ConfigValue.new(bool: true)
36
+ assert_equal true, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
37
+
38
+ config_value = PrefabProto::ConfigValue.new(bool: false)
39
+ assert_equal false, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
40
+ end
41
+
42
+ def test_unwrapping_log_level
43
+ config_value = PrefabProto::ConfigValue.new(log_level: :INFO)
44
+ assert_equal :INFO, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
45
+ end
46
+
47
+ def test_unwrapping_string_list
48
+ config_value = PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: %w[a b c]))
49
+ assert_equal %w[a b c], unwrap(config_value, CONFIG, EMPTY_CONTEXT)
50
+ end
51
+
52
+ def test_unwrapping_duration
53
+ duration = PrefabProto::IsoDuration.new(definition: "PT1.5S")
54
+ config_value = PrefabProto::ConfigValue.new(duration: duration)
55
+ assert_equal 1.5, unwrap(config_value, CONFIG, EMPTY_CONTEXT).in_seconds
56
+ assert_equal 0.025, unwrap(config_value, CONFIG, EMPTY_CONTEXT).in_minutes
57
+ assert_in_delta 0.00041667, unwrap(config_value, CONFIG, EMPTY_CONTEXT).in_hours, 0.00001
58
+
59
+ duration = PrefabProto::IsoDuration.new(definition: "P4DT12H30M5S")
60
+ config_value = PrefabProto::ConfigValue.new(duration: duration)
61
+ assert_in_delta 0.6458, unwrap(config_value, CONFIG, EMPTY_CONTEXT).in_weeks, 0.0001
62
+ assert_in_delta 4.521, unwrap(config_value, CONFIG, EMPTY_CONTEXT).in_days, 0.001
63
+ assert_in_delta 108.5, unwrap(config_value, CONFIG, EMPTY_CONTEXT).in_hours, 0.1
64
+ assert_in_delta 6510.083, unwrap(config_value, CONFIG, EMPTY_CONTEXT).in_minutes, 0.001
65
+ assert_equal 390605 ,unwrap(config_value, CONFIG, EMPTY_CONTEXT).in_seconds
66
+ end
67
+
68
+ def test_unwrapping_json
69
+ json = PrefabProto::Json.new(json: '{"a": 1, "b": "c"}')
70
+ config_value = PrefabProto::ConfigValue.new(json: json)
71
+ assert_equal({"a" => 1, "b" => "c"}, unwrap(config_value, CONFIG, EMPTY_CONTEXT))
72
+ end
73
+
74
+ def test_unwrapping_json_with_symbolize_json_names_true
75
+ @mock_resolver = MockResolver.new(symbolize_json_names: true)
76
+ json = PrefabProto::Json.new(json: '{"a": 1, "b": "c"}')
77
+ config_value = PrefabProto::ConfigValue.new(json: json)
78
+ assert_equal({:a => 1, :b => "c"}, unwrap(config_value, CONFIG, EMPTY_CONTEXT))
79
+
80
+ json = PrefabProto::Json.new(json: '{"foo": { "bar": "baz"}, "cow": "moo"}')
81
+ config_value = PrefabProto::ConfigValue.new(json: json)
82
+ assert_equal({:foo=>{:bar=>"baz"}, :cow=>"moo"}, unwrap(config_value, CONFIG, EMPTY_CONTEXT))
83
+
84
+ json = PrefabProto::Json.new(json: '[{"foo": { "bar": "baz"}}, {"cow": "moo"}]')
85
+ config_value = PrefabProto::ConfigValue.new(json: json)
86
+ assert_equal([{:foo=>{:bar=>"baz"}}, {:cow=>"moo"}], unwrap(config_value, CONFIG, EMPTY_CONTEXT))
87
+ end
88
+
89
+ def test_unwrapping_weighted_values
90
+ # single value
91
+ config_value = PrefabProto::ConfigValue.new(weighted_values: weighted_values([['abc', 1]]))
92
+
93
+ assert_equal 'abc', unwrap(config_value, CONFIG, EMPTY_CONTEXT)
94
+
95
+ # multiple values, evenly distributed
96
+ config_value = PrefabProto::ConfigValue.new(weighted_values: weighted_values([['abc', 1], ['def', 1], ['ghi', 1]]))
97
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:000'))
98
+ assert_equal 'ghi', unwrap(config_value, CONFIG, context_with_key('user:456'))
99
+ assert_equal 'abc', unwrap(config_value, CONFIG, context_with_key('user:789'))
100
+ assert_equal 'ghi', unwrap(config_value, CONFIG, context_with_key('user:888'))
101
+
102
+ # multiple values, unevenly distributed
103
+ config_value = PrefabProto::ConfigValue.new(weighted_values: weighted_values([['abc', 1], ['def', 99], ['ghi', 1]]))
104
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:123'))
105
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:456'))
106
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:789'))
107
+ assert_equal 'def', unwrap(config_value, CONFIG, context_with_key('user:012'))
108
+ assert_equal 'ghi', unwrap(config_value, CONFIG, context_with_key('user:428'))
109
+ assert_equal 'abc', unwrap(config_value, CONFIG, context_with_key('user:548'))
110
+ end
111
+
112
+ def test_unwrapping_provided_values
113
+ with_env('ENV_VAR_NAME', 'unit test value')do
114
+ value = PrefabProto::Provided.new(
115
+ source: :ENV_VAR,
116
+ lookup: "ENV_VAR_NAME"
117
+ )
118
+ config_value = PrefabProto::ConfigValue.new(provided: value)
119
+ assert_equal 'unit test value', unwrap(config_value, CONFIG, EMPTY_CONTEXT)
120
+ end
121
+ end
122
+
123
+ def test_unwrapping_provided_values_of_type_string_list
124
+ with_env('ENV_VAR_NAME', '["bob","cary"]')do
125
+ value = PrefabProto::Provided.new(
126
+ source: :ENV_VAR,
127
+ lookup: "ENV_VAR_NAME"
128
+ )
129
+ config_value = PrefabProto::ConfigValue.new(provided: value)
130
+ assert_equal ["bob", "cary"], unwrap(config_value, CONFIG, EMPTY_CONTEXT)
131
+ end
132
+ end
133
+
134
+ def test_unwrapping_provided_values_coerces_to_int
135
+ with_env('ENV_VAR_NAME', '42')do
136
+ value = PrefabProto::Provided.new(
137
+ source: :ENV_VAR,
138
+ lookup: "ENV_VAR_NAME"
139
+ )
140
+ config_value = PrefabProto::ConfigValue.new(provided: value)
141
+ assert_equal 42, unwrap(config_value, config_of(PrefabProto::Config::ValueType::INT), EMPTY_CONTEXT)
142
+ end
143
+ end
144
+
145
+ def test_unwrapping_provided_values_when_value_type_mismatch
146
+ with_env('ENV_VAR_NAME', 'not an int')do
147
+ value = PrefabProto::Provided.new(
148
+ source: :ENV_VAR,
149
+ lookup: "ENV_VAR_NAME"
150
+ )
151
+ config_value = PrefabProto::ConfigValue.new(provided: value)
152
+
153
+ assert_raises Reforge::Errors::EnvVarParseError do
154
+ unwrap(config_value, config_of(PrefabProto::Config::ValueType::INT), EMPTY_CONTEXT)
155
+ end
156
+ end
157
+ end
158
+
159
+ def test_coerce
160
+ assert_equal "string", Reforge::ConfigValueUnwrapper.coerce_into_type("string", CONFIG, "ENV")
161
+ assert_equal 42, Reforge::ConfigValueUnwrapper.coerce_into_type("42", CONFIG, "ENV")
162
+ assert_equal false, Reforge::ConfigValueUnwrapper.coerce_into_type("false", CONFIG, "ENV")
163
+ assert_equal 42.42, Reforge::ConfigValueUnwrapper.coerce_into_type("42.42", CONFIG, "ENV")
164
+ assert_equal ["a","b"], Reforge::ConfigValueUnwrapper.coerce_into_type("['a','b']", CONFIG, "ENV")
165
+
166
+ assert_equal "string", Reforge::ConfigValueUnwrapper.coerce_into_type("string", config_of(PrefabProto::Config::ValueType::STRING),"ENV")
167
+ assert_equal "42", Reforge::ConfigValueUnwrapper.coerce_into_type("42", config_of(PrefabProto::Config::ValueType::STRING),"ENV")
168
+ assert_equal "42.42", Reforge::ConfigValueUnwrapper.coerce_into_type("42.42", config_of(PrefabProto::Config::ValueType::STRING),"ENV")
169
+ assert_equal 42, Reforge::ConfigValueUnwrapper.coerce_into_type("42", config_of(PrefabProto::Config::ValueType::INT),"ENV")
170
+ assert_equal false, Reforge::ConfigValueUnwrapper.coerce_into_type("false", config_of(PrefabProto::Config::ValueType::BOOL),"ENV")
171
+ assert_equal 42.42, Reforge::ConfigValueUnwrapper.coerce_into_type("42.42", config_of(PrefabProto::Config::ValueType::DOUBLE),"ENV")
172
+ assert_equal ["a","b"], Reforge::ConfigValueUnwrapper.coerce_into_type("['a','b']", config_of(PrefabProto::Config::ValueType::STRING_LIST),"ENV")
173
+
174
+ assert_raises Reforge::Errors::EnvVarParseError do
175
+ Reforge::ConfigValueUnwrapper.coerce_into_type("not an int", config_of(PrefabProto::Config::ValueType::INT), "ENV")
176
+ end
177
+ assert_raises Reforge::Errors::EnvVarParseError do
178
+ Reforge::ConfigValueUnwrapper.coerce_into_type("not bool", config_of(PrefabProto::Config::ValueType::BOOL), "ENV")
179
+ end
180
+ assert_raises Reforge::Errors::EnvVarParseError do
181
+ Reforge::ConfigValueUnwrapper.coerce_into_type("not a double", config_of(PrefabProto::Config::ValueType::DOUBLE), "ENV")
182
+ end
183
+ assert_raises Reforge::Errors::EnvVarParseError do
184
+ Reforge::ConfigValueUnwrapper.coerce_into_type("not a list", config_of(PrefabProto::Config::ValueType::STRING_LIST), "ENV")
185
+ end
186
+ end
187
+
188
+ def test_unwrapping_provided_values_with_missing_env_var
189
+ value = PrefabProto::Provided.new(
190
+ source: :ENV_VAR,
191
+ lookup: "NON_EXISTENT_ENV_VAR_NAME"
192
+ )
193
+ config_value = PrefabProto::ConfigValue.new(provided: value)
194
+ assert_raises(Reforge::Errors::MissingEnvVarError) do
195
+ unwrap(config_value, CONFIG, EMPTY_CONTEXT)
196
+ end
197
+ end
198
+
199
+ def test_unwrapping_encrypted_values_decrypts
200
+ clear_text = "very secret stuff"
201
+ encrypted = Reforge::Encryption.new(DECRYPTION_KEY_VALUE).encrypt(clear_text)
202
+ config_value = PrefabProto::ConfigValue.new(string: encrypted, decrypt_with: "decryption.key")
203
+ assert_equal clear_text, unwrap(config_value, CONFIG, EMPTY_CONTEXT)
204
+ assert reportable_value(config_value, CONFIG, EMPTY_CONTEXT).start_with? Reforge::ConfigValueUnwrapper::CONFIDENTIAL_PREFIX
205
+ end
206
+
207
+ def test_confidential
208
+ config_value = PrefabProto::ConfigValue.new(confidential: true, string: "something confidential")
209
+ assert reportable_value(config_value, CONFIG, EMPTY_CONTEXT).start_with? Reforge::ConfigValueUnwrapper::CONFIDENTIAL_PREFIX
210
+ end
211
+
212
+ def test_unwrap_confiential_provided
213
+ with_env('PAAS_PASSWORD', "the password")do
214
+ value = PrefabProto::Provided.new(
215
+ source: :ENV_VAR,
216
+ lookup: "PAAS_PASSWORD"
217
+ )
218
+ config_value = PrefabProto::ConfigValue.new(provided: value, confidential: true)
219
+ assert_equal "the password", unwrap(config_value, CONFIG, EMPTY_CONTEXT)
220
+ assert reportable_value(config_value, CONFIG, EMPTY_CONTEXT).start_with? Reforge::ConfigValueUnwrapper::CONFIDENTIAL_PREFIX
221
+ end
222
+ end
223
+
224
+ private
225
+
226
+ def config_of(value_type)
227
+ PrefabProto::Config.new(
228
+ key: 'config-key',
229
+ value_type: value_type
230
+ )
231
+ end
232
+
233
+ def context_with_key(key)
234
+ Reforge::Context.new(user: { key: key })
235
+ end
236
+
237
+ def unwrap(config_value, config_key, context)
238
+ Reforge::ConfigValueUnwrapper.deepest_value(config_value, config_key, context, @mock_resolver).unwrap
239
+ end
240
+
241
+ def reportable_value(config_value, config_key, context)
242
+ Reforge::ConfigValueUnwrapper.deepest_value(config_value, config_key, context, @mock_resolver).reportable_value
243
+ end
244
+
245
+ class MockResolver
246
+
247
+ def initialize(symbolize_json_names: false)
248
+ @symbolize_json_names = symbolize_json_names
249
+ end
250
+
251
+ def symbolize_json_names?
252
+ @symbolize_json_names
253
+ end
254
+
255
+ def get(key)
256
+ if DECRYPTION_KEY_NAME == key
257
+ Reforge::Evaluation.new(config: PrefabProto::Config.new(key: key),
258
+ value: PrefabProto::ConfigValue.new(string: DECRYPTION_KEY_VALUE),
259
+ value_index: 0,
260
+ config_row_index: 0,
261
+ context: Reforge::Context.new,
262
+ resolver: self
263
+ )
264
+
265
+ else
266
+ raise "unexpected key"
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestConfigValueWrapper < Minitest::Test
6
+ def test_wrap_integer
7
+ result = Reforge::ConfigValueWrapper.wrap(42)
8
+ assert_instance_of PrefabProto::ConfigValue, result
9
+ assert_equal 42, result.int
10
+ end
11
+
12
+ def test_wrap_float
13
+ result = Reforge::ConfigValueWrapper.wrap(3.14)
14
+ assert_instance_of PrefabProto::ConfigValue, result
15
+ assert_equal 3.14, result.double
16
+ end
17
+
18
+ def test_wrap_boolean_true
19
+ result = Reforge::ConfigValueWrapper.wrap(true)
20
+ assert_instance_of PrefabProto::ConfigValue, result
21
+ assert_equal true, result.bool
22
+ end
23
+
24
+ def test_wrap_boolean_false
25
+ result = Reforge::ConfigValueWrapper.wrap(false)
26
+ assert_instance_of PrefabProto::ConfigValue, result
27
+ assert_equal false, result.bool
28
+ end
29
+
30
+ def test_wrap_array
31
+ result = Reforge::ConfigValueWrapper.wrap(['one', 'two', 'three'])
32
+ assert_instance_of PrefabProto::ConfigValue, result
33
+ assert_instance_of PrefabProto::StringList, result.string_list
34
+ assert_equal ['one', 'two', 'three'], result.string_list.values
35
+ end
36
+
37
+ def test_wrap_string
38
+ result = Reforge::ConfigValueWrapper.wrap('hello')
39
+ assert_instance_of PrefabProto::ConfigValue, result
40
+ assert_equal 'hello', result.string
41
+ end
42
+ end
@@ -0,0 +1,271 @@
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
+ Reforge::Context.current = nil
11
+ end
12
+
13
+ def test_initialize_with_empty_context
14
+ context = Reforge::Context.new({})
15
+ assert_empty context.contexts
16
+ end
17
+
18
+ def test_initialize_with_hash
19
+ context = Reforge::Context.new(test: { foo: 'bar' })
20
+ assert_equal 1, context.contexts.size
21
+ assert_equal 'bar', context.get("test.foo")
22
+ end
23
+
24
+ def test_initialize_with_multiple_hashes
25
+ context = Reforge::Context.new(test: { foo: 'bar' }, other: { foo: 'baz' })
26
+ assert_equal 2, context.contexts.size
27
+ assert_equal 'bar', context.get("test.foo")
28
+ assert_equal 'baz', context.get("other.foo")
29
+ end
30
+
31
+ def test_initialize_with_invalid_argument
32
+ assert_raises(ArgumentError) { Reforge::Context.new([]) }
33
+ end
34
+
35
+ def test_current
36
+ context = Reforge::Context.current
37
+ assert_instance_of Reforge::Context, context
38
+ assert_empty context.to_h
39
+ end
40
+
41
+ def test_current_set
42
+ context = Reforge::Context.new(EXAMPLE_PROPERTIES)
43
+ Reforge::Context.current = context.to_h
44
+ assert_instance_of Reforge::Context, context
45
+ assert_equal stringify(EXAMPLE_PROPERTIES), context.to_h
46
+ end
47
+
48
+ def test_with_context
49
+ Reforge::Context.with_context(EXAMPLE_PROPERTIES) do
50
+ context = Reforge::Context.current
51
+ assert_equal(stringify(EXAMPLE_PROPERTIES), context.to_h)
52
+ assert_equal('some-user-key', context.get('user.key'))
53
+ end
54
+ end
55
+
56
+ def test_with_context_nesting
57
+ Reforge::Context.with_context(EXAMPLE_PROPERTIES) do
58
+ Reforge::Context.with_context({ user: { key: 'abc', other: 'different' } }) do
59
+ context = Reforge::Context.current
60
+ assert_equal({ 'user' => { 'key' => 'abc', 'other' => 'different' } }, context.to_h)
61
+ end
62
+
63
+ context = Reforge::Context.current
64
+ assert_equal(stringify(EXAMPLE_PROPERTIES), context.to_h)
65
+ end
66
+ end
67
+
68
+ def test_with_context_merge_nesting
69
+ Reforge::Context.with_context(EXAMPLE_PROPERTIES) do
70
+ Reforge::Context.with_merged_context({ user: { key: 'hij', other: 'different' } }) do
71
+ context = Reforge::Context.current
72
+ assert_nil context.get('user.name')
73
+ assert_equal context.get('user.key'), 'hij'
74
+ assert_equal context.get('user.other'), 'different'
75
+
76
+ assert_equal context.get('team.key'), 'abc'
77
+ assert_equal context.get('team.plan'), 'pro'
78
+ end
79
+
80
+ context = Reforge::Context.current
81
+ assert_equal(stringify(EXAMPLE_PROPERTIES), context.to_h)
82
+ end
83
+ end
84
+
85
+ def test_setting
86
+ context = Reforge::Context.new({})
87
+ context.set('user', { key: 'value' })
88
+ context.set(:other, { key: 'different', something: 'other' })
89
+ assert_equal(stringify({ user: { key: 'value' }, other: { key: 'different', something: 'other' } }), context.to_h)
90
+ end
91
+
92
+ def test_getting
93
+ context = Reforge::Context.new(EXAMPLE_PROPERTIES)
94
+ assert_equal('some-user-key', context.get('user.key'))
95
+ assert_equal('pro', context.get('team.plan'))
96
+ end
97
+
98
+ def test_dot_notation_getting
99
+ context = Reforge::Context.new({ 'user' => { 'key' => 'value' } })
100
+ assert_equal('value', context.get('user.key'))
101
+ end
102
+
103
+ def test_dot_notation_getting_with_symbols
104
+ context = Reforge::Context.new({ user: { key: 'value' } })
105
+ assert_equal('value', context.get('user.key'))
106
+ end
107
+
108
+ def test_clear
109
+ context = Reforge::Context.new(EXAMPLE_PROPERTIES)
110
+ context.clear
111
+
112
+ assert_empty context.to_h
113
+ end
114
+
115
+ def test_to_proto
116
+ namespace = "my.namespace"
117
+
118
+ contexts = Reforge::Context.new({
119
+ user: {
120
+ id: 1,
121
+ email: 'user-email'
122
+ },
123
+ team: {
124
+ id: 2,
125
+ name: 'team-name'
126
+ }
127
+ })
128
+
129
+ assert_equal PrefabProto::ContextSet.new(
130
+ contexts: [
131
+ PrefabProto::Context.new(
132
+ type: "user",
133
+ values: {
134
+ "id" => PrefabProto::ConfigValue.new(int: 1),
135
+ "email" => PrefabProto::ConfigValue.new(string: "user-email")
136
+ }
137
+ ),
138
+ PrefabProto::Context.new(
139
+ type: "team",
140
+ values: {
141
+ "id" => PrefabProto::ConfigValue.new(int: 2),
142
+ "name" => PrefabProto::ConfigValue.new(string: "team-name")
143
+ }
144
+ )
145
+ ]
146
+ ), contexts.to_proto(namespace)
147
+ end
148
+
149
+ def test_to_proto_with_parent
150
+ global_context = { cpu: { count: 4, speed: '2.4GHz' }, clock: { timezone: 'UTC' }, magic: { key: "global-key" } }
151
+ default_context = { 'prefab-api-key' => { 'user-id' => 123 } }
152
+
153
+ Reforge::Context.global_context = global_context
154
+ Reforge::Context.default_context = default_context
155
+
156
+ Reforge::Context.current = {
157
+ user: { id: 2, email: 'parent-email' },
158
+ magic: { key: 'parent-key', rabbits: 3 },
159
+ clock: { timezone: 'PST' }
160
+ }
161
+
162
+ contexts = Reforge::Context.join(hash: {
163
+ user: {
164
+ id: 1,
165
+ email: 'user-email'
166
+ },
167
+ team: {
168
+ id: 2,
169
+ name: 'team-name'
170
+ }
171
+ }, id: :jit, parent: Reforge::Context.current)
172
+
173
+ expected = PrefabProto::ContextSet.new(
174
+ contexts: [
175
+ # Via global
176
+ PrefabProto::Context.new(
177
+ type: "cpu",
178
+ values: {
179
+ "count" => PrefabProto::ConfigValue.new(int: 4),
180
+ "speed" => PrefabProto::ConfigValue.new(string: "2.4GHz")
181
+ }
182
+ ),
183
+ # Via default
184
+ PrefabProto::Context.new(
185
+ type: "clock",
186
+ values: {
187
+ "timezone" => PrefabProto::ConfigValue.new(string: 'PST'),
188
+ }
189
+ ),
190
+ # via current
191
+ PrefabProto::Context.new(
192
+ type: "magic",
193
+ values: {
194
+ "key" => PrefabProto::ConfigValue.new(string: 'parent-key'),
195
+ "rabbits" => PrefabProto::ConfigValue.new(int: 3)
196
+ }
197
+ ),
198
+ # via default
199
+ PrefabProto::Context.new(
200
+ type: "prefab-api-key",
201
+ values: {
202
+ "user-id" => PrefabProto::ConfigValue.new(int: 123)
203
+ }
204
+ ),
205
+ # via jit
206
+ PrefabProto::Context.new(
207
+ type: "user",
208
+ values: {
209
+ "id" => PrefabProto::ConfigValue.new(int: 1),
210
+ "email" => PrefabProto::ConfigValue.new(string: "user-email")
211
+ }
212
+ ),
213
+ # via jit
214
+ PrefabProto::Context.new(
215
+ type: "team",
216
+ values: {
217
+ "id" => PrefabProto::ConfigValue.new(int: 2),
218
+ "name" => PrefabProto::ConfigValue.new(string: "team-name")
219
+ }
220
+ ),
221
+ ]
222
+ )
223
+
224
+ actual = contexts.to_proto("")
225
+
226
+ assert_equal expected, actual
227
+ end
228
+
229
+ def test_parent_lookup
230
+ global_context = { cpu: { count: 4, speed: '2.4GHz' }, clock: { timezone: 'UTC' } }
231
+ default_context = { 'prefab-api-key' => { 'user-id' => 123 } }
232
+ local_context = { clock: { timezone: 'PST' }, user: { name: 'Ted', email: 'ted@example.com' } }
233
+ jit_context = { user: { name: 'Frank' } }
234
+
235
+ Reforge::Context.global_context = global_context
236
+ Reforge::Context.default_context = default_context
237
+ Reforge::Context.current = local_context
238
+
239
+ context = Reforge::Context.join(parent: Reforge::Context.current, hash: jit_context, id: :jit)
240
+
241
+ # This digs all the way to the global context
242
+ assert_equal 4, context.get('cpu.count')
243
+ assert_equal '2.4GHz', context.get('cpu.speed')
244
+
245
+ # This digs to the default context
246
+ assert_equal 123, context.get('prefab-api-key.user-id')
247
+
248
+ # This digs to the local context
249
+ assert_equal 'PST', context.get('clock.timezone')
250
+
251
+ # This uses the jit context
252
+ assert_equal 'Frank', context.get('user.name')
253
+
254
+ # This is nil in the jit context because `user` was clobbered
255
+ assert_nil context.get('user.email')
256
+ end
257
+
258
+ private
259
+
260
+ def stringify(hash)
261
+ hash.map { |k, v| [k.to_s, stringify_keys(v)] }.to_h
262
+ end
263
+
264
+ def stringify_keys(value)
265
+ if value.is_a?(Hash)
266
+ value.transform_keys(&:to_s)
267
+ else
268
+ value
269
+ end
270
+ end
271
+ 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 = Reforge::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, :duration, :json, :schema]
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 = Reforge::ContextShape::MAPPING.values.uniq
45
+
46
+ unless mapped == supported
47
+ raise "ContextShape MAPPING needs update: #{mapped} != #{supported}"
48
+ end
49
+ end
50
+ end