amplitude-experiment 1.6.0 → 1.7.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/amplitude-experiment.gemspec +1 -1
- data/lib/amplitude/processor.rb +1 -1
- data/lib/amplitude/timeline.rb +1 -1
- data/lib/amplitude-experiment.rb +1 -6
- data/lib/experiment/evaluation/evaluation.rb +250 -248
- data/lib/experiment/evaluation/flag.rb +90 -88
- data/lib/experiment/evaluation/murmur3.rb +90 -86
- data/lib/experiment/evaluation/select.rb +10 -8
- data/lib/experiment/evaluation/semantic_version.rb +39 -35
- data/lib/experiment/evaluation/topological_sort.rb +46 -49
- data/lib/experiment/evaluation.rb +6 -0
- data/lib/experiment/local/client.rb +4 -7
- data/lib/experiment/local/config.rb +21 -3
- data/lib/experiment/remote/client.rb +1 -6
- data/lib/experiment/remote/config.rb +24 -4
- data/lib/experiment/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6562a2b13fb38d240e3d83762e442a558478db59f3fff36bdff10b6589cfab0d
|
|
4
|
+
data.tar.gz: 432ea0ff64724c882ad97aae3ce95ae8d8a203f9c90bfe34afd5629e00dbba28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 507368b7f4db8a28dfc866e0df203f724a1d2911af19dab24fada0bb7327e362d185057c891d534421c7816ec12953ad6555a0252b9bfa7d4922ffaea16bc935
|
|
7
|
+
data.tar.gz: d7ad16548702c918919a9d3b4afe874d396c8c5e98fb5010ebb3f8c0f0b476ad86d4568ed4660a74040a5ee85d4a9e01197560039b19d94659ec0e38d67b0868
|
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.require_paths = ['lib']
|
|
19
19
|
spec.extra_rdoc_files = ['README.md']
|
|
20
20
|
|
|
21
|
-
spec.add_dependency 'concurrent-ruby', '~> 1.2
|
|
21
|
+
spec.add_dependency 'concurrent-ruby', '~> 1.2'
|
|
22
22
|
|
|
23
23
|
spec.add_development_dependency 'psych', '~> 4.0'
|
|
24
24
|
spec.add_development_dependency 'rake', '~> 13.0'
|
data/lib/amplitude/processor.rb
CHANGED
|
@@ -87,7 +87,7 @@ module AmplitudeAnalytics
|
|
|
87
87
|
@configuration.callback.call(event, code, message) if @configuration.callback.respond_to?(:call)
|
|
88
88
|
event.callback(code, message)
|
|
89
89
|
rescue StandardError => e
|
|
90
|
-
@configuration.logger.
|
|
90
|
+
@configuration.logger.error("Error callback for event #{event}: #{e.message}")
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
|
data/lib/amplitude/timeline.rb
CHANGED
|
@@ -49,7 +49,7 @@ module AmplitudeAnalytics
|
|
|
49
49
|
@plugins[PluginType::DESTINATION].each do |destination|
|
|
50
50
|
destination_futures << destination.flush
|
|
51
51
|
rescue StandardError
|
|
52
|
-
logger.
|
|
52
|
+
logger.error('Error for flush events')
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
destination_futures
|
data/lib/amplitude-experiment.rb
CHANGED
|
@@ -27,12 +27,7 @@ require 'experiment/cohort/cohort_storage'
|
|
|
27
27
|
require 'experiment/cohort/cohort_sync_config'
|
|
28
28
|
require 'experiment/deployment/deployment_runner'
|
|
29
29
|
require 'experiment/util/poller'
|
|
30
|
-
require 'experiment/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'
|
|
30
|
+
require 'experiment/evaluation'
|
|
36
31
|
|
|
37
32
|
# Amplitude Experiment Module
|
|
38
33
|
module AmplitudeExperiment
|
|
@@ -1,311 +1,313 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
module AmplitudeExperiment
|
|
4
|
+
module Evaluation
|
|
5
|
+
# Engine for evaluating feature flags based on context
|
|
6
|
+
class Engine
|
|
7
|
+
def evaluate(context, flags)
|
|
8
|
+
results = {}
|
|
9
|
+
target = {
|
|
10
|
+
'context' => context,
|
|
11
|
+
'result' => results
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
flags.each do |flag|
|
|
15
|
+
variant = evaluate_flag(target, flag)
|
|
16
|
+
results[flag.key] = variant if variant
|
|
17
|
+
end
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
results
|
|
20
|
+
end
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def evaluate_flag(target, flag)
|
|
25
|
+
result = nil
|
|
26
|
+
flag.segments.each do |segment|
|
|
27
|
+
result = evaluate_segment(target, flag, segment)
|
|
28
|
+
next unless result
|
|
29
|
+
|
|
30
|
+
# Merge all metadata into the result
|
|
31
|
+
metadata = {}
|
|
32
|
+
metadata.merge!(flag.metadata) if flag.metadata
|
|
33
|
+
metadata.merge!(segment.metadata) if segment.metadata
|
|
34
|
+
metadata.merge!(result.metadata) if result.metadata
|
|
35
|
+
result.metadata = metadata
|
|
36
|
+
break
|
|
37
|
+
end
|
|
38
|
+
result
|
|
36
39
|
end
|
|
37
|
-
result
|
|
38
|
-
end
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
def evaluate_segment(target, flag, segment)
|
|
42
|
+
if segment.conditions
|
|
43
|
+
match = evaluate_conditions(target, segment.conditions)
|
|
44
|
+
if match
|
|
45
|
+
variant_key = bucket(target, segment)
|
|
46
|
+
variant_key ? flag.variants[variant_key] : nil
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
# Null conditions always match
|
|
44
50
|
variant_key = bucket(target, segment)
|
|
45
51
|
variant_key ? flag.variants[variant_key] : nil
|
|
46
52
|
end
|
|
47
|
-
else
|
|
48
|
-
# Null conditions always match
|
|
49
|
-
variant_key = bucket(target, segment)
|
|
50
|
-
variant_key ? flag.variants[variant_key] : nil
|
|
51
53
|
end
|
|
52
|
-
end
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
def evaluate_conditions(target, conditions)
|
|
56
|
+
# Outer list logic is "or" (||)
|
|
57
|
+
conditions.any? do |inner_conditions|
|
|
58
|
+
match = true
|
|
59
|
+
inner_conditions.each do |condition|
|
|
60
|
+
match = match_condition(target, condition)
|
|
61
|
+
break unless match
|
|
62
|
+
end
|
|
63
|
+
match
|
|
61
64
|
end
|
|
62
|
-
match
|
|
63
65
|
end
|
|
64
|
-
end
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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)
|
|
67
|
+
def match_condition(target, condition)
|
|
68
|
+
prop_value = Evaluation.select(target, condition.selector)
|
|
69
|
+
# Special matching for null properties and set type prop values and operators
|
|
70
|
+
if !prop_value
|
|
71
|
+
match_null(condition.op, condition.values)
|
|
72
|
+
elsif set_operator?(condition.op)
|
|
73
|
+
prop_value_string_list = coerce_string_array(prop_value)
|
|
74
|
+
return false unless prop_value_string_list
|
|
75
|
+
|
|
76
|
+
match_set(prop_value_string_list, condition.op, condition.values)
|
|
80
77
|
else
|
|
81
|
-
|
|
78
|
+
prop_value_string = coerce_string(prop_value)
|
|
79
|
+
if prop_value_string
|
|
80
|
+
match_string(prop_value_string, condition.op, condition.values)
|
|
81
|
+
else
|
|
82
|
+
false
|
|
83
|
+
end
|
|
82
84
|
end
|
|
83
85
|
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def get_hash(key)
|
|
87
|
-
Murmur3.hash32x86(key)
|
|
88
|
-
end
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# Null bucket means segment is fully rolled out
|
|
93
|
-
return segment.variant
|
|
87
|
+
def get_hash(key)
|
|
88
|
+
Murmur3.hash32x86(key)
|
|
94
89
|
end
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
def bucket(target, segment)
|
|
92
|
+
unless segment.bucket
|
|
93
|
+
# Null bucket means segment is fully rolled out
|
|
94
|
+
return segment.variant
|
|
95
|
+
end
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
bucketing_value = coerce_string(Evaluation.select(target, segment.bucket.selector))
|
|
98
|
+
if !bucketing_value || bucketing_value.empty?
|
|
99
|
+
# Null or empty bucketing value cannot be bucketed
|
|
100
|
+
return segment.variant
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
key_to_hash = "#{segment.bucket.salt}/#{bucketing_value}"
|
|
104
|
+
hash = get_hash(key_to_hash)
|
|
105
|
+
allocation_value = hash % 100
|
|
106
|
+
distribution_value = (hash / 100).floor
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
segment.bucket.allocations.each do |allocation|
|
|
109
|
+
allocation_start = allocation.range[0]
|
|
110
|
+
allocation_end = allocation.range[1]
|
|
111
|
+
next unless allocation_value >= allocation_start && allocation_value < allocation_end
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
allocation.distributions.each do |distribution|
|
|
114
|
+
distribution_start = distribution.range[0]
|
|
115
|
+
distribution_end = distribution.range[1]
|
|
116
|
+
return distribution.variant if distribution_value >= distribution_start && distribution_value < distribution_end
|
|
117
|
+
end
|
|
116
118
|
end
|
|
117
|
-
end
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
segment.variant
|
|
121
|
+
end
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
123
|
+
def match_null(op, filter_values)
|
|
124
|
+
contains_none = contains_none?(filter_values)
|
|
125
|
+
case op
|
|
126
|
+
when Operator::IS, Operator::CONTAINS, Operator::LESS_THAN,
|
|
127
|
+
Operator::LESS_THAN_EQUALS, Operator::GREATER_THAN,
|
|
128
|
+
Operator::GREATER_THAN_EQUALS, Operator::VERSION_LESS_THAN,
|
|
129
|
+
Operator::VERSION_LESS_THAN_EQUALS, Operator::VERSION_GREATER_THAN,
|
|
130
|
+
Operator::VERSION_GREATER_THAN_EQUALS, Operator::SET_IS,
|
|
131
|
+
Operator::SET_CONTAINS, Operator::SET_CONTAINS_ANY
|
|
132
|
+
contains_none
|
|
133
|
+
when Operator::IS_NOT, Operator::DOES_NOT_CONTAIN,
|
|
134
|
+
Operator::SET_DOES_NOT_CONTAIN, Operator::SET_DOES_NOT_CONTAIN_ANY
|
|
135
|
+
!contains_none
|
|
136
|
+
else
|
|
137
|
+
false
|
|
138
|
+
end
|
|
137
139
|
end
|
|
138
|
-
end
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
141
|
+
def match_set(prop_values, op, filter_values)
|
|
142
|
+
case op
|
|
143
|
+
when Operator::SET_IS
|
|
144
|
+
set_equals?(prop_values, filter_values)
|
|
145
|
+
when Operator::SET_IS_NOT
|
|
146
|
+
!set_equals?(prop_values, filter_values)
|
|
147
|
+
when Operator::SET_CONTAINS
|
|
148
|
+
matches_set_contains_all?(prop_values, filter_values)
|
|
149
|
+
when Operator::SET_DOES_NOT_CONTAIN
|
|
150
|
+
!matches_set_contains_all?(prop_values, filter_values)
|
|
151
|
+
when Operator::SET_CONTAINS_ANY
|
|
152
|
+
matches_set_contains_any?(prop_values, filter_values)
|
|
153
|
+
when Operator::SET_DOES_NOT_CONTAIN_ANY
|
|
154
|
+
!matches_set_contains_any?(prop_values, filter_values)
|
|
155
|
+
else
|
|
156
|
+
false
|
|
157
|
+
end
|
|
156
158
|
end
|
|
157
|
-
end
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
160
|
+
def match_string(prop_value, op, filter_values)
|
|
161
|
+
case op
|
|
162
|
+
when Operator::IS
|
|
163
|
+
matches_is?(prop_value, filter_values)
|
|
164
|
+
when Operator::IS_NOT
|
|
165
|
+
!matches_is?(prop_value, filter_values)
|
|
166
|
+
when Operator::CONTAINS
|
|
167
|
+
matches_contains?(prop_value, filter_values)
|
|
168
|
+
when Operator::DOES_NOT_CONTAIN
|
|
169
|
+
!matches_contains?(prop_value, filter_values)
|
|
170
|
+
when Operator::LESS_THAN, Operator::LESS_THAN_EQUALS,
|
|
171
|
+
Operator::GREATER_THAN, Operator::GREATER_THAN_EQUALS
|
|
172
|
+
matches_comparable?(prop_value, op, filter_values,
|
|
173
|
+
method(:parse_number),
|
|
174
|
+
method(:comparator))
|
|
175
|
+
when Operator::VERSION_LESS_THAN, Operator::VERSION_LESS_THAN_EQUALS,
|
|
176
|
+
Operator::VERSION_GREATER_THAN, Operator::VERSION_GREATER_THAN_EQUALS
|
|
177
|
+
matches_comparable?(prop_value, op, filter_values,
|
|
178
|
+
SemanticVersion.method(:parse),
|
|
179
|
+
method(:comparator))
|
|
180
|
+
when Operator::REGEX_MATCH
|
|
181
|
+
matches_regex?(prop_value, filter_values)
|
|
182
|
+
when Operator::REGEX_DOES_NOT_MATCH
|
|
183
|
+
!matches_regex?(prop_value, filter_values)
|
|
184
|
+
else
|
|
185
|
+
false
|
|
186
|
+
end
|
|
185
187
|
end
|
|
186
|
-
end
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
def matches_is?(prop_value, filter_values)
|
|
190
|
+
if contains_booleans?(filter_values)
|
|
191
|
+
lower = prop_value.downcase
|
|
192
|
+
return filter_values.any? { |value| value.downcase == lower } if %w[true false].include?(lower)
|
|
193
|
+
end
|
|
194
|
+
filter_values.any? { |value| prop_value == value }
|
|
192
195
|
end
|
|
193
|
-
filter_values.any? { |value| prop_value == value }
|
|
194
|
-
end
|
|
195
196
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
def matches_contains?(prop_value, filter_values)
|
|
198
|
+
filter_values.any? do |filter_value|
|
|
199
|
+
prop_value.downcase.include?(filter_value.downcase)
|
|
200
|
+
end
|
|
199
201
|
end
|
|
200
|
-
end
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
203
|
+
def matches_comparable?(prop_value, op, filter_values, type_transformer, type_comparator)
|
|
204
|
+
prop_value_transformed = type_transformer.call(prop_value)
|
|
205
|
+
filter_values_transformed = filter_values
|
|
206
|
+
.map { |filter_value| type_transformer.call(filter_value) }
|
|
207
|
+
.compact
|
|
208
|
+
|
|
209
|
+
if !prop_value_transformed || filter_values_transformed.empty?
|
|
210
|
+
filter_values.any? { |filter_value| comparator(prop_value, op, filter_value) }
|
|
211
|
+
else
|
|
212
|
+
filter_values_transformed.any? do |filter_value_transformed|
|
|
213
|
+
type_comparator.call(prop_value_transformed, op, filter_value_transformed)
|
|
214
|
+
end
|
|
213
215
|
end
|
|
214
216
|
end
|
|
215
|
-
end
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
def comparator(prop_value, op, filter_value)
|
|
219
|
+
case op
|
|
220
|
+
when Operator::LESS_THAN, Operator::VERSION_LESS_THAN
|
|
221
|
+
prop_value < filter_value
|
|
222
|
+
when Operator::LESS_THAN_EQUALS, Operator::VERSION_LESS_THAN_EQUALS
|
|
223
|
+
prop_value <= filter_value
|
|
224
|
+
when Operator::GREATER_THAN, Operator::VERSION_GREATER_THAN
|
|
225
|
+
prop_value > filter_value
|
|
226
|
+
when Operator::GREATER_THAN_EQUALS, Operator::VERSION_GREATER_THAN_EQUALS
|
|
227
|
+
prop_value >= filter_value
|
|
228
|
+
else
|
|
229
|
+
false
|
|
230
|
+
end
|
|
229
231
|
end
|
|
230
|
-
end
|
|
231
232
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
def matches_regex?(prop_value, filter_values)
|
|
234
|
+
filter_values.any? { |filter_value| !!(Regexp.new(filter_value) =~ prop_value) }
|
|
235
|
+
end
|
|
235
236
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
def contains_none?(filter_values)
|
|
238
|
+
filter_values.any? { |filter_value| filter_value == '(none)' }
|
|
239
|
+
end
|
|
239
240
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
def contains_booleans?(filter_values)
|
|
242
|
+
filter_values.any? do |filter_value|
|
|
243
|
+
case filter_value.downcase
|
|
244
|
+
when 'true', 'false'
|
|
245
|
+
true
|
|
246
|
+
else
|
|
247
|
+
false
|
|
248
|
+
end
|
|
247
249
|
end
|
|
248
250
|
end
|
|
249
|
-
end
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
def parse_number(value)
|
|
253
|
+
Float(value)
|
|
254
|
+
rescue StandardError
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
256
257
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
def coerce_string(value)
|
|
259
|
+
return nil if value.nil?
|
|
260
|
+
return value.to_json if value.is_a?(Hash)
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
value.to_s
|
|
263
|
+
end
|
|
263
264
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
265
|
+
def coerce_string_array(value)
|
|
266
|
+
if value.is_a?(Array)
|
|
267
|
+
value.map { |e| coerce_string(e) }.compact
|
|
268
|
+
else
|
|
269
|
+
string_value = value.to_s
|
|
270
|
+
begin
|
|
271
|
+
parsed_value = JSON.parse(string_value)
|
|
272
|
+
if parsed_value.is_a?(Array)
|
|
273
|
+
parsed_value.map { |e| coerce_string(e) }.compact
|
|
274
|
+
else
|
|
275
|
+
s = coerce_string(string_value)
|
|
276
|
+
s ? [s] : nil
|
|
277
|
+
end
|
|
278
|
+
rescue JSON::ParserError
|
|
274
279
|
s = coerce_string(string_value)
|
|
275
280
|
s ? [s] : nil
|
|
276
281
|
end
|
|
277
|
-
rescue JSON::ParserError
|
|
278
|
-
s = coerce_string(string_value)
|
|
279
|
-
s ? [s] : nil
|
|
280
282
|
end
|
|
281
283
|
end
|
|
282
|
-
end
|
|
283
284
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
def set_operator?(op)
|
|
286
|
+
case op
|
|
287
|
+
when Operator::SET_IS, Operator::SET_IS_NOT,
|
|
288
|
+
Operator::SET_CONTAINS, Operator::SET_DOES_NOT_CONTAIN,
|
|
289
|
+
Operator::SET_CONTAINS_ANY, Operator::SET_DOES_NOT_CONTAIN_ANY
|
|
290
|
+
true
|
|
291
|
+
else
|
|
292
|
+
false
|
|
293
|
+
end
|
|
292
294
|
end
|
|
293
|
-
end
|
|
294
295
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
296
|
+
def set_equals?(xa, ya)
|
|
297
|
+
xs = Set.new(xa)
|
|
298
|
+
ys = Set.new(ya)
|
|
299
|
+
xs.size == ys.size && ys.all? { |y| xs.include?(y) }
|
|
300
|
+
end
|
|
300
301
|
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
def matches_set_contains_all?(prop_values, filter_values)
|
|
303
|
+
return false if prop_values.length < filter_values.length
|
|
303
304
|
|
|
304
|
-
|
|
305
|
-
|
|
305
|
+
filter_values.all? { |filter_value| matches_is?(filter_value, prop_values) }
|
|
306
|
+
end
|
|
306
307
|
|
|
307
|
-
|
|
308
|
-
|
|
308
|
+
def matches_set_contains_any?(prop_values, filter_values)
|
|
309
|
+
filter_values.any? { |filter_value| matches_is?(filter_value, prop_values) }
|
|
310
|
+
end
|
|
309
311
|
end
|
|
310
312
|
end
|
|
311
313
|
end
|