Skip to content

Commit 09b96ed

Browse files
authored
Merge pull request #32 from eplusminus/eb/semver-operators
add semantic version operators
2 parents 6bfbdd4 + 71a8c7a commit 09b96ed

File tree

3 files changed

+188
-27
lines changed

3 files changed

+188
-27
lines changed

ldclient-rb.gemspec

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ Gem::Specification.new do |spec|
2828
spec.add_development_dependency "redis", "~> 3.3.5"
2929
spec.add_development_dependency "connection_pool", ">= 2.1.2"
3030
spec.add_development_dependency "moneta", "~> 1.0.0"
31-
31+
3232
spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
3333
spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
3434
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
35+
spec.add_runtime_dependency "semantic", "~> 1.6.0"
3536
spec.add_runtime_dependency "thread_safe", "~> 0.3"
3637
spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
3738
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0.4"

lib/ldclient-rb/evaluation.rb

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,57 @@
11
require "date"
2+
require "semantic"
23

34
module LaunchDarkly
45
module Evaluation
56
BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
67

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+
755
OPERATORS = {
856
in:
957
lambda do |a, b|
@@ -42,33 +90,15 @@ module Evaluation
4290
(a.is_a? Numeric) && (a >= b)
4391
end,
4492
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 },
5894
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 }
72102
}
73103

74104
class EvaluationError < StandardError

spec/evaluation_spec.rb

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
require "spec_helper"
2+
3+
describe LaunchDarkly::Evaluation do
4+
subject { LaunchDarkly::Evaluation }
5+
6+
include LaunchDarkly::Evaluation
7+
8+
describe "clause_match_user" do
9+
it "can match built-in attribute" do
10+
user = { key: 'x', name: 'Bob' }
11+
clause = { attribute: 'name', op: 'in', values: ['Bob'] }
12+
expect(clause_match_user(clause, user)).to be true
13+
end
14+
15+
it "can match custom attribute" do
16+
user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
17+
clause = { attribute: 'legs', op: 'in', values: [4] }
18+
expect(clause_match_user(clause, user)).to be true
19+
end
20+
21+
it "returns false for missing attribute" do
22+
user = { key: 'x', name: 'Bob' }
23+
clause = { attribute: 'legs', op: 'in', values: [4] }
24+
expect(clause_match_user(clause, user)).to be false
25+
end
26+
end
27+
28+
describe "operators" do
29+
dateStr1 = "2017-12-06T00:00:00.000-07:00"
30+
dateStr2 = "2017-12-06T00:01:01.000-07:00"
31+
dateMs1 = 10000000
32+
dateMs2 = 10000001
33+
invalidDate = "hey what's this?"
34+
35+
operatorTests = [
36+
# numeric comparisons
37+
[ :in, 99, 99, true ],
38+
[ :in, 99.0001, 99.0001, true ],
39+
[ :in, 99, 99.0001, false ],
40+
[ :in, 99.0001, 99, false ],
41+
[ :lessThan, 99, 99.0001, true ],
42+
[ :lessThan, 99.0001, 99, false ],
43+
[ :lessThan, 99, 99, false ],
44+
[ :lessThanOrEqual, 99, 99.0001, true ],
45+
[ :lessThanOrEqual, 99.0001, 99, false ],
46+
[ :lessThanOrEqual, 99, 99, true ],
47+
[ :greaterThan, 99.0001, 99, true ],
48+
[ :greaterThan, 99, 99.0001, false ],
49+
[ :greaterThan, 99, 99, false ],
50+
[ :greaterThanOrEqual, 99.0001, 99, true ],
51+
[ :greaterThanOrEqual, 99, 99.0001, false ],
52+
[ :greaterThanOrEqual, 99, 99, true ],
53+
54+
# string comparisons
55+
[ :in, "x", "x", true ],
56+
[ :in, "x", "xyz", false ],
57+
[ :startsWith, "xyz", "x", true ],
58+
[ :startsWith, "x", "xyz", false ],
59+
[ :endsWith, "xyz", "z", true ],
60+
[ :endsWith, "z", "xyz", false ],
61+
[ :contains, "xyz", "y", true ],
62+
[ :contains, "y", "xyz", false ],
63+
64+
# mixed strings and numbers
65+
[ :in, "99", 99, false ],
66+
[ :in, 99, "99", false ],
67+
#[ :contains, "99", 99, false ], # currently throws exception - would return false in Java SDK
68+
#[ :startsWith, "99", 99, false ], # currently throws exception - would return false in Java SDK
69+
#[ :endsWith, "99", 99, false ] # currently throws exception - would return false in Java SDK
70+
[ :lessThanOrEqual, "99", 99, false ],
71+
#[ :lessThanOrEqual, 99, "99", false ], # currently throws exception - would return false in Java SDK
72+
[ :greaterThanOrEqual, "99", 99, false ],
73+
#[ :greaterThanOrEqual, 99, "99", false ], # currently throws exception - would return false in Java SDK
74+
75+
# regex
76+
[ :matches, "hello world", "hello.*rld", true ],
77+
[ :matches, "hello world", "hello.*orl", true ],
78+
[ :matches, "hello world", "l+", true ],
79+
[ :matches, "hello world", "(world|planet)", true ],
80+
[ :matches, "hello world", "aloha", false ],
81+
#[ :matches, "hello world", "***not a regex", false ] # currently throws exception - same as Java SDK
82+
83+
# dates
84+
[ :before, dateStr1, dateStr2, true ],
85+
[ :before, dateMs1, dateMs2, true ],
86+
[ :before, dateStr2, dateStr1, false ],
87+
[ :before, dateMs2, dateMs1, false ],
88+
[ :before, dateStr1, dateStr1, false ],
89+
[ :before, dateMs1, dateMs1, false ],
90+
[ :before, dateStr1, invalidDate, false ],
91+
[ :after, dateStr1, dateStr2, false ],
92+
[ :after, dateMs1, dateMs2, false ],
93+
[ :after, dateStr2, dateStr1, true ],
94+
[ :after, dateMs2, dateMs1, true ],
95+
[ :after, dateStr1, dateStr1, false ],
96+
[ :after, dateMs1, dateMs1, false ],
97+
[ :after, dateStr1, invalidDate, false ],
98+
99+
# semver
100+
[ :semVerEqual, "2.0.1", "2.0.1", true ],
101+
[ :semVerEqual, "2.0", "2.0.0", true ],
102+
[ :semVerEqual, "2-rc1", "2.0.0-rc1", true ],
103+
[ :semVerEqual, "2+build2", "2.0.0+build2", true ],
104+
[ :semVerLessThan, "2.0.0", "2.0.1", true ],
105+
[ :semVerLessThan, "2.0", "2.0.1", true ],
106+
[ :semVerLessThan, "2.0.1", "2.0.0", false ],
107+
[ :semVerLessThan, "2.0.1", "2.0", false ],
108+
[ :semVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", true ],
109+
[ :semVerGreaterThan, "2.0.1", "2.0.0", true ],
110+
[ :semVerGreaterThan, "2.0.1", "2.0", true ],
111+
[ :semVerGreaterThan, "2.0.0", "2.0.1", false ],
112+
[ :semVerGreaterThan, "2.0", "2.0.1", false ],
113+
[ :semVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", true ],
114+
[ :semVerLessThan, "2.0.1", "xbad%ver", false ],
115+
[ :semVerGreaterThan, "2.0.1", "xbad%ver", false ]
116+
]
117+
118+
operatorTests.each do |params|
119+
op = params[0]
120+
value1 = params[1]
121+
value2 = params[2]
122+
shouldBe = params[3]
123+
it "should return #{shouldBe} for #{value1} #{op} #{value2}" do
124+
user = { key: 'x', custom: { foo: value1 } }
125+
clause = { attribute: 'foo', op: op, values: [value2] }
126+
expect(clause_match_user(clause, user)).to be shouldBe
127+
end
128+
end
129+
end
130+
end

0 commit comments

Comments
 (0)