prefab-cloud-ruby 0.23.8 → 0.24.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.
@@ -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.0 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.0"
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
@@ -44,32 +44,36 @@ class TestClient < Minitest::Test
44
44
  assert_equal false, @client.enabled?('flag_with_a_value')
45
45
  end
46
46
 
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')
47
+ def test_ff_enabled_with_user_key_match
48
+ assert_equal_context_and_jit(false, :enabled?, 'user_key_match', { user: { key: 'jimmy' } })
49
+ assert_equal_context_and_jit(true, :enabled?, 'user_key_match', { user: { key: 'abc123' } })
50
+ assert_equal_context_and_jit(true, :enabled?, 'user_key_match', { user: { key: 'xyz987' } })
51
51
  end
52
52
 
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')
53
+ def test_ff_enabled_with_context
54
+ assert_equal_context_and_jit(false, :enabled?, 'just_my_domain', user: { domain: 'gmail.com' })
55
+ assert_equal_context_and_jit(false, :enabled?, 'just_my_domain', user: { domain: 'prefab.cloud' })
56
+ assert_equal_context_and_jit(false, :enabled?, 'just_my_domain', user: { domain: 'example.com' })
59
57
  end
60
58
 
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' })
59
+ def test_ff_get_with_context
60
+ assert_nil @client.get('just_my_domain', 'abc123', user: { domain: 'gmail.com' })
61
+ assert_equal 'DEFAULT', @client.get('just_my_domain', 'abc123', { user: { domain: 'gmail.com' } }, 'DEFAULT')
62
+
63
+ assert_equal_context_and_jit('new-version', :get, 'just_my_domain', { user: { domain: 'prefab.cloud' } })
64
+ assert_equal_context_and_jit('new-version', :get, 'just_my_domain', { user: { domain: 'example.com' } })
65
65
  end
66
66
 
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')
67
+ def test_deprecated_no_dot_notation_ff_enabled_with_jit_context
68
+ # with no lookup key
69
+ assert_equal false, @client.enabled?('deprecated_no_dot_notation', { domain: 'gmail.com' })
70
+ assert_equal true, @client.enabled?('deprecated_no_dot_notation', { domain: 'prefab.cloud' })
71
+ assert_equal true, @client.enabled?('deprecated_no_dot_notation', { domain: 'example.com' })
70
72
 
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' })
73
+ # with a lookup key
74
+ assert_equal false, @client.enabled?('deprecated_no_dot_notation', 'some-lookup-key', { domain: 'gmail.com' })
75
+ assert_equal true, @client.enabled?('deprecated_no_dot_notation', 'some-lookup-key', { domain: 'prefab.cloud' })
76
+ assert_equal true, @client.enabled?('deprecated_no_dot_notation', 'some-lookup-key', { domain: 'example.com' })
73
77
  end
74
78
 
75
79
  def test_getting_feature_flag_value
@@ -103,14 +107,11 @@ class TestClient < Minitest::Test
103
107
 
104
108
  private
105
109
 
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))
110
+ def assert_equal_context_and_jit(expected, method, key, context)
111
+ assert_equal expected, @client.send(method, key, context)
113
112
 
114
- Prefab::Client.new(options)
113
+ Prefab::Context.with_context(context) do
114
+ assert_equal expected, @client.send(method, key)
115
+ end
115
116
  end
116
117
  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