amplitude-experiment 1.5.0 → 1.6.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/amplitude-experiment.gemspec +5 -5
  3. data/lib/amplitude-experiment.rb +6 -1
  4. data/lib/experiment/deployment/deployment_runner.rb +3 -3
  5. data/lib/experiment/evaluation/evaluation.rb +311 -0
  6. data/lib/experiment/evaluation/flag.rb +123 -0
  7. data/lib/experiment/evaluation/murmur3.rb +104 -0
  8. data/lib/experiment/evaluation/select.rb +16 -0
  9. data/lib/experiment/evaluation/semantic_version.rb +52 -0
  10. data/lib/experiment/evaluation/topological_sort.rb +56 -0
  11. data/lib/experiment/flag/flag_config_fetcher.rb +1 -1
  12. data/lib/experiment/flag/flag_config_storage.rb +1 -1
  13. data/lib/experiment/local/client.rb +7 -8
  14. data/lib/experiment/persistent_http_client.rb +1 -1
  15. data/lib/experiment/remote/client.rb +8 -6
  16. data/lib/experiment/remote/config.rb +8 -1
  17. data/lib/experiment/util/flag_config.rb +10 -10
  18. data/lib/experiment/version.rb +1 -1
  19. metadata +20 -24
  20. data/lib/experiment/local/evaluation/evaluation.rb +0 -76
  21. data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so +0 -0
  22. data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h +0 -110
  23. data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so +0 -0
  24. data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h +0 -110
  25. data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib +0 -0
  26. data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h +0 -110
  27. data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib +0 -0
  28. data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h +0 -110
  29. data/lib/experiment/util/topological_sort.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c79062c9e6a6f0877449f49dab03aa2445cf31700ecb92010f88d5779759d73a
4
- data.tar.gz: 1cf7d73b53cb125ecfaa6987bb4569d7416c5b02e194ef9f90be65c8da0b2fe6
3
+ metadata.gz: 6d05f9400e7dd25e87eb6f372c0c1c7fa5c083fe392a75dd25152e0ad0c010a1
4
+ data.tar.gz: 19df424d3a56c5a44d6495282f3e1f5022e1a50c47b4a5e0fcfc53c0535576ea
5
5
  SHA512:
6
- metadata.gz: 45e8a5540fc7a0e9a9213bdfe7c8828137d35aee0ab6d5dab50e79cac47bf858f11db626a545f131c7a5e48bf05b04b9b82944441d5c307d9562fd42973e5edb
7
- data.tar.gz: ffebca0974025ed2e983b106468be3806eb724cb5f0a2723d0f2782b67005d679ca8cfec4edb1b5236578a855472838a1f20e81d7641e362301d35fc5bfec32c
6
+ metadata.gz: 4e2e412e261f918990f23890200e9dc7b2cf0ed5b52e38fe585f10a3bd84f3b2d062c21a60dbf6811fdf27c395635f214f433d5dfb77c5573e3ae7e82a803352
7
+ data.tar.gz: 3bde003e6e5cb0d8b139218d408c5235949bed823f6973fb3f45f3eb542d7adfc7cdfe74f416bf62f06d6cd910eace6490bb6e4e3e6a0b7ef8f4abe686c854b4
@@ -14,21 +14,21 @@ Gem::Specification.new do |spec|
14
14
  spec.files = Dir['README.md',
15
15
  'lib/**/*.rb',
16
16
  'amplitude-experiment.gemspec',
17
- 'Gemfile',
18
- 'lib/experiment/local/evaluation/lib/**/*']
17
+ 'Gemfile']
19
18
  spec.require_paths = ['lib']
20
19
  spec.extra_rdoc_files = ['README.md']
21
20
 
22
- spec.add_development_dependency 'concurrent-ruby', '~> 1.2.2'
21
+ spec.add_dependency 'concurrent-ruby', '~> 1.2.2'
22
+
23
23
  spec.add_development_dependency 'psych', '~> 4.0'
24
24
  spec.add_development_dependency 'rake', '~> 13.0'
25
- spec.add_development_dependency 'rdoc', '= 6.4'
25
+ spec.add_development_dependency 'rdoc', '= 6.10'
26
26
  spec.add_development_dependency 'rspec', '~> 3.6'
27
27
  spec.add_development_dependency 'rubocop', '= 1.22.3'
28
28
  spec.add_development_dependency 'simplecov', '~> 0.21'
29
29
  spec.add_development_dependency 'webmock', '~> 3.14'
30
30
  spec.add_development_dependency 'yard', '~> 0.9'
31
31
  spec.add_development_dependency 'dotenv', '~> 2.8.1'
32
+ spec.add_development_dependency 'jar-dependencies', '= 0.4.1'
32
33
  spec.metadata['rubygems_mfa_required'] = 'false'
33
- spec.add_runtime_dependency 'ffi', '~> 1.15'
34
34
  end
@@ -14,7 +14,6 @@ require 'experiment/local/assignment/assignment_service'
14
14
  require 'experiment/local/assignment/assignment_config'
15
15
  require 'experiment/util/lru_cache'
16
16
  require 'experiment/util/hash'
17
- require 'experiment/util/topological_sort'
18
17
  require 'experiment/util/user'
19
18
  require 'experiment/util/variant'
20
19
  require 'experiment/error'
@@ -28,6 +27,12 @@ require 'experiment/cohort/cohort_storage'
28
27
  require 'experiment/cohort/cohort_sync_config'
29
28
  require 'experiment/deployment/deployment_runner'
30
29
  require 'experiment/util/poller'
30
+ require 'experiment/evaluation/evaluation'
31
+ require 'experiment/evaluation/flag'
32
+ require 'experiment/evaluation/murmur3'
33
+ require 'experiment/evaluation/select'
34
+ require 'experiment/evaluation/semantic_version'
35
+ require 'experiment/evaluation/topological_sort'
31
36
 
32
37
  # Amplitude Experiment Module
33
38
  module AmplitudeExperiment
@@ -60,9 +60,9 @@ module AmplitudeExperiment
60
60
 
61
61
  def update_flag_configs
62
62
  flags = @flag_config_fetcher.fetch_v2
63
- flag_configs = flags.each_with_object({}) { |flag, hash| hash[flag['key']] = flag }
64
- flag_keys = flag_configs.values.map { |flag| flag['key'] }.to_set
65
- @flag_config_storage.remove_if { |f| !flag_keys.include?(f['key']) }
63
+ flag_configs = flags.map { |f| [f.key, f] }.to_h
64
+ flag_keys = flag_configs.keys.to_set
65
+ @flag_config_storage.remove_if { |f| !flag_keys.include?(f.key) }
66
66
 
67
67
  unless @cohort_loader
68
68
  flag_configs.each do |flag_key, flag_config|
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evaluation
4
+ # Engine for evaluating feature flags based on context
5
+ class Engine
6
+ def evaluate(context, flags)
7
+ results = {}
8
+ target = {
9
+ 'context' => context,
10
+ 'result' => results
11
+ }
12
+
13
+ flags.each do |flag|
14
+ variant = evaluate_flag(target, flag)
15
+ results[flag.key] = variant if variant
16
+ end
17
+
18
+ results
19
+ end
20
+
21
+ private
22
+
23
+ def evaluate_flag(target, flag)
24
+ result = nil
25
+ flag.segments.each do |segment|
26
+ result = evaluate_segment(target, flag, segment)
27
+ next unless result
28
+
29
+ # Merge all metadata into the result
30
+ metadata = {}
31
+ metadata.merge!(flag.metadata) if flag.metadata
32
+ metadata.merge!(segment.metadata) if segment.metadata
33
+ metadata.merge!(result.metadata) if result.metadata
34
+ result.metadata = metadata
35
+ break
36
+ end
37
+ result
38
+ end
39
+
40
+ def evaluate_segment(target, flag, segment)
41
+ if segment.conditions
42
+ match = evaluate_conditions(target, segment.conditions)
43
+ if match
44
+ variant_key = bucket(target, segment)
45
+ variant_key ? flag.variants[variant_key] : nil
46
+ end
47
+ else
48
+ # Null conditions always match
49
+ variant_key = bucket(target, segment)
50
+ variant_key ? flag.variants[variant_key] : nil
51
+ end
52
+ end
53
+
54
+ def evaluate_conditions(target, conditions)
55
+ # Outer list logic is "or" (||)
56
+ conditions.any? do |inner_conditions|
57
+ match = true
58
+ inner_conditions.each do |condition|
59
+ match = match_condition(target, condition)
60
+ break unless match
61
+ end
62
+ match
63
+ end
64
+ end
65
+
66
+ def match_condition(target, condition)
67
+ prop_value = Evaluation.select(target, condition.selector)
68
+ # Special matching for null properties and set type prop values and operators
69
+ if !prop_value
70
+ match_null(condition.op, condition.values)
71
+ elsif set_operator?(condition.op)
72
+ prop_value_string_list = coerce_string_array(prop_value)
73
+ return false unless prop_value_string_list
74
+
75
+ match_set(prop_value_string_list, condition.op, condition.values)
76
+ else
77
+ prop_value_string = coerce_string(prop_value)
78
+ if prop_value_string
79
+ match_string(prop_value_string, condition.op, condition.values)
80
+ else
81
+ false
82
+ end
83
+ end
84
+ end
85
+
86
+ def get_hash(key)
87
+ Murmur3.hash32x86(key)
88
+ end
89
+
90
+ def bucket(target, segment)
91
+ unless segment.bucket
92
+ # Null bucket means segment is fully rolled out
93
+ return segment.variant
94
+ end
95
+
96
+ bucketing_value = coerce_string(Evaluation.select(target, segment.bucket.selector))
97
+ if !bucketing_value || bucketing_value.empty?
98
+ # Null or empty bucketing value cannot be bucketed
99
+ return segment.variant
100
+ end
101
+
102
+ key_to_hash = "#{segment.bucket.salt}/#{bucketing_value}"
103
+ hash = get_hash(key_to_hash)
104
+ allocation_value = hash % 100
105
+ distribution_value = (hash / 100).floor
106
+
107
+ segment.bucket.allocations.each do |allocation|
108
+ allocation_start = allocation.range[0]
109
+ allocation_end = allocation.range[1]
110
+ next unless allocation_value >= allocation_start && allocation_value < allocation_end
111
+
112
+ allocation.distributions.each do |distribution|
113
+ distribution_start = distribution.range[0]
114
+ distribution_end = distribution.range[1]
115
+ return distribution.variant if distribution_value >= distribution_start && distribution_value < distribution_end
116
+ end
117
+ end
118
+
119
+ segment.variant
120
+ end
121
+
122
+ def match_null(op, filter_values)
123
+ contains_none = contains_none?(filter_values)
124
+ case op
125
+ when Operator::IS, Operator::CONTAINS, Operator::LESS_THAN,
126
+ Operator::LESS_THAN_EQUALS, Operator::GREATER_THAN,
127
+ Operator::GREATER_THAN_EQUALS, Operator::VERSION_LESS_THAN,
128
+ Operator::VERSION_LESS_THAN_EQUALS, Operator::VERSION_GREATER_THAN,
129
+ Operator::VERSION_GREATER_THAN_EQUALS, Operator::SET_IS,
130
+ Operator::SET_CONTAINS, Operator::SET_CONTAINS_ANY
131
+ contains_none
132
+ when Operator::IS_NOT, Operator::DOES_NOT_CONTAIN,
133
+ Operator::SET_DOES_NOT_CONTAIN, Operator::SET_DOES_NOT_CONTAIN_ANY
134
+ !contains_none
135
+ else
136
+ false
137
+ end
138
+ end
139
+
140
+ def match_set(prop_values, op, filter_values)
141
+ case op
142
+ when Operator::SET_IS
143
+ set_equals?(prop_values, filter_values)
144
+ when Operator::SET_IS_NOT
145
+ !set_equals?(prop_values, filter_values)
146
+ when Operator::SET_CONTAINS
147
+ matches_set_contains_all?(prop_values, filter_values)
148
+ when Operator::SET_DOES_NOT_CONTAIN
149
+ !matches_set_contains_all?(prop_values, filter_values)
150
+ when Operator::SET_CONTAINS_ANY
151
+ matches_set_contains_any?(prop_values, filter_values)
152
+ when Operator::SET_DOES_NOT_CONTAIN_ANY
153
+ !matches_set_contains_any?(prop_values, filter_values)
154
+ else
155
+ false
156
+ end
157
+ end
158
+
159
+ def match_string(prop_value, op, filter_values)
160
+ case op
161
+ when Operator::IS
162
+ matches_is?(prop_value, filter_values)
163
+ when Operator::IS_NOT
164
+ !matches_is?(prop_value, filter_values)
165
+ when Operator::CONTAINS
166
+ matches_contains?(prop_value, filter_values)
167
+ when Operator::DOES_NOT_CONTAIN
168
+ !matches_contains?(prop_value, filter_values)
169
+ when Operator::LESS_THAN, Operator::LESS_THAN_EQUALS,
170
+ Operator::GREATER_THAN, Operator::GREATER_THAN_EQUALS
171
+ matches_comparable?(prop_value, op, filter_values,
172
+ method(:parse_number),
173
+ method(:comparator))
174
+ when Operator::VERSION_LESS_THAN, Operator::VERSION_LESS_THAN_EQUALS,
175
+ Operator::VERSION_GREATER_THAN, Operator::VERSION_GREATER_THAN_EQUALS
176
+ matches_comparable?(prop_value, op, filter_values,
177
+ SemanticVersion.method(:parse),
178
+ method(:comparator))
179
+ when Operator::REGEX_MATCH
180
+ matches_regex?(prop_value, filter_values)
181
+ when Operator::REGEX_DOES_NOT_MATCH
182
+ !matches_regex?(prop_value, filter_values)
183
+ else
184
+ false
185
+ end
186
+ end
187
+
188
+ def matches_is?(prop_value, filter_values)
189
+ if contains_booleans?(filter_values)
190
+ lower = prop_value.downcase
191
+ return filter_values.any? { |value| value.downcase == lower } if %w[true false].include?(lower)
192
+ end
193
+ filter_values.any? { |value| prop_value == value }
194
+ end
195
+
196
+ def matches_contains?(prop_value, filter_values)
197
+ filter_values.any? do |filter_value|
198
+ prop_value.downcase.include?(filter_value.downcase)
199
+ end
200
+ end
201
+
202
+ def matches_comparable?(prop_value, op, filter_values, type_transformer, type_comparator)
203
+ prop_value_transformed = type_transformer.call(prop_value)
204
+ filter_values_transformed = filter_values
205
+ .map { |filter_value| type_transformer.call(filter_value) }
206
+ .compact
207
+
208
+ if !prop_value_transformed || filter_values_transformed.empty?
209
+ filter_values.any? { |filter_value| comparator(prop_value, op, filter_value) }
210
+ else
211
+ filter_values_transformed.any? do |filter_value_transformed|
212
+ type_comparator.call(prop_value_transformed, op, filter_value_transformed)
213
+ end
214
+ end
215
+ end
216
+
217
+ def comparator(prop_value, op, filter_value)
218
+ case op
219
+ when Operator::LESS_THAN, Operator::VERSION_LESS_THAN
220
+ prop_value < filter_value
221
+ when Operator::LESS_THAN_EQUALS, Operator::VERSION_LESS_THAN_EQUALS
222
+ prop_value <= filter_value
223
+ when Operator::GREATER_THAN, Operator::VERSION_GREATER_THAN
224
+ prop_value > filter_value
225
+ when Operator::GREATER_THAN_EQUALS, Operator::VERSION_GREATER_THAN_EQUALS
226
+ prop_value >= filter_value
227
+ else
228
+ false
229
+ end
230
+ end
231
+
232
+ def matches_regex?(prop_value, filter_values)
233
+ filter_values.any? { |filter_value| !!(Regexp.new(filter_value) =~ prop_value) }
234
+ end
235
+
236
+ def contains_none?(filter_values)
237
+ filter_values.any? { |filter_value| filter_value == '(none)' }
238
+ end
239
+
240
+ def contains_booleans?(filter_values)
241
+ filter_values.any? do |filter_value|
242
+ case filter_value.downcase
243
+ when 'true', 'false'
244
+ true
245
+ else
246
+ false
247
+ end
248
+ end
249
+ end
250
+
251
+ def parse_number(value)
252
+ Float(value)
253
+ rescue StandardError
254
+ nil
255
+ end
256
+
257
+ def coerce_string(value)
258
+ return nil if value.nil?
259
+ return value.to_json if value.is_a?(Hash)
260
+
261
+ value.to_s
262
+ end
263
+
264
+ def coerce_string_array(value)
265
+ if value.is_a?(Array)
266
+ value.map { |e| coerce_string(e) }.compact
267
+ else
268
+ string_value = value.to_s
269
+ begin
270
+ parsed_value = JSON.parse(string_value)
271
+ if parsed_value.is_a?(Array)
272
+ parsed_value.map { |e| coerce_string(e) }.compact
273
+ else
274
+ s = coerce_string(string_value)
275
+ s ? [s] : nil
276
+ end
277
+ rescue JSON::ParserError
278
+ s = coerce_string(string_value)
279
+ s ? [s] : nil
280
+ end
281
+ end
282
+ end
283
+
284
+ def set_operator?(op)
285
+ case op
286
+ when Operator::SET_IS, Operator::SET_IS_NOT,
287
+ Operator::SET_CONTAINS, Operator::SET_DOES_NOT_CONTAIN,
288
+ Operator::SET_CONTAINS_ANY, Operator::SET_DOES_NOT_CONTAIN_ANY
289
+ true
290
+ else
291
+ false
292
+ end
293
+ end
294
+
295
+ def set_equals?(xa, ya)
296
+ xs = Set.new(xa)
297
+ ys = Set.new(ya)
298
+ xs.size == ys.size && ys.all? { |y| xs.include?(y) }
299
+ end
300
+
301
+ def matches_set_contains_all?(prop_values, filter_values)
302
+ return false if prop_values.length < filter_values.length
303
+
304
+ filter_values.all? { |filter_value| matches_is?(filter_value, prop_values) }
305
+ end
306
+
307
+ def matches_set_contains_any?(prop_values, filter_values)
308
+ filter_values.any? { |filter_value| matches_is?(filter_value, prop_values) }
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Evaluation
6
+ class Distribution
7
+ attr_accessor :variant, :range
8
+
9
+ def self.from_hash(hash)
10
+ new.tap do |dist|
11
+ dist.variant = hash['variant']
12
+ dist.range = hash['range']
13
+ end
14
+ end
15
+ end
16
+
17
+ class Allocation
18
+ attr_accessor :range, :distributions
19
+
20
+ def self.from_hash(hash)
21
+ new.tap do |alloc|
22
+ alloc.range = hash['range']
23
+ alloc.distributions = hash['distributions']&.map { |d| Distribution.from_hash(d) }
24
+ end
25
+ end
26
+ end
27
+
28
+ class Condition
29
+ attr_accessor :selector, :op, :values
30
+
31
+ def self.from_hash(hash)
32
+ new.tap do |cond|
33
+ cond.selector = hash['selector']
34
+ cond.op = hash['op']
35
+ cond.values = hash['values']
36
+ end
37
+ end
38
+ end
39
+
40
+ class Bucket
41
+ attr_accessor :selector, :salt, :allocations
42
+
43
+ def self.from_hash(hash)
44
+ new.tap do |bucket|
45
+ bucket.selector = hash['selector']
46
+ bucket.salt = hash['salt']
47
+ bucket.allocations = hash['allocations']&.map { |a| Allocation.from_hash(a) }
48
+ end
49
+ end
50
+ end
51
+
52
+ class Segment
53
+ attr_accessor :bucket, :conditions, :variant, :metadata
54
+
55
+ def self.from_hash(hash)
56
+ new.tap do |segment|
57
+ segment.bucket = hash['bucket'] && Bucket.from_hash(hash['bucket'])
58
+ segment.conditions = hash['conditions']&.map { |c| c.map { |inner| Condition.from_hash(inner) } }
59
+ segment.variant = hash['variant']
60
+ segment.metadata = hash['metadata']
61
+ end
62
+ end
63
+ end
64
+
65
+ class Variant
66
+ attr_accessor :key, :value, :payload, :metadata
67
+
68
+ def [](key)
69
+ instance_variable_get("@#{key}")
70
+ end
71
+
72
+ def self.from_hash(hash)
73
+ new.tap do |variant|
74
+ variant.key = hash['key']
75
+ variant.value = hash['value']
76
+ variant.payload = hash['payload']
77
+ variant.metadata = hash['metadata']
78
+ end
79
+ end
80
+ end
81
+
82
+ class Flag
83
+ attr_accessor :key, :variants, :segments, :dependencies, :metadata
84
+
85
+ def self.from_hash(hash)
86
+ new.tap do |flag|
87
+ flag.key = hash['key']
88
+ flag.variants = hash['variants'].transform_values { |v| Variant.from_hash(v) }
89
+ flag.segments = hash['segments'].map { |s| Segment.from_hash(s) }
90
+ flag.dependencies = hash['dependencies']
91
+ flag.metadata = hash['metadata']
92
+ end
93
+ end
94
+
95
+ # Used for testing
96
+ def ==(other)
97
+ key == other.key
98
+ end
99
+ end
100
+
101
+ module Operator
102
+ IS = 'is'
103
+ IS_NOT = 'is not'
104
+ CONTAINS = 'contains'
105
+ DOES_NOT_CONTAIN = 'does not contain'
106
+ LESS_THAN = 'less'
107
+ LESS_THAN_EQUALS = 'less or equal'
108
+ GREATER_THAN = 'greater'
109
+ GREATER_THAN_EQUALS = 'greater or equal'
110
+ VERSION_LESS_THAN = 'version less'
111
+ VERSION_LESS_THAN_EQUALS = 'version less or equal'
112
+ VERSION_GREATER_THAN = 'version greater'
113
+ VERSION_GREATER_THAN_EQUALS = 'version greater or equal'
114
+ SET_IS = 'set is'
115
+ SET_IS_NOT = 'set is not'
116
+ SET_CONTAINS = 'set contains'
117
+ SET_DOES_NOT_CONTAIN = 'set does not contain'
118
+ SET_CONTAINS_ANY = 'set contains any'
119
+ SET_DOES_NOT_CONTAIN_ANY = 'set does not contain any'
120
+ REGEX_MATCH = 'regex match'
121
+ REGEX_DOES_NOT_MATCH = 'regex does not match'
122
+ end
123
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Implements 32-bit x86 MurmurHash3
4
+ class Murmur3
5
+ C1_32 = -0x3361d2af
6
+ C2_32 = 0x1b873593
7
+ R1_32 = 15
8
+ R2_32 = 13
9
+ M_32 = 5
10
+ N_32 = -0x19ab949c
11
+
12
+ class << self
13
+ def hash32x86(input, seed = 0)
14
+ data = string_to_utf8_bytes(input)
15
+ length = data.length
16
+ n_blocks = length >> 2
17
+ hash = seed
18
+
19
+ # Process body
20
+ n_blocks.times do |i|
21
+ index = i << 2
22
+ k = read_int_le(data, index)
23
+ hash = mix32(k, hash)
24
+ end
25
+
26
+ # Process tail
27
+ index = n_blocks << 2
28
+ k1 = 0
29
+
30
+ case length - index
31
+ when 3
32
+ k1 ^= data[index + 2] << 16
33
+ k1 ^= data[index + 1] << 8
34
+ k1 ^= data[index]
35
+ k1 = (k1 * C1_32) & 0xffffffff
36
+ k1 = rotate_left(k1, R1_32)
37
+ k1 = (k1 * C2_32) & 0xffffffff
38
+ hash ^= k1
39
+ when 2
40
+ k1 ^= data[index + 1] << 8
41
+ k1 ^= data[index]
42
+ k1 = (k1 * C1_32) & 0xffffffff
43
+ k1 = rotate_left(k1, R1_32)
44
+ k1 = (k1 * C2_32) & 0xffffffff
45
+ hash ^= k1
46
+ when 1
47
+ k1 ^= data[index]
48
+ k1 = (k1 * C1_32) & 0xffffffff
49
+ k1 = rotate_left(k1, R1_32)
50
+ k1 = (k1 * C2_32) & 0xffffffff
51
+ hash ^= k1
52
+ end
53
+
54
+ hash ^= length
55
+ fmix32(hash) & 0xffffffff
56
+ end
57
+
58
+ private
59
+
60
+ def mix32(k, hash)
61
+ k = (k * C1_32) & 0xffffffff
62
+ k = rotate_left(k, R1_32)
63
+ k = (k * C2_32) & 0xffffffff
64
+ hash ^= k
65
+ hash = rotate_left(hash, R2_32)
66
+ ((hash * M_32) + N_32) & 0xffffffff
67
+ end
68
+
69
+ def fmix32(hash)
70
+ hash ^= hash >> 16
71
+ hash = (hash * -0x7a143595) & 0xffffffff
72
+ hash ^= hash >> 13
73
+ hash = (hash * -0x3d4d51cb) & 0xffffffff
74
+ hash ^= hash >> 16
75
+ hash
76
+ end
77
+
78
+ def rotate_left(x, n, width = 32)
79
+ n = n % width if n > width
80
+ mask = (0xffffffff << (width - n)) & 0xffffffff
81
+ r = ((x & mask) >> (width - n)) & 0xffffffff
82
+ ((x << n) | r) & 0xffffffff
83
+ end
84
+
85
+ def read_int_le(data, index = 0)
86
+ n = (data[index] << 24) |
87
+ (data[index + 1] << 16) |
88
+ (data[index + 2] << 8) |
89
+ data[index + 3]
90
+ reverse_bytes(n)
91
+ end
92
+
93
+ def reverse_bytes(n)
94
+ ((n & -0x1000000) >> 24) |
95
+ ((n & 0x00ff0000) >> 8) |
96
+ ((n & 0x0000ff00) << 8) |
97
+ ((n & 0x000000ff) << 24)
98
+ end
99
+
100
+ def string_to_utf8_bytes(str)
101
+ str.encode('UTF-8').bytes
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Selects a value from a nested object using an array of selector keys
4
+ module Evaluation
5
+ def self.select(selectable, selector)
6
+ return nil if selector.nil? || selector.empty?
7
+
8
+ selector.each do |selector_element|
9
+ return nil if selector_element.nil? || selectable.nil?
10
+
11
+ selectable = selectable[selector_element]
12
+ end
13
+
14
+ selectable.nil? ? nil : selectable
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SemanticVersion
4
+ include Comparable
5
+
6
+ attr_reader :major, :minor, :patch, :pre_release
7
+
8
+ MAJOR_MINOR_REGEX = '(\d+)\.(\d+)'
9
+ PATCH_REGEX = '(\d+)'
10
+ PRERELEASE_REGEX = '(-(([-\w]+\.?)*))?'
11
+ VERSION_PATTERN = /^#{MAJOR_MINOR_REGEX}(\.#{PATCH_REGEX}#{PRERELEASE_REGEX})?$/.freeze
12
+
13
+ def initialize(major, minor, patch, pre_release = nil)
14
+ @major = major
15
+ @minor = minor
16
+ @patch = patch
17
+ @pre_release = pre_release
18
+ end
19
+
20
+ def self.parse(version)
21
+ return nil if version.nil?
22
+
23
+ match = VERSION_PATTERN.match(version)
24
+ return nil unless match
25
+
26
+ major = match[1].to_i
27
+ minor = match[2].to_i
28
+ patch = match[4]&.to_i || 0
29
+ pre_release = match[5]
30
+
31
+ new(major, minor, patch, pre_release)
32
+ end
33
+
34
+ def <=>(other)
35
+ return nil unless other.is_a?(SemanticVersion)
36
+
37
+ result = major <=> other.major
38
+ return result unless result.zero?
39
+
40
+ result = minor <=> other.minor
41
+ return result unless result.zero?
42
+
43
+ result = patch <=> other.patch
44
+ return result unless result.zero?
45
+
46
+ return 1 if !pre_release && other.pre_release
47
+ return -1 if pre_release && !other.pre_release
48
+ return 0 if !pre_release && !other.pre_release
49
+
50
+ pre_release <=> other.pre_release
51
+ end
52
+ end