ldclient-rb 2.5.0 → 3.0.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.
@@ -26,6 +26,18 @@ module LaunchDarkly
26
26
  make_request("/sdk/latest-flags/" + key)
27
27
  end
28
28
 
29
+ def request_all_segments()
30
+ make_request("/sdk/latest-segments")
31
+ end
32
+
33
+ def request_segment(key)
34
+ make_request("/sdk/latest-segments/" + key)
35
+ end
36
+
37
+ def request_all_data()
38
+ make_request("/sdk/latest-all")
39
+ end
40
+
29
41
  def make_request(path)
30
42
  uri = @config.base_uri + path
31
43
  res = @client.get (uri) do |req|
@@ -10,11 +10,16 @@ module LaunchDarkly
10
10
  INDIRECT_PATCH = :'indirect/patch'
11
11
  READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
12
12
 
13
+ KEY_PATHS = {
14
+ FEATURES => "/flags/",
15
+ SEGMENTS => "/segments/"
16
+ }
17
+
13
18
  class StreamProcessor
14
19
  def initialize(sdk_key, config, requestor)
15
20
  @sdk_key = sdk_key
16
21
  @config = config
17
- @store = config.feature_store
22
+ @feature_store = config.feature_store
18
23
  @requestor = requestor
19
24
  @initialized = Concurrent::AtomicBoolean.new(false)
20
25
  @started = Concurrent::AtomicBoolean.new(false)
@@ -36,7 +41,7 @@ module LaunchDarkly
36
41
  'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION
37
42
  }
38
43
  opts = {:headers => headers, :with_credentials => true, :proxy => @config.proxy, :read_timeout => READ_TIMEOUT_SECONDS}
39
- @es = Celluloid::EventSource.new(@config.stream_uri + "/flags", opts) do |conn|
44
+ @es = Celluloid::EventSource.new(@config.stream_uri + "/all", opts) do |conn|
40
45
  conn.on(PUT) { |message| process_message(message, PUT) }
41
46
  conn.on(PATCH) { |message| process_message(message, PATCH) }
42
47
  conn.on(DELETE) { |message| process_message(message, DELETE) }
@@ -66,30 +71,61 @@ module LaunchDarkly
66
71
  end
67
72
  end
68
73
 
74
+ private
75
+
69
76
  def process_message(message, method)
70
77
  @config.logger.debug("[LDClient] Stream received #{method} message: #{message.data}")
71
78
  if method == PUT
72
79
  message = JSON.parse(message.data, symbolize_names: true)
73
- @store.init(message)
80
+ @feature_store.init({
81
+ FEATURES => message[:data][:flags],
82
+ SEGMENTS => message[:data][:segments]
83
+ })
74
84
  @initialized.make_true
75
85
  @config.logger.info("[LDClient] Stream initialized")
76
86
  elsif method == PATCH
77
87
  message = JSON.parse(message.data, symbolize_names: true)
78
- @store.upsert(message[:path][1..-1], message[:data])
88
+ for kind in [FEATURES, SEGMENTS]
89
+ key = key_for_path(kind, message[:path])
90
+ if key
91
+ @feature_store.upsert(kind, message[:data])
92
+ break
93
+ end
94
+ end
79
95
  elsif method == DELETE
80
96
  message = JSON.parse(message.data, symbolize_names: true)
81
- @store.delete(message[:path][1..-1], message[:version])
97
+ for kind in [FEATURES, SEGMENTS]
98
+ key = key_for_path(kind, message[:path])
99
+ if key
100
+ @feature_store.delete(kind, key, message[:version])
101
+ break
102
+ end
103
+ end
82
104
  elsif method == INDIRECT_PUT
83
- @store.init(@requestor.request_all_flags)
105
+ all_data = @requestor.request_all_data
106
+ @feature_store.init({
107
+ FEATURES => all_data[:flags],
108
+ SEGMENTS => all_data[:segments]
109
+ })
84
110
  @initialized.make_true
85
111
  @config.logger.info("[LDClient] Stream initialized (via indirect message)")
86
112
  elsif method == INDIRECT_PATCH
87
- @store.upsert(message.data, @requestor.request_flag(message.data))
113
+ key = feature_key_for_path(message.data)
114
+ if key
115
+ @feature_store.upsert(FEATURES, @requestor.request_flag(key))
116
+ else
117
+ key = segment_key_for_path(message.data)
118
+ if key
119
+ @feature_store.upsert(SEGMENTS, key, @requestor.request_segment(key))
120
+ end
121
+ end
88
122
  else
89
123
  @config.logger.warn("[LDClient] Unknown message received: #{method}")
90
124
  end
91
125
  end
92
126
 
93
- private :process_message
127
+ def key_for_path(kind, path)
128
+ path.start_with?(KEY_PATHS[kind]) ? path[KEY_PATHS[kind].length..-1] : nil
129
+ end
94
130
  end
95
131
  end
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "2.5.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -4,6 +4,14 @@ describe LaunchDarkly::Evaluation do
4
4
  subject { LaunchDarkly::Evaluation }
5
5
  let(:features) { LaunchDarkly::InMemoryFeatureStore.new }
6
6
 
7
+ let(:user) {
8
+ {
9
+ key: "userkey",
10
+ email: "test@example.com",
11
+ name: "Bob"
12
+ }
13
+ }
14
+
7
15
  include LaunchDarkly::Evaluation
8
16
 
9
17
  describe "evaluate" do
@@ -60,7 +68,7 @@ describe LaunchDarkly::Evaluation do
60
68
  variations: ['d', 'e'],
61
69
  version: 2
62
70
  }
63
- features.upsert('feature1', flag1)
71
+ features.upsert(LaunchDarkly::FEATURES, flag1)
64
72
  user = { key: 'x' }
65
73
  events_should_be = [{kind: 'feature', key: 'feature1', value: 'd', version: 2, prereqOf: 'feature0'}]
66
74
  expect(evaluate(flag, user, features)).to eq({value: 'b', events: events_should_be})
@@ -83,7 +91,7 @@ describe LaunchDarkly::Evaluation do
83
91
  variations: ['d', 'e'],
84
92
  version: 2
85
93
  }
86
- features.upsert('feature1', flag1)
94
+ features.upsert(LaunchDarkly::FEATURES, flag1)
87
95
  user = { key: 'x' }
88
96
  events_should_be = [{kind: 'feature', key: 'feature1', value: 'e', version: 2, prereqOf: 'feature0'}]
89
97
  expect(evaluate(flag, user, features)).to eq({value: 'a', events: events_should_be})
@@ -133,19 +141,47 @@ describe LaunchDarkly::Evaluation do
133
141
  it "can match built-in attribute" do
134
142
  user = { key: 'x', name: 'Bob' }
135
143
  clause = { attribute: 'name', op: 'in', values: ['Bob'] }
136
- expect(clause_match_user(clause, user)).to be true
144
+ expect(clause_match_user(clause, user, features)).to be true
137
145
  end
138
146
 
139
147
  it "can match custom attribute" do
140
148
  user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
141
149
  clause = { attribute: 'legs', op: 'in', values: [4] }
142
- expect(clause_match_user(clause, user)).to be true
150
+ expect(clause_match_user(clause, user, features)).to be true
143
151
  end
144
152
 
145
153
  it "returns false for missing attribute" do
146
154
  user = { key: 'x', name: 'Bob' }
147
155
  clause = { attribute: 'legs', op: 'in', values: [4] }
148
- expect(clause_match_user(clause, user)).to be false
156
+ expect(clause_match_user(clause, user, features)).to be false
157
+ end
158
+
159
+ it "can be negated" do
160
+ user = { key: 'x', name: 'Bob' }
161
+ clause = { attribute: 'name', op: 'in', values: ['Bob'], negate: true }
162
+ expect(clause_match_user(clause, user, features)).to be false
163
+ end
164
+
165
+ it "retrieves segment from segment store for segmentMatch operator" do
166
+ segment = {
167
+ key: 'segkey',
168
+ included: [ 'userkey' ],
169
+ version: 1,
170
+ deleted: false
171
+ }
172
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
173
+
174
+ user = { key: 'userkey' }
175
+ clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
176
+
177
+ expect(clause_match_user(clause, user, features)).to be true
178
+ end
179
+
180
+ it "falls through with no errors if referenced segment is not found" do
181
+ user = { key: 'userkey' }
182
+ clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
183
+
184
+ expect(clause_match_user(clause, user, features)).to be false
149
185
  end
150
186
 
151
187
  it "can be negated" do
@@ -153,7 +189,7 @@ describe LaunchDarkly::Evaluation do
153
189
  clause = { attribute: 'name', op: 'in', values: ['Bob'] }
154
190
  expect {
155
191
  clause[:negate] = true
156
- }.to change {clause_match_user(clause, user)}.from(true).to(false)
192
+ }.to change {clause_match_user(clause, user, features)}.from(true).to(false)
157
193
  end
158
194
  end
159
195
 
@@ -255,7 +291,7 @@ describe LaunchDarkly::Evaluation do
255
291
  it "should return #{shouldBe} for #{value1} #{op} #{value2}" do
256
292
  user = { key: 'x', custom: { foo: value1 } }
257
293
  clause = { attribute: 'foo', op: op, values: [value2] }
258
- expect(clause_match_user(clause, user)).to be shouldBe
294
+ expect(clause_match_user(clause, user, features)).to be shouldBe
259
295
  end
260
296
  end
261
297
  end
@@ -313,4 +349,165 @@ describe LaunchDarkly::Evaluation do
313
349
  expect(result).to eq(0.0)
314
350
  end
315
351
  end
352
+
353
+ def make_flag(key)
354
+ {
355
+ key: key,
356
+ rules: [],
357
+ variations: [ false, true ],
358
+ on: true,
359
+ fallthrough: { variation: 0 },
360
+ version: 1
361
+ }
362
+ end
363
+
364
+ def make_segment(key)
365
+ {
366
+ key: key,
367
+ included: [],
368
+ excluded: [],
369
+ salt: 'abcdef',
370
+ version: 1
371
+ }
372
+ end
373
+
374
+ def make_segment_match_clause(segment)
375
+ {
376
+ op: :segmentMatch,
377
+ values: [ segment[:key] ],
378
+ negate: false
379
+ }
380
+ end
381
+
382
+ def make_user_matching_clause(user, attr)
383
+ {
384
+ attribute: attr.to_s,
385
+ op: :in,
386
+ values: [ user[attr.to_sym] ],
387
+ negate: false
388
+ }
389
+ end
390
+
391
+ describe 'segment matching' do
392
+ it 'explicitly includes user' do
393
+ segment = make_segment('segkey')
394
+ segment[:included] = [ user[:key] ]
395
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
396
+ clause = make_segment_match_clause(segment)
397
+
398
+ result = clause_match_user(clause, user, features)
399
+ expect(result).to be true
400
+ end
401
+
402
+ it 'explicitly excludes user' do
403
+ segment = make_segment('segkey')
404
+ segment[:excluded] = [ user[:key] ]
405
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
406
+ clause = make_segment_match_clause(segment)
407
+
408
+ result = clause_match_user(clause, user, features)
409
+ expect(result).to be false
410
+ end
411
+
412
+ it 'both includes and excludes user; include takes priority' do
413
+ segment = make_segment('segkey')
414
+ segment[:included] = [ user[:key] ]
415
+ segment[:excluded] = [ user[:key] ]
416
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
417
+ clause = make_segment_match_clause(segment)
418
+
419
+ result = clause_match_user(clause, user, features)
420
+ expect(result).to be true
421
+ end
422
+
423
+ it 'matches user by rule when weight is absent' do
424
+ segClause = make_user_matching_clause(user, :email)
425
+ segRule = {
426
+ clauses: [ segClause ]
427
+ }
428
+ segment = make_segment('segkey')
429
+ segment[:rules] = [ segRule ]
430
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
431
+ clause = make_segment_match_clause(segment)
432
+
433
+ result = clause_match_user(clause, user, features)
434
+ expect(result).to be true
435
+ end
436
+
437
+ it 'matches user by rule when weight is nil' do
438
+ segClause = make_user_matching_clause(user, :email)
439
+ segRule = {
440
+ clauses: [ segClause ],
441
+ weight: nil
442
+ }
443
+ segment = make_segment('segkey')
444
+ segment[:rules] = [ segRule ]
445
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
446
+ clause = make_segment_match_clause(segment)
447
+
448
+ result = clause_match_user(clause, user, features)
449
+ expect(result).to be true
450
+ end
451
+
452
+ it 'matches user with full rollout' do
453
+ segClause = make_user_matching_clause(user, :email)
454
+ segRule = {
455
+ clauses: [ segClause ],
456
+ weight: 100000
457
+ }
458
+ segment = make_segment('segkey')
459
+ segment[:rules] = [ segRule ]
460
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
461
+ clause = make_segment_match_clause(segment)
462
+
463
+ result = clause_match_user(clause, user, features)
464
+ expect(result).to be true
465
+ end
466
+
467
+ it "doesn't match user with zero rollout" do
468
+ segClause = make_user_matching_clause(user, :email)
469
+ segRule = {
470
+ clauses: [ segClause ],
471
+ weight: 0
472
+ }
473
+ segment = make_segment('segkey')
474
+ segment[:rules] = [ segRule ]
475
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
476
+ clause = make_segment_match_clause(segment)
477
+
478
+ result = clause_match_user(clause, user, features)
479
+ expect(result).to be false
480
+ end
481
+
482
+ it "matches user with multiple clauses" do
483
+ segClause1 = make_user_matching_clause(user, :email)
484
+ segClause2 = make_user_matching_clause(user, :name)
485
+ segRule = {
486
+ clauses: [ segClause1, segClause2 ]
487
+ }
488
+ segment = make_segment('segkey')
489
+ segment[:rules] = [ segRule ]
490
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
491
+ clause = make_segment_match_clause(segment)
492
+
493
+ result = clause_match_user(clause, user, features)
494
+ expect(result).to be true
495
+ end
496
+
497
+ it "doesn't match user with multiple clauses if a clause doesn't match" do
498
+ segClause1 = make_user_matching_clause(user, :email)
499
+ segClause2 = make_user_matching_clause(user, :name)
500
+ segClause2[:values] = [ 'wrong' ]
501
+ segRule = {
502
+ clauses: [ segClause1, segClause2 ]
503
+ }
504
+ segment = make_segment('segkey')
505
+ segment[:rules] = [ segRule ]
506
+ features.upsert(LaunchDarkly::SEGMENTS, segment)
507
+ clause = make_segment_match_clause(segment)
508
+
509
+ result = clause_match_user(clause, user, features)
510
+ expect(result).to be false
511
+ end
512
+ end
316
513
  end
@@ -31,7 +31,7 @@ RSpec.shared_examples "feature_store" do |create_store_method|
31
31
 
32
32
  let!(:store) do
33
33
  s = create_store_method.call()
34
- s.init({ key0 => feature0 })
34
+ s.init(LaunchDarkly::FEATURES => { key0 => feature0 })
35
35
  s
36
36
  end
37
37
 
@@ -48,15 +48,15 @@ RSpec.shared_examples "feature_store" do |create_store_method|
48
48
  end
49
49
 
50
50
  it "can get existing feature with symbol key" do
51
- expect(store.get(key0)).to eq feature0
51
+ expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
52
52
  end
53
53
 
54
54
  it "can get existing feature with string key" do
55
- expect(store.get(key0.to_s)).to eq feature0
55
+ expect(store.get(LaunchDarkly::FEATURES, key0.to_s)).to eq feature0
56
56
  end
57
57
 
58
58
  it "gets nil for nonexisting feature" do
59
- expect(store.get('nope')).to be_nil
59
+ expect(store.get(LaunchDarkly::FEATURES, 'nope')).to be_nil
60
60
  end
61
61
 
62
62
  it "can get all features" do
@@ -64,8 +64,8 @@ RSpec.shared_examples "feature_store" do |create_store_method|
64
64
  feature1[:key] = "test-feature-flag1"
65
65
  feature1[:version] = 5
66
66
  feature1[:on] = false
67
- store.upsert(:"test-feature-flag1", feature1)
68
- expect(store.all).to eq ({ key0 => feature0, :"test-feature-flag1" => feature1 })
67
+ store.upsert(LaunchDarkly::FEATURES, feature1)
68
+ expect(store.all(LaunchDarkly::FEATURES)).to eq ({ key0 => feature0, :"test-feature-flag1" => feature1 })
69
69
  end
70
70
 
71
71
  it "can add new feature" do
@@ -73,40 +73,40 @@ RSpec.shared_examples "feature_store" do |create_store_method|
73
73
  feature1[:key] = "test-feature-flag1"
74
74
  feature1[:version] = 5
75
75
  feature1[:on] = false
76
- store.upsert(:"test-feature-flag1", feature1)
77
- expect(store.get(:"test-feature-flag1")).to eq feature1
76
+ store.upsert(LaunchDarkly::FEATURES, feature1)
77
+ expect(store.get(LaunchDarkly::FEATURES, :"test-feature-flag1")).to eq feature1
78
78
  end
79
79
 
80
80
  it "can update feature with newer version" do
81
81
  f1 = new_version_plus(feature0, 1, { on: !feature0[:on] })
82
- store.upsert(key0, f1)
83
- expect(store.get(key0)).to eq f1
82
+ store.upsert(LaunchDarkly::FEATURES, f1)
83
+ expect(store.get(LaunchDarkly::FEATURES, key0)).to eq f1
84
84
  end
85
85
 
86
86
  it "cannot update feature with same version" do
87
87
  f1 = new_version_plus(feature0, 0, { on: !feature0[:on] })
88
- store.upsert(key0, f1)
89
- expect(store.get(key0)).to eq feature0
88
+ store.upsert(LaunchDarkly::FEATURES, f1)
89
+ expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
90
90
  end
91
91
 
92
92
  it "cannot update feature with older version" do
93
93
  f1 = new_version_plus(feature0, -1, { on: !feature0[:on] })
94
- store.upsert(key0, f1)
95
- expect(store.get(key0)).to eq feature0
94
+ store.upsert(LaunchDarkly::FEATURES, f1)
95
+ expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
96
96
  end
97
97
 
98
98
  it "can delete feature with newer version" do
99
- store.delete(key0, feature0[:version] + 1)
100
- expect(store.get(key0)).to be_nil
99
+ store.delete(LaunchDarkly::FEATURES, key0, feature0[:version] + 1)
100
+ expect(store.get(LaunchDarkly::FEATURES, key0)).to be_nil
101
101
  end
102
102
 
103
103
  it "cannot delete feature with same version" do
104
- store.delete(key0, feature0[:version])
105
- expect(store.get(key0)).to eq feature0
104
+ store.delete(LaunchDarkly::FEATURES, key0, feature0[:version])
105
+ expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
106
106
  end
107
107
 
108
108
  it "cannot delete feature with older version" do
109
- store.delete(key0, feature0[:version] - 1)
110
- expect(store.get(key0)).to eq feature0
109
+ store.delete(LaunchDarkly::FEATURES, key0, feature0[:version] - 1)
110
+ expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
111
111
  end
112
112
  end