ldclient-rb 2.4.1 → 2.5.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 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