featurevisor 3.0.0 → 4.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/README.md +307 -193
- data/bin/cli.rb +84 -17
- data/bin/commands/assess_distribution.rb +10 -52
- data/bin/commands/benchmark.rb +10 -48
- data/bin/commands/node_cli.rb +20 -0
- data/bin/commands/runtime_project.rb +63 -0
- data/bin/commands/test.rb +771 -259
- data/bin/commands.rb +1 -0
- data/bin/segment_tests.rb +89 -0
- data/bin/test_checks.rb +767 -0
- data/lib/featurevisor/child_instance.rb +15 -2
- data/lib/featurevisor/compare_versions.rb +18 -36
- data/lib/featurevisor/conditions.rb +91 -9
- data/lib/featurevisor/dependencies.rb +150 -0
- data/lib/featurevisor/emitter.rb +4 -0
- data/lib/featurevisor/evaluate.rb +64 -115
- data/lib/featurevisor/evaluation_data_provider.rb +100 -51
- data/lib/featurevisor/evaluation_session.rb +191 -0
- data/lib/featurevisor/events.rb +39 -100
- data/lib/featurevisor/instance.rb +258 -59
- data/lib/featurevisor/modules.rb +6 -46
- data/lib/featurevisor/mutations.rb +213 -0
- data/lib/featurevisor/normalize.rb +105 -0
- data/lib/featurevisor/unified_conditions.rb +79 -0
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +7 -0
- metadata +10 -1
|
@@ -35,6 +35,8 @@ module Featurevisor
|
|
|
35
35
|
@sticky_features = options[:sticky_features] || options[:stickyFeatures] || {}
|
|
36
36
|
@sticky_variables = options[:sticky_variables] || options[:stickyVariables] || {}
|
|
37
37
|
@closed = false
|
|
38
|
+
@evaluation_generation = 0
|
|
39
|
+
@datafile_mutex = Mutex.new
|
|
38
40
|
@module_diagnostic_subscriptions = []
|
|
39
41
|
|
|
40
42
|
# datafile
|
|
@@ -47,6 +49,7 @@ module Featurevisor
|
|
|
47
49
|
modules: options[:modules] || [],
|
|
48
50
|
report_diagnostic: method(:report_diagnostic),
|
|
49
51
|
module_api_factory: method(:create_module_api),
|
|
52
|
+
on_change: method(:invalidate_evaluation_memo),
|
|
50
53
|
clear_module_diagnostic_subscriptions: method(:clear_module_diagnostic_subscriptions)
|
|
51
54
|
)
|
|
52
55
|
|
|
@@ -68,6 +71,8 @@ module Featurevisor
|
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def handle_evaluation_diagnostic(level, message, details = nil)
|
|
74
|
+
return unless diagnostic_interested?(level)
|
|
75
|
+
|
|
71
76
|
details = (details || {}).dup
|
|
72
77
|
code = details.delete(:code) || details.delete("code") || details[:reason] || details["reason"] || message
|
|
73
78
|
code = "deprecated_feature" if message == "feature is deprecated"
|
|
@@ -97,8 +102,9 @@ module Featurevisor
|
|
|
97
102
|
# Set the datafile
|
|
98
103
|
# @param datafile [Hash, String] Datafile content or JSON string
|
|
99
104
|
# @param replace [Boolean] Whether to replace instead of merge
|
|
105
|
+
# @return [Boolean] Whether the datafile was accepted, even if unchanged
|
|
100
106
|
def set_datafile(datafile, replace = false)
|
|
101
|
-
return if @closed
|
|
107
|
+
return false if @closed
|
|
102
108
|
|
|
103
109
|
begin
|
|
104
110
|
parsed_datafile = if datafile.is_a?(String)
|
|
@@ -114,14 +120,26 @@ module Featurevisor
|
|
|
114
120
|
(!parsed_datafile.key?(:variables) || parsed_datafile[:variables].is_a?(Hash))
|
|
115
121
|
raise ArgumentError, "Invalid datafile"
|
|
116
122
|
end
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
datafile
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
normalization_errors = []
|
|
124
|
+
if parsed_datafile[:schemaVersion] == "2"
|
|
125
|
+
parsed_datafile, normalization_errors = Normalize.datafile(parsed_datafile)
|
|
126
|
+
end
|
|
127
|
+
# Serialize merge and publication, leaving application callbacks outside.
|
|
128
|
+
details = @datafile_mutex.synchronize do
|
|
129
|
+
return false if @closed
|
|
130
|
+
next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile.get_datafile, parsed_datafile)
|
|
131
|
+
new_datafile = InstanceEvaluationDataProvider.new(
|
|
132
|
+
datafile: next_datafile,
|
|
133
|
+
diagnostics: @diagnostics
|
|
134
|
+
)
|
|
135
|
+
new_datafile.retain_normalized(@datafile, parsed_datafile) unless replace
|
|
136
|
+
event = Featurevisor::Events.get_params_for_datafile_set_event(@datafile, new_datafile, replace, parsed_datafile)
|
|
137
|
+
@datafile = new_datafile
|
|
138
|
+
@evaluation_generation += 1
|
|
139
|
+
event
|
|
140
|
+
end
|
|
122
141
|
|
|
123
|
-
|
|
124
|
-
@datafile = new_datafile
|
|
142
|
+
normalization_errors.each { |diagnostic| report_diagnostic(diagnostic) }
|
|
125
143
|
|
|
126
144
|
report_diagnostic(
|
|
127
145
|
level: "info",
|
|
@@ -130,6 +148,7 @@ module Featurevisor
|
|
|
130
148
|
details: details
|
|
131
149
|
)
|
|
132
150
|
@emitter.trigger("datafile_set", details)
|
|
151
|
+
true
|
|
133
152
|
rescue => e
|
|
134
153
|
report_diagnostic(
|
|
135
154
|
level: "error",
|
|
@@ -137,6 +156,7 @@ module Featurevisor
|
|
|
137
156
|
message: "Could not parse datafile",
|
|
138
157
|
original_error: e
|
|
139
158
|
)
|
|
159
|
+
false
|
|
140
160
|
end
|
|
141
161
|
end
|
|
142
162
|
|
|
@@ -156,6 +176,7 @@ module Featurevisor
|
|
|
156
176
|
end
|
|
157
177
|
|
|
158
178
|
params = Featurevisor::Events.get_params_for_sticky_features_set_event(previous_sticky_features, @sticky_features, replace)
|
|
179
|
+
@evaluation_generation += 1
|
|
159
180
|
|
|
160
181
|
report_diagnostic(
|
|
161
182
|
level: "info",
|
|
@@ -169,6 +190,7 @@ module Featurevisor
|
|
|
169
190
|
def set_sticky_variables(sticky, replace = false)
|
|
170
191
|
previous = @sticky_variables || {}
|
|
171
192
|
@sticky_variables = replace ? sticky : { **@sticky_variables, **sticky }
|
|
193
|
+
@evaluation_generation += 1
|
|
172
194
|
params = Featurevisor::Events.get_params_for_sticky_variables_set_event(previous, @sticky_variables, replace)
|
|
173
195
|
report_diagnostic(level: "info", code: "sticky_variables_set", message: "Sticky variables set", details: params)
|
|
174
196
|
@emitter.trigger("sticky_variables_set", params)
|
|
@@ -214,6 +236,11 @@ module Featurevisor
|
|
|
214
236
|
@modules_manager.add(mod)
|
|
215
237
|
end
|
|
216
238
|
|
|
239
|
+
def invalidate_evaluation_memo
|
|
240
|
+
@evaluation_generation += 1
|
|
241
|
+
end
|
|
242
|
+
private :invalidate_evaluation_memo
|
|
243
|
+
|
|
217
244
|
def remove_module(name)
|
|
218
245
|
@modules_manager.remove(name)
|
|
219
246
|
end
|
|
@@ -230,12 +257,15 @@ module Featurevisor
|
|
|
230
257
|
|
|
231
258
|
# Close the instance
|
|
232
259
|
def close
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
260
|
+
@datafile_mutex.synchronize do
|
|
261
|
+
return if @closed
|
|
262
|
+
@closed = true
|
|
263
|
+
end
|
|
236
264
|
@modules_manager.close_all
|
|
237
265
|
@module_diagnostic_subscriptions = []
|
|
238
266
|
@emitter.clear_all
|
|
267
|
+
@on_diagnostic = nil
|
|
268
|
+
@datafile.clear_caches
|
|
239
269
|
end
|
|
240
270
|
|
|
241
271
|
# Set context
|
|
@@ -247,6 +277,7 @@ module Featurevisor
|
|
|
247
277
|
else
|
|
248
278
|
@context = { **@context, **context }
|
|
249
279
|
end
|
|
280
|
+
@evaluation_generation += 1
|
|
250
281
|
|
|
251
282
|
@emitter.trigger("context_set", {
|
|
252
283
|
context: @context,
|
|
@@ -315,6 +346,7 @@ module Featurevisor
|
|
|
315
346
|
evaluation = evaluate_flag(feature_key, context, options)
|
|
316
347
|
evaluation[:enabled] == true
|
|
317
348
|
rescue => e
|
|
349
|
+
raise if e.is_a?(RequirementError) && options[:__featurevisor_prerequisite]
|
|
318
350
|
report_diagnostic(level: "error", code: "evaluation_error", message: "isEnabled failed", originalError: e, details: { featureKey: feature_key })
|
|
319
351
|
false
|
|
320
352
|
end
|
|
@@ -351,6 +383,7 @@ module Featurevisor
|
|
|
351
383
|
nil
|
|
352
384
|
end
|
|
353
385
|
rescue => e
|
|
386
|
+
raise if e.is_a?(RequirementError) && options[:__featurevisor_prerequisite]
|
|
354
387
|
report_diagnostic(level: "error", code: "evaluation_error", message: "getVariation failed", originalError: e, details: { featureKey: feature_key })
|
|
355
388
|
nil
|
|
356
389
|
end
|
|
@@ -389,7 +422,7 @@ module Featurevisor
|
|
|
389
422
|
|
|
390
423
|
if evaluation.key?(:variable_value)
|
|
391
424
|
variable_type = evaluation.dig(:variable_schema, :type) || evaluation.dig(:variable, :type)
|
|
392
|
-
if variable_type == "json" &&
|
|
425
|
+
if variable_type == "json" && !evaluation[:__featurevisor_decoded_json] && !evaluation.dig(:variable, :compose) &&
|
|
393
426
|
evaluation[:variable_value].is_a?(String)
|
|
394
427
|
JSON.parse(evaluation[:variable_value], symbolize_names: true)
|
|
395
428
|
else
|
|
@@ -399,6 +432,8 @@ module Featurevisor
|
|
|
399
432
|
nil
|
|
400
433
|
end
|
|
401
434
|
rescue => e
|
|
435
|
+
getter_options = variable_key_or_context.is_a?(String) ? options : context_or_options
|
|
436
|
+
raise if e.is_a?(RequirementError) && getter_options[:__featurevisor_prerequisite]
|
|
402
437
|
report_diagnostic(level: "error", code: "evaluation_error", message: "getVariable failed", originalError: e, details: { variableKey: variable_key_or_context || feature_or_variable_key })
|
|
403
438
|
nil
|
|
404
439
|
end
|
|
@@ -437,15 +472,15 @@ module Featurevisor
|
|
|
437
472
|
get_value_by_type(variable_value, "integer")
|
|
438
473
|
end
|
|
439
474
|
|
|
440
|
-
# Get variable as
|
|
475
|
+
# Get variable as a number, whole or fractional
|
|
441
476
|
# @param feature_key [String] Feature key
|
|
442
477
|
# @param variable_key [String] Variable key
|
|
443
478
|
# @param context [Hash] Context
|
|
444
479
|
# @param options [Hash] Override options
|
|
445
480
|
# @return [Float, nil] Float value or nil
|
|
446
|
-
def
|
|
481
|
+
def get_variable_number(*args)
|
|
447
482
|
variable_value = get_variable(*args)
|
|
448
|
-
get_value_by_type(variable_value, "
|
|
483
|
+
get_value_by_type(variable_value, "number")
|
|
449
484
|
end
|
|
450
485
|
|
|
451
486
|
# Get variable as array
|
|
@@ -538,14 +573,24 @@ module Featurevisor
|
|
|
538
573
|
evaluation_options = {
|
|
539
574
|
type: "variable",
|
|
540
575
|
variable_key: variable_key.to_s,
|
|
541
|
-
context: get_context(context)
|
|
576
|
+
context: options[:__featurevisor_effective_context] ? context : get_context(context)
|
|
542
577
|
}
|
|
578
|
+
session = options[:__featurevisor_session] || EvaluationSession.new
|
|
579
|
+
evaluation_options[:dependency_of] = session.dependency_of if options[:__featurevisor_prerequisite]
|
|
580
|
+
evaluation_options[:dependency_purpose] = options[:dependency_purpose] || "condition" if options[:__featurevisor_prerequisite]
|
|
543
581
|
evaluation_options[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
|
|
544
582
|
begin
|
|
545
|
-
|
|
583
|
+
# Only prerequisite entries fingerprint effective dependency inputs.
|
|
584
|
+
session_options = options[:__featurevisor_prerequisite] ? global_variable_session_options(evaluation_options, options) : evaluation_options
|
|
585
|
+
return session.run(session_options,
|
|
586
|
+
prerequisite: options[:__featurevisor_prerequisite]) do
|
|
587
|
+
unless @modules_manager.get_all.empty?
|
|
588
|
+
evaluation_options = @modules_manager.run_before_evaluation_modules(evaluation_options)
|
|
589
|
+
end
|
|
590
|
+
session.update_current(evaluation_options)
|
|
546
591
|
resolved_key = evaluation_options[:variable_key]
|
|
547
592
|
variable = @datafile.get_global_variable(resolved_key)
|
|
548
|
-
sticky = options[:__featurevisor_child_sticky_variables] || @sticky_variables
|
|
593
|
+
sticky = options[:__featurevisor_sticky_variables_source]&.call || options[:__featurevisor_child_sticky_variables] || @sticky_variables
|
|
549
594
|
evaluation = { type: "variable", variable_key: resolved_key, reason: Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND }
|
|
550
595
|
|
|
551
596
|
if sticky.key?(resolved_key) || sticky.key?(resolved_key.to_sym)
|
|
@@ -553,28 +598,49 @@ module Featurevisor
|
|
|
553
598
|
evaluation.merge!(reason: Featurevisor::EvaluationReason::STICKY, variable: variable,
|
|
554
599
|
variable_value: sticky_value)
|
|
555
600
|
elsif variable
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
601
|
+
dependencies = get_evaluation_dependencies(evaluation_options[:context], options.merge(__featurevisor_effective_context: true, __featurevisor_session: session))
|
|
602
|
+
if variable.key?(:conditions) && !UnifiedConditions.matches?(variable[:conditions], dependencies)
|
|
603
|
+
value_key = !variable[:compose] && variable[:useDefaultWhenDisabled] ? :defaultValue : :disabledValue
|
|
604
|
+
evaluation.merge!(reason: Featurevisor::EvaluationReason::CONDITIONS_UNMET,
|
|
605
|
+
conditions: variable[:conditions], variable: variable)
|
|
560
606
|
evaluation[:variable_value] = variable[value_key] if variable.key?(value_key)
|
|
607
|
+
evaluation[:variable_value] = nil if variable[:compose] && !variable.key?(value_key)
|
|
608
|
+
if variable[:compose].is_a?(Array) && evaluation[:variable_value].is_a?(Hash)
|
|
609
|
+
evaluation[:variable_value] = evaluation[:variable_value].dup
|
|
610
|
+
end
|
|
561
611
|
else
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
612
|
+
if variable[:compose]
|
|
613
|
+
evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_COMPOSED,
|
|
614
|
+
variable: variable, variable_value: compose_value(variable[:compose], dependencies))
|
|
615
|
+
end
|
|
616
|
+
program = variable[:overrideProgram]
|
|
617
|
+
overrides = program || (variable[:compose] ? [] : variable[:overrides]) || []
|
|
618
|
+
accumulated_value = variable[:compose] ? evaluation[:variable_value] : variable[:defaultValue]
|
|
619
|
+
applied_paths = []
|
|
620
|
+
index = 0
|
|
621
|
+
while index < overrides.length
|
|
622
|
+
override = overrides[index]
|
|
623
|
+
matched = !override.key?(:conditions) || UnifiedConditions.matches?(override[:conditions], dependencies)
|
|
624
|
+
if matched
|
|
625
|
+
if program
|
|
626
|
+
accumulated_value = Mutations.apply(accumulated_value, override[:operations])
|
|
627
|
+
if variable[:compose].is_a?(Array) && override[:operations].any? { |operation| operation[:op] == "set" && operation[:path].empty? }
|
|
628
|
+
accumulated_value = accumulated_value.dup
|
|
629
|
+
end
|
|
630
|
+
applied_paths << (override[:keyPath] || [override[:key]])
|
|
631
|
+
end
|
|
632
|
+
evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE_RULE,
|
|
633
|
+
variable: variable, variable_override_index: index,
|
|
634
|
+
variable_override_key: override[:key], variable_override_path: override[:keyPath])
|
|
635
|
+
evaluation[:variable_value] = program ? accumulated_value : override[:value]
|
|
636
|
+
evaluation[:variable_override_paths] = applied_paths.dup if program
|
|
637
|
+
break unless program
|
|
638
|
+
end
|
|
639
|
+
next_index = program ? override[matched ? :onMatch : :onMiss] : index + 1
|
|
640
|
+
if program && (!next_index.is_a?(Integer) || next_index <= index || next_index > overrides.length)
|
|
641
|
+
raise ArgumentError, "Invalid variable override program jump"
|
|
642
|
+
end
|
|
643
|
+
index = next_index
|
|
578
644
|
end
|
|
579
645
|
if evaluation[:reason] == Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND
|
|
580
646
|
evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_DEFAULT,
|
|
@@ -582,58 +648,167 @@ module Featurevisor
|
|
|
582
648
|
evaluation[:variable_value] = variable[:defaultValue] if variable.key?(:defaultValue)
|
|
583
649
|
end
|
|
584
650
|
end
|
|
585
|
-
report_diagnostic(level: "warn", code: "
|
|
651
|
+
report_diagnostic(level: "warn", code: "deprecated_variable", message: "Variable \"#{resolved_key}\" is deprecated",
|
|
586
652
|
details: { variableKey: resolved_key, evaluation: evaluation }) if variable[:deprecated]
|
|
587
653
|
end
|
|
588
654
|
|
|
589
655
|
if !evaluation.key?(:variable_value) && evaluation_options.key?(:default_variable_value)
|
|
590
656
|
evaluation[:variable_value] = evaluation_options[:default_variable_value]
|
|
591
657
|
end
|
|
592
|
-
|
|
658
|
+
unless @modules_manager.get_all.empty?
|
|
659
|
+
evaluation = @modules_manager.run_after_evaluation_modules(evaluation, evaluation_options)
|
|
660
|
+
end
|
|
593
661
|
report_diagnostic(level: "debug", code: evaluation[:reason], message: "Global variable evaluated", details: evaluation)
|
|
594
662
|
evaluation
|
|
663
|
+
end
|
|
595
664
|
rescue => e
|
|
665
|
+
raise if e.is_a?(RequirementError) && options[:__featurevisor_prerequisite]
|
|
596
666
|
evaluation = { type: "variable", variable_key: evaluation_options[:variable_key],
|
|
597
667
|
reason: Featurevisor::EvaluationReason::ERROR, error: e }
|
|
668
|
+
session.remember(global_variable_session_options(evaluation_options, options).merge(__featurevisor_prerequisite: true), evaluation) if options[:__featurevisor_prerequisite]
|
|
598
669
|
report_diagnostic(level: "error", code: "evaluation_error", message: "Global variable evaluation failed",
|
|
599
670
|
originalError: e, details: evaluation)
|
|
600
671
|
evaluation
|
|
601
672
|
end
|
|
602
673
|
end
|
|
603
674
|
|
|
604
|
-
def
|
|
605
|
-
|
|
675
|
+
def global_variable_session_options(evaluation_options, options)
|
|
676
|
+
evaluation_options.merge(
|
|
677
|
+
sticky: options[:__featurevisor_sticky_features_source]&.call || options[:__featurevisor_child_sticky_features] || @sticky_features,
|
|
678
|
+
sticky_variables: options[:__featurevisor_sticky_variables_source]&.call || options[:__featurevisor_child_sticky_variables] || @sticky_variables,
|
|
679
|
+
datafile: @datafile,
|
|
680
|
+
__featurevisor_generation: -> { @evaluation_generation },
|
|
681
|
+
__featurevisor_child_generation: options[:__featurevisor_child_generation],
|
|
682
|
+
__featurevisor_project_variable: options[:__featurevisor_project_variable]
|
|
683
|
+
)
|
|
684
|
+
end
|
|
606
685
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
686
|
+
def project_variable_dependency(evaluation)
|
|
687
|
+
return evaluation unless evaluation.key?(:variable_value)
|
|
688
|
+
type = evaluation.dig(:variable_schema, :type) || evaluation.dig(:variable, :type)
|
|
689
|
+
return evaluation unless type == "json" && !evaluation.dig(:variable, :compose) && evaluation[:variable_value].is_a?(String)
|
|
690
|
+
|
|
691
|
+
evaluation.merge(variable_value: JSON.parse(evaluation[:variable_value], symbolize_names: true), __featurevisor_decoded_json: true)
|
|
692
|
+
rescue JSON::ParserError => error
|
|
693
|
+
report_diagnostic(level: "error", code: "evaluation_error", message: "getVariable failed", originalError: error,
|
|
694
|
+
details: { featureKey: evaluation[:feature_key], variableKey: evaluation[:variable_key] })
|
|
695
|
+
evaluation.merge(variable_value: nil, __featurevisor_decoded_json: true)
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def evaluate_required_variable(required, dependencies)
|
|
699
|
+
options = {
|
|
700
|
+
__featurevisor_sticky_features_source: dependencies[:__featurevisor_sticky_features_source],
|
|
701
|
+
__featurevisor_sticky_variables_source: dependencies[:__featurevisor_sticky_variables_source],
|
|
702
|
+
__featurevisor_child_generation: dependencies[:__featurevisor_child_generation],
|
|
703
|
+
__featurevisor_session: dependencies[:__featurevisor_session],
|
|
704
|
+
__featurevisor_prerequisite: true,
|
|
705
|
+
__featurevisor_effective_context: true,
|
|
706
|
+
dependency_purpose: "condition",
|
|
707
|
+
__featurevisor_project_variable: method(:project_variable_dependency)
|
|
708
|
+
}
|
|
709
|
+
if required[:feature]
|
|
710
|
+
get_variable(required[:feature], required[:variable], dependencies[:context], options)
|
|
711
|
+
else
|
|
712
|
+
get_variable(required[:variable], dependencies[:context], options)
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def composition_source(reference, dependencies)
|
|
717
|
+
context = dependencies[:context]
|
|
718
|
+
options = {
|
|
719
|
+
__featurevisor_session: dependencies[:__featurevisor_session],
|
|
720
|
+
__featurevisor_prerequisite: true,
|
|
721
|
+
__featurevisor_effective_context: true,
|
|
722
|
+
__featurevisor_sticky_features_source: dependencies[:__featurevisor_sticky_features_source],
|
|
723
|
+
__featurevisor_sticky_variables_source: dependencies[:__featurevisor_sticky_variables_source],
|
|
724
|
+
__featurevisor_child_generation: dependencies[:__featurevisor_child_generation],
|
|
725
|
+
dependency_purpose: "composition",
|
|
726
|
+
__featurevisor_project_variable: method(:project_variable_dependency)
|
|
727
|
+
}
|
|
728
|
+
if reference.key?(:variable)
|
|
729
|
+
reference.key?(:feature) ? get_variable(reference[:feature], reference[:variable], context, options) : get_variable(reference[:variable], context, options)
|
|
730
|
+
elsif reference[:variation]
|
|
731
|
+
get_variation(reference[:feature], context, options)
|
|
732
|
+
else
|
|
733
|
+
is_enabled(reference[:feature], context, options)
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def compose_value(compose, dependencies)
|
|
738
|
+
return composition_source({ variable: compose }, dependencies) if compose.is_a?(String)
|
|
739
|
+
raise RequirementError, "Invalid composition" unless compose.is_a?(Array) && !compose.empty?
|
|
740
|
+
compose.each_with_object({}) do |reference, result|
|
|
741
|
+
key = reference[:key]
|
|
742
|
+
if !key.is_a?(String) || key.empty? || %w[__proto__ prototype constructor].include?(key)
|
|
743
|
+
raise RequirementError, "Invalid composition property"
|
|
618
744
|
end
|
|
619
|
-
|
|
620
|
-
expected_variation.nil? || get_variation(key, context, clean_options) == expected_variation
|
|
745
|
+
result[key.to_sym] = composition_source(reference, dependencies)
|
|
621
746
|
end
|
|
622
747
|
end
|
|
623
748
|
|
|
749
|
+
def variable_condition_matches?(required, dependencies)
|
|
750
|
+
value = evaluate_required_variable(required, dependencies)
|
|
751
|
+
if required.key?(:path)
|
|
752
|
+
raise ArgumentError, "Variable requirement path must be compiled" unless required[:path].is_a?(Array)
|
|
753
|
+
required[:path].each do |part|
|
|
754
|
+
raise ArgumentError, "Unsafe variable requirement path" if %w[__proto__ prototype constructor].include?(part)
|
|
755
|
+
index = Mutations.send(:array_index, part)
|
|
756
|
+
if part.is_a?(Numeric) && index.nil?
|
|
757
|
+
raise ArgumentError, "Invalid variable requirement path"
|
|
758
|
+
end
|
|
759
|
+
value = if !index.nil?
|
|
760
|
+
value.is_a?(Array) && index < value.length ? value[index] : Conditions.missing_value
|
|
761
|
+
elsif part.is_a?(String)
|
|
762
|
+
if value.is_a?(Hash)
|
|
763
|
+
key = value.key?(part.to_sym) ? part.to_sym : part
|
|
764
|
+
value.key?(key) ? value[key] : Conditions.missing_value
|
|
765
|
+
elsif value.is_a?(Array) && part == "length"
|
|
766
|
+
value.length
|
|
767
|
+
elsif value.is_a?(Array) && part.match?(/\A(?:0|[1-9]\d*)\z/) && part.to_i < value.length
|
|
768
|
+
value[part.to_i]
|
|
769
|
+
else
|
|
770
|
+
Conditions.missing_value
|
|
771
|
+
end
|
|
772
|
+
else
|
|
773
|
+
raise ArgumentError, "Invalid variable requirement path"
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
Conditions.value_is_matched(value, required, ->(pattern, flags) { @datafile.get_regex(pattern, flags) })
|
|
778
|
+
rescue RequirementError
|
|
779
|
+
raise
|
|
780
|
+
rescue => error
|
|
781
|
+
raise RequirementError, error.message
|
|
782
|
+
end
|
|
783
|
+
|
|
624
784
|
# Get evaluation dependencies
|
|
625
785
|
# @param context [Hash] Context
|
|
626
786
|
# @param options [Hash] Override options
|
|
627
787
|
# @return [Hash] Evaluation dependencies
|
|
628
788
|
def get_evaluation_dependencies(context, options = {})
|
|
789
|
+
sticky_source = options[:__featurevisor_sticky_features_source] ||
|
|
790
|
+
-> { options[:__featurevisor_child_sticky_features] || @sticky_features }
|
|
791
|
+
variable_source = options[:__featurevisor_sticky_variables_source] ||
|
|
792
|
+
-> { options[:__featurevisor_child_sticky_variables] || @sticky_variables }
|
|
629
793
|
{
|
|
630
|
-
context: get_context(context),
|
|
794
|
+
context: options[:__featurevisor_effective_context] ? context : get_context(context),
|
|
631
795
|
diagnostics: @diagnostics,
|
|
632
796
|
modules_manager: @modules_manager,
|
|
633
797
|
datafile: @datafile,
|
|
634
|
-
|
|
635
|
-
|
|
798
|
+
__featurevisor_instance: self,
|
|
799
|
+
sticky: sticky_source.call,
|
|
800
|
+
sticky_variables: variable_source.call,
|
|
801
|
+
__featurevisor_project_variable: options[:__featurevisor_project_variable],
|
|
802
|
+
__featurevisor_session: options[:__featurevisor_session],
|
|
803
|
+
__featurevisor_prerequisite: options[:__featurevisor_prerequisite],
|
|
804
|
+
__featurevisor_generation: -> { @evaluation_generation },
|
|
805
|
+
__featurevisor_child_generation: options[:__featurevisor_child_generation],
|
|
806
|
+
__featurevisor_sticky_features_source: sticky_source,
|
|
807
|
+
__featurevisor_sticky_variables_source: variable_source,
|
|
808
|
+
__featurevisor_datafile_source: -> { @datafile },
|
|
636
809
|
}.tap do |dependencies|
|
|
810
|
+
dependencies[:dependency_purpose] = options[:dependency_purpose] if options[:dependency_purpose]
|
|
811
|
+
dependencies[:dependency_of] = options[:__featurevisor_session].dependency_of if options[:__featurevisor_prerequisite] && options[:__featurevisor_session]
|
|
637
812
|
dependencies[:default_variation_value] = options[:default_variation_value] if options.key?(:default_variation_value)
|
|
638
813
|
dependencies[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
|
|
639
814
|
end
|
|
@@ -652,7 +827,7 @@ module Featurevisor
|
|
|
652
827
|
when "integer"
|
|
653
828
|
return value if value.is_a?(Integer)
|
|
654
829
|
value.is_a?(Float) && value.finite? && value == value.to_i ? value.to_i : nil
|
|
655
|
-
when "
|
|
830
|
+
when "number"
|
|
656
831
|
value.is_a?(Numeric) && value.finite? ? value.to_f : nil
|
|
657
832
|
when "boolean"
|
|
658
833
|
value == true || value == false ? value : nil
|
|
@@ -696,6 +871,8 @@ module Featurevisor
|
|
|
696
871
|
{
|
|
697
872
|
get_revision: -> { instance.get_revision },
|
|
698
873
|
on_diagnostic: lambda do |handler, options = {}|
|
|
874
|
+
next -> {} if @closed
|
|
875
|
+
|
|
699
876
|
subscription = {
|
|
700
877
|
id: SecureRandom.uuid,
|
|
701
878
|
module_id: mod.id,
|
|
@@ -714,6 +891,8 @@ module Featurevisor
|
|
|
714
891
|
end
|
|
715
892
|
|
|
716
893
|
def report_diagnostic(diagnostic, source_module = nil)
|
|
894
|
+
return unless diagnostic_interested?((diagnostic || {})[:level] || "info", source_module)
|
|
895
|
+
|
|
717
896
|
diagnostic = (diagnostic || {}).dup
|
|
718
897
|
diagnostic[:level] ||= "info"
|
|
719
898
|
diagnostic[:module] = source_module.name if source_module && source_module.name
|
|
@@ -772,6 +951,26 @@ module Featurevisor
|
|
|
772
951
|
subscriber_index >= diagnostic_index
|
|
773
952
|
end
|
|
774
953
|
|
|
954
|
+
# Check live subscriptions before copying and formatting evaluation details.
|
|
955
|
+
# Errors also reach the error event, independently of diagnostic levels.
|
|
956
|
+
def diagnostic_interested?(level, source_module = nil)
|
|
957
|
+
return true if level == "error"
|
|
958
|
+
|
|
959
|
+
# The default console reporter and explicit handlers have historically
|
|
960
|
+
# handled unrecognized levels differently. Preserve both policies.
|
|
961
|
+
instance_interested = if @on_diagnostic
|
|
962
|
+
should_report_diagnostic?(level, @diagnostics.level)
|
|
963
|
+
else
|
|
964
|
+
LOG_LEVELS.index(level).to_i <= LOG_LEVELS.index(@diagnostics.level).to_i
|
|
965
|
+
end
|
|
966
|
+
instance_interested ||
|
|
967
|
+
@module_diagnostic_subscriptions.any? do |subscription|
|
|
968
|
+
(!source_module || subscription[:module_id] != source_module.id) &&
|
|
969
|
+
should_report_diagnostic?(level, subscription[:level])
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
private :diagnostic_interested?
|
|
973
|
+
|
|
775
974
|
def camelize_diagnostic_value(value)
|
|
776
975
|
case value
|
|
777
976
|
when Hash
|
data/lib/featurevisor/modules.rb
CHANGED
|
@@ -12,11 +12,9 @@ module Featurevisor
|
|
|
12
12
|
@id = SecureRandom.uuid
|
|
13
13
|
@name = options[:name]
|
|
14
14
|
@setup = options[:setup]
|
|
15
|
-
@before = options[:before]
|
|
16
15
|
@before_evaluation = options[:before_evaluation] || options[:beforeEvaluation]
|
|
17
16
|
@bucket_key = options[:bucket_key]
|
|
18
17
|
@bucket_value = options[:bucket_value]
|
|
19
|
-
@after = options[:after]
|
|
20
18
|
@after_evaluation = options[:after_evaluation] || options[:afterEvaluation]
|
|
21
19
|
@close = options[:close]
|
|
22
20
|
end
|
|
@@ -25,12 +23,6 @@ module Featurevisor
|
|
|
25
23
|
@setup.call(api) if @setup
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
def call_before(options)
|
|
29
|
-
return options unless @before
|
|
30
|
-
|
|
31
|
-
@before.call(options)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
26
|
def call_before_evaluation(options)
|
|
35
27
|
return options unless @before_evaluation
|
|
36
28
|
|
|
@@ -49,12 +41,6 @@ module Featurevisor
|
|
|
49
41
|
@bucket_value.call(options)
|
|
50
42
|
end
|
|
51
43
|
|
|
52
|
-
def call_after(evaluation, options)
|
|
53
|
-
return evaluation unless @after
|
|
54
|
-
|
|
55
|
-
@after.call(evaluation, options)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
44
|
def call_after_evaluation(evaluation, options)
|
|
59
45
|
return evaluation unless @after_evaluation
|
|
60
46
|
|
|
@@ -71,6 +57,7 @@ module Featurevisor
|
|
|
71
57
|
|
|
72
58
|
def initialize(options = {})
|
|
73
59
|
@modules = []
|
|
60
|
+
@on_change = options[:on_change]
|
|
74
61
|
@report_diagnostic = options[:report_diagnostic]
|
|
75
62
|
@module_api_factory = options[:module_api_factory]
|
|
76
63
|
@clear_module_diagnostic_subscriptions = options[:clear_module_diagnostic_subscriptions]
|
|
@@ -114,7 +101,9 @@ module Featurevisor
|
|
|
114
101
|
close_module(mod)
|
|
115
102
|
return nil
|
|
116
103
|
end
|
|
117
|
-
|
|
104
|
+
# Replace membership so an active phase keeps its starting list.
|
|
105
|
+
@modules = @modules + [mod]
|
|
106
|
+
@on_change&.call
|
|
118
107
|
|
|
119
108
|
-> { remove(mod) }
|
|
120
109
|
end
|
|
@@ -126,6 +115,7 @@ module Featurevisor
|
|
|
126
115
|
removed << mod if matches
|
|
127
116
|
matches
|
|
128
117
|
end
|
|
118
|
+
@on_change&.call unless removed.empty?
|
|
129
119
|
|
|
130
120
|
removed.each do |mod|
|
|
131
121
|
@clear_module_diagnostic_subscriptions.call(mod) if @clear_module_diagnostic_subscriptions
|
|
@@ -137,15 +127,6 @@ module Featurevisor
|
|
|
137
127
|
@modules
|
|
138
128
|
end
|
|
139
129
|
|
|
140
|
-
def run_before_modules(options)
|
|
141
|
-
result = @modules.reduce(options) do |current, mod|
|
|
142
|
-
mod.call_before(current)
|
|
143
|
-
end
|
|
144
|
-
@modules.reduce(result) do |current, mod|
|
|
145
|
-
mod.call_before_evaluation(current)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
130
|
def run_before_evaluation_modules(options)
|
|
150
131
|
@modules.reduce(options) do |result, mod|
|
|
151
132
|
mod.call_before_evaluation(result)
|
|
@@ -158,28 +139,6 @@ module Featurevisor
|
|
|
158
139
|
end
|
|
159
140
|
end
|
|
160
141
|
|
|
161
|
-
def run_after_modules(evaluation, options)
|
|
162
|
-
result = @modules.reduce(evaluation) do |current, mod|
|
|
163
|
-
mod.call_after_evaluation(current, options)
|
|
164
|
-
end
|
|
165
|
-
@modules.reduce(result) do |current, mod|
|
|
166
|
-
mod.call_after(current, options)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Deprecated feature-only callbacks are intentionally excluded here.
|
|
171
|
-
def run_global_after_modules(evaluation, options)
|
|
172
|
-
@modules.reduce(evaluation) do |result, mod|
|
|
173
|
-
mod.call_after_evaluation(result, options)
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def run_legacy_before_modules(options)
|
|
178
|
-
@modules.reduce(options) do |result, mod|
|
|
179
|
-
mod.call_before(result)
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
142
|
def run_bucket_key_modules(options)
|
|
184
143
|
bucket_key = options[:bucket_key]
|
|
185
144
|
@modules.each do |mod|
|
|
@@ -197,6 +156,7 @@ module Featurevisor
|
|
|
197
156
|
end
|
|
198
157
|
|
|
199
158
|
def close_all
|
|
159
|
+
@on_change&.call unless @modules.empty?
|
|
200
160
|
@modules.each do |mod|
|
|
201
161
|
@clear_module_diagnostic_subscriptions.call(mod) if @clear_module_diagnostic_subscriptions
|
|
202
162
|
close_module(mod)
|