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.
- checksums.yaml +7 -0
- data/.envrc.sample +3 -0
- data/.github/workflows/ruby.yml +46 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +169 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +188 -0
- data/LICENSE.txt +20 -0
- data/README.md +94 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/bin/console +21 -0
- data/compile_protos.sh +18 -0
- data/lib/prefab/client.rb +153 -0
- data/lib/prefab/config_client.rb +292 -0
- data/lib/prefab/config_client_presenter.rb +18 -0
- data/lib/prefab/config_loader.rb +84 -0
- data/lib/prefab/config_resolver.rb +77 -0
- data/lib/prefab/config_value_unwrapper.rb +115 -0
- data/lib/prefab/config_value_wrapper.rb +18 -0
- data/lib/prefab/context.rb +179 -0
- data/lib/prefab/context_shape.rb +20 -0
- data/lib/prefab/context_shape_aggregator.rb +65 -0
- data/lib/prefab/criteria_evaluator.rb +136 -0
- data/lib/prefab/encryption.rb +65 -0
- data/lib/prefab/error.rb +6 -0
- data/lib/prefab/errors/env_var_parse_error.rb +11 -0
- data/lib/prefab/errors/initialization_timeout_error.rb +13 -0
- data/lib/prefab/errors/invalid_api_key_error.rb +19 -0
- data/lib/prefab/errors/missing_default_error.rb +13 -0
- data/lib/prefab/errors/missing_env_var_error.rb +11 -0
- data/lib/prefab/errors/uninitialized_error.rb +13 -0
- data/lib/prefab/evaluation.rb +52 -0
- data/lib/prefab/evaluation_summary_aggregator.rb +87 -0
- data/lib/prefab/example_contexts_aggregator.rb +78 -0
- data/lib/prefab/exponential_backoff.rb +21 -0
- data/lib/prefab/feature_flag_client.rb +42 -0
- data/lib/prefab/http_connection.rb +41 -0
- data/lib/prefab/internal_logger.rb +16 -0
- data/lib/prefab/local_config_parser.rb +151 -0
- data/lib/prefab/log_path_aggregator.rb +69 -0
- data/lib/prefab/logger_client.rb +264 -0
- data/lib/prefab/murmer3.rb +50 -0
- data/lib/prefab/options.rb +208 -0
- data/lib/prefab/periodic_sync.rb +69 -0
- data/lib/prefab/prefab.rb +56 -0
- data/lib/prefab/rate_limit_cache.rb +41 -0
- data/lib/prefab/resolved_config_presenter.rb +86 -0
- data/lib/prefab/time_helpers.rb +7 -0
- data/lib/prefab/weighted_value_resolver.rb +42 -0
- data/lib/prefab/yaml_config_parser.rb +34 -0
- data/lib/prefab-cloud-ruby.rb +57 -0
- data/lib/prefab_pb.rb +93 -0
- data/prefab-cloud-ruby.gemspec +155 -0
- data/test/.prefab.default.config.yaml +2 -0
- data/test/.prefab.unit_tests.config.yaml +28 -0
- data/test/integration_test.rb +150 -0
- data/test/integration_test_helpers.rb +151 -0
- data/test/support/common_helpers.rb +180 -0
- data/test/support/mock_base_client.rb +42 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_client.rb +444 -0
- data/test/test_config_client.rb +109 -0
- data/test/test_config_loader.rb +117 -0
- data/test/test_config_resolver.rb +430 -0
- data/test/test_config_value_unwrapper.rb +224 -0
- data/test/test_config_value_wrapper.rb +42 -0
- data/test/test_context.rb +203 -0
- data/test/test_context_shape.rb +50 -0
- data/test/test_context_shape_aggregator.rb +147 -0
- data/test/test_criteria_evaluator.rb +726 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +238 -0
- data/test/test_exponential_backoff.rb +18 -0
- data/test/test_feature_flag_client.rb +48 -0
- data/test/test_helper.rb +17 -0
- data/test/test_integration.rb +58 -0
- data/test/test_local_config_parser.rb +147 -0
- data/test/test_log_path_aggregator.rb +62 -0
- data/test/test_logger.rb +621 -0
- data/test/test_logger_initialization.rb +12 -0
- data/test/test_options.rb +75 -0
- data/test/test_prefab.rb +12 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_weighted_value_resolver.rb +71 -0
- 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
|