flagger 2.0.9 → 3.0.1
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/lib/flagger.rb +63 -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 +70 -39
- data/lib/flagger/cloud.rb +0 -495
- data/lib/flagger/microservice.rb +0 -63
- data/lib/flagger/models.rb +0 -656
- 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,656 +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
|
-
|
238
|
-
@env_hash_key = env_hash_key
|
239
|
-
|
240
|
-
validate
|
241
|
-
end
|
242
|
-
|
243
|
-
def treatment(entity)
|
244
|
-
if @flag_type == 'uncategorized' or !entity then
|
245
|
-
nil
|
246
|
-
else
|
247
|
-
resolved_allocation(@env_hash_key, entity)
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
def payload(entity)
|
252
|
-
if @flag_type == 'uncategorized' or !entity then
|
253
|
-
nil
|
254
|
-
else
|
255
|
-
resolved_allocation(@env_hash_key, entity)
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def eligible?(entity)
|
260
|
-
if @flag_type == 'uncategorized' or !entity then
|
261
|
-
false
|
262
|
-
else
|
263
|
-
resolved_allocation(@env_hash_key, entity)
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def enabled?(entity)
|
268
|
-
if @flag_type == 'uncategorized' or !entity then
|
269
|
-
false
|
270
|
-
else
|
271
|
-
resolved_allocation(@env_hash_key, entity)
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
def resolved_allocation(env_hash_key, entity)
|
276
|
-
resolve_allocations(
|
277
|
-
allocation(@env_hash_key, entity),
|
278
|
-
allocation(@env_hash_key, (entity.group if defined? entity.group))
|
279
|
-
)
|
280
|
-
end
|
281
|
-
|
282
|
-
def allocation(env_hash_key, entity)
|
283
|
-
off_allocation = {
|
284
|
-
treatment: @treatments.find {|treatment_id, treatment| treatment.is_off_treatment}[1],
|
285
|
-
eligible: false
|
286
|
-
}
|
287
|
-
|
288
|
-
if !entity.is_a?(BaseEntity) then
|
289
|
-
return off_allocation
|
290
|
-
end
|
291
|
-
|
292
|
-
if @flag_status == 'archived' then
|
293
|
-
STDERR.puts 'The flag has been archived'
|
294
|
-
return off_allocation
|
295
|
-
end
|
296
|
-
|
297
|
-
if @is_paused then
|
298
|
-
return off_allocation
|
299
|
-
end
|
300
|
-
|
301
|
-
treatment = @treatments[@overrides["#{entity.type}_#{entity.id}"]&.treatment_id]
|
302
|
-
if treatment then
|
303
|
-
return {
|
304
|
-
treatment: treatment,
|
305
|
-
eligible: !treatment.is_off_treatment,
|
306
|
-
from_override: true
|
307
|
-
}
|
308
|
-
end
|
309
|
-
|
310
|
-
use_universes = @flag_type == 'experiment'
|
311
|
-
|
312
|
-
is_eligible = false
|
313
|
-
@populations.each do |population|
|
314
|
-
eligible, treatment = population.gate_values(
|
315
|
-
entity,
|
316
|
-
env_hash_key,
|
317
|
-
self,
|
318
|
-
use_universes
|
319
|
-
).values_at(:eligible, :treatment)
|
320
|
-
|
321
|
-
if treatment != nil then
|
322
|
-
return {
|
323
|
-
treatment: treatment,
|
324
|
-
eligible: eligible
|
325
|
-
}
|
326
|
-
end
|
327
|
-
|
328
|
-
is_eligible ||= eligible
|
329
|
-
end
|
330
|
-
|
331
|
-
{
|
332
|
-
treatment: off_allocation[:treatment],
|
333
|
-
eligible: is_eligible
|
334
|
-
}
|
335
|
-
end
|
336
|
-
|
337
|
-
def resolve_allocations(individual_alloc, group_alloc)
|
338
|
-
if individual_alloc[:from_override] then
|
339
|
-
individual_alloc
|
340
|
-
elsif group_alloc[:from_override] then
|
341
|
-
group_alloc
|
342
|
-
elsif not individual_alloc[:treatment].is_off_treatment then
|
343
|
-
individual_alloc
|
344
|
-
elsif not group_alloc[:treatment].is_off_treatment then
|
345
|
-
group_alloc
|
346
|
-
else
|
347
|
-
individual_alloc
|
348
|
-
end
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
class Override < GatingInfoModel
|
353
|
-
SCHEMA = Dry::Validation.Schema do
|
354
|
-
required(:@treatment_id) { str? }
|
355
|
-
required(:@entity_id) { str? }
|
356
|
-
required(:@entity_type) { str? }
|
357
|
-
end
|
358
|
-
|
359
|
-
attr_reader :treatment_id
|
360
|
-
attr_reader :entity_id
|
361
|
-
attr_reader :entity_type
|
362
|
-
|
363
|
-
def initialize(json)
|
364
|
-
@treatment_id = json['treatment_id']
|
365
|
-
@entity_id = json['entity_id']
|
366
|
-
@entity_type = json['entity_type']
|
367
|
-
|
368
|
-
validate
|
369
|
-
end
|
370
|
-
end
|
371
|
-
|
372
|
-
class Population < GatingInfoModel
|
373
|
-
SCHEMA = Dry::Validation.Schema do
|
374
|
-
required(:@rules) { array? }
|
375
|
-
required(:@universes) { array? }
|
376
|
-
required(:@percentage) { gteq?(0) & lteq?(1) }
|
377
|
-
required(:@hash_key) { str? }
|
378
|
-
required(:@entity_type) { str? }
|
379
|
-
end
|
380
|
-
|
381
|
-
attr_reader :rules
|
382
|
-
attr_reader :universes
|
383
|
-
attr_reader :percentage
|
384
|
-
attr_reader :hash_key
|
385
|
-
attr_reader :entity_type
|
386
|
-
|
387
|
-
def initialize(json)
|
388
|
-
@rules = (json['rules'] || []).map {|rule| Rule.new(rule)}
|
389
|
-
@universes = (json['universes'] || []).map {|universe| Hash[universe.map {|split| [split['treatment_id'], Split.new(split)]}]}
|
390
|
-
@percentage = json['percentage']
|
391
|
-
@hash_key = json['hash_key']
|
392
|
-
@entity_type = json['entity_type']
|
393
|
-
|
394
|
-
validate
|
395
|
-
end
|
396
|
-
|
397
|
-
def gate_values(entity, env_hash_key, flag, sticky)
|
398
|
-
if @entity_type != entity.type
|
399
|
-
return {eligible: false}
|
400
|
-
end
|
401
|
-
|
402
|
-
matches = @rules.all? {|rule| rule.match(entity)}
|
403
|
-
if matches then
|
404
|
-
samplingHashKey = "SAMPLING:control_#{flag.hash_key}:env_#{env_hash_key}:rule_set_#{@hash_key}:client_object_#{entity.type}_#{entity.id}"
|
405
|
-
hashedPercentage = Population.getHashedPercentage(samplingHashKey)
|
406
|
-
if hashedPercentage <= @percentage and @percentage > 0 then
|
407
|
-
|
408
|
-
allocationHashKey = "DISTRIBUTION:control_#{flag.hash_key}:env_#{env_hash_key}:client_object_#{entity.type}_#{entity.id}"
|
409
|
-
allocationHashedPercentage = Population.getHashedPercentage(allocationHashKey)
|
410
|
-
|
411
|
-
splits = if sticky then
|
412
|
-
@universes[[(hashedPercentage * 100).floor - 1, 0].max]
|
413
|
-
else
|
414
|
-
flag.splits
|
415
|
-
end
|
416
|
-
|
417
|
-
sum = 0
|
418
|
-
{
|
419
|
-
eligible: true,
|
420
|
-
treatment: flag.treatments.find {|treatment_id, treatment|
|
421
|
-
if treatment.is_off_treatment then
|
422
|
-
false
|
423
|
-
else
|
424
|
-
sum = (sum + (splits[treatment.treatment_id]&.percentage || 0)).round(3)
|
425
|
-
allocationHashedPercentage <= sum
|
426
|
-
end
|
427
|
-
}[1]
|
428
|
-
}
|
429
|
-
else
|
430
|
-
{eligible: true}
|
431
|
-
end
|
432
|
-
else
|
433
|
-
{eligible: false}
|
434
|
-
end
|
435
|
-
end
|
436
|
-
|
437
|
-
def self.getHashedPercentage(s)
|
438
|
-
Digest::MD5.hexdigest(s).to_i(16).to_f / 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
439
|
-
end
|
440
|
-
end
|
441
|
-
|
442
|
-
module RuleType
|
443
|
-
STRING = 'string'
|
444
|
-
INT = 'int'
|
445
|
-
FLOAT = 'float'
|
446
|
-
BOOLEAN = 'boolean'
|
447
|
-
DATE = 'date'
|
448
|
-
DATETIME = 'datetime'
|
449
|
-
end
|
450
|
-
|
451
|
-
module RuleOperator
|
452
|
-
IS = 'is'
|
453
|
-
IS_NOT = 'is_not'
|
454
|
-
IN = 'in'
|
455
|
-
NOT_IN = 'not_in'
|
456
|
-
LT = 'lt'
|
457
|
-
LTE = 'lte'
|
458
|
-
GT = 'gt'
|
459
|
-
GTE = 'gte'
|
460
|
-
FROM = 'from'
|
461
|
-
UNTIL = 'until'
|
462
|
-
AFTER = 'after'
|
463
|
-
BEFORE = 'before'
|
464
|
-
end
|
465
|
-
|
466
|
-
class Rule < GatingInfoModel
|
467
|
-
SCHEMA = Dry::Validation.Schema do
|
468
|
-
required(:@operator) { included_in? RuleOperator.constants(false).map(&RuleOperator.method(:const_get)) }
|
469
|
-
required(:@attribute_type) { included_in? RuleType.constants(false).map(&RuleType.method(:const_get)) }
|
470
|
-
required(:@attribute_name) { str? }
|
471
|
-
optional(:@value_list) { array? { each { str? | bool? | type?(Numeric) } } }
|
472
|
-
optional(:@value) { str? | bool? | type?(Numeric) }
|
473
|
-
end
|
474
|
-
|
475
|
-
attr_reader :operator
|
476
|
-
attr_reader :attribute_type
|
477
|
-
attr_reader :attribute_name
|
478
|
-
attr_reader :value_list
|
479
|
-
attr_reader :value
|
480
|
-
|
481
|
-
def initialize(json)
|
482
|
-
@operator = json['operator']
|
483
|
-
@attribute_type = json['attribute_type']
|
484
|
-
@attribute_name = json['attribute_name']
|
485
|
-
@value_list = json['value_list']
|
486
|
-
@value = json['value']
|
487
|
-
|
488
|
-
validate
|
489
|
-
end
|
490
|
-
|
491
|
-
def self.categorize_value_type(value)
|
492
|
-
case value
|
493
|
-
when true
|
494
|
-
'boolean'
|
495
|
-
when false
|
496
|
-
'boolean'
|
497
|
-
when Float
|
498
|
-
'float'
|
499
|
-
when Integer
|
500
|
-
'int'
|
501
|
-
when String
|
502
|
-
begin
|
503
|
-
date = DateTime.parse(value)
|
504
|
-
if date.hour == 0 && date.minute == 0 && date.second == 0 && date.second_fraction == 0 then
|
505
|
-
'date'
|
506
|
-
else
|
507
|
-
'datetime'
|
508
|
-
end
|
509
|
-
rescue ArgumentError
|
510
|
-
'string'
|
511
|
-
end
|
512
|
-
else
|
513
|
-
STDERR.puts 'Unexpected attribute value type encountered'
|
514
|
-
nil
|
515
|
-
end
|
516
|
-
end
|
517
|
-
|
518
|
-
def match(entity)
|
519
|
-
begin
|
520
|
-
if not entity.attributes.key?(@attribute_name) then
|
521
|
-
return false
|
522
|
-
end
|
523
|
-
|
524
|
-
value = entity.attributes[@attribute_name]
|
525
|
-
attribute_type = Rule.categorize_value_type(value)
|
526
|
-
number_types = [RuleType::INT, RuleType::FLOAT].sort
|
527
|
-
|
528
|
-
if attribute_type != @attribute_type &&
|
529
|
-
[attribute_type, @attribute_type].sort != number_types then
|
530
|
-
return false
|
531
|
-
end
|
532
|
-
|
533
|
-
if attribute_type == RuleType::STRING then
|
534
|
-
if @operator == RuleOperator::IS then
|
535
|
-
value == @value
|
536
|
-
elsif @operator == RuleOperator::IS_NOT then
|
537
|
-
value != @value
|
538
|
-
elsif @operator == RuleOperator::IN then
|
539
|
-
@value_list.include?(value)
|
540
|
-
elsif @operator == RuleOperator::NOT_IN then
|
541
|
-
not @value_list.include?(value)
|
542
|
-
else
|
543
|
-
STDERR.puts 'Invalid rule operator encountered'
|
544
|
-
false
|
545
|
-
end
|
546
|
-
elsif number_types.include?(attribute_type) then
|
547
|
-
if @operator == RuleOperator::IS then
|
548
|
-
value == @value
|
549
|
-
elsif @operator == RuleOperator::IS_NOT then
|
550
|
-
value != @value
|
551
|
-
elsif @operator == RuleOperator::IN then
|
552
|
-
@value_list.include?(value)
|
553
|
-
elsif @operator == RuleOperator::NOT_IN then
|
554
|
-
not @value_list.include?(value)
|
555
|
-
elsif @operator == RuleOperator::LT then
|
556
|
-
value < @value
|
557
|
-
elsif @operator == RuleOperator::LTE then
|
558
|
-
value <= @value
|
559
|
-
elsif @operator == RuleOperator::GT then
|
560
|
-
value > @value
|
561
|
-
elsif @operator == RuleOperator::GTE then
|
562
|
-
value >= @value
|
563
|
-
else
|
564
|
-
STDERR.puts 'Invalid rule operator encountered'
|
565
|
-
false
|
566
|
-
end
|
567
|
-
elsif attribute_type == RuleType::BOOLEAN then
|
568
|
-
if @operator == RuleOperator::IS then
|
569
|
-
value == @value
|
570
|
-
elsif @operator == RuleOperator::IS_NOT then
|
571
|
-
value != @value
|
572
|
-
else
|
573
|
-
STDERR.puts 'Invalid rule operator encountered'
|
574
|
-
false
|
575
|
-
end
|
576
|
-
elsif attribute_type == RuleType::DATE || attribute_type == RuleType::DATETIME then
|
577
|
-
target_time = @value && DateTime.parse(@value)
|
578
|
-
target_time_list = @value_list&.map {|tv| DateTime.parse(tv)}
|
579
|
-
value_time = DateTime.parse(value)
|
580
|
-
|
581
|
-
if @operator == RuleOperator::IS then
|
582
|
-
value_time == target_time
|
583
|
-
elsif @operator == RuleOperator::IS_NOT then
|
584
|
-
value_time != target_time
|
585
|
-
elsif @operator == RuleOperator::IN then
|
586
|
-
target_time_list.include?(value_time)
|
587
|
-
elsif @operator == RuleOperator::NOT_IN then
|
588
|
-
not target_time_list.include?(value_time)
|
589
|
-
elsif @operator == RuleOperator::FROM then
|
590
|
-
value_time >= target_time
|
591
|
-
elsif @operator == RuleOperator::UNTIL then
|
592
|
-
value_time <= target_time
|
593
|
-
elsif @operator == RuleOperator::AFTER then
|
594
|
-
value_time > target_time
|
595
|
-
elsif @operator == RuleOperator::BEFORE then
|
596
|
-
value_time < target_time
|
597
|
-
else
|
598
|
-
STDERR.puts 'Invalid rule operator encountered'
|
599
|
-
false
|
600
|
-
end
|
601
|
-
else
|
602
|
-
STDERR.puts 'Invalid attribute type encountered'
|
603
|
-
false
|
604
|
-
end
|
605
|
-
rescue Exception => e
|
606
|
-
STDERR.puts "Traceback:"
|
607
|
-
STDERR.puts e.backtrace
|
608
|
-
STDERR.puts "#{e.class} (#{e.message})"
|
609
|
-
false
|
610
|
-
end
|
611
|
-
end
|
612
|
-
end
|
613
|
-
|
614
|
-
class Split < GatingInfoModel
|
615
|
-
SCHEMA = Dry::Validation.Schema do
|
616
|
-
required(:@treatment_id) { str? }
|
617
|
-
required(:@percentage) { gteq?(0) & lteq?(1) }
|
618
|
-
end
|
619
|
-
|
620
|
-
attr_reader :treatment_id
|
621
|
-
attr_reader :percentage
|
622
|
-
|
623
|
-
def initialize(json)
|
624
|
-
@treatment_id = json['treatment_id']
|
625
|
-
@percentage = json['percentage']
|
626
|
-
|
627
|
-
validate
|
628
|
-
end
|
629
|
-
end
|
630
|
-
|
631
|
-
class Treatment < GatingInfoModel
|
632
|
-
SCHEMA = Dry::Validation.Schema do
|
633
|
-
required(:@treatment_id) { str? }
|
634
|
-
required(:@is_control) { bool? }
|
635
|
-
required(:@codename) { str? }
|
636
|
-
required(:@is_off_treatment) { bool? }
|
637
|
-
optional(:@payload) { str? | type?(Numeric) | bool? | array? | type?(Hash) }
|
638
|
-
end
|
639
|
-
|
640
|
-
attr_reader :treatment_id
|
641
|
-
attr_reader :is_control
|
642
|
-
attr_reader :codename
|
643
|
-
attr_reader :is_off_treatment
|
644
|
-
attr_reader :payload
|
645
|
-
|
646
|
-
def initialize(json)
|
647
|
-
@treatment_id = json['treatment_id']
|
648
|
-
@is_control = json['is_control']
|
649
|
-
@codename = json['codename']
|
650
|
-
@is_off_treatment = json['is_off_treatment']
|
651
|
-
@payload = json['payload']
|
652
|
-
|
653
|
-
validate
|
654
|
-
end
|
655
|
-
end
|
656
|
-
end
|