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.
- checksums.yaml +4 -4
- data/amplitude-experiment.gemspec +5 -5
- data/lib/amplitude-experiment.rb +6 -1
- data/lib/experiment/deployment/deployment_runner.rb +3 -3
- data/lib/experiment/evaluation/evaluation.rb +311 -0
- data/lib/experiment/evaluation/flag.rb +123 -0
- data/lib/experiment/evaluation/murmur3.rb +104 -0
- data/lib/experiment/evaluation/select.rb +16 -0
- data/lib/experiment/evaluation/semantic_version.rb +52 -0
- data/lib/experiment/evaluation/topological_sort.rb +56 -0
- data/lib/experiment/flag/flag_config_fetcher.rb +1 -1
- data/lib/experiment/flag/flag_config_storage.rb +1 -1
- data/lib/experiment/local/client.rb +7 -8
- data/lib/experiment/persistent_http_client.rb +1 -1
- data/lib/experiment/remote/client.rb +8 -6
- data/lib/experiment/remote/config.rb +8 -1
- data/lib/experiment/util/flag_config.rb +10 -10
- data/lib/experiment/version.rb +1 -1
- metadata +20 -24
- data/lib/experiment/local/evaluation/evaluation.rb +0 -76
- data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so +0 -0
- data/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h +0 -110
- data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so +0 -0
- data/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h +0 -110
- data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib +0 -0
- data/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h +0 -110
- data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib +0 -0
- data/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h +0 -110
- data/lib/experiment/util/topological_sort.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d05f9400e7dd25e87eb6f372c0c1c7fa5c083fe392a75dd25152e0ad0c010a1
|
4
|
+
data.tar.gz: 19df424d3a56c5a44d6495282f3e1f5022e1a50c47b4a5e0fcfc53c0535576ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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
|
data/lib/amplitude-experiment.rb
CHANGED
@@ -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.
|
64
|
-
flag_keys = flag_configs.
|
65
|
-
@flag_config_storage.remove_if { |f| !flag_keys.include?(f
|
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
|