prefab-cloud-ruby 0.23.8 → 0.24.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class ResolvedConfigPresenter
5
+ class ConfigRow
6
+ include Comparable
7
+
8
+ attr_reader :key, :value, :match, :source
9
+
10
+ def initialize(key, value, match, source)
11
+ @key = key
12
+ @value = value
13
+ @match = match
14
+ @source = source
15
+ end
16
+
17
+ def <=>(other)
18
+ inspect <=> other.inspect
19
+ end
20
+
21
+ def inspect
22
+ [@key, @value, @match, @source].inspect
23
+ end
24
+ end
25
+
26
+ def initialize(resolver, lock, local_store)
27
+ @resolver = resolver
28
+ @lock = lock
29
+ @local_store = local_store
30
+ end
31
+
32
+ def each(&block)
33
+ to_h.each(&block)
34
+ end
35
+
36
+ def to_h
37
+ hash = {}
38
+
39
+ Prefab::Context.with_context({}) do
40
+ @lock.with_read_lock do
41
+ @local_store.keys.sort.each do |k|
42
+ v = @local_store[k]
43
+
44
+ if v.nil?
45
+ hash[k] = ConfigRow.new(k, nil, nil, nil)
46
+ else
47
+ config = @resolver.evaluate(v[:config])
48
+ value = Prefab::ConfigValueUnwrapper.unwrap(config, k, {})
49
+ hash[k] = ConfigRow.new(k, value, v[:match], v[:source])
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ hash
56
+ end
57
+
58
+ def to_s
59
+ str = "\n"
60
+
61
+ Prefab::Context.with_context({}) do
62
+ @lock.with_read_lock do
63
+ @local_store.keys.sort.each do |k|
64
+ v = @local_store[k]
65
+ elements = [k.slice(0..49).ljust(50)]
66
+ if v.nil?
67
+ elements << 'tombstone'
68
+ else
69
+ config = @resolver.evaluate(v[:config], {})
70
+ value = Prefab::ConfigValueUnwrapper.unwrap(config, k, {})
71
+ elements << value.to_s.slice(0..34).ljust(35)
72
+ elements << value.class.to_s.slice(0..6).ljust(7)
73
+ elements << "Match: #{v[:match]}".slice(0..29).ljust(30)
74
+ elements << "Source: #{v[:source]}"
75
+ end
76
+ str += elements.join(' | ') << "\n"
77
+ end
78
+ end
79
+ end
80
+
81
+ str
82
+ end
83
+ end
84
+ end
@@ -4,14 +4,14 @@ module Prefab
4
4
  class WeightedValueResolver
5
5
  MAX_32_FLOAT = 4_294_967_294.0
6
6
 
7
- def initialize(weights, config_key, lookup_key)
7
+ def initialize(weights, config_key, context_hash_value)
8
8
  @weights = weights
9
9
  @config_key = config_key
10
- @lookup_key = lookup_key
10
+ @context_hash_value = context_hash_value
11
11
  end
12
12
 
13
13
  def resolve
14
- percent = @lookup_key ? user_percent : rand
14
+ percent = @context_hash_value ? user_percent : rand
15
15
 
16
16
  index = variant_index(percent)
17
17
 
@@ -19,7 +19,7 @@ module Prefab
19
19
  end
20
20
 
21
21
  def user_percent
22
- to_hash = "#{@config_key}#{@lookup_key}"
22
+ to_hash = "#{@config_key}#{@context_hash_value}"
23
23
  int_value = Murmur3.murmur3_32(to_hash)
24
24
  int_value / MAX_32_FLOAT
25
25
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module Prefab
4
+ NO_DEFAULT_PROVIDED = :no_default_provided
5
+ end
6
+
3
7
  require 'concurrent/atomics'
4
8
  require 'concurrent'
5
9
  require 'faraday'
@@ -21,8 +25,10 @@ require 'prefab/criteria_evaluator'
21
25
  require 'prefab/config_loader'
22
26
  require 'prefab/local_config_parser'
23
27
  require 'prefab/yaml_config_parser'
28
+ require 'prefab/resolved_config_presenter'
24
29
  require 'prefab/config_resolver'
25
30
  require 'prefab/http_connection'
31
+ require 'prefab/context'
26
32
  require 'prefab/client'
27
33
  require 'prefab/config_client'
28
34
  require 'prefab/feature_flag_client'
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: prefab-cloud-ruby 0.23.8 ruby lib
5
+ # stub: prefab-cloud-ruby 0.24.1 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "prefab-cloud-ruby".freeze
9
- s.version = "0.23.8"
9
+ s.version = "0.24.1"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Jeff Dwyer".freeze]
14
- s.date = "2023-04-21"
14
+ s.date = "2023-04-26"
15
15
  s.description = "Feature Flags, Live Config, and Dynamic Log Levels as a service".freeze
16
16
  s.email = "jdwyer@prefab.cloud".freeze
17
17
  s.extra_rdoc_files = [
@@ -38,6 +38,7 @@ Gem::Specification.new do |s|
38
38
  "lib/prefab/config_loader.rb",
39
39
  "lib/prefab/config_resolver.rb",
40
40
  "lib/prefab/config_value_unwrapper.rb",
41
+ "lib/prefab/context.rb",
41
42
  "lib/prefab/criteria_evaluator.rb",
42
43
  "lib/prefab/error.rb",
43
44
  "lib/prefab/errors/initialization_timeout_error.rb",
@@ -54,6 +55,7 @@ Gem::Specification.new do |s|
54
55
  "lib/prefab/noop_cache.rb",
55
56
  "lib/prefab/noop_stats.rb",
56
57
  "lib/prefab/options.rb",
58
+ "lib/prefab/resolved_config_presenter.rb",
57
59
  "lib/prefab/sse_logger.rb",
58
60
  "lib/prefab/weighted_value_resolver.rb",
59
61
  "lib/prefab/yaml_config_parser.rb",
@@ -68,6 +70,7 @@ Gem::Specification.new do |s|
68
70
  "test/test_config_loader.rb",
69
71
  "test/test_config_resolver.rb",
70
72
  "test/test_config_value_unwrapper.rb",
73
+ "test/test_context.rb",
71
74
  "test/test_criteria_evaluator.rb",
72
75
  "test/test_exponential_backoff.rb",
73
76
  "test/test_feature_flag_client.rb",
@@ -9,8 +9,9 @@ sample: test sample value
9
9
  enabled_flag: true
10
10
  disabled_flag: false
11
11
  flag_with_a_value: { "feature_flag": "true", value: "all-features" }
12
- in_lookup_key: { "feature_flag": "true", value: true, criterion: { operator: LOOKUP_KEY_IN, values: [ "abc123", "xyz987" ] } }
13
- just_my_domain: { "feature_flag": "true", value: "new-version", criterion: { operator: PROP_IS_ONE_OF, property: "domain", values: [ "prefab.cloud", "example.com" ] } }
12
+ user_key_match: { "feature_flag": "true", value: true, criterion: { operator: PROP_IS_ONE_OF, values: [ "abc123", "xyz987" ], property: "user.key" } }
13
+ just_my_domain: { "feature_flag": "true", value: "new-version", criterion: { operator: PROP_IS_ONE_OF, property: "user.domain", values: [ "prefab.cloud", "example.com" ] } }
14
+ deprecated_no_dot_notation: { "feature_flag": "true", value: true, criterion: { operator: PROP_IS_ONE_OF, property: "domain", values: [ "prefab.cloud", "example.com" ] } }
14
15
 
15
16
  nested:
16
17
  values:
@@ -1,18 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class IntegrationTest
4
- attr_reader :func
5
- attr_reader :input
6
- attr_reader :expected
7
- attr_reader :test_client
4
+ attr_reader :func, :input, :expected, :test_client
8
5
 
9
6
  def initialize(test_data)
10
7
  @client_overrides = parse_client_overrides(test_data['client_overrides'])
11
8
  @func = parse_function(test_data['function'])
12
9
  @input = parse_input(test_data['input'])
13
10
  @expected = parse_expected(test_data['expected'])
14
- test_client = :"#{test_data['client']}"
15
- @test_client = base_client.send(test_client)
11
+ @test_client = base_client
16
12
  end
17
13
 
18
14
  def test_type
@@ -40,7 +36,7 @@ class IntegrationTest
40
36
  def parse_function(function)
41
37
  case function
42
38
  when 'get_or_raise' then :get
43
- when 'enabled' then :feature_is_on_for?
39
+ when 'enabled' then :enabled?
44
40
  else :"#{function}"
45
41
  end
46
42
  end
@@ -62,7 +58,7 @@ class IntegrationTest
62
58
  end
63
59
 
64
60
  def parse_ff_input(input)
65
- [input['flag'], input['lookup_key'], input['properties'] || {}]
61
+ [input['flag'], input['context']]
66
62
  end
67
63
 
68
64
  def parse_expected(expected)
data/test/test_client.rb CHANGED
@@ -8,8 +8,12 @@ class TestClient < Minitest::Test
8
8
  end
9
9
 
10
10
  def test_get
11
- assert_equal 'test sample value', @client.get('sample')
12
- assert_equal 123, @client.get('sample_int')
11
+ _, err = capture_io do
12
+ assert_equal 'default', @client.get('does.not.exist', 'default')
13
+ assert_equal 'test sample value', @client.get('sample')
14
+ assert_equal 123, @client.get('sample_int')
15
+ end
16
+ assert_equal '', err
13
17
  end
14
18
 
15
19
  def test_get_with_default
@@ -44,32 +48,36 @@ class TestClient < Minitest::Test
44
48
  assert_equal false, @client.enabled?('flag_with_a_value')
45
49
  end
46
50
 
47
- def test_ff_enabled_with_lookup_key
48
- assert_equal false, @client.enabled?('in_lookup_key', 'jimmy')
49
- assert_equal true, @client.enabled?('in_lookup_key', 'abc123')
50
- assert_equal true, @client.enabled?('in_lookup_key', 'xyz987')
51
+ def test_ff_enabled_with_user_key_match
52
+ assert_equal_context_and_jit(false, :enabled?, 'user_key_match', { user: { key: 'jimmy' } })
53
+ assert_equal_context_and_jit(true, :enabled?, 'user_key_match', { user: { key: 'abc123' } })
54
+ assert_equal_context_and_jit(true, :enabled?, 'user_key_match', { user: { key: 'xyz987' } })
51
55
  end
52
56
 
53
- def test_ff_get_with_lookup_key
54
- assert_nil @client.get('in_lookup_key', 'jimmy')
55
- assert_equal 'DEFAULT', @client.get('in_lookup_key', 'jimmy', {}, 'DEFAULT')
56
-
57
- assert_equal true, @client.get('in_lookup_key', 'abc123')
58
- assert_equal true, @client.get('in_lookup_key', 'xyz987')
57
+ def test_ff_enabled_with_context
58
+ assert_equal_context_and_jit(false, :enabled?, 'just_my_domain', user: { domain: 'gmail.com' })
59
+ assert_equal_context_and_jit(false, :enabled?, 'just_my_domain', user: { domain: 'prefab.cloud' })
60
+ assert_equal_context_and_jit(false, :enabled?, 'just_my_domain', user: { domain: 'example.com' })
59
61
  end
60
62
 
61
- def test_ff_enabled_with_attributes
62
- assert_equal false, @client.enabled?('just_my_domain', 'abc123', { domain: 'gmail.com' })
63
- assert_equal false, @client.enabled?('just_my_domain', 'abc123', { domain: 'prefab.cloud' })
64
- assert_equal false, @client.enabled?('just_my_domain', 'abc123', { domain: 'example.com' })
63
+ def test_ff_get_with_context
64
+ assert_nil @client.get('just_my_domain', 'abc123', user: { domain: 'gmail.com' })
65
+ assert_equal 'DEFAULT', @client.get('just_my_domain', 'abc123', { user: { domain: 'gmail.com' } }, 'DEFAULT')
66
+
67
+ assert_equal_context_and_jit('new-version', :get, 'just_my_domain', { user: { domain: 'prefab.cloud' } })
68
+ assert_equal_context_and_jit('new-version', :get, 'just_my_domain', { user: { domain: 'example.com' } })
65
69
  end
66
70
 
67
- def test_ff_get_with_attributes
68
- assert_nil @client.get('just_my_domain', 'abc123', { domain: 'gmail.com' })
69
- assert_equal 'DEFAULT', @client.get('just_my_domain', 'abc123', { domain: 'gmail.com' }, 'DEFAULT')
71
+ def test_deprecated_no_dot_notation_ff_enabled_with_jit_context
72
+ # with no lookup key
73
+ assert_equal false, @client.enabled?('deprecated_no_dot_notation', { domain: 'gmail.com' })
74
+ assert_equal true, @client.enabled?('deprecated_no_dot_notation', { domain: 'prefab.cloud' })
75
+ assert_equal true, @client.enabled?('deprecated_no_dot_notation', { domain: 'example.com' })
70
76
 
71
- assert_equal 'new-version', @client.get('just_my_domain', 'abc123', { domain: 'prefab.cloud' })
72
- assert_equal 'new-version', @client.get('just_my_domain', 'abc123', { domain: 'example.com' })
77
+ # with a lookup key
78
+ assert_equal false, @client.enabled?('deprecated_no_dot_notation', 'some-lookup-key', { domain: 'gmail.com' })
79
+ assert_equal true, @client.enabled?('deprecated_no_dot_notation', 'some-lookup-key', { domain: 'prefab.cloud' })
80
+ assert_equal true, @client.enabled?('deprecated_no_dot_notation', 'some-lookup-key', { domain: 'example.com' })
73
81
  end
74
82
 
75
83
  def test_getting_feature_flag_value
@@ -103,14 +111,11 @@ class TestClient < Minitest::Test
103
111
 
104
112
  private
105
113
 
106
- def new_client(overrides = {})
107
- options = Prefab::Options.new(**{
108
- prefab_config_override_dir: 'none',
109
- prefab_config_classpath_dir: 'test',
110
- prefab_envs: ['unit_tests'],
111
- prefab_datasources: Prefab::Options::DATASOURCES::LOCAL_ONLY
112
- }.merge(overrides))
114
+ def assert_equal_context_and_jit(expected, method, key, context)
115
+ assert_equal expected, @client.send(method, key, context)
113
116
 
114
- Prefab::Client.new(options)
117
+ Prefab::Context.with_context(context) do
118
+ assert_equal expected, @client.send(method, key)
119
+ end
115
120
  end
116
121
  end
@@ -9,10 +9,19 @@ class TestConfigResolver < Minitest::Test
9
9
  SEGMENT_KEY = 'segment_key'
10
10
  CONFIG_KEY = 'config_key'
11
11
  DEFAULT_VALUE = 'default_value'
12
+ DESIRED_VALUE = 'desired_value'
12
13
  IN_SEGMENT_VALUE = 'in_segment_value'
13
14
  WRONG_ENV_VALUE = 'wrong_env_value'
14
15
  NOT_IN_SEGMENT_VALUE = 'not_in_segment_value'
15
16
 
17
+ DEFAULT_ROW = Prefab::ConfigRow.new(
18
+ values: [
19
+ Prefab::ConditionalValue.new(
20
+ value: Prefab::ConfigValue.new(string: DEFAULT_VALUE)
21
+ )
22
+ ]
23
+ )
24
+
16
25
  def test_resolution
17
26
  @loader = MockConfigLoader.new
18
27
 
@@ -20,13 +29,7 @@ class TestConfigResolver < Minitest::Test
20
29
  'key' => { config: Prefab::Config.new(
21
30
  key: 'key',
22
31
  rows: [
23
- Prefab::ConfigRow.new(
24
- values: [
25
- Prefab::ConditionalValue.new(
26
- value: Prefab::ConfigValue.new(string: 'value_no_env_default')
27
- )
28
- ]
29
- ),
32
+ DEFAULT_ROW,
30
33
  Prefab::ConfigRow.new(
31
34
  project_env_id: TEST_ENV_ID,
32
35
  values: [
@@ -114,31 +117,49 @@ class TestConfigResolver < Minitest::Test
114
117
 
115
118
  @loader.stub :calc_config, loaded_values do
116
119
  @resolverA = resolver_for_namespace('', @loader, project_env_id: PRODUCTION_ENV_ID)
117
- assert_equal 'value_no_env_default', @resolverA.get('key', nil).string
120
+ assert_equal_context_and_jit DEFAULT_VALUE, @resolverA, 'key', {}, :string
118
121
 
119
122
  ## below here in the test env
120
123
  @resolverA = resolver_for_namespace('', @loader)
121
- assert_equal 'value_none', @resolverA.get('key', nil).string
124
+ assert_equal_context_and_jit 'value_none', @resolverA, 'key', {}, :string
122
125
 
123
126
  @resolverA = resolver_for_namespace('projectA', @loader)
124
- assert_equal 'valueA', @resolverA.get('key', nil).string
127
+ assert_equal_context_and_jit 'valueA', @resolverA, 'key', {}, :string
125
128
 
126
129
  @resolverB = resolver_for_namespace('projectB', @loader)
127
- assert_equal 'valueB', @resolverB.get('key', nil).string
130
+ assert_equal_context_and_jit 'valueB', @resolverB, 'key', {}, :string
128
131
 
129
132
  @resolverBX = resolver_for_namespace('projectB.subprojectX', @loader)
130
- assert_equal 'projectB.subprojectX', @resolverBX.get('key', nil).string
133
+ assert_equal_context_and_jit 'projectB.subprojectX', @resolverBX, 'key', {}, :string
131
134
 
132
135
  @resolverBX = resolver_for_namespace('projectB.subprojectX', @loader)
133
- assert_equal 'valueB2', @resolverBX.get('key2', nil).string
136
+ assert_equal_context_and_jit 'valueB2', @resolverBX, 'key2', {}, :string
134
137
 
135
- @resolverUndefinedSubProject = resolver_for_namespace('projectB.subprojectX.subsubQ', @loader)
136
- assert_equal 'projectB.subprojectX', @resolverUndefinedSubProject.get('key', nil).string
138
+ @resolverUndefinedSubProject = resolver_for_namespace('projectB.subprojectX.subsubQ',
139
+ @loader)
140
+ assert_equal_context_and_jit 'projectB.subprojectX', @resolverUndefinedSubProject, 'key',
141
+ {}, :string
137
142
 
138
143
  @resolverBX = resolver_for_namespace('projectC', @loader)
139
- assert_equal 'value_none', @resolverBX.get('key', nil).string
144
+ assert_equal_context_and_jit 'value_none', @resolverBX, 'key', {}, :string
140
145
 
141
146
  assert_nil @resolverBX.get('key_that_doesnt_exist', nil)
147
+
148
+ assert_equal @resolverBX.to_s.strip.split("\n").map(&:strip), [
149
+ 'key | value_none | String | Match: | Source:',
150
+ 'key2 | valueB2 | String | Match: | Source:'
151
+ ]
152
+
153
+ assert_equal @resolverBX.presenter.to_h, {
154
+ 'key' => Prefab::ResolvedConfigPresenter::ConfigRow.new('key', 'value_none', nil, nil),
155
+ 'key2' => Prefab::ResolvedConfigPresenter::ConfigRow.new('key2', 'valueB2', nil, nil)
156
+ }
157
+
158
+ resolved_lines = []
159
+ @resolverBX.presenter.each do |key, row|
160
+ resolved_lines << [key, row.value]
161
+ end
162
+ assert_equal resolved_lines, [%w[key value_none], %w[key2 valueB2]]
142
163
  end
143
164
  end
144
165
 
@@ -155,7 +176,7 @@ class TestConfigResolver < Minitest::Test
155
176
  Prefab::Criterion.new(
156
177
  operator: Prefab::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
157
178
  value_to_match: string_list(['hotmail.com', 'gmail.com']),
158
- property_name: 'email'
179
+ property_name: 'user.email'
159
180
  )
160
181
  ]
161
182
  ),
@@ -222,8 +243,10 @@ class TestConfigResolver < Minitest::Test
222
243
  resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
223
244
  resolver.project_env_id = PRODUCTION_ENV_ID
224
245
 
225
- assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY, nil, { email: 'test@something-else.com' }).string
226
- assert_equal IN_SEGMENT_VALUE, resolver.get(CONFIG_KEY, nil, { email: 'test@hotmail.com' }).string
246
+ assert_equal_context_and_jit DEFAULT_VALUE, resolver, CONFIG_KEY,
247
+ { user: { email: 'test@something-else.com' } }, :string
248
+ assert_equal_context_and_jit IN_SEGMENT_VALUE, resolver, CONFIG_KEY,
249
+ { user: { email: 'test@hotmail.com' } }, :string
227
250
  end
228
251
  end
229
252
 
@@ -240,7 +263,7 @@ class TestConfigResolver < Minitest::Test
240
263
  Prefab::Criterion.new(
241
264
  operator: Prefab::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
242
265
  value_to_match: string_list(['hotmail.com', 'gmail.com']),
243
- property_name: 'email'
266
+ property_name: 'user.email'
244
267
  )
245
268
  ]
246
269
  ),
@@ -289,77 +312,98 @@ class TestConfigResolver < Minitest::Test
289
312
  options = Prefab::Options.new
290
313
  resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
291
314
 
292
- assert_equal IN_SEGMENT_VALUE, resolver.get(CONFIG_KEY, nil, { email: 'test@hotmail.com' }).string
293
- assert_equal NOT_IN_SEGMENT_VALUE, resolver.get(CONFIG_KEY, nil, { email: 'test@something-else.com' }).string
315
+ assert_equal_context_and_jit IN_SEGMENT_VALUE, resolver, CONFIG_KEY, { user: { email: 'test@hotmail.com' } },
316
+ :string
317
+ assert_equal_context_and_jit NOT_IN_SEGMENT_VALUE, resolver, CONFIG_KEY, { user: { email: 'test@something-else.com' } },
318
+ :string
294
319
  end
295
320
  end
296
321
 
297
- def test_resolving_in_segment_with_lookup_key
298
- segment_config = Prefab::Config.new(
299
- config_type: Prefab::ConfigType::SEGMENT,
300
- key: SEGMENT_KEY,
322
+ def test_jit_context_merges_with_existing_context
323
+ config = Prefab::Config.new(
324
+ key: CONFIG_KEY,
301
325
  rows: [
326
+ DEFAULT_ROW,
302
327
  Prefab::ConfigRow.new(
328
+ project_env_id: TEST_ENV_ID,
303
329
  values: [
304
330
  Prefab::ConditionalValue.new(
305
- value: Prefab::ConfigValue.new(bool: true),
306
331
  criteria: [
307
332
  Prefab::Criterion.new(
308
- operator: Prefab::Criterion::CriterionOperator::LOOKUP_KEY_IN,
309
- value_to_match: string_list(['user:1234', 'user:4567']),
310
- property_name: 'LOOKUP'
333
+ operator: Prefab::Criterion::CriterionOperator::PROP_IS_ONE_OF,
334
+ value_to_match: string_list(%w[pro advanced]),
335
+ property_name: 'team.plan'
336
+ ),
337
+
338
+ Prefab::Criterion.new(
339
+ operator: Prefab::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
340
+ value_to_match: string_list(%w[@example.com]),
341
+ property_name: 'user.email'
311
342
  )
312
- ]
313
- ),
314
- Prefab::ConditionalValue.new(value: Prefab::ConfigValue.new(bool: false))
343
+ ],
344
+ value: Prefab::ConfigValue.new(string: DESIRED_VALUE)
345
+ )
315
346
  ]
316
347
  )
317
348
  ]
318
349
  )
319
350
 
351
+ loader = MockConfigLoader.new
352
+
353
+ loader.stub :calc_config, { CONFIG_KEY => { config: config } } do
354
+ options = Prefab::Options.new
355
+ resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
356
+ resolver.project_env_id = TEST_ENV_ID
357
+
358
+ Prefab::Context.with_context({ user: { email: 'test@example.com' } }) do
359
+ assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY).string
360
+ assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY, { team: { plan: 'freebie' } }).string
361
+ assert_equal DESIRED_VALUE, resolver.get(CONFIG_KEY, { team: { plan: 'pro' } }).string
362
+ end
363
+ end
364
+ end
365
+
366
+ def test_jit_can_clobber_existing_context
320
367
  config = Prefab::Config.new(
321
368
  key: CONFIG_KEY,
322
369
  rows: [
370
+ DEFAULT_ROW,
323
371
  Prefab::ConfigRow.new(
372
+ project_env_id: TEST_ENV_ID,
324
373
  values: [
325
374
  Prefab::ConditionalValue.new(
326
375
  criteria: [
327
376
  Prefab::Criterion.new(
328
- operator: Prefab::Criterion::CriterionOperator::IN_SEG,
329
- value_to_match: Prefab::ConfigValue.new(string: SEGMENT_KEY)
330
- )
331
- ],
332
- value: Prefab::ConfigValue.new(string: IN_SEGMENT_VALUE)
333
- ),
334
- Prefab::ConditionalValue.new(
335
- criteria: [
377
+ operator: Prefab::Criterion::CriterionOperator::PROP_IS_ONE_OF,
378
+ value_to_match: string_list(%w[pro advanced]),
379
+ property_name: 'team.plan'
380
+ ),
381
+
336
382
  Prefab::Criterion.new(
337
- operator: Prefab::Criterion::CriterionOperator::NOT_IN_SEG,
338
- value_to_match: Prefab::ConfigValue.new(string: SEGMENT_KEY)
383
+ operator: Prefab::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
384
+ value_to_match: string_list(%w[@example.com]),
385
+ property_name: 'user.email'
339
386
  )
340
387
  ],
341
- value: Prefab::ConfigValue.new(string: NOT_IN_SEGMENT_VALUE)
388
+ value: Prefab::ConfigValue.new(string: DESIRED_VALUE)
342
389
  )
343
390
  ]
344
391
  )
345
392
  ]
346
393
  )
347
394
 
348
- loaded_values = {
349
- SEGMENT_KEY => { config: segment_config },
350
- CONFIG_KEY => { config: config }
351
- }
352
-
353
395
  loader = MockConfigLoader.new
354
396
 
355
- loader.stub :calc_config, loaded_values do
397
+ loader.stub :calc_config, { CONFIG_KEY => { config: config } } do
356
398
  options = Prefab::Options.new
357
399
  resolver = Prefab::ConfigResolver.new(MockBaseClient.new(options), loader)
400
+ resolver.project_env_id = TEST_ENV_ID
358
401
 
359
- assert_equal IN_SEGMENT_VALUE, resolver.get(CONFIG_KEY, 'user:1234', {}).string
360
- assert_equal IN_SEGMENT_VALUE, resolver.get(CONFIG_KEY, 'user:4567', {}).string
361
- assert_equal NOT_IN_SEGMENT_VALUE, resolver.get(CONFIG_KEY, nil, {}).string
362
- assert_equal NOT_IN_SEGMENT_VALUE, resolver.get(CONFIG_KEY, 'user:9999', {}).string
402
+ Prefab::Context.with_context({ user: { email: 'test@hotmail.com' }, team: { plan: 'pro' } }) do
403
+ assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY).string
404
+ assert_equal DESIRED_VALUE, resolver.get(CONFIG_KEY, { user: { email: 'test@example.com' } }).string
405
+ assert_equal DEFAULT_VALUE, resolver.get(CONFIG_KEY, { team: { plan: 'freebie' } }).string
406
+ end
363
407
  end
364
408
  end
365
409
 
@@ -374,4 +418,12 @@ class TestConfigResolver < Minitest::Test
374
418
  resolver.update
375
419
  resolver
376
420
  end
421
+
422
+ def assert_equal_context_and_jit(expected_value, resolver, key, properties, type)
423
+ assert_equal expected_value, resolver.get(key, properties).send(type)
424
+
425
+ Prefab::Context.with_context(properties) do
426
+ assert_equal expected_value, resolver.get(key).send(type)
427
+ end
428
+ end
377
429
  end
@@ -41,34 +41,34 @@ class TestConfigValueUnwrapper < Minitest::Test
41
41
  def test_unwrapping_weighted_values
42
42
  # single value
43
43
  config_value = Prefab::ConfigValue.new(weighted_values: weighted_values([['abc', 1]]))
44
+
44
45
  assert_equal 'abc', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, {})
45
46
 
46
47
  # multiple values, evenly distributed
47
48
  config_value = Prefab::ConfigValue.new(weighted_values: weighted_values([['abc', 1], ['def', 1], ['ghi', 1]]))
48
- assert_equal 'ghi', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:123'))
49
- assert_equal 'ghi', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:456'))
50
- assert_equal 'abc', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:789'))
51
- assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:012'))
49
+ assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:000'))
50
+ assert_equal 'ghi', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:456'))
51
+ assert_equal 'abc', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:789'))
52
+ assert_equal 'ghi', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:888'))
52
53
 
53
54
  # multiple values, unevenly distributed
54
55
  config_value = Prefab::ConfigValue.new(weighted_values: weighted_values([['abc', 1], ['def', 99], ['ghi', 1]]))
55
- assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:123'))
56
- assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:456'))
57
- assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:789'))
58
- assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:012'))
59
-
60
- assert_equal 'ghi', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:103'))
61
- assert_equal 'abc', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, lookup_properties('user:119'))
56
+ assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:123'))
57
+ assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:456'))
58
+ assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:789'))
59
+ assert_equal 'def', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:012'))
60
+ assert_equal 'ghi', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:428'))
61
+ assert_equal 'abc', Prefab::ConfigValueUnwrapper.unwrap(config_value, CONFIG_KEY, context_with_key('user:548'))
62
62
  end
63
63
 
64
64
  private
65
65
 
66
- def weighted_values(values_and_weights)
66
+ def weighted_values(values_and_weights, hash_by_property_name: 'user.key')
67
67
  values = values_and_weights.map do |value, weight|
68
68
  weighted_value(value, weight)
69
69
  end
70
70
 
71
- Prefab::WeightedValues.new(weighted_values: values)
71
+ Prefab::WeightedValues.new(weighted_values: values, hash_by_property_name: hash_by_property_name)
72
72
  end
73
73
 
74
74
  def weighted_value(string, weight)
@@ -77,7 +77,7 @@ class TestConfigValueUnwrapper < Minitest::Test
77
77
  )
78
78
  end
79
79
 
80
- def lookup_properties(lookup_key)
81
- { Prefab::CriteriaEvaluator::LOOKUP_KEY => lookup_key }
80
+ def context_with_key(key)
81
+ Prefab::Context.new(user: { key: key })
82
82
  end
83
83
  end