featurevisor 1.1.0 → 3.0.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/LICENSE +1 -1
- data/README.md +68 -19
- data/bin/cli.rb +2 -2
- data/bin/commands/benchmark.rb +14 -4
- data/bin/commands/test.rb +46 -5
- data/lib/featurevisor/bucketer.rb +62 -8
- data/lib/featurevisor/child_instance.rb +73 -256
- data/lib/featurevisor/conditions.rb +63 -34
- data/lib/featurevisor/{logger.rb → diagnostics.rb} +19 -52
- data/lib/featurevisor/emitter.rb +1 -1
- data/lib/featurevisor/evaluate.rb +104 -105
- data/lib/featurevisor/{datafile_reader.rb → evaluation_data_provider.rb} +41 -18
- data/lib/featurevisor/events.rb +120 -4
- data/lib/featurevisor/instance.rb +226 -78
- data/lib/featurevisor/modules.rb +51 -6
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +4 -4
- metadata +3 -3
|
@@ -1,309 +1,126 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Featurevisor
|
|
4
|
-
# Child instance
|
|
4
|
+
# Child instance with isolated context and sticky state.
|
|
5
5
|
class ChildInstance
|
|
6
|
-
# Initialize a new child instance
|
|
7
|
-
# @param options [Hash] Child instance options
|
|
8
|
-
# @option options [Instance] :parent Parent instance
|
|
9
|
-
# @option options [Hash] :context Child context
|
|
10
|
-
# @option options [Hash] :sticky Child sticky features
|
|
11
6
|
def initialize(options)
|
|
12
7
|
@parent = options[:parent]
|
|
13
8
|
@context = options[:context] || {}
|
|
14
|
-
@
|
|
9
|
+
@sticky_features = options[:sticky_features] || {}
|
|
10
|
+
@sticky_variables = options[:sticky_variables] || {}
|
|
15
11
|
@emitter = Featurevisor::Emitter.new
|
|
12
|
+
@parent_unsubscribers = []
|
|
16
13
|
end
|
|
17
14
|
|
|
18
|
-
# Subscribe to an event
|
|
19
|
-
# @param event_name [String] Event name
|
|
20
|
-
# @param callback [Proc] Callback function
|
|
21
|
-
# @return [Proc] Unsubscribe function
|
|
22
15
|
def on(event_name, callback = nil, &block)
|
|
23
16
|
callback = block if block_given?
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
if %w[context_set sticky_features_set sticky_variables_set].include?(event_name)
|
|
18
|
+
return @emitter.on(event_name, callback)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
parent_unsubscribe = @parent.on(event_name, callback)
|
|
22
|
+
active = true
|
|
23
|
+
unsubscribe = nil
|
|
24
|
+
unsubscribe = proc do
|
|
25
|
+
next unless active
|
|
26
|
+
|
|
27
|
+
active = false
|
|
28
|
+
parent_unsubscribe.call
|
|
29
|
+
@parent_unsubscribers.delete(unsubscribe)
|
|
29
30
|
end
|
|
31
|
+
@parent_unsubscribers << unsubscribe
|
|
32
|
+
unsubscribe
|
|
30
33
|
end
|
|
31
34
|
|
|
32
|
-
# Close the child instance
|
|
33
35
|
def close
|
|
36
|
+
@parent_unsubscribers.dup.each(&:call)
|
|
37
|
+
@parent_unsubscribers.clear
|
|
34
38
|
@emitter.clear_all
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
# Set context
|
|
38
|
-
# @param context [Hash] Context to set
|
|
39
|
-
# @param replace [Boolean] Whether to replace existing context
|
|
40
41
|
def set_context(context, replace = false)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
else
|
|
44
|
-
@context = { **@context, **context }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
@emitter.trigger("context_set", {
|
|
48
|
-
context: @context,
|
|
49
|
-
replaced: replace
|
|
50
|
-
})
|
|
42
|
+
@context = replace ? context : { **@context, **context }
|
|
43
|
+
@emitter.trigger("context_set", context: @context, replaced: replace)
|
|
51
44
|
end
|
|
52
45
|
|
|
53
|
-
# Get context
|
|
54
|
-
# @param context [Hash, nil] Additional context to merge
|
|
55
|
-
# @return [Hash] Merged context
|
|
56
46
|
def get_context(context = nil)
|
|
57
|
-
@parent.get_context({
|
|
58
|
-
**@context,
|
|
59
|
-
**(context || {})
|
|
60
|
-
})
|
|
47
|
+
@parent.get_context({ **@context, **(context || {}) })
|
|
61
48
|
end
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if replace
|
|
70
|
-
@sticky = sticky
|
|
71
|
-
else
|
|
72
|
-
@sticky = {
|
|
73
|
-
**@sticky,
|
|
74
|
-
**sticky
|
|
75
|
-
}
|
|
76
|
-
end
|
|
50
|
+
def set_sticky_features(sticky, replace = false)
|
|
51
|
+
previous = @sticky_features
|
|
52
|
+
@sticky_features = replace ? sticky : { **@sticky_features, **sticky }
|
|
53
|
+
params = Featurevisor::Events.get_params_for_sticky_features_set_event(previous, @sticky_features, replace)
|
|
54
|
+
@emitter.trigger("sticky_features_set", params)
|
|
55
|
+
end
|
|
77
56
|
|
|
78
|
-
|
|
79
|
-
@
|
|
57
|
+
def set_sticky_variables(sticky, replace = false)
|
|
58
|
+
previous = @sticky_variables
|
|
59
|
+
@sticky_variables = replace ? sticky : { **@sticky_variables, **sticky }
|
|
60
|
+
@emitter.trigger(
|
|
61
|
+
"sticky_variables_set",
|
|
62
|
+
Featurevisor::Events.get_params_for_sticky_variables_set_event(previous, @sticky_variables, replace)
|
|
63
|
+
)
|
|
80
64
|
end
|
|
81
65
|
|
|
82
|
-
# Check if a feature is enabled
|
|
83
|
-
# @param feature_key [String] Feature key
|
|
84
|
-
# @param context [Hash] Context
|
|
85
|
-
# @param options [Hash] Override options
|
|
86
|
-
# @return [Boolean] True if feature is enabled
|
|
87
66
|
def is_enabled(feature_key, context = {}, options = {})
|
|
88
|
-
@parent.is_enabled(
|
|
89
|
-
feature_key,
|
|
90
|
-
{
|
|
91
|
-
**@context,
|
|
92
|
-
**context
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
**options,
|
|
96
|
-
__featurevisor_child_sticky: @sticky
|
|
97
|
-
}
|
|
98
|
-
)
|
|
67
|
+
@parent.is_enabled(feature_key, child_context(context), child_options(options))
|
|
99
68
|
end
|
|
100
69
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# @param context [Hash] Context
|
|
104
|
-
# @param options [Hash] Override options
|
|
105
|
-
# @return [String, nil] Variation value or nil
|
|
106
|
-
def get_variation(feature_key, context = {}, options = {})
|
|
107
|
-
@parent.get_variation(
|
|
108
|
-
feature_key,
|
|
109
|
-
{
|
|
110
|
-
**@context,
|
|
111
|
-
**context
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
**options,
|
|
115
|
-
__featurevisor_child_sticky: @sticky
|
|
116
|
-
}
|
|
117
|
-
)
|
|
70
|
+
def evaluate_flag(feature_key, context = {}, options = {})
|
|
71
|
+
@parent.evaluate_flag(feature_key, child_context(context), child_options(options))
|
|
118
72
|
end
|
|
119
73
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# @param variable_key [String] Variable key
|
|
123
|
-
# @param context [Hash] Context
|
|
124
|
-
# @param options [Hash] Override options
|
|
125
|
-
# @return [Object, nil] Variable value or nil
|
|
126
|
-
def get_variable(feature_key, variable_key, context = {}, options = {})
|
|
127
|
-
@parent.get_variable(
|
|
128
|
-
feature_key,
|
|
129
|
-
variable_key,
|
|
130
|
-
{
|
|
131
|
-
**@context,
|
|
132
|
-
**context
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
**options,
|
|
136
|
-
__featurevisor_child_sticky: @sticky
|
|
137
|
-
}
|
|
138
|
-
)
|
|
74
|
+
def get_variation(feature_key, context = {}, options = {})
|
|
75
|
+
@parent.get_variation(feature_key, child_context(context), child_options(options))
|
|
139
76
|
end
|
|
140
77
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# @param variable_key [String] Variable key
|
|
144
|
-
# @param context [Hash] Context
|
|
145
|
-
# @param options [Hash] Override options
|
|
146
|
-
# @return [Boolean, nil] Boolean value or nil
|
|
147
|
-
def get_variable_boolean(feature_key, variable_key, context = {}, options = {})
|
|
148
|
-
@parent.get_variable_boolean(
|
|
149
|
-
feature_key,
|
|
150
|
-
variable_key,
|
|
151
|
-
{
|
|
152
|
-
**@context,
|
|
153
|
-
**context
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
**options,
|
|
157
|
-
__featurevisor_child_sticky: @sticky
|
|
158
|
-
}
|
|
159
|
-
)
|
|
78
|
+
def evaluate_variation(feature_key, context = {}, options = {})
|
|
79
|
+
@parent.evaluate_variation(feature_key, child_context(context), child_options(options))
|
|
160
80
|
end
|
|
161
81
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# @param variable_key [String] Variable key
|
|
165
|
-
# @param context [Hash] Context
|
|
166
|
-
# @param options [Hash] Override options
|
|
167
|
-
# @return [String, nil] String value or nil
|
|
168
|
-
def get_variable_string(feature_key, variable_key, context = {}, options = {})
|
|
169
|
-
@parent.get_variable_string(
|
|
170
|
-
feature_key,
|
|
171
|
-
variable_key,
|
|
172
|
-
{
|
|
173
|
-
**@context,
|
|
174
|
-
**context
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
**options,
|
|
178
|
-
__featurevisor_child_sticky: @sticky
|
|
179
|
-
}
|
|
180
|
-
)
|
|
82
|
+
def get_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
|
|
83
|
+
delegate_variable(:get_variable, feature_or_variable_key, variable_key_or_context, context_or_options, options)
|
|
181
84
|
end
|
|
182
85
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# @param variable_key [String] Variable key
|
|
186
|
-
# @param context [Hash] Context
|
|
187
|
-
# @param options [Hash] Override options
|
|
188
|
-
# @return [Integer, nil] Integer value or nil
|
|
189
|
-
def get_variable_integer(feature_key, variable_key, context = {}, options = {})
|
|
190
|
-
@parent.get_variable_integer(
|
|
191
|
-
feature_key,
|
|
192
|
-
variable_key,
|
|
193
|
-
{
|
|
194
|
-
**@context,
|
|
195
|
-
**context
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
**options,
|
|
199
|
-
__featurevisor_child_sticky: @sticky
|
|
200
|
-
}
|
|
201
|
-
)
|
|
86
|
+
def evaluate_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
|
|
87
|
+
delegate_variable(:evaluate_variable, feature_or_variable_key, variable_key_or_context, context_or_options, options)
|
|
202
88
|
end
|
|
203
89
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# @param options [Hash] Override options
|
|
209
|
-
# @return [Float, nil] Float value or nil
|
|
210
|
-
def get_variable_double(feature_key, variable_key, context = {}, options = {})
|
|
211
|
-
@parent.get_variable_double(
|
|
212
|
-
feature_key,
|
|
213
|
-
variable_key,
|
|
214
|
-
{
|
|
215
|
-
**@context,
|
|
216
|
-
**context
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
**options,
|
|
220
|
-
__featurevisor_child_sticky: @sticky
|
|
221
|
-
}
|
|
222
|
-
)
|
|
90
|
+
%i[boolean string integer double array object json].each do |type|
|
|
91
|
+
define_method("get_variable_#{type}") do |feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {}|
|
|
92
|
+
delegate_variable("get_variable_#{type}".to_sym, feature_or_variable_key, variable_key_or_context, context_or_options, options)
|
|
93
|
+
end
|
|
223
94
|
end
|
|
224
95
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
# @param variable_key [String] Variable key
|
|
228
|
-
# @param context [Hash] Context
|
|
229
|
-
# @param options [Hash] Override options
|
|
230
|
-
# @return [Array, nil] Array value or nil
|
|
231
|
-
def get_variable_array(feature_key, variable_key, context = {}, options = {})
|
|
232
|
-
@parent.get_variable_array(
|
|
233
|
-
feature_key,
|
|
234
|
-
variable_key,
|
|
235
|
-
{
|
|
236
|
-
**@context,
|
|
237
|
-
**context
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
**options,
|
|
241
|
-
__featurevisor_child_sticky: @sticky
|
|
242
|
-
}
|
|
243
|
-
)
|
|
96
|
+
def get_feature_evaluations(context = {}, feature_keys = [], options = {})
|
|
97
|
+
@parent.get_feature_evaluations(child_context(context), feature_keys, child_options(options))
|
|
244
98
|
end
|
|
245
99
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# @param variable_key [String] Variable key
|
|
249
|
-
# @param context [Hash] Context
|
|
250
|
-
# @param options [Hash] Override options
|
|
251
|
-
# @return [Hash, nil] Object value or nil
|
|
252
|
-
def get_variable_object(feature_key, variable_key, context = {}, options = {})
|
|
253
|
-
@parent.get_variable_object(
|
|
254
|
-
feature_key,
|
|
255
|
-
variable_key,
|
|
256
|
-
{
|
|
257
|
-
**@context,
|
|
258
|
-
**context
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
**options,
|
|
262
|
-
__featurevisor_child_sticky: @sticky
|
|
263
|
-
}
|
|
264
|
-
)
|
|
100
|
+
def get_variable_evaluations(context = {}, variable_keys = [], options = {})
|
|
101
|
+
@parent.get_variable_evaluations(child_context(context), variable_keys, child_options(options))
|
|
265
102
|
end
|
|
266
103
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# @param options [Hash] Override options
|
|
272
|
-
# @return [Object, nil] JSON value or nil
|
|
273
|
-
def get_variable_json(feature_key, variable_key, context = {}, options = {})
|
|
274
|
-
@parent.get_variable_json(
|
|
275
|
-
feature_key,
|
|
276
|
-
variable_key,
|
|
277
|
-
{
|
|
278
|
-
**@context,
|
|
279
|
-
**context
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
**options,
|
|
283
|
-
__featurevisor_child_sticky: @sticky
|
|
284
|
-
}
|
|
285
|
-
)
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def child_context(context)
|
|
107
|
+
{ **@context, **context }
|
|
286
108
|
end
|
|
287
109
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
@parent.get_all_evaluations(
|
|
295
|
-
{
|
|
296
|
-
**@context,
|
|
297
|
-
**context
|
|
298
|
-
},
|
|
299
|
-
feature_keys,
|
|
300
|
-
{
|
|
301
|
-
**options,
|
|
302
|
-
__featurevisor_child_sticky: @sticky
|
|
303
|
-
}
|
|
304
|
-
)
|
|
110
|
+
def child_options(options)
|
|
111
|
+
{
|
|
112
|
+
**options,
|
|
113
|
+
__featurevisor_child_sticky_features: @sticky_features,
|
|
114
|
+
__featurevisor_child_sticky_variables: @sticky_variables
|
|
115
|
+
}
|
|
305
116
|
end
|
|
306
117
|
|
|
307
|
-
|
|
118
|
+
def delegate_variable(method, first, second, third, fourth)
|
|
119
|
+
if second.nil? || second.is_a?(Hash)
|
|
120
|
+
@parent.public_send(method, first, child_context(second || {}), child_options(third))
|
|
121
|
+
else
|
|
122
|
+
@parent.public_send(method, first, second, child_context(third), child_options(fourth))
|
|
123
|
+
end
|
|
124
|
+
end
|
|
308
125
|
end
|
|
309
126
|
end
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
module Featurevisor
|
|
6
7
|
# Conditions module for evaluating feature flags and segments
|
|
7
8
|
module Conditions
|
|
9
|
+
MISSING = Object.new.freeze
|
|
10
|
+
private_constant :MISSING
|
|
11
|
+
|
|
8
12
|
# Get value from context object using dot notation path
|
|
9
13
|
# @param obj [Hash] Context object
|
|
10
14
|
# @param path [String] Dot-separated path to the value
|
|
@@ -12,12 +16,36 @@ module Featurevisor
|
|
|
12
16
|
def self.get_value_from_context(obj, path)
|
|
13
17
|
return nil if obj.nil? || path.nil?
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
value = get_value_with_presence(obj, path)
|
|
20
|
+
value.equal?(MISSING) ? nil : value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.get_value_with_presence(obj, path)
|
|
24
|
+
return MISSING unless obj.is_a?(Hash) && path.is_a?(String)
|
|
25
|
+
|
|
26
|
+
path.split(".").reduce(obj) do |current, key|
|
|
27
|
+
break MISSING unless current.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
if current.key?(key.to_sym)
|
|
30
|
+
current[key.to_sym]
|
|
31
|
+
elsif current.key?(key)
|
|
32
|
+
current[key]
|
|
33
|
+
else
|
|
34
|
+
break MISSING
|
|
35
|
+
end
|
|
17
36
|
end
|
|
37
|
+
end
|
|
38
|
+
private_class_method :get_value_with_presence
|
|
39
|
+
|
|
40
|
+
def self.strict_equal?(left, right)
|
|
41
|
+
return true if left.nil? && right.nil?
|
|
42
|
+
return left.to_f == right.to_f if left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
43
|
+
return left == right if left.is_a?(String) && right.is_a?(String)
|
|
44
|
+
return left == right if (left == true || left == false) && (right == true || right == false)
|
|
18
45
|
|
|
19
|
-
|
|
46
|
+
false
|
|
20
47
|
end
|
|
48
|
+
private_class_method :strict_equal?
|
|
21
49
|
|
|
22
50
|
# Check if a condition is matched against context
|
|
23
51
|
# @param condition [Hash] Condition to evaluate
|
|
@@ -30,18 +58,19 @@ module Featurevisor
|
|
|
30
58
|
value = condition["value"] || condition[:value]
|
|
31
59
|
regex_flags = condition["regexFlags"] || condition[:regexFlags]
|
|
32
60
|
|
|
33
|
-
|
|
61
|
+
raw_context_value = get_value_with_presence(context, attribute)
|
|
62
|
+
context_value_from_path = raw_context_value.equal?(MISSING) ? nil : raw_context_value
|
|
63
|
+
attribute_exists = !raw_context_value.equal?(MISSING)
|
|
34
64
|
|
|
35
65
|
case operator
|
|
36
66
|
when "equals"
|
|
37
|
-
context_value_from_path
|
|
67
|
+
attribute_exists && strict_equal?(context_value_from_path, value)
|
|
38
68
|
when "notEquals"
|
|
39
|
-
context_value_from_path
|
|
69
|
+
!attribute_exists || !strict_equal?(context_value_from_path, value)
|
|
40
70
|
when "before", "after"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
date_in_condition = value.is_a?(Date) ? value : Date.parse(value.to_s)
|
|
71
|
+
date_in_context = portable_date(context_value_from_path)
|
|
72
|
+
date_in_condition = portable_date(value)
|
|
73
|
+
return false unless date_in_context && date_in_condition
|
|
45
74
|
|
|
46
75
|
if operator == "before"
|
|
47
76
|
date_in_context < date_in_condition
|
|
@@ -50,21 +79,13 @@ module Featurevisor
|
|
|
50
79
|
end
|
|
51
80
|
when "in", "notIn"
|
|
52
81
|
# in / notIn (where condition value is an array)
|
|
53
|
-
if value.is_a?(Array) &&
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# If key doesn't exist, notIn should fail (return false), in should also fail
|
|
58
|
-
if !key_exists
|
|
59
|
-
return false
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
value_in_context = context_value_from_path.to_s
|
|
63
|
-
|
|
82
|
+
if attribute_exists && value.is_a?(Array) &&
|
|
83
|
+
(context_value_from_path.is_a?(String) || context_value_from_path.is_a?(Numeric) || context_value_from_path.nil?)
|
|
84
|
+
matched = value.any? { |candidate| strict_equal?(candidate, context_value_from_path) }
|
|
64
85
|
if operator == "in"
|
|
65
|
-
|
|
86
|
+
matched
|
|
66
87
|
else # notIn
|
|
67
|
-
!
|
|
88
|
+
!matched
|
|
68
89
|
end
|
|
69
90
|
else
|
|
70
91
|
false
|
|
@@ -124,18 +145,18 @@ module Featurevisor
|
|
|
124
145
|
false
|
|
125
146
|
end
|
|
126
147
|
when "exists"
|
|
127
|
-
|
|
148
|
+
attribute_exists
|
|
128
149
|
when "notExists"
|
|
129
|
-
|
|
150
|
+
!attribute_exists
|
|
130
151
|
when "includes", "notIncludes"
|
|
131
152
|
# includes / notIncludes (where context value is an array)
|
|
132
|
-
if context_value_from_path.is_a?(Array) &&
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
if context_value_from_path.is_a?(Array) &&
|
|
154
|
+
(value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false || value.nil?)
|
|
155
|
+
matched = context_value_from_path.any? { |candidate| strict_equal?(candidate, value) }
|
|
135
156
|
if operator == "includes"
|
|
136
|
-
|
|
157
|
+
matched
|
|
137
158
|
else # notIncludes
|
|
138
|
-
!
|
|
159
|
+
!matched
|
|
139
160
|
end
|
|
140
161
|
else
|
|
141
162
|
false
|
|
@@ -143,10 +164,18 @@ module Featurevisor
|
|
|
143
164
|
else
|
|
144
165
|
false
|
|
145
166
|
end
|
|
146
|
-
rescue => e
|
|
147
|
-
# Log error but don't stop execution
|
|
148
|
-
warn "Error in condition evaluation: #{e.message}"
|
|
149
|
-
false
|
|
150
167
|
end
|
|
168
|
+
|
|
169
|
+
def self.portable_date(value)
|
|
170
|
+
return value if value.is_a?(Time) || value.is_a?(DateTime)
|
|
171
|
+
return value.to_time if value.is_a?(Date)
|
|
172
|
+
return nil unless value.is_a?(String)
|
|
173
|
+
return nil unless value.match?(/T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:\d{2})\z/)
|
|
174
|
+
|
|
175
|
+
Time.iso8601(value)
|
|
176
|
+
rescue ArgumentError
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
private_class_method :portable_date
|
|
151
180
|
end
|
|
152
181
|
end
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Featurevisor
|
|
4
|
-
#
|
|
4
|
+
# Diagnostic severity levels
|
|
5
5
|
LOG_LEVELS = %w[fatal error warn info debug].freeze
|
|
6
6
|
DEFAULT_LOG_LEVEL = "info".freeze
|
|
7
|
-
|
|
7
|
+
DIAGNOSTIC_PREFIX = "[Featurevisor]".freeze
|
|
8
8
|
|
|
9
|
-
#
|
|
10
|
-
class
|
|
9
|
+
# Private evaluator adapter for structured diagnostics.
|
|
10
|
+
class DiagnosticReporter
|
|
11
11
|
attr_reader :level, :handler
|
|
12
12
|
|
|
13
|
-
# Initialize a
|
|
14
|
-
# @param options [Hash]
|
|
13
|
+
# Initialize a diagnostic reporter.
|
|
14
|
+
# @param options [Hash] Reporter options
|
|
15
15
|
# @option options [String] :level Log level (default: "info")
|
|
16
|
-
# @option options [Proc] :handler
|
|
16
|
+
# @option options [Proc] :handler Internal structured diagnostic sink
|
|
17
17
|
def initialize(options = {})
|
|
18
18
|
@level = options[:level] || DEFAULT_LOG_LEVEL
|
|
19
|
+
@filter = !options.key?(:handler)
|
|
19
20
|
@handler = options[:handler] || method(:default_log_handler)
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -25,12 +26,13 @@ module Featurevisor
|
|
|
25
26
|
@level = level
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# @param
|
|
29
|
+
# Forward an evaluator diagnostic. Filtering is performed centrally by
|
|
30
|
+
# Instance so module subscriptions and error events remain independent.
|
|
31
|
+
# @param level [String] Diagnostic level
|
|
32
|
+
# @param message [String] Diagnostic message
|
|
31
33
|
# @param details [Hash, nil] Additional details
|
|
32
34
|
def log(level, message, details = nil)
|
|
33
|
-
return
|
|
35
|
+
return if @filter && !should_handle?(level)
|
|
34
36
|
|
|
35
37
|
@handler.call(level, message, details)
|
|
36
38
|
end
|
|
@@ -72,16 +74,8 @@ module Featurevisor
|
|
|
72
74
|
|
|
73
75
|
private
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# @return [Boolean] True if should handle
|
|
78
|
-
def should_handle?(log_level)
|
|
79
|
-
current_index = LOG_LEVELS.index(@level)
|
|
80
|
-
target_index = LOG_LEVELS.index(log_level)
|
|
81
|
-
|
|
82
|
-
return false if current_index.nil? || target_index.nil?
|
|
83
|
-
|
|
84
|
-
current_index >= target_index
|
|
77
|
+
def should_handle?(level)
|
|
78
|
+
LOG_LEVELS.index(level).to_i <= LOG_LEVELS.index(@level).to_i
|
|
85
79
|
end
|
|
86
80
|
|
|
87
81
|
# Default log handler that outputs to console
|
|
@@ -99,45 +93,18 @@ module Featurevisor
|
|
|
99
93
|
case method_name
|
|
100
94
|
when "puts"
|
|
101
95
|
if details && !details.empty?
|
|
102
|
-
Kernel.puts("#{
|
|
96
|
+
Kernel.puts("#{DIAGNOSTIC_PREFIX} #{message} #{details.inspect}")
|
|
103
97
|
else
|
|
104
|
-
Kernel.puts("#{
|
|
98
|
+
Kernel.puts("#{DIAGNOSTIC_PREFIX} #{message}")
|
|
105
99
|
end
|
|
106
100
|
when "warn"
|
|
107
101
|
if details && !details.empty?
|
|
108
|
-
Kernel.warn("#{
|
|
102
|
+
Kernel.warn("#{DIAGNOSTIC_PREFIX} #{message} #{details.inspect}")
|
|
109
103
|
else
|
|
110
|
-
Kernel.warn("#{
|
|
104
|
+
Kernel.warn("#{DIAGNOSTIC_PREFIX} #{message}")
|
|
111
105
|
end
|
|
112
106
|
end
|
|
113
107
|
end
|
|
114
108
|
end
|
|
115
109
|
|
|
116
|
-
# Default log handler function
|
|
117
|
-
# @param level [String] Log level
|
|
118
|
-
# @param message [String] Log message
|
|
119
|
-
# @param details [Hash, nil] Additional details
|
|
120
|
-
def self.default_log_handler(level, message, details = nil)
|
|
121
|
-
method_name = case level
|
|
122
|
-
when "info" then "puts"
|
|
123
|
-
when "warn" then "warn"
|
|
124
|
-
when "error", "fatal" then "warn"
|
|
125
|
-
else "puts"
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
case method_name
|
|
129
|
-
when "puts"
|
|
130
|
-
if details && !details.empty?
|
|
131
|
-
Kernel.puts("#{LOGGER_PREFIX} #{message} #{details.inspect}")
|
|
132
|
-
else
|
|
133
|
-
Kernel.puts("#{LOGGER_PREFIX} #{message}")
|
|
134
|
-
end
|
|
135
|
-
when "warn"
|
|
136
|
-
if details && !details.empty?
|
|
137
|
-
Kernel.warn("#{LOGGER_PREFIX} #{message} #{details.inspect}")
|
|
138
|
-
else
|
|
139
|
-
Kernel.warn("#{LOGGER_PREFIX} #{message}")
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
110
|
end
|
data/lib/featurevisor/emitter.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Featurevisor
|
|
4
4
|
# Event names for the emitter
|
|
5
|
-
EVENT_NAMES = %w[datafile_set context_set
|
|
5
|
+
EVENT_NAMES = %w[datafile_set context_set sticky_features_set sticky_variables_set error].freeze
|
|
6
6
|
|
|
7
7
|
# Event emitter class for handling event subscriptions and triggers
|
|
8
8
|
class Emitter
|