ldclient-rb 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 96e2f113f163f9d2649f515efc8a16e3d9f01fcd
4
- data.tar.gz: 9f11bffdc1358b86a3fec18abf62a3e9e927a72e
3
+ metadata.gz: 9261ef0e3f59657592492b3fe391e2096bc6eef2
4
+ data.tar.gz: 418f0fbd33e9ebc9afd7fff88fe79c08da5ffbed
5
5
  SHA512:
6
- metadata.gz: 3224fce749cbb694334d2b9482991c7de8d3274b8bd31b5bda201c0f594fbc86a1f09ac1b23a319a89d04ed2aba8dfa3d39cb25409352085802d1979c069f0b3
7
- data.tar.gz: 4391bb0850a0a8affa9c7150838467471eee58848040af851080a9da4a75a5d4f68131e2ae36d9cdefca5a5e165623b9d778d06a60ab9ed89251da5a80bd217e
6
+ metadata.gz: 42f9a2a262c821cc0624bff69226d3ed0ffda35387c5504740b55919f732eaf874c7e2b4db698fe12697131a23a9482788379e21429a099eb94f00912bbaafd7
7
+ data.tar.gz: 0cb478da375473bb01ca3d733c8b85212a45c60210ad4f36ef49c8817eaadff21669e374ed990a1407ce4b6c51f9deec8fe7d9a5458079872979009359395de5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
4
4
 
5
+ 2.5.0 (2018-02-12)
6
+
7
+ ## Added
8
+ - Adds support for a future LaunchDarkly feature, coming soon: semantic version user attributes.
9
+
10
+ ## Changed
11
+ - It is now possible to compute rollouts based on an integer attribute of a user, not just a string attribute.
12
+
13
+
5
14
  ## [2.4.1] - 2018-01-23
6
15
  ## Changed
7
16
  - Reduce logging level for missing flags
data/ldclient-rb.gemspec CHANGED
@@ -28,10 +28,11 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency "redis", "~> 3.3.5"
29
29
  spec.add_development_dependency "connection_pool", ">= 2.1.2"
30
30
  spec.add_development_dependency "moneta", "~> 1.0.0"
31
-
31
+
32
32
  spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
33
33
  spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
34
34
  spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
35
+ spec.add_runtime_dependency "semantic", "~> 1.6.0"
35
36
  spec.add_runtime_dependency "thread_safe", "~> 0.3"
36
37
  spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
37
38
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0.4"
@@ -1,9 +1,57 @@
1
1
  require "date"
2
+ require "semantic"
2
3
 
3
4
  module LaunchDarkly
4
5
  module Evaluation
5
6
  BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
6
7
 
8
+ NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
9
+
10
+ DATE_OPERAND = lambda do |v|
11
+ if v.is_a? String
12
+ begin
13
+ DateTime.rfc3339(v).strftime("%Q").to_i
14
+ rescue => e
15
+ nil
16
+ end
17
+ elsif v.is_a? Numeric
18
+ v
19
+ else
20
+ nil
21
+ end
22
+ end
23
+
24
+ SEMVER_OPERAND = lambda do |v|
25
+ if v.is_a? String
26
+ for _ in 0..2 do
27
+ begin
28
+ return Semantic::Version.new(v)
29
+ rescue ArgumentError
30
+ v = addZeroVersionComponent(v)
31
+ end
32
+ end
33
+ end
34
+ nil
35
+ end
36
+
37
+ def self.addZeroVersionComponent(v)
38
+ NUMERIC_VERSION_COMPONENTS_REGEX.match(v) { |m|
39
+ m[0] + ".0" + v[m[0].length..-1]
40
+ }
41
+ end
42
+
43
+ def self.comparator(converter)
44
+ lambda do |a, b|
45
+ av = converter.call(a)
46
+ bv = converter.call(b)
47
+ if !av.nil? && !bv.nil?
48
+ yield av <=> bv
49
+ else
50
+ return false
51
+ end
52
+ end
53
+ end
54
+
7
55
  OPERATORS = {
8
56
  in:
9
57
  lambda do |a, b|
@@ -42,33 +90,15 @@ module LaunchDarkly
42
90
  (a.is_a? Numeric) && (a >= b)
43
91
  end,
44
92
  before:
45
- lambda do |a, b|
46
- begin
47
- if a.is_a? String
48
- a = DateTime.rfc3339(a).strftime('%Q').to_i
49
- end
50
- if b.is_a? String
51
- b = DateTime.rfc3339(b).strftime('%Q').to_i
52
- end
53
- (a.is_a? Numeric) ? a < b : false
54
- rescue => e
55
- false
56
- end
57
- end,
93
+ comparator(DATE_OPERAND) { |n| n < 0 },
58
94
  after:
59
- lambda do |a, b|
60
- begin
61
- if a.is_a? String
62
- a = DateTime.rfc3339(a).strftime("%Q").to_i
63
- end
64
- if b.is_a? String
65
- b = DateTime.rfc3339(b).strftime("%Q").to_i
66
- end
67
- (a.is_a? Numeric) ? a > b : false
68
- rescue => e
69
- false
70
- end
71
- end
95
+ comparator(DATE_OPERAND) { |n| n > 0 },
96
+ semVerEqual:
97
+ comparator(SEMVER_OPERAND) { |n| n == 0 },
98
+ semVerLessThan:
99
+ comparator(SEMVER_OPERAND) { |n| n < 0 },
100
+ semVerGreaterThan:
101
+ comparator(SEMVER_OPERAND) { |n| n > 0 }
72
102
  }
73
103
 
74
104
  class EvaluationError < StandardError
@@ -223,7 +253,10 @@ module LaunchDarkly
223
253
  def bucket_user(user, key, bucket_by, salt)
224
254
  return nil unless user[:key]
225
255
 
226
- id_hash = user_value(user, bucket_by)
256
+ id_hash = bucketable_string_value(user_value(user, bucket_by))
257
+ if id_hash.nil?
258
+ return 0.0
259
+ end
227
260
 
228
261
  if user[:secondary]
229
262
  id_hash += "." + user[:secondary]
@@ -235,6 +268,12 @@ module LaunchDarkly
235
268
  hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
236
269
  end
237
270
 
271
+ def bucketable_string_value(value)
272
+ return value if value.is_a? String
273
+ return value.to_s if value.is_a? Integer
274
+ nil
275
+ end
276
+
238
277
  def user_value(user, attribute)
239
278
  attribute = attribute.to_sym
240
279
 
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "2.4.1"
2
+ VERSION = "2.5.0"
3
3
  end
@@ -0,0 +1,316 @@
1
+ require "spec_helper"
2
+
3
+ describe LaunchDarkly::Evaluation do
4
+ subject { LaunchDarkly::Evaluation }
5
+ let(:features) { LaunchDarkly::InMemoryFeatureStore.new }
6
+
7
+ include LaunchDarkly::Evaluation
8
+
9
+ describe "evaluate" do
10
+ it "returns off variation if flag is off" do
11
+ flag = {
12
+ key: 'feature',
13
+ on: false,
14
+ offVariation: 1,
15
+ fallthrough: { variation: 0 },
16
+ variations: ['a', 'b', 'c']
17
+ }
18
+ user = { key: 'x' }
19
+ expect(evaluate(flag, user, features)).to eq({value: 'b', events: []})
20
+ end
21
+
22
+ it "returns nil if flag is off and off variation is unspecified" do
23
+ flag = {
24
+ key: 'feature',
25
+ on: false,
26
+ fallthrough: { variation: 0 },
27
+ variations: ['a', 'b', 'c']
28
+ }
29
+ user = { key: 'x' }
30
+ expect(evaluate(flag, user, features)).to eq({value: nil, events: []})
31
+ end
32
+
33
+ it "returns off variation if prerequisite is not found" do
34
+ flag = {
35
+ key: 'feature0',
36
+ on: true,
37
+ prerequisites: [{key: 'badfeature', variation: 1}],
38
+ fallthrough: { variation: 0 },
39
+ offVariation: 1,
40
+ variations: ['a', 'b', 'c']
41
+ }
42
+ user = { key: 'x' }
43
+ expect(evaluate(flag, user, features)).to eq({value: 'b', events: []})
44
+ end
45
+
46
+ it "returns off variation and event if prerequisite is not met" do
47
+ flag = {
48
+ key: 'feature0',
49
+ on: true,
50
+ prerequisites: [{key: 'feature1', variation: 1}],
51
+ fallthrough: { variation: 0 },
52
+ offVariation: 1,
53
+ variations: ['a', 'b', 'c'],
54
+ version: 1
55
+ }
56
+ flag1 = {
57
+ key: 'feature1',
58
+ on: true,
59
+ fallthrough: { variation: 0 },
60
+ variations: ['d', 'e'],
61
+ version: 2
62
+ }
63
+ features.upsert('feature1', flag1)
64
+ user = { key: 'x' }
65
+ events_should_be = [{kind: 'feature', key: 'feature1', value: 'd', version: 2, prereqOf: 'feature0'}]
66
+ expect(evaluate(flag, user, features)).to eq({value: 'b', events: events_should_be})
67
+ end
68
+
69
+ it "returns fallthrough variation and event if prerequisite is met and there are no rules" do
70
+ flag = {
71
+ key: 'feature0',
72
+ on: true,
73
+ prerequisites: [{key: 'feature1', variation: 1}],
74
+ fallthrough: { variation: 0 },
75
+ offVariation: 1,
76
+ variations: ['a', 'b', 'c'],
77
+ version: 1
78
+ }
79
+ flag1 = {
80
+ key: 'feature1',
81
+ on: true,
82
+ fallthrough: { variation: 1 },
83
+ variations: ['d', 'e'],
84
+ version: 2
85
+ }
86
+ features.upsert('feature1', flag1)
87
+ user = { key: 'x' }
88
+ events_should_be = [{kind: 'feature', key: 'feature1', value: 'e', version: 2, prereqOf: 'feature0'}]
89
+ expect(evaluate(flag, user, features)).to eq({value: 'a', events: events_should_be})
90
+ end
91
+
92
+ it "matches user from targets" do
93
+ flag = {
94
+ key: 'feature0',
95
+ on: true,
96
+ targets: [
97
+ { values: [ 'whoever', 'userkey' ], variation: 2 }
98
+ ],
99
+ fallthrough: { variation: 0 },
100
+ offVariation: 1,
101
+ variations: ['a', 'b', 'c']
102
+ }
103
+ user = { key: 'userkey' }
104
+ expect(evaluate(flag, user, features)).to eq({value: 'c', events: []})
105
+ end
106
+
107
+ it "matches user from rules" do
108
+ flag = {
109
+ key: 'feature0',
110
+ on: true,
111
+ rules: [
112
+ {
113
+ clauses: [
114
+ {
115
+ attribute: 'key',
116
+ op: 'in',
117
+ values: [ 'userkey' ]
118
+ }
119
+ ],
120
+ variation: 2
121
+ }
122
+ ],
123
+ fallthrough: { variation: 0 },
124
+ offVariation: 1,
125
+ variations: ['a', 'b', 'c']
126
+ }
127
+ user = { key: 'userkey' }
128
+ expect(evaluate(flag, user, features)).to eq({value: 'c', events: []})
129
+ end
130
+ end
131
+
132
+ describe "clause_match_user" do
133
+ it "can match built-in attribute" do
134
+ user = { key: 'x', name: 'Bob' }
135
+ clause = { attribute: 'name', op: 'in', values: ['Bob'] }
136
+ expect(clause_match_user(clause, user)).to be true
137
+ end
138
+
139
+ it "can match custom attribute" do
140
+ user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
141
+ clause = { attribute: 'legs', op: 'in', values: [4] }
142
+ expect(clause_match_user(clause, user)).to be true
143
+ end
144
+
145
+ it "returns false for missing attribute" do
146
+ user = { key: 'x', name: 'Bob' }
147
+ clause = { attribute: 'legs', op: 'in', values: [4] }
148
+ expect(clause_match_user(clause, user)).to be false
149
+ end
150
+
151
+ it "can be negated" do
152
+ user = { key: 'x', name: 'Bob' }
153
+ clause = { attribute: 'name', op: 'in', values: ['Bob'] }
154
+ expect {
155
+ clause[:negate] = true
156
+ }.to change {clause_match_user(clause, user)}.from(true).to(false)
157
+ end
158
+ end
159
+
160
+ describe "operators" do
161
+ dateStr1 = "2017-12-06T00:00:00.000-07:00"
162
+ dateStr2 = "2017-12-06T00:01:01.000-07:00"
163
+ dateMs1 = 10000000
164
+ dateMs2 = 10000001
165
+ invalidDate = "hey what's this?"
166
+
167
+ operatorTests = [
168
+ # numeric comparisons
169
+ [ :in, 99, 99, true ],
170
+ [ :in, 99.0001, 99.0001, true ],
171
+ [ :in, 99, 99.0001, false ],
172
+ [ :in, 99.0001, 99, false ],
173
+ [ :lessThan, 99, 99.0001, true ],
174
+ [ :lessThan, 99.0001, 99, false ],
175
+ [ :lessThan, 99, 99, false ],
176
+ [ :lessThanOrEqual, 99, 99.0001, true ],
177
+ [ :lessThanOrEqual, 99.0001, 99, false ],
178
+ [ :lessThanOrEqual, 99, 99, true ],
179
+ [ :greaterThan, 99.0001, 99, true ],
180
+ [ :greaterThan, 99, 99.0001, false ],
181
+ [ :greaterThan, 99, 99, false ],
182
+ [ :greaterThanOrEqual, 99.0001, 99, true ],
183
+ [ :greaterThanOrEqual, 99, 99.0001, false ],
184
+ [ :greaterThanOrEqual, 99, 99, true ],
185
+
186
+ # string comparisons
187
+ [ :in, "x", "x", true ],
188
+ [ :in, "x", "xyz", false ],
189
+ [ :startsWith, "xyz", "x", true ],
190
+ [ :startsWith, "x", "xyz", false ],
191
+ [ :endsWith, "xyz", "z", true ],
192
+ [ :endsWith, "z", "xyz", false ],
193
+ [ :contains, "xyz", "y", true ],
194
+ [ :contains, "y", "xyz", false ],
195
+
196
+ # mixed strings and numbers
197
+ [ :in, "99", 99, false ],
198
+ [ :in, 99, "99", false ],
199
+ #[ :contains, "99", 99, false ], # currently throws exception - would return false in Java SDK
200
+ #[ :startsWith, "99", 99, false ], # currently throws exception - would return false in Java SDK
201
+ #[ :endsWith, "99", 99, false ] # currently throws exception - would return false in Java SDK
202
+ [ :lessThanOrEqual, "99", 99, false ],
203
+ #[ :lessThanOrEqual, 99, "99", false ], # currently throws exception - would return false in Java SDK
204
+ [ :greaterThanOrEqual, "99", 99, false ],
205
+ #[ :greaterThanOrEqual, 99, "99", false ], # currently throws exception - would return false in Java SDK
206
+
207
+ # regex
208
+ [ :matches, "hello world", "hello.*rld", true ],
209
+ [ :matches, "hello world", "hello.*orl", true ],
210
+ [ :matches, "hello world", "l+", true ],
211
+ [ :matches, "hello world", "(world|planet)", true ],
212
+ [ :matches, "hello world", "aloha", false ],
213
+ #[ :matches, "hello world", "***not a regex", false ] # currently throws exception - same as Java SDK
214
+
215
+ # dates
216
+ [ :before, dateStr1, dateStr2, true ],
217
+ [ :before, dateMs1, dateMs2, true ],
218
+ [ :before, dateStr2, dateStr1, false ],
219
+ [ :before, dateMs2, dateMs1, false ],
220
+ [ :before, dateStr1, dateStr1, false ],
221
+ [ :before, dateMs1, dateMs1, false ],
222
+ [ :before, dateStr1, invalidDate, false ],
223
+ [ :after, dateStr1, dateStr2, false ],
224
+ [ :after, dateMs1, dateMs2, false ],
225
+ [ :after, dateStr2, dateStr1, true ],
226
+ [ :after, dateMs2, dateMs1, true ],
227
+ [ :after, dateStr1, dateStr1, false ],
228
+ [ :after, dateMs1, dateMs1, false ],
229
+ [ :after, dateStr1, invalidDate, false ],
230
+
231
+ # semver
232
+ [ :semVerEqual, "2.0.1", "2.0.1", true ],
233
+ [ :semVerEqual, "2.0", "2.0.0", true ],
234
+ [ :semVerEqual, "2-rc1", "2.0.0-rc1", true ],
235
+ [ :semVerEqual, "2+build2", "2.0.0+build2", true ],
236
+ [ :semVerLessThan, "2.0.0", "2.0.1", true ],
237
+ [ :semVerLessThan, "2.0", "2.0.1", true ],
238
+ [ :semVerLessThan, "2.0.1", "2.0.0", false ],
239
+ [ :semVerLessThan, "2.0.1", "2.0", false ],
240
+ [ :semVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", true ],
241
+ [ :semVerGreaterThan, "2.0.1", "2.0.0", true ],
242
+ [ :semVerGreaterThan, "2.0.1", "2.0", true ],
243
+ [ :semVerGreaterThan, "2.0.0", "2.0.1", false ],
244
+ [ :semVerGreaterThan, "2.0", "2.0.1", false ],
245
+ [ :semVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", true ],
246
+ [ :semVerLessThan, "2.0.1", "xbad%ver", false ],
247
+ [ :semVerGreaterThan, "2.0.1", "xbad%ver", false ]
248
+ ]
249
+
250
+ operatorTests.each do |params|
251
+ op = params[0]
252
+ value1 = params[1]
253
+ value2 = params[2]
254
+ shouldBe = params[3]
255
+ it "should return #{shouldBe} for #{value1} #{op} #{value2}" do
256
+ user = { key: 'x', custom: { foo: value1 } }
257
+ clause = { attribute: 'foo', op: op, values: [value2] }
258
+ expect(clause_match_user(clause, user)).to be shouldBe
259
+ end
260
+ end
261
+ end
262
+
263
+ describe "bucket_user" do
264
+ it "gets expected bucket values for specific keys" do
265
+ user = { key: "userKeyA" }
266
+ bucket = bucket_user(user, "hashKey", "key", "saltyA")
267
+ expect(bucket).to be_within(0.0000001).of(0.42157587);
268
+
269
+ user = { key: "userKeyB" }
270
+ bucket = bucket_user(user, "hashKey", "key", "saltyA")
271
+ expect(bucket).to be_within(0.0000001).of(0.6708485);
272
+
273
+ user = { key: "userKeyC" }
274
+ bucket = bucket_user(user, "hashKey", "key", "saltyA")
275
+ expect(bucket).to be_within(0.0000001).of(0.10343106);
276
+ end
277
+
278
+ it "can bucket by int value (equivalent to string)" do
279
+ user = {
280
+ key: "userkey",
281
+ custom: {
282
+ stringAttr: "33333",
283
+ intAttr: 33333
284
+ }
285
+ }
286
+ stringResult = bucket_user(user, "hashKey", "stringAttr", "saltyA")
287
+ intResult = bucket_user(user, "hashKey", "intAttr", "saltyA")
288
+
289
+ expect(intResult).to be_within(0.0000001).of(0.54771423)
290
+ expect(intResult).to eq(stringResult)
291
+ end
292
+
293
+ it "cannot bucket by float value" do
294
+ user = {
295
+ key: "userkey",
296
+ custom: {
297
+ floatAttr: 33.5
298
+ }
299
+ }
300
+ result = bucket_user(user, "hashKey", "floatAttr", "saltyA")
301
+ expect(result).to eq(0.0)
302
+ end
303
+
304
+
305
+ it "cannot bucket by bool value" do
306
+ user = {
307
+ key: "userkey",
308
+ custom: {
309
+ boolAttr: true
310
+ }
311
+ }
312
+ result = bucket_user(user, "hashKey", "boolAttr", "saltyA")
313
+ expect(result).to eq(0.0)
314
+ end
315
+ end
316
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ldclient-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.1
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-24 00:00:00.000000000 Z
11
+ date: 2018-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -168,6 +168,20 @@ dependencies:
168
168
  - - "<"
169
169
  - !ruby/object:Gem::Version
170
170
  version: '3'
171
+ - !ruby/object:Gem::Dependency
172
+ name: semantic
173
+ requirement: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - "~>"
176
+ - !ruby/object:Gem::Version
177
+ version: 1.6.0
178
+ type: :runtime
179
+ prerelease: false
180
+ version_requirements: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - "~>"
183
+ - !ruby/object:Gem::Version
184
+ version: 1.6.0
171
185
  - !ruby/object:Gem::Dependency
172
186
  name: thread_safe
173
187
  requirement: !ruby/object:Gem::Requirement
@@ -320,6 +334,7 @@ files:
320
334
  - lib/ldclient-rb/version.rb
321
335
  - scripts/release.sh
322
336
  - spec/config_spec.rb
337
+ - spec/evaluation_spec.rb
323
338
  - spec/event_serializer_spec.rb
324
339
  - spec/feature_store_spec_base.rb
325
340
  - spec/fixtures/feature.json
@@ -362,6 +377,7 @@ specification_version: 4
362
377
  summary: LaunchDarkly SDK for Ruby
363
378
  test_files:
364
379
  - spec/config_spec.rb
380
+ - spec/evaluation_spec.rb
365
381
  - spec/event_serializer_spec.rb
366
382
  - spec/feature_store_spec_base.rb
367
383
  - spec/fixtures/feature.json