Skip to content

Commit 8cc7ff5

Browse files
authored
Merge pull request #514 from splitio/semver-class
Semver class
2 parents c770cff + f12ee2a commit 8cc7ff5

File tree

7 files changed

+316
-0
lines changed

7 files changed

+316
-0
lines changed

lib/splitclient-rb.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
require 'splitclient-rb/engine/matchers/equal_to_boolean_matcher'
9191
require 'splitclient-rb/engine/matchers/equal_to_matcher'
9292
require 'splitclient-rb/engine/matchers/matches_string_matcher'
93+
require 'splitclient-rb/engine/matchers/semver'
9394
require 'splitclient-rb/engine/evaluator/splitter'
9495
require 'splitclient-rb/engine/impressions/noop_unique_keys_tracker'
9596
require 'splitclient-rb/engine/impressions/unique_keys_tracker'
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# frozen_string_literal: true
2+
3+
module SplitIoClient
4+
class Semver
5+
METADATA_DELIMITER = '+'
6+
PRE_RELEASE_DELIMITER = '-'
7+
VALUE_DELIMITER = '.'
8+
9+
attr_reader :major, :minor, :patch, :pre_release, :is_stable, :old_version
10+
11+
def initialize(version)
12+
@major = 0
13+
@minor = 0
14+
@patch = 0
15+
@pre_release = []
16+
@is_stable = false
17+
@old_version = version
18+
parse
19+
end
20+
21+
#
22+
# Class builder
23+
#
24+
# @param version [String] raw version as read from splitChanges response.
25+
#
26+
# @return [type] Semver instance
27+
def self.build(version, logger)
28+
new(version)
29+
rescue RuntimeError => e
30+
logger.warn("Failed to parse Semver data: #{e}")
31+
nil
32+
end
33+
34+
#
35+
# Check if there is any metadata characters in self._old_version.
36+
#
37+
# @return [type] String semver without the metadata
38+
#
39+
def remove_metadata_if_exists
40+
index = @old_version.index(METADATA_DELIMITER)
41+
return @old_version if index.nil?
42+
43+
@old_version[0, index]
44+
end
45+
46+
# Compare the current Semver object to a given Semver object, return:
47+
# 0: if self == passed
48+
# 1: if self > passed
49+
# -1: if self < passed
50+
#
51+
# @param to_compare [trype] splitio.models.grammar.matchers.semver.Semver object
52+
#
53+
# @returns [Integer] based on comparison
54+
def compare(to_compare)
55+
return 0 if @old_version == to_compare.old_version
56+
57+
# Compare major, minor, and patch versions numerically
58+
return compare_attributes(to_compare) if compare_attributes(to_compare) != 0
59+
60+
# Compare pre-release versions lexically
61+
compare_pre_release(to_compare)
62+
end
63+
64+
private
65+
66+
def integer?(value)
67+
value.to_i.to_s == value
68+
end
69+
70+
#
71+
# Parse the string in self._old_version to update the other internal variables
72+
#
73+
def parse
74+
without_metadata = remove_metadata_if_exists
75+
76+
index = without_metadata.index(PRE_RELEASE_DELIMITER)
77+
if index.nil?
78+
@is_stable = true
79+
else
80+
pre_release_data = without_metadata[index + 1..-1]
81+
without_metadata = without_metadata[0, index]
82+
@pre_release = pre_release_data.split(VALUE_DELIMITER)
83+
end
84+
assign_major_minor_and_patch(without_metadata)
85+
end
86+
87+
#
88+
# Set the major, minor and patch internal variables based on string passed.
89+
#
90+
# @param version [String] raw version containing major.minor.patch numbers.
91+
def assign_major_minor_and_patch(version)
92+
parts = version.split(VALUE_DELIMITER)
93+
if parts.length != 3 ||
94+
!(integer?(parts[0]) &&
95+
integer?(parts[1]) &&
96+
integer?(parts[2]))
97+
raise "Unable to convert to Semver, incorrect format: #{version}"
98+
end
99+
100+
@major = parts[0].to_i
101+
@minor = parts[1].to_i
102+
@patch = parts[2].to_i
103+
end
104+
105+
#
106+
# Compare 2 variables and return int as follows:
107+
# 0: if var1 == var2
108+
# 1: if var1 > var2
109+
# -1: if var1 < var2
110+
#
111+
# @param var1 [type] String/Integer object that accept ==, < or > operators
112+
# @param var2 [type] String/Integer object that accept ==, < or > operators
113+
#
114+
# @returns [Integer] based on comparison
115+
def compare_vars(var1, var2)
116+
return 0 if var1 == var2
117+
118+
return 1 if var1 > var2
119+
120+
-1
121+
end
122+
123+
# Compare the current Semver object's major, minor, patch and is_stable attributes to a given Semver object, return:
124+
# 0: if self == passed
125+
# 1: if self > passed
126+
# -1: if self < passed
127+
#
128+
# @param to_compare [trype] splitio.models.grammar.matchers.semver.Semver object
129+
#
130+
# @returns [Integer] based on comparison
131+
def compare_attributes(to_compare)
132+
result = compare_vars(@major, to_compare.major)
133+
return result if result != 0
134+
135+
result = compare_vars(@minor, to_compare.minor)
136+
return result if result != 0
137+
138+
result = compare_vars(@patch, to_compare.patch)
139+
return result if result != 0
140+
141+
return -1 if !@is_stable && to_compare.is_stable
142+
143+
return 1 if @is_stable && !to_compare.is_stable
144+
145+
0
146+
end
147+
148+
# Compare the current Semver object's pre_release attribute to a given Semver object, return:
149+
# 0: if self == passed
150+
# 1: if self > passed
151+
# -1: if self < passed
152+
#
153+
# @param to_compare [trype] splitio.models.grammar.matchers.semver.Semver object
154+
#
155+
# @returns [Integer] based on comparison
156+
def compare_pre_release(to_compare)
157+
min_length = get_pre_min_length(to_compare)
158+
0.upto(min_length - 1) do |i|
159+
next if @pre_release[i] == to_compare.pre_release[i]
160+
161+
if integer?(@pre_release[i]) && integer?(to_compare.pre_release[i])
162+
return compare_vars(@pre_release[i].to_i, to_compare.pre_release[i].to_i)
163+
end
164+
165+
return compare_vars(@pre_release[i], to_compare.pre_release[i])
166+
end
167+
# Compare lengths of pre-release versions
168+
compare_vars(@pre_release.length, to_compare.pre_release.length)
169+
end
170+
171+
# Get minimum of current Semver object's pre_release attributes length to a given Semver object
172+
#
173+
# @param to_compare [trype] splitio.models.grammar.matchers.semver.Semver object
174+
#
175+
# @returns [Integer]
176+
def get_pre_min_length(to_compare)
177+
[@pre_release.length, to_compare.pre_release.length].min
178+
end
179+
end
180+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
require 'spec_helper'
2+
require 'csv'
3+
4+
describe SplitIoClient::Semver do
5+
let(:valid_versions) do
6+
CSV.parse(File.read(File.expand_path(File.join(File.dirname(__FILE__),
7+
'../../test_data/splits/semver/valid-semantic-versions.csv'))))
8+
end
9+
let(:invalid_versions) do
10+
CSV.parse(File.read(File.expand_path(File.join(File.dirname(__FILE__),
11+
'../../test_data/splits/semver/invalid-semantic-versions.csv'))))
12+
end
13+
let(:equal_to_versions) do
14+
CSV.parse(File.read(File.expand_path(File.join(File.dirname(__FILE__),
15+
'../../test_data/splits/semver/equal-to-semver.csv'))))
16+
end
17+
18+
let(:logger) { Logger.new('/dev/null') }
19+
20+
context 'check versions' do
21+
it 'accept valid versions' do
22+
for i in (0..valid_versions.length-1)
23+
expect(described_class.build(valid_versions[i][0], logger)).should_not be_nil
24+
end
25+
end
26+
it 'reject invalid versions' do
27+
for version in invalid_versions
28+
expect(described_class.build(version[0], logger)).to eq(nil)
29+
end
30+
end
31+
end
32+
33+
context 'compare versions' do
34+
it 'equal and not equal' do
35+
for i in (1..valid_versions.length-1)
36+
expect(described_class.build(valid_versions[i][0], logger).compare(described_class.build(valid_versions[i][1], logger))).to eq(1)
37+
expect(described_class.build(valid_versions[i][1], logger).compare(described_class.build(valid_versions[i][0], logger))).to eq(-1)
38+
expect(described_class.build(valid_versions[i][0], logger).compare(described_class.build(valid_versions[i][0], logger))).to eq(0)
39+
expect(described_class.build(valid_versions[i][1], logger).compare(described_class.build(valid_versions[i][1], logger))).to eq(0)
40+
end
41+
for i in (1..equal_to_versions.length-1)
42+
if valid_versions[i][2]
43+
expect(described_class.build(valid_versions[i][0], logger).compare(described_class.build(valid_versions[i][1], logger))).to eq(0)
44+
else
45+
expect(described_class.build(valid_versions[i][0], logger).compare(described_class.build(valid_versions[i][1], logger))).not_to eq(0)
46+
end
47+
end
48+
49+
end
50+
end
51+
52+
def verify_version(semver, major, minor, patch, pre_release="", is_stable=True)
53+
if semver.major == major && semver.minor == minor && semver.patch == patch &&
54+
semver.pre_release == pre_release && semver.is_stable == is_stable
55+
return true
56+
end
57+
return false
58+
end
59+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# version1, version2, version3, expected
2+
1.1.1,2.2.2,3.3.3,true
3+
1.1.1-rc.1,1.1.1-rc.2,1.1.1-rc.3,true
4+
1.0.0-alpha,1.0.0-alpha.1,1.0.0-alpha.beta,true
5+
1.0.0-alpha.1,1.0.0-alpha.beta,1.0.0-beta,true
6+
1.0.0-alpha.beta,1.0.0-beta,1.0.0-beta.2,true
7+
1.0.0-beta,1.0.0-beta.2,1.0.0-beta.11,true
8+
1.0.0-beta.2,1.0.0-beta.11,1.0.0-rc.1,true
9+
1.0.0-beta.11,1.0.0-rc.1,1.0.0,true
10+
1.1.2,1.1.3,1.1.4,true
11+
1.2.1,1.3.1,1.4.1,true
12+
2.0.0,3.0.0,4.0.0,true
13+
2.2.2,2.2.3-rc1,2.2.3,true
14+
2.2.2,2.3.2-rc100,2.3.3,true
15+
1.0.0-rc.1+build.1,1.2.3-beta,1.2.3-rc.1+build.123,true
16+
3.3.3,3.3.3-alpha,3.3.4,false
17+
2.2.2-rc.1,2.2.2+metadata,2.2.2-rc.10,false
18+
1.1.1-rc.1,1.1.1-rc.3,1.1.1-rc.2,false
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# version1, version2, equals
2+
1.1.1,1.1.1,true
3+
1.1.1,1.1.1+metadata,false
4+
1.1.1,1.1.1-rc.1,false
5+
88.88.88,88.88.88,true
6+
1.2.3----RC-SNAPSHOT.12.9.1--.12,1.2.3----RC-SNAPSHOT.12.9.1--.12,true
7+
10.2.3-DEV-SNAPSHOT,10.2.3-SNAPSHOT-123,false
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# invalid
2+
1
3+
1.2
4+
1.alpha.2
5+
+invalid
6+
-invalid
7+
-invalid+invalid
8+
-invalid.01
9+
alpha
10+
alpha.beta
11+
alpha.beta.1
12+
alpha.1
13+
alpha+beta
14+
alpha_beta
15+
alpha.
16+
alpha..
17+
beta
18+
-alpha.
19+
1.2
20+
1.2.3.DEV
21+
1.2-SNAPSHOT
22+
1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788
23+
1.2-RC-SNAPSHOT
24+
-1.0.3-gamma+b7718
25+
+justmeta
26+
#99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# higher, lower
2+
1.1.2,1.1.1
3+
1.0.0,1.0.0-rc.1
4+
1.1.0-rc.1,1.0.0-beta.11
5+
1.0.0-beta.11,1.0.0-beta.2
6+
1.0.0-beta.2,1.0.0-beta
7+
1.0.0-beta,1.0.0-alpha.beta
8+
1.0.0-alpha.beta,1.0.0-alpha.1
9+
1.0.0-alpha.1,1.0.0-alpha
10+
2.2.2-rc.2+metadata-lalala,2.2.2-rc.1.2
11+
1.2.3,0.0.4
12+
1.1.2+meta,1.1.2-prerelease+meta
13+
1.0.0-beta,1.0.0-alpha
14+
1.0.0-alpha0.valid,1.0.0-alpha.0valid
15+
1.0.0-rc.1+build.1,1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay
16+
10.2.3-DEV-SNAPSHOT,1.2.3-SNAPSHOT-123
17+
1.1.1-rc2,1.0.0-0A.is.legal
18+
1.2.3----RC-SNAPSHOT.12.9.1--.12+788,1.2.3----R-S.12.9.1--.12+meta
19+
1.2.3----RC-SNAPSHOT.12.9.1--.12.88,1.2.3----RC-SNAPSHOT.12.9.1--.12
20+
9223372036854775807.9223372036854775807.9223372036854775807,9223372036854775807.9223372036854775807.9223372036854775806
21+
1.1.1-alpha.beta.rc.build.java.pr.support.10,1.1.1-alpha.beta.rc.build.java.pr.support
22+
1.1.2,1.1.1
23+
1.2.1,1.1.1
24+
2.1.1,1.1.1
25+
1.1.1-rc.1,1.1.1-rc.0

0 commit comments

Comments
 (0)