flagger 2.0.8 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/flagger.rb +80 -77
- data/lib/flagger/entity.rb +49 -0
- data/lib/flagger/flagger-386.dll +0 -0
- data/lib/flagger/flagger-amd64.dll +0 -0
- data/lib/flagger/libflagger-386.so +0 -0
- data/lib/flagger/libflagger-amd64.so +0 -0
- data/lib/flagger/libflagger.dylib +0 -0
- data/lib/flagger/native.rb +31 -0
- data/lib/flagger/response.rb +7 -0
- data/lib/flagger/version.rb +3 -4
- metadata +67 -36
- data/lib/flagger/cloud.rb +0 -499
- data/lib/flagger/microservice.rb +0 -63
- data/lib/flagger/models.rb +0 -657
- data/lib/flagger/stat.rb +0 -56
data/lib/flagger/microservice.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
require 'faraday'
|
2
|
-
require 'json'
|
3
|
-
|
4
|
-
module FlaggerEnvironments
|
5
|
-
class MicroserviceDelegate
|
6
|
-
attr_accessor :env_key
|
7
|
-
attr_accessor :edge_url
|
8
|
-
attr_accessor :request_timeout
|
9
|
-
|
10
|
-
def initialize(env_key, edge_url, request_timeout)
|
11
|
-
@env_key = env_key
|
12
|
-
@edge_url = edge_url
|
13
|
-
@request_timeout = request_timeout
|
14
|
-
end
|
15
|
-
|
16
|
-
def treatment(flag, entity)
|
17
|
-
get_object_values(flag, entity)['treatment'] rescue nil
|
18
|
-
end
|
19
|
-
|
20
|
-
def payload(flag, entity)
|
21
|
-
get_object_values(flag, entity)['payload'] rescue nil
|
22
|
-
end
|
23
|
-
|
24
|
-
def eligible?(flag, entity)
|
25
|
-
get_object_values(flag, entity)['isEligible'] rescue nil
|
26
|
-
end
|
27
|
-
|
28
|
-
def enabled?(flag, entity)
|
29
|
-
get_object_values(flag, entity)['isEnabled'] rescue nil
|
30
|
-
end
|
31
|
-
|
32
|
-
def publish(entities)
|
33
|
-
end
|
34
|
-
|
35
|
-
def cleanup()
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
def get_object_values(flag, entity)
|
40
|
-
begin
|
41
|
-
conn = Faraday.new(url: "#{@edge_url}/v2/object-values/#{@env_key}")
|
42
|
-
response = conn.post do |req|
|
43
|
-
req.options.timeout = @request_timeout
|
44
|
-
req.headers['Content-Type'] = 'application/json'
|
45
|
-
req.body = JSON.generate({
|
46
|
-
'flag' => flag.flag_name,
|
47
|
-
'entity' => entity,
|
48
|
-
})
|
49
|
-
end
|
50
|
-
if response.status == 200
|
51
|
-
object_values = JSON.parse(response.body)
|
52
|
-
object_values
|
53
|
-
else
|
54
|
-
puts 'Failed to connect to Airship edge server'
|
55
|
-
nil
|
56
|
-
end
|
57
|
-
rescue Exception => e
|
58
|
-
puts 'Failed to connect to Airship edge server', e
|
59
|
-
nil
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
data/lib/flagger/models.rb
DELETED
@@ -1,657 +0,0 @@
|
|
1
|
-
require 'digest'
|
2
|
-
require 'dry-validation'
|
3
|
-
|
4
|
-
module FlaggerModels
|
5
|
-
class BaseEntity
|
6
|
-
SCHEMA = Dry::Validation.Schema do
|
7
|
-
configure do
|
8
|
-
def valid_attribute_names?(attributes)
|
9
|
-
attributes.keys.all? do |name|
|
10
|
-
name.is_a?(String) && /^[a-zA-Z]$|^[a-zA-Z_]{0,48}[a-zA-Z]$/ =~ name
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
def valid_attribute_values?(attributes)
|
15
|
-
attributes.values.all? do |v|
|
16
|
-
(v.is_a?(String) && v.length <= 3000) || [true, false].include?(v) || v.is_a?(Numeric)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.messages
|
21
|
-
super.merge(
|
22
|
-
en: {
|
23
|
-
errors: {
|
24
|
-
:valid_attribute_names? => 'Each attribute must begin and end with an alphabet letter (a-z, A-Z). In between, allowed characters are a-z, A-Z, and "_". For example: isStudent or is_student. Preceding or trailing underscore is not allowed (i.e., _is_student or is_student_).',
|
25
|
-
:valid_attribute_values? => "An entity's attribute value must be a string no more than 3000 characters, boolean, or number."
|
26
|
-
}
|
27
|
-
}
|
28
|
-
)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
optional('type') { str? & size?(1..50) & format?(/^([A-Z][a-zA-Z]*)+$/) }
|
33
|
-
required('id') { str? & size?(1..250) }
|
34
|
-
optional('display_name') { str? & size?(1..250) }
|
35
|
-
optional('attributes') { type?(Hash) & size?(0..100) & valid_attribute_names? & valid_attribute_values? }
|
36
|
-
required('is_group') { bool? }
|
37
|
-
end
|
38
|
-
|
39
|
-
attr_accessor :type
|
40
|
-
attr_accessor :id
|
41
|
-
attr_accessor :display_name
|
42
|
-
attr_accessor :attributes
|
43
|
-
attr_accessor :is_group
|
44
|
-
|
45
|
-
def initialize(json)
|
46
|
-
@type = json['type']
|
47
|
-
@id = json['id']
|
48
|
-
if @id.is_a?(Numeric) then
|
49
|
-
@id = @id.to_s
|
50
|
-
end
|
51
|
-
@display_name = json['display_name'] || @id if @id.is_a?(String)
|
52
|
-
@attributes = json['attributes'] || {}
|
53
|
-
end
|
54
|
-
|
55
|
-
def attrs()
|
56
|
-
instance_variables.map do |var|
|
57
|
-
if var == :@group then
|
58
|
-
group = instance_variable_get(var)
|
59
|
-
[var[1..-1], if defined? group.attrs then group.attrs else group end]
|
60
|
-
else
|
61
|
-
[var[1..-1], instance_variable_get(var)]
|
62
|
-
end
|
63
|
-
end.to_h.delete_if { |k,v| v.nil? }
|
64
|
-
end
|
65
|
-
|
66
|
-
def hash()
|
67
|
-
attrs.hash
|
68
|
-
end
|
69
|
-
|
70
|
-
def validation_errors()
|
71
|
-
SCHEMA.(attrs).errors
|
72
|
-
end
|
73
|
-
|
74
|
-
def valid?()
|
75
|
-
validation_errors.length === 0
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
class GroupEntity < BaseEntity
|
80
|
-
def initialize(json)
|
81
|
-
super(json)
|
82
|
-
@is_group = true
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
class SingleEntity < BaseEntity
|
87
|
-
SCHEMA = Dry::Validation.Schema do
|
88
|
-
optional('group').schema(GroupEntity::SCHEMA)
|
89
|
-
end
|
90
|
-
|
91
|
-
attr_accessor :group
|
92
|
-
|
93
|
-
def initialize(json)
|
94
|
-
super(json)
|
95
|
-
@group = json['group']&.is_a?(Hash) && GroupEntity.new(json['group'])
|
96
|
-
@is_group = false
|
97
|
-
|
98
|
-
if @type == nil then
|
99
|
-
@type = 'User'
|
100
|
-
end
|
101
|
-
|
102
|
-
if @group && @group.type.nil? then
|
103
|
-
@group.type = @type + 'Group'
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def validation_errors()
|
108
|
-
if @group != nil then
|
109
|
-
group_errors = SingleEntity::SCHEMA.(attrs).errors
|
110
|
-
super.merge(group_errors)
|
111
|
-
else
|
112
|
-
super
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
class GatingInfoModel
|
118
|
-
def validate()
|
119
|
-
messages = self.class::SCHEMA.(attrs).messages
|
120
|
-
raise RuntimeError.new(messages) if messages.length > 0
|
121
|
-
end
|
122
|
-
|
123
|
-
def attrs()
|
124
|
-
instance_variables.map do |var|
|
125
|
-
value = instance_variable_get(var)
|
126
|
-
if value != nil then
|
127
|
-
[var, value]
|
128
|
-
else
|
129
|
-
nil
|
130
|
-
end
|
131
|
-
end.compact.to_h
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
class GatingInfo
|
136
|
-
attr_reader :sdk_info
|
137
|
-
attr_reader :flags
|
138
|
-
attr_reader :env
|
139
|
-
|
140
|
-
def initialize(json)
|
141
|
-
@sdk_info = SdkInfo.new(json['sdk_info'] || {})
|
142
|
-
@env = Env.new(json['env'])
|
143
|
-
@flags = Hash[json['flags'].map {|flag| [flag['codename'], Flag.new(flag, @env.hash_key)]}]
|
144
|
-
end
|
145
|
-
|
146
|
-
def flag(name)
|
147
|
-
@flags[name]
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
class SdkInfo < GatingInfoModel
|
152
|
-
SCHEMA = Dry::Validation.Schema do
|
153
|
-
optional(:@ingestion_interval) { int? }
|
154
|
-
optional(:@ingestion_max_items) { int? }
|
155
|
-
optional(:@should_ingest_entities) { bool? }
|
156
|
-
optional(:@should_ingest_stats) { bool? }
|
157
|
-
optional(:@should_ingest_exposures) { bool? }
|
158
|
-
optional(:@should_ingest_flags) { bool? }
|
159
|
-
end
|
160
|
-
|
161
|
-
attr_reader :ingestion_interval
|
162
|
-
attr_reader :ingestion_max_items
|
163
|
-
attr_reader :should_ingest_entities
|
164
|
-
attr_reader :should_ingest_stats
|
165
|
-
attr_reader :should_ingest_exposures
|
166
|
-
attr_reader :should_ingest_flags
|
167
|
-
|
168
|
-
def initialize(json)
|
169
|
-
@ingestion_interval = json['SDK_INGESTION_INTERVAL']
|
170
|
-
@ingestion_max_items = json['SDK_INGESTION_MAX_ITEMS']
|
171
|
-
@should_ingest_entities = json['SDK_SHOULD_INGEST_OBJECTS']
|
172
|
-
@should_ingest_stats = json['SDK_SHOULD_INGEST_STATS']
|
173
|
-
@should_ingest_exposures = json['SDK_SHOULD_INGEST_EXPOSURES']
|
174
|
-
@should_ingest_flags = json['SDK_SHOULD_INGEST_FLAGS']
|
175
|
-
|
176
|
-
validate
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
class Env < GatingInfoModel
|
181
|
-
SCHEMA = Dry::Validation.Schema do
|
182
|
-
required(:@env_key) { str? }
|
183
|
-
required(:@hash_key) { str? }
|
184
|
-
end
|
185
|
-
|
186
|
-
attr_reader :env_key
|
187
|
-
attr_reader :hash_key
|
188
|
-
|
189
|
-
def initialize(json)
|
190
|
-
@env_key = json['env_key']
|
191
|
-
@hash_key = json['hash_key']
|
192
|
-
|
193
|
-
validate
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
class Flag < GatingInfoModel
|
198
|
-
SCHEMA = Dry::Validation.Schema do
|
199
|
-
required(:@codename) { str? }
|
200
|
-
required(:@flag_status) { included_in? ['operational', 'archived'] }
|
201
|
-
required(:@flag_type) { included_in? ['basic', 'experiment', 'uncategorized'] }
|
202
|
-
required(:@hash_key) { str? }
|
203
|
-
optional(:@is_paused) { bool? }
|
204
|
-
required(:@is_web_accessible) { bool? }
|
205
|
-
required(:@overrides) { hash? }
|
206
|
-
required(:@populations) { array? }
|
207
|
-
required(:@splits) { hash? }
|
208
|
-
required(:@treatments) { hash? }
|
209
|
-
|
210
|
-
required(:@env_hash_key) { str? }
|
211
|
-
end
|
212
|
-
|
213
|
-
attr_reader :codename
|
214
|
-
attr_reader :flag_status
|
215
|
-
attr_reader :flag_type
|
216
|
-
attr_reader :hash_key
|
217
|
-
attr_reader :is_paused
|
218
|
-
attr_reader :is_web_accessible
|
219
|
-
attr_reader :overrides
|
220
|
-
attr_reader :populations
|
221
|
-
attr_reader :splits
|
222
|
-
attr_reader :treatments
|
223
|
-
|
224
|
-
attr_reader :env_hash_key
|
225
|
-
|
226
|
-
def initialize(json, env_hash_key)
|
227
|
-
@codename = json['codename']
|
228
|
-
@flag_status = json['flag_status']
|
229
|
-
@flag_type = json['flag_type']
|
230
|
-
@hash_key = json['hash_key']
|
231
|
-
@is_paused = json['is_paused']
|
232
|
-
@is_web_accessible = json['is_web_accessible']
|
233
|
-
@overrides = Hash[(json['overrides'] || []).map {|override| ["#{override['entity_type']}_#{override['entity_id']}", Override.new(override)]}]
|
234
|
-
@populations = (json['populations'] || []).map {|population| Population.new(population)}
|
235
|
-
@splits = Hash[(json['splits'] || []).map {|split| [split['treatment_id'], Split.new(split)]}]
|
236
|
-
@treatments = Hash[(json['treatments'] || []).map {|treatment| [treatment['treatment_id'], Treatment.new(treatment)]}]
|
237
|
-
@off_treatment = json['treatments'].find {|treatment| treatment['is_off_treatment'] }
|
238
|
-
|
239
|
-
@env_hash_key = env_hash_key
|
240
|
-
|
241
|
-
validate
|
242
|
-
end
|
243
|
-
|
244
|
-
def treatment(entity)
|
245
|
-
if @flag_type == 'uncategorized' or !entity then
|
246
|
-
nil
|
247
|
-
else
|
248
|
-
resolved_allocation(@env_hash_key, entity)
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
def payload(entity)
|
253
|
-
if @flag_type == 'uncategorized' or !entity then
|
254
|
-
nil
|
255
|
-
else
|
256
|
-
resolved_allocation(@env_hash_key, entity)
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
def eligible?(entity)
|
261
|
-
if @flag_type == 'uncategorized' or !entity then
|
262
|
-
nil
|
263
|
-
else
|
264
|
-
resolved_allocation(@env_hash_key, entity)
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
def enabled?(entity)
|
269
|
-
if @flag_type == 'uncategorized' or !entity then
|
270
|
-
nil
|
271
|
-
else
|
272
|
-
resolved_allocation(@env_hash_key, entity)
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
def resolved_allocation(env_hash_key, entity)
|
277
|
-
resolve_allocations(
|
278
|
-
allocation(@env_hash_key, entity),
|
279
|
-
allocation(@env_hash_key, (entity.group if defined? entity.group))
|
280
|
-
)
|
281
|
-
end
|
282
|
-
|
283
|
-
def allocation(env_hash_key, entity)
|
284
|
-
off_allocation = {
|
285
|
-
treatment: @treatments.find {|treatment_id, treatment| treatment.is_off_treatment}[1],
|
286
|
-
eligible: false
|
287
|
-
}
|
288
|
-
|
289
|
-
if !entity.is_a?(BaseEntity) then
|
290
|
-
return off_allocation
|
291
|
-
end
|
292
|
-
|
293
|
-
if @flag_status == 'archived' then
|
294
|
-
STDERR.puts 'The flag has been archived'
|
295
|
-
return off_allocation
|
296
|
-
end
|
297
|
-
|
298
|
-
if @is_paused then
|
299
|
-
return off_allocation
|
300
|
-
end
|
301
|
-
|
302
|
-
treatment = @treatments[@overrides["#{entity.type}_#{entity.id}"]&.treatment_id]
|
303
|
-
if treatment then
|
304
|
-
return {
|
305
|
-
treatment: treatment,
|
306
|
-
eligible: !treatment.is_off_treatment,
|
307
|
-
from_override: true
|
308
|
-
}
|
309
|
-
end
|
310
|
-
|
311
|
-
use_universes = @flag_type == 'experiment'
|
312
|
-
|
313
|
-
is_eligible = false
|
314
|
-
@populations.each do |population|
|
315
|
-
eligible, treatment = population.gate_values(
|
316
|
-
entity,
|
317
|
-
env_hash_key,
|
318
|
-
self,
|
319
|
-
use_universes
|
320
|
-
).values_at(:eligible, :treatment)
|
321
|
-
|
322
|
-
if treatment != nil then
|
323
|
-
return {
|
324
|
-
treatment: treatment,
|
325
|
-
eligible: eligible
|
326
|
-
}
|
327
|
-
end
|
328
|
-
|
329
|
-
is_eligible ||= eligible
|
330
|
-
end
|
331
|
-
|
332
|
-
{
|
333
|
-
treatment: off_allocation[:treatment],
|
334
|
-
eligible: is_eligible
|
335
|
-
}
|
336
|
-
end
|
337
|
-
|
338
|
-
def resolve_allocations(individual_alloc, group_alloc)
|
339
|
-
if individual_alloc[:from_override] then
|
340
|
-
individual_alloc
|
341
|
-
elsif group_alloc[:from_override] then
|
342
|
-
group_alloc
|
343
|
-
elsif not individual_alloc[:treatment].is_off_treatment then
|
344
|
-
individual_alloc
|
345
|
-
elsif not group_alloc[:treatment].is_off_treatment then
|
346
|
-
group_alloc
|
347
|
-
else
|
348
|
-
individual_alloc
|
349
|
-
end
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
class Override < GatingInfoModel
|
354
|
-
SCHEMA = Dry::Validation.Schema do
|
355
|
-
required(:@treatment_id) { str? }
|
356
|
-
required(:@entity_id) { str? }
|
357
|
-
required(:@entity_type) { str? }
|
358
|
-
end
|
359
|
-
|
360
|
-
attr_reader :treatment_id
|
361
|
-
attr_reader :entity_id
|
362
|
-
attr_reader :entity_type
|
363
|
-
|
364
|
-
def initialize(json)
|
365
|
-
@treatment_id = json['treatment_id']
|
366
|
-
@entity_id = json['entity_id']
|
367
|
-
@entity_type = json['entity_type']
|
368
|
-
|
369
|
-
validate
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
class Population < GatingInfoModel
|
374
|
-
SCHEMA = Dry::Validation.Schema do
|
375
|
-
required(:@rules) { array? }
|
376
|
-
required(:@universes) { array? }
|
377
|
-
required(:@percentage) { gteq?(0) & lteq?(1) }
|
378
|
-
required(:@hash_key) { str? }
|
379
|
-
required(:@entity_type) { str? }
|
380
|
-
end
|
381
|
-
|
382
|
-
attr_reader :rules
|
383
|
-
attr_reader :universes
|
384
|
-
attr_reader :percentage
|
385
|
-
attr_reader :hash_key
|
386
|
-
attr_reader :entity_type
|
387
|
-
|
388
|
-
def initialize(json)
|
389
|
-
@rules = (json['rules'] || []).map {|rule| Rule.new(rule)}
|
390
|
-
@universes = (json['universes'] || []).map {|universe| Hash[universe.map {|split| [split['treatment_id'], Split.new(split)]}]}
|
391
|
-
@percentage = json['percentage']
|
392
|
-
@hash_key = json['hash_key']
|
393
|
-
@entity_type = json['entity_type']
|
394
|
-
|
395
|
-
validate
|
396
|
-
end
|
397
|
-
|
398
|
-
def gate_values(entity, env_hash_key, flag, sticky)
|
399
|
-
if @entity_type != entity.type
|
400
|
-
return {eligible: false}
|
401
|
-
end
|
402
|
-
|
403
|
-
matches = @rules.all? {|rule| rule.match(entity)}
|
404
|
-
if matches then
|
405
|
-
samplingHashKey = "SAMPLING:control_#{flag.hash_key}:env_#{env_hash_key}:rule_set_#{@hash_key}:client_object_#{entity.type}_#{entity.id}"
|
406
|
-
hashedPercentage = Population.getHashedPercentage(samplingHashKey)
|
407
|
-
if hashedPercentage <= @percentage and @percentage > 0 then
|
408
|
-
|
409
|
-
allocationHashKey = "DISTRIBUTION:control_#{flag.hash_key}:env_#{env_hash_key}:client_object_#{entity.type}_#{entity.id}"
|
410
|
-
allocationHashedPercentage = Population.getHashedPercentage(allocationHashKey)
|
411
|
-
|
412
|
-
splits = if sticky then
|
413
|
-
@universes[[(hashedPercentage * 100).floor - 1, 0].max]
|
414
|
-
else
|
415
|
-
flag.splits
|
416
|
-
end
|
417
|
-
|
418
|
-
sum = 0
|
419
|
-
{
|
420
|
-
eligible: true,
|
421
|
-
treatment: flag.treatments.find {|treatment_id, treatment|
|
422
|
-
if treatment.is_off_treatment then
|
423
|
-
false
|
424
|
-
else
|
425
|
-
sum = (sum + (splits[treatment.treatment_id]&.percentage || 0)).round(3)
|
426
|
-
allocationHashedPercentage <= sum
|
427
|
-
end
|
428
|
-
}[1]
|
429
|
-
}
|
430
|
-
else
|
431
|
-
{eligible: true}
|
432
|
-
end
|
433
|
-
else
|
434
|
-
{eligible: false}
|
435
|
-
end
|
436
|
-
end
|
437
|
-
|
438
|
-
def self.getHashedPercentage(s)
|
439
|
-
Digest::MD5.hexdigest(s).to_i(16).to_f / 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
|
-
module RuleType
|
444
|
-
STRING = 'string'
|
445
|
-
INT = 'int'
|
446
|
-
FLOAT = 'float'
|
447
|
-
BOOLEAN = 'boolean'
|
448
|
-
DATE = 'date'
|
449
|
-
DATETIME = 'datetime'
|
450
|
-
end
|
451
|
-
|
452
|
-
module RuleOperator
|
453
|
-
IS = 'is'
|
454
|
-
IS_NOT = 'is_not'
|
455
|
-
IN = 'in'
|
456
|
-
NOT_IN = 'not_in'
|
457
|
-
LT = 'lt'
|
458
|
-
LTE = 'lte'
|
459
|
-
GT = 'gt'
|
460
|
-
GTE = 'gte'
|
461
|
-
FROM = 'from'
|
462
|
-
UNTIL = 'until'
|
463
|
-
AFTER = 'after'
|
464
|
-
BEFORE = 'before'
|
465
|
-
end
|
466
|
-
|
467
|
-
class Rule < GatingInfoModel
|
468
|
-
SCHEMA = Dry::Validation.Schema do
|
469
|
-
required(:@operator) { included_in? RuleOperator.constants(false).map(&RuleOperator.method(:const_get)) }
|
470
|
-
required(:@attribute_type) { included_in? RuleType.constants(false).map(&RuleType.method(:const_get)) }
|
471
|
-
required(:@attribute_name) { str? }
|
472
|
-
optional(:@value_list) { array? { each { str? | bool? | type?(Numeric) } } }
|
473
|
-
optional(:@value) { str? | bool? | type?(Numeric) }
|
474
|
-
end
|
475
|
-
|
476
|
-
attr_reader :operator
|
477
|
-
attr_reader :attribute_type
|
478
|
-
attr_reader :attribute_name
|
479
|
-
attr_reader :value_list
|
480
|
-
attr_reader :value
|
481
|
-
|
482
|
-
def initialize(json)
|
483
|
-
@operator = json['operator']
|
484
|
-
@attribute_type = json['attribute_type']
|
485
|
-
@attribute_name = json['attribute_name']
|
486
|
-
@value_list = json['value_list']
|
487
|
-
@value = json['value']
|
488
|
-
|
489
|
-
validate
|
490
|
-
end
|
491
|
-
|
492
|
-
def self.categorize_value_type(value)
|
493
|
-
case value
|
494
|
-
when true
|
495
|
-
'boolean'
|
496
|
-
when false
|
497
|
-
'boolean'
|
498
|
-
when Float
|
499
|
-
'float'
|
500
|
-
when Integer
|
501
|
-
'int'
|
502
|
-
when String
|
503
|
-
begin
|
504
|
-
date = DateTime.parse(value)
|
505
|
-
if date.hour == 0 && date.minute == 0 && date.second == 0 && date.second_fraction == 0 then
|
506
|
-
'date'
|
507
|
-
else
|
508
|
-
'datetime'
|
509
|
-
end
|
510
|
-
rescue ArgumentError
|
511
|
-
'string'
|
512
|
-
end
|
513
|
-
else
|
514
|
-
STDERR.puts 'Unexpected attribute value type encountered'
|
515
|
-
nil
|
516
|
-
end
|
517
|
-
end
|
518
|
-
|
519
|
-
def match(entity)
|
520
|
-
begin
|
521
|
-
if not entity.attributes.key?(@attribute_name) then
|
522
|
-
return false
|
523
|
-
end
|
524
|
-
|
525
|
-
value = entity.attributes[@attribute_name]
|
526
|
-
attribute_type = Rule.categorize_value_type(value)
|
527
|
-
number_types = [RuleType::INT, RuleType::FLOAT].sort
|
528
|
-
|
529
|
-
if attribute_type != @attribute_type &&
|
530
|
-
[attribute_type, @attribute_type].sort != number_types then
|
531
|
-
return false
|
532
|
-
end
|
533
|
-
|
534
|
-
if attribute_type == RuleType::STRING then
|
535
|
-
if @operator == RuleOperator::IS then
|
536
|
-
value == @value
|
537
|
-
elsif @operator == RuleOperator::IS_NOT then
|
538
|
-
value != @value
|
539
|
-
elsif @operator == RuleOperator::IN then
|
540
|
-
@value_list.include?(value)
|
541
|
-
elsif @operator == RuleOperator::NOT_IN then
|
542
|
-
not @value_list.include?(value)
|
543
|
-
else
|
544
|
-
STDERR.puts 'Invalid rule operator encountered'
|
545
|
-
false
|
546
|
-
end
|
547
|
-
elsif number_types.include?(attribute_type) then
|
548
|
-
if @operator == RuleOperator::IS then
|
549
|
-
value == @value
|
550
|
-
elsif @operator == RuleOperator::IS_NOT then
|
551
|
-
value != @value
|
552
|
-
elsif @operator == RuleOperator::IN then
|
553
|
-
@value_list.include?(value)
|
554
|
-
elsif @operator == RuleOperator::NOT_IN then
|
555
|
-
not @value_list.include?(value)
|
556
|
-
elsif @operator == RuleOperator::LT then
|
557
|
-
value < @value
|
558
|
-
elsif @operator == RuleOperator::LTE then
|
559
|
-
value <= @value
|
560
|
-
elsif @operator == RuleOperator::GT then
|
561
|
-
value > @value
|
562
|
-
elsif @operator == RuleOperator::GTE then
|
563
|
-
value >= @value
|
564
|
-
else
|
565
|
-
STDERR.puts 'Invalid rule operator encountered'
|
566
|
-
false
|
567
|
-
end
|
568
|
-
elsif attribute_type == RuleType::BOOLEAN then
|
569
|
-
if @operator == RuleOperator::IS then
|
570
|
-
value == @value
|
571
|
-
elsif @operator == RuleOperator::IS_NOT then
|
572
|
-
value != @value
|
573
|
-
else
|
574
|
-
STDERR.puts 'Invalid rule operator encountered'
|
575
|
-
false
|
576
|
-
end
|
577
|
-
elsif attribute_type == RuleType::DATE || attribute_type == RuleType::DATETIME then
|
578
|
-
target_time = @value && DateTime.parse(@value)
|
579
|
-
target_time_list = @value_list&.map {|tv| DateTime.parse(tv)}
|
580
|
-
value_time = DateTime.parse(value)
|
581
|
-
|
582
|
-
if @operator == RuleOperator::IS then
|
583
|
-
value_time == target_time
|
584
|
-
elsif @operator == RuleOperator::IS_NOT then
|
585
|
-
value_time != target_time
|
586
|
-
elsif @operator == RuleOperator::IN then
|
587
|
-
target_time_list.include?(value_time)
|
588
|
-
elsif @operator == RuleOperator::NOT_IN then
|
589
|
-
not target_time_list.include?(value_time)
|
590
|
-
elsif @operator == RuleOperator::FROM then
|
591
|
-
value_time >= target_time
|
592
|
-
elsif @operator == RuleOperator::UNTIL then
|
593
|
-
value_time <= target_time
|
594
|
-
elsif @operator == RuleOperator::AFTER then
|
595
|
-
value_time > target_time
|
596
|
-
elsif @operator == RuleOperator::BEFORE then
|
597
|
-
value_time < target_time
|
598
|
-
else
|
599
|
-
STDERR.puts 'Invalid rule operator encountered'
|
600
|
-
false
|
601
|
-
end
|
602
|
-
else
|
603
|
-
STDERR.puts 'Invalid attribute type encountered'
|
604
|
-
false
|
605
|
-
end
|
606
|
-
rescue Exception => e
|
607
|
-
STDERR.puts "Traceback:"
|
608
|
-
STDERR.puts e.backtrace
|
609
|
-
STDERR.puts "#{e.class} (#{e.message})"
|
610
|
-
false
|
611
|
-
end
|
612
|
-
end
|
613
|
-
end
|
614
|
-
|
615
|
-
class Split < GatingInfoModel
|
616
|
-
SCHEMA = Dry::Validation.Schema do
|
617
|
-
required(:@treatment_id) { str? }
|
618
|
-
required(:@percentage) { gteq?(0) & lteq?(1) }
|
619
|
-
end
|
620
|
-
|
621
|
-
attr_reader :treatment_id
|
622
|
-
attr_reader :percentage
|
623
|
-
|
624
|
-
def initialize(json)
|
625
|
-
@treatment_id = json['treatment_id']
|
626
|
-
@percentage = json['percentage']
|
627
|
-
|
628
|
-
validate
|
629
|
-
end
|
630
|
-
end
|
631
|
-
|
632
|
-
class Treatment < GatingInfoModel
|
633
|
-
SCHEMA = Dry::Validation.Schema do
|
634
|
-
required(:@treatment_id) { str? }
|
635
|
-
required(:@is_ghost) { bool? }
|
636
|
-
required(:@codename) { str? }
|
637
|
-
required(:@is_off_treatment) { bool? }
|
638
|
-
optional(:@payload) { str? | type?(Numeric) | bool? | array? | type?(Hash) }
|
639
|
-
end
|
640
|
-
|
641
|
-
attr_reader :treatment_id
|
642
|
-
attr_reader :is_ghost
|
643
|
-
attr_reader :codename
|
644
|
-
attr_reader :is_off_treatment
|
645
|
-
attr_reader :payload
|
646
|
-
|
647
|
-
def initialize(json)
|
648
|
-
@treatment_id = json['treatment_id']
|
649
|
-
@is_ghost = json['is_ghost']
|
650
|
-
@codename = json['codename']
|
651
|
-
@is_off_treatment = json['is_off_treatment']
|
652
|
-
@payload = json['payload']
|
653
|
-
|
654
|
-
validate
|
655
|
-
end
|
656
|
-
end
|
657
|
-
end
|