featurevisor 3.1.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 +300 -182
- 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 +735 -260
- 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
|
@@ -6,8 +6,6 @@ module Featurevisor
|
|
|
6
6
|
# Feature specific
|
|
7
7
|
FEATURE_NOT_FOUND = "feature_not_found" # feature is not found in datafile
|
|
8
8
|
DISABLED = "disabled" # feature is disabled
|
|
9
|
-
REQUIRED = "required" # required features are not enabled
|
|
10
|
-
OUT_OF_RANGE = "out_of_range" # out of range when mutually exclusive experiments are involved via Groups
|
|
11
9
|
|
|
12
10
|
# Variations specific
|
|
13
11
|
NO_VARIATIONS = "no_variations" # feature has no variations
|
|
@@ -19,7 +17,8 @@ module Featurevisor
|
|
|
19
17
|
VARIABLE_DISABLED = "variable_disabled" # feature is disabled, and variable's disabledValue is used
|
|
20
18
|
VARIABLE_OVERRIDE_VARIATION = "variable_override_variation" # variable overridden from inside a variation
|
|
21
19
|
VARIABLE_OVERRIDE_RULE = "variable_override_rule" # variable overridden from inside a rule
|
|
22
|
-
|
|
20
|
+
CONDITIONS_UNMET = "conditions_unmet"
|
|
21
|
+
VARIABLE_COMPOSED = "variable_composed"
|
|
23
22
|
|
|
24
23
|
# Common
|
|
25
24
|
NO_MATCH = "no_match" # no rules matched
|
|
@@ -36,49 +35,8 @@ module Featurevisor
|
|
|
36
35
|
|
|
37
36
|
# Evaluation module for feature flag evaluation
|
|
38
37
|
module Evaluate
|
|
39
|
-
def self.required_features_are_matched(requirements, datafile, options)
|
|
40
|
-
return true if requirements.nil?
|
|
41
|
-
|
|
42
|
-
items = requirements.is_a?(Array) ? requirements : [requirements]
|
|
43
|
-
clean_options = options.reject do |key, _|
|
|
44
|
-
%i[feature_key variable_key default_variation_value default_variable_value].include?(key)
|
|
45
|
-
end
|
|
46
|
-
items.all? do |required|
|
|
47
|
-
if required.is_a?(String)
|
|
48
|
-
key = required
|
|
49
|
-
enabled = true
|
|
50
|
-
variation = nil
|
|
51
|
-
elsif required.key?(:feature)
|
|
52
|
-
key = required[:feature]
|
|
53
|
-
enabled = required.key?(:enabled) ? required[:enabled] : true
|
|
54
|
-
variation = required[:variation]
|
|
55
|
-
else
|
|
56
|
-
key = required[:key]
|
|
57
|
-
enabled = true
|
|
58
|
-
variation = required[:variation]
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
flag = evaluate_with_modules(clean_options.merge(type: "flag", feature_key: key, datafile: datafile))
|
|
62
|
-
next false unless (flag[:enabled] == true) == enabled
|
|
63
|
-
next true if variation.nil?
|
|
64
|
-
|
|
65
|
-
evaluated_variation = evaluate_with_modules(clean_options.merge(type: "variation", feature_key: key, datafile: datafile))
|
|
66
|
-
value = evaluated_variation.key?(:variation_value) ? evaluated_variation[:variation_value] : evaluated_variation.dig(:variation, :value)
|
|
67
|
-
value == variation
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
38
|
def self.variable_override_matches?(override, datafile, context, options)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
conditions_match = !override[:conditions] || datafile.all_conditions_are_matched(
|
|
75
|
-
datafile.parse_conditions_if_stringified(override[:conditions]), context
|
|
76
|
-
)
|
|
77
|
-
segments_match = !override[:segments] || datafile.all_segments_are_matched(
|
|
78
|
-
datafile.parse_segments_if_stringified(override[:segments]), context
|
|
79
|
-
)
|
|
80
|
-
conditions_match && segments_match &&
|
|
81
|
-
(override.key?(:conditions) || override.key?(:segments) || override.key?(:requiredFeatures))
|
|
39
|
+
override.key?(:conditions) && UnifiedConditions.matches?(override[:conditions], options)
|
|
82
40
|
end
|
|
83
41
|
|
|
84
42
|
# Evaluate with modules
|
|
@@ -86,13 +44,36 @@ module Featurevisor
|
|
|
86
44
|
# @return [Hash] Evaluation result
|
|
87
45
|
def self.evaluate_with_modules(options)
|
|
88
46
|
begin
|
|
47
|
+
session = options[:__featurevisor_session] || EvaluationSession.new
|
|
48
|
+
options = options.merge(__featurevisor_session: session)
|
|
49
|
+
if options[:__featurevisor_prerequisite]
|
|
50
|
+
options[:sticky] = options[:__featurevisor_sticky_features_source].call if options[:__featurevisor_sticky_features_source]
|
|
51
|
+
options[:sticky_variables] = options[:__featurevisor_sticky_variables_source].call if options[:__featurevisor_sticky_variables_source]
|
|
52
|
+
options[:datafile] = options[:__featurevisor_datafile_source].call if options[:__featurevisor_datafile_source]
|
|
53
|
+
end
|
|
54
|
+
options[:dependency_of] = session.dependency_of if options[:__featurevisor_prerequisite]
|
|
55
|
+
options[:dependency_purpose] ||= "condition" if options[:__featurevisor_prerequisite]
|
|
56
|
+
return session.run(options, prerequisite: options[:__featurevisor_prerequisite]) do
|
|
89
57
|
modules_manager = options[:modules_manager]
|
|
90
|
-
|
|
91
|
-
#
|
|
58
|
+
# The usual read has no modules. Keep its evaluation options intact so
|
|
59
|
+
# it does not allocate two filtered hashes and a merged hash.
|
|
92
60
|
result_options = options
|
|
93
|
-
if modules_manager
|
|
94
|
-
|
|
61
|
+
if modules_manager && !modules_manager.get_all.empty?
|
|
62
|
+
private_options = options.select { |key, _| private_evaluation_option?(key) }
|
|
63
|
+
public_options = options.reject { |key, _| private_evaluation_option?(key) }
|
|
64
|
+
result_options = modules_manager.run_before_evaluation_modules(public_options)
|
|
65
|
+
result_options = result_options.reject { |key, _| private_evaluation_option?(key) }.merge(private_options)
|
|
95
66
|
end
|
|
67
|
+
{ sticky: :__featurevisor_sticky_features_source,
|
|
68
|
+
sticky_variables: :__featurevisor_sticky_variables_source }.each do |field, source|
|
|
69
|
+
if result_options[field].equal?(options[field])
|
|
70
|
+
result_options[field] = options[source].call if options[source]
|
|
71
|
+
else
|
|
72
|
+
replacement = result_options[field]
|
|
73
|
+
result_options[source] = -> { replacement }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
session.update_current(result_options)
|
|
96
77
|
|
|
97
78
|
# Evaluate
|
|
98
79
|
evaluation = evaluate(result_options)
|
|
@@ -113,12 +94,18 @@ module Featurevisor
|
|
|
113
94
|
end
|
|
114
95
|
|
|
115
96
|
# Run after modules
|
|
116
|
-
|
|
117
|
-
|
|
97
|
+
# A callback during evaluation may add a module. In that case its
|
|
98
|
+
# after hook still receives only public options.
|
|
99
|
+
if modules_manager && !modules_manager.get_all.empty?
|
|
100
|
+
evaluation = modules_manager.run_after_evaluation_modules(
|
|
101
|
+
evaluation, result_options.reject { |key, _| private_evaluation_option?(key) }
|
|
102
|
+
)
|
|
118
103
|
end
|
|
119
104
|
|
|
120
105
|
evaluation
|
|
106
|
+
end
|
|
121
107
|
rescue => e
|
|
108
|
+
raise if e.is_a?(RequirementError) && options[:__featurevisor_prerequisite]
|
|
122
109
|
type = options[:type]
|
|
123
110
|
feature_key = options[:feature_key]
|
|
124
111
|
variable_key = options[:variable_key]
|
|
@@ -132,12 +119,18 @@ module Featurevisor
|
|
|
132
119
|
error: e
|
|
133
120
|
}
|
|
134
121
|
|
|
122
|
+
session.remember(options, evaluation) if session
|
|
123
|
+
|
|
135
124
|
diagnostics.error("Error during evaluation", evaluation)
|
|
136
125
|
|
|
137
126
|
evaluation
|
|
138
127
|
end
|
|
139
128
|
end
|
|
140
129
|
|
|
130
|
+
def self.private_evaluation_option?(key)
|
|
131
|
+
key.to_s.start_with?("__featurevisor_")
|
|
132
|
+
end
|
|
133
|
+
|
|
141
134
|
# Main evaluation function
|
|
142
135
|
# @param options [Hash] Evaluation options
|
|
143
136
|
# @return [Hash] Evaluation result
|
|
@@ -228,7 +221,7 @@ module Featurevisor
|
|
|
228
221
|
type: type,
|
|
229
222
|
feature_key: feature_key,
|
|
230
223
|
reason: Featurevisor::EvaluationReason::STICKY,
|
|
231
|
-
|
|
224
|
+
sticky_feature: sticky_feature,
|
|
232
225
|
enabled: sticky_feature[:enabled]
|
|
233
226
|
}
|
|
234
227
|
|
|
@@ -341,7 +334,7 @@ module Featurevisor
|
|
|
341
334
|
end
|
|
342
335
|
|
|
343
336
|
# Forced
|
|
344
|
-
force_result = datafile.get_matched_force(feature, context)
|
|
337
|
+
force_result = datafile.get_matched_force(feature, context, options)
|
|
345
338
|
force = force_result[:force]
|
|
346
339
|
force_index = force_result[:forceIndex]
|
|
347
340
|
|
|
@@ -402,25 +395,17 @@ module Featurevisor
|
|
|
402
395
|
end
|
|
403
396
|
end
|
|
404
397
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
required_features: feature[:requiredFeatures],
|
|
417
|
-
enabled: required_features_are_enabled
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
diagnostics.debug("required features not enabled", evaluation)
|
|
421
|
-
|
|
422
|
-
return evaluation
|
|
423
|
-
end
|
|
398
|
+
if type == "flag" && feature.key?(:conditions) &&
|
|
399
|
+
!UnifiedConditions.matches?(feature[:conditions], options)
|
|
400
|
+
evaluation = {
|
|
401
|
+
type: type,
|
|
402
|
+
feature_key: feature_key,
|
|
403
|
+
reason: Featurevisor::EvaluationReason::CONDITIONS_UNMET,
|
|
404
|
+
conditions: feature[:conditions],
|
|
405
|
+
enabled: false
|
|
406
|
+
}
|
|
407
|
+
diagnostics.debug("conditions did not match", evaluation)
|
|
408
|
+
return evaluation
|
|
424
409
|
end
|
|
425
410
|
|
|
426
411
|
# Bucketing
|
|
@@ -455,13 +440,13 @@ module Featurevisor
|
|
|
455
440
|
matched_allocation = nil
|
|
456
441
|
|
|
457
442
|
if type != "flag"
|
|
458
|
-
matched_traffic = datafile.get_matched_traffic(feature[:traffic], context)
|
|
443
|
+
matched_traffic = datafile.get_matched_traffic(feature[:traffic], context, options)
|
|
459
444
|
|
|
460
445
|
if matched_traffic
|
|
461
446
|
matched_allocation = datafile.get_matched_allocation(matched_traffic, bucket_value)
|
|
462
447
|
end
|
|
463
448
|
else
|
|
464
|
-
matched_traffic = datafile.get_matched_traffic(feature[:traffic], context)
|
|
449
|
+
matched_traffic = datafile.get_matched_traffic(feature[:traffic], context, options)
|
|
465
450
|
end
|
|
466
451
|
|
|
467
452
|
if matched_traffic
|
|
@@ -485,45 +470,6 @@ module Featurevisor
|
|
|
485
470
|
|
|
486
471
|
# flag
|
|
487
472
|
if type == "flag"
|
|
488
|
-
# flag: check if mutually exclusive
|
|
489
|
-
if feature[:ranges] && feature[:ranges].length > 0
|
|
490
|
-
matched_range = feature[:ranges].find do |range|
|
|
491
|
-
bucket_value >= range[0] && bucket_value < range[1]
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
# matched
|
|
495
|
-
if matched_range
|
|
496
|
-
evaluation = {
|
|
497
|
-
type: type,
|
|
498
|
-
feature_key: feature_key,
|
|
499
|
-
reason: Featurevisor::EvaluationReason::ALLOCATED,
|
|
500
|
-
bucket_key: bucket_key,
|
|
501
|
-
bucket_value: bucket_value,
|
|
502
|
-
rule_key: matched_traffic[:key],
|
|
503
|
-
traffic: matched_traffic,
|
|
504
|
-
enabled: matched_traffic[:enabled].nil? ? true : matched_traffic[:enabled]
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
diagnostics.debug("matched", evaluation)
|
|
508
|
-
|
|
509
|
-
return evaluation
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
# no match
|
|
513
|
-
evaluation = {
|
|
514
|
-
type: type,
|
|
515
|
-
feature_key: feature_key,
|
|
516
|
-
reason: Featurevisor::EvaluationReason::OUT_OF_RANGE,
|
|
517
|
-
bucket_key: bucket_key,
|
|
518
|
-
bucket_value: bucket_value,
|
|
519
|
-
enabled: false
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
diagnostics.debug("not matched", evaluation)
|
|
523
|
-
|
|
524
|
-
return evaluation
|
|
525
|
-
end
|
|
526
|
-
|
|
527
473
|
# flag: override from rule
|
|
528
474
|
if matched_traffic.key?(:enabled)
|
|
529
475
|
evaluation = {
|
|
@@ -634,7 +580,8 @@ module Featurevisor
|
|
|
634
580
|
variable_schema: variable_schema,
|
|
635
581
|
variable_value: override[:value],
|
|
636
582
|
variable_override_index: override_index,
|
|
637
|
-
variable_override_key: override[:key]
|
|
583
|
+
variable_override_key: override[:key],
|
|
584
|
+
variable_override_path: override[:keyPath]
|
|
638
585
|
}
|
|
639
586
|
|
|
640
587
|
diagnostics.debug("variable override from rule", evaluation)
|
|
@@ -698,7 +645,8 @@ module Featurevisor
|
|
|
698
645
|
variable_schema: variable_schema,
|
|
699
646
|
variable_value: override[:value],
|
|
700
647
|
variable_override_index: override_index,
|
|
701
|
-
variable_override_key: override[:key]
|
|
648
|
+
variable_override_key: override[:key],
|
|
649
|
+
variable_override_path: override[:keyPath]
|
|
702
650
|
}
|
|
703
651
|
|
|
704
652
|
diagnostics.debug("variable override from variation", evaluation)
|
|
@@ -791,6 +739,7 @@ module Featurevisor
|
|
|
791
739
|
|
|
792
740
|
evaluation
|
|
793
741
|
rescue => e
|
|
742
|
+
raise if e.is_a?(RequirementError)
|
|
794
743
|
evaluation = {
|
|
795
744
|
type: type,
|
|
796
745
|
feature_key: feature_key,
|
|
@@ -5,7 +5,8 @@ require "json"
|
|
|
5
5
|
module Featurevisor
|
|
6
6
|
# Private datafile and matching adapter used by the evaluator.
|
|
7
7
|
class InstanceEvaluationDataProvider
|
|
8
|
-
attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :variables, :diagnostics, :regex_cache
|
|
8
|
+
attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :variables, :diagnostics, :regex_cache, :normalized
|
|
9
|
+
attr_accessor :dependency_index
|
|
9
10
|
|
|
10
11
|
# Initialize a new evaluation data provider.
|
|
11
12
|
# @param options [Hash] Options hash containing datafile and diagnostics
|
|
@@ -21,38 +22,14 @@ module Featurevisor
|
|
|
21
22
|
@segments = (datafile[:segments] || {}).transform_keys(&:to_sym)
|
|
22
23
|
@features = (datafile[:features] || {}).transform_keys(&:to_sym)
|
|
23
24
|
@variables = (datafile[:variables] || {}).transform_keys(&:to_sym)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@
|
|
27
|
-
if feature[:variablesSchema]
|
|
28
|
-
feature[:variablesSchema] = feature[:variablesSchema].transform_keys(&:to_sym)
|
|
29
|
-
end
|
|
30
|
-
if feature[:variations]
|
|
31
|
-
feature[:variations].each do |variation|
|
|
32
|
-
if variation[:variables]
|
|
33
|
-
variation[:variables] = variation[:variables].transform_keys(&:to_sym)
|
|
34
|
-
end
|
|
35
|
-
if variation[:variableOverrides]
|
|
36
|
-
variation[:variableOverrides] = variation[:variableOverrides].transform_keys(&:to_sym)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
if feature[:force]
|
|
41
|
-
feature[:force].each do |force_rule|
|
|
42
|
-
if force_rule[:variables]
|
|
43
|
-
force_rule[:variables] = force_rule[:variables].transform_keys(&:to_sym)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
if feature[:traffic]
|
|
48
|
-
feature[:traffic].each do |traffic_rule|
|
|
49
|
-
if traffic_rule[:variables]
|
|
50
|
-
traffic_rule[:variables] = traffic_rule[:variables].transform_keys(&:to_sym)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
25
|
+
@normalized = { feature: {}, segment: {}, variable: {} }
|
|
26
|
+
@normalization_slots = { feature: {}, segment: {}, variable: {} }
|
|
27
|
+
@normalization_mutex = Mutex.new
|
|
55
28
|
@regex_cache = {}
|
|
29
|
+
@conditions_cache = {}
|
|
30
|
+
@segments_cache = {}
|
|
31
|
+
@attribute_conditions_cache = {}
|
|
32
|
+
@get_regex = method(:get_regex)
|
|
56
33
|
end
|
|
57
34
|
|
|
58
35
|
# Get the revision of the datafile
|
|
@@ -82,12 +59,7 @@ module Featurevisor
|
|
|
82
59
|
# @param segment_key [String] Segment key to retrieve
|
|
83
60
|
# @return [Hash, nil] Segment data or nil if not found
|
|
84
61
|
def get_segment(segment_key)
|
|
85
|
-
segment
|
|
86
|
-
|
|
87
|
-
return nil unless segment
|
|
88
|
-
|
|
89
|
-
segment[:conditions] = parse_conditions_if_stringified(segment[:conditions])
|
|
90
|
-
segment
|
|
62
|
+
read_entity(:segment, segment_key, @segments)
|
|
91
63
|
end
|
|
92
64
|
|
|
93
65
|
def get_segment_keys
|
|
@@ -104,7 +76,7 @@ module Featurevisor
|
|
|
104
76
|
# @param feature_key [String] Feature key to retrieve
|
|
105
77
|
# @return [Hash, nil] Feature data or nil if not found
|
|
106
78
|
def get_feature(feature_key)
|
|
107
|
-
|
|
79
|
+
read_entity(:feature, feature_key, @features)
|
|
108
80
|
end
|
|
109
81
|
|
|
110
82
|
# Get variable keys for a feature
|
|
@@ -121,7 +93,60 @@ module Featurevisor
|
|
|
121
93
|
end
|
|
122
94
|
|
|
123
95
|
def get_global_variable(variable_key)
|
|
124
|
-
|
|
96
|
+
read_entity(:variable, variable_key, @variables)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def attribute_only_conditions?(conditions)
|
|
100
|
+
key = conditions.object_id
|
|
101
|
+
cached = @attribute_conditions_cache[key]
|
|
102
|
+
return cached[1] if cached
|
|
103
|
+
|
|
104
|
+
# Retain the syntax with its identity. Cache classification, never matches.
|
|
105
|
+
@attribute_conditions_cache[key] = [conditions, UnifiedConditions.send(:attribute_only?, conditions)]
|
|
106
|
+
@attribute_conditions_cache[key][1]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def retain_normalized(previous, incoming)
|
|
110
|
+
@normalization_mutex, @normalization_slots, @normalized = previous.normalization_snapshot
|
|
111
|
+
{ feature: :features, segment: :segments, variable: :variables }.each do |kind, name|
|
|
112
|
+
(incoming[name] || {}).each_key do |key|
|
|
113
|
+
@normalization_slots[kind][key.to_sym] = []
|
|
114
|
+
@normalized[kind].delete(key.to_sym)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Share individual cold slots, never a chain of previous providers.
|
|
120
|
+
def normalization_snapshot
|
|
121
|
+
@normalization_mutex.synchronize do
|
|
122
|
+
{ feature: @features, segment: @segments, variable: @variables }.each do |kind, definitions|
|
|
123
|
+
next if @normalization_slots[kind].size == definitions.size
|
|
124
|
+
definitions.each_key { |key| @normalization_slots[kind][key] ||= [] }
|
|
125
|
+
end
|
|
126
|
+
[@normalization_mutex, @normalization_slots.transform_values(&:dup), @normalized.transform_values(&:dup)]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def read_entity(kind, key, definitions)
|
|
131
|
+
name = key.to_sym
|
|
132
|
+
cached = @normalized[kind][name]
|
|
133
|
+
return cached if cached
|
|
134
|
+
source = definitions[name] || definitions[key]
|
|
135
|
+
return nil unless source
|
|
136
|
+
errors = []
|
|
137
|
+
value = @normalization_mutex.synchronize do
|
|
138
|
+
slot = @normalization_slots[kind][name] ||= []
|
|
139
|
+
if slot.empty?
|
|
140
|
+
normalized, errors = Normalize.entity(kind, name, source)
|
|
141
|
+
slot << normalized
|
|
142
|
+
end
|
|
143
|
+
@normalized[kind][name] = slot.first
|
|
144
|
+
end
|
|
145
|
+
# Publication precedes diagnostics, and application callbacks never hold the mutex.
|
|
146
|
+
errors.each do |error|
|
|
147
|
+
@diagnostics.error(error[:message], { code: error[:code], error: error[:original_error], details: error[:details] })
|
|
148
|
+
end
|
|
149
|
+
value
|
|
125
150
|
end
|
|
126
151
|
|
|
127
152
|
# Check if a feature has variations
|
|
@@ -153,7 +178,7 @@ module Featurevisor
|
|
|
153
178
|
invalid_flags = flags.delete("gimsuy")
|
|
154
179
|
raise ArgumentError, "invalid regular expression flags: #{invalid_flags}" unless invalid_flags.empty?
|
|
155
180
|
|
|
156
|
-
regex = Regexp.new(regex_string, ruby_flags)
|
|
181
|
+
regex = Regexp.new(Conditions.normalize_portable_regex(regex_string), ruby_flags)
|
|
157
182
|
@regex_cache[cache_key] = regex
|
|
158
183
|
@regex_cache[cache_key]
|
|
159
184
|
end
|
|
@@ -168,11 +193,9 @@ module Featurevisor
|
|
|
168
193
|
return false
|
|
169
194
|
end
|
|
170
195
|
|
|
171
|
-
get_regex_proc = ->(regex_string, regex_flags) { get_regex(regex_string, regex_flags) }
|
|
172
|
-
|
|
173
196
|
if conditions.is_a?(Hash) && (conditions[:attribute] || conditions["attribute"])
|
|
174
197
|
begin
|
|
175
|
-
result = Conditions.condition_is_matched(conditions, context,
|
|
198
|
+
result = Conditions.condition_is_matched(conditions, context, @get_regex)
|
|
176
199
|
return result
|
|
177
200
|
rescue => e
|
|
178
201
|
@diagnostics.warn(e.message, {
|
|
@@ -186,11 +209,11 @@ module Featurevisor
|
|
|
186
209
|
end
|
|
187
210
|
|
|
188
211
|
if conditions.is_a?(Hash) && conditions[:and] && conditions[:and].is_a?(Array)
|
|
189
|
-
return conditions[:and].all? { |c| all_conditions_are_matched(c, context) }
|
|
212
|
+
return !conditions[:and].empty? && conditions[:and].all? { |c| all_conditions_are_matched(c, context) }
|
|
190
213
|
end
|
|
191
214
|
|
|
192
215
|
if conditions.is_a?(Hash) && conditions["and"] && conditions["and"].is_a?(Array)
|
|
193
|
-
return conditions["and"].all? { |c| all_conditions_are_matched(c, context) }
|
|
216
|
+
return !conditions["and"].empty? && conditions["and"].all? { |c| all_conditions_are_matched(c, context) }
|
|
194
217
|
end
|
|
195
218
|
|
|
196
219
|
if conditions.is_a?(Hash) && conditions[:or] && conditions[:or].is_a?(Array)
|
|
@@ -214,7 +237,7 @@ module Featurevisor
|
|
|
214
237
|
|
|
215
238
|
|
|
216
239
|
if conditions.is_a?(Array)
|
|
217
|
-
return conditions.all? { |c| all_conditions_are_matched(c, context) }
|
|
240
|
+
return !conditions.empty? && conditions.all? { |c| all_conditions_are_matched(c, context) }
|
|
218
241
|
end
|
|
219
242
|
|
|
220
243
|
false
|
|
@@ -288,8 +311,9 @@ module Featurevisor
|
|
|
288
311
|
# @param traffic [Array<Hash>] Traffic array to search
|
|
289
312
|
# @param context [Hash] Context to evaluate against
|
|
290
313
|
# @return [Hash, nil] Matched traffic or nil
|
|
291
|
-
def get_matched_traffic(traffic, context)
|
|
314
|
+
def get_matched_traffic(traffic, context, options = nil)
|
|
292
315
|
traffic.find do |t|
|
|
316
|
+
next t.key?(:conditions) && UnifiedConditions.matches?(t[:conditions], options) if options
|
|
293
317
|
segments = parse_segments_if_stringified(t[:segments])
|
|
294
318
|
matched = all_segments_are_matched(segments, context)
|
|
295
319
|
next false unless matched
|
|
@@ -314,7 +338,7 @@ module Featurevisor
|
|
|
314
338
|
# @param feature_key [String, Hash] Feature key or feature object
|
|
315
339
|
# @param context [Hash] Context to evaluate against
|
|
316
340
|
# @return [Hash] Force result with force and forceIndex
|
|
317
|
-
def get_matched_force(feature_key, context)
|
|
341
|
+
def get_matched_force(feature_key, context, options = nil)
|
|
318
342
|
result = {
|
|
319
343
|
force: nil,
|
|
320
344
|
forceIndex: nil
|
|
@@ -325,6 +349,14 @@ module Featurevisor
|
|
|
325
349
|
return result unless feature && feature[:force]
|
|
326
350
|
|
|
327
351
|
feature[:force].each_with_index do |current_force, i|
|
|
352
|
+
if options
|
|
353
|
+
if current_force.key?(:conditions) && UnifiedConditions.matches?(current_force[:conditions], options)
|
|
354
|
+
result[:force] = current_force
|
|
355
|
+
result[:forceIndex] = i
|
|
356
|
+
break
|
|
357
|
+
end
|
|
358
|
+
next
|
|
359
|
+
end
|
|
328
360
|
if current_force[:conditions] && all_conditions_are_matched(
|
|
329
361
|
parse_conditions_if_stringified(current_force[:conditions]), context
|
|
330
362
|
)
|
|
@@ -352,9 +384,10 @@ module Featurevisor
|
|
|
352
384
|
return conditions unless conditions.is_a?(String)
|
|
353
385
|
|
|
354
386
|
return conditions if conditions == "*"
|
|
387
|
+
return @conditions_cache[conditions] if @conditions_cache.key?(conditions)
|
|
355
388
|
|
|
356
389
|
begin
|
|
357
|
-
JSON.parse(conditions)
|
|
390
|
+
@conditions_cache[conditions] = JSON.parse(conditions)
|
|
358
391
|
rescue => e
|
|
359
392
|
@diagnostics.error("Error parsing conditions", {
|
|
360
393
|
error: e,
|
|
@@ -371,11 +404,27 @@ module Featurevisor
|
|
|
371
404
|
# @return [Array, Hash, String] Parsed segments
|
|
372
405
|
def parse_segments_if_stringified(segments)
|
|
373
406
|
if segments.is_a?(String) && (segments.start_with?("{") || segments.start_with?("["))
|
|
374
|
-
return
|
|
407
|
+
return @segments_cache[segments] if @segments_cache.key?(segments)
|
|
408
|
+
|
|
409
|
+
return @segments_cache[segments] = JSON.parse(segments)
|
|
375
410
|
end
|
|
376
411
|
|
|
377
412
|
segments
|
|
378
413
|
end
|
|
414
|
+
|
|
415
|
+
# Cache parsed expressions, never context dependent matching results.
|
|
416
|
+
# A new provider owns a new generation after each datafile update.
|
|
417
|
+
def clear_caches
|
|
418
|
+
@dependency_index = nil
|
|
419
|
+
@normalization_mutex.synchronize do
|
|
420
|
+
@normalized.each_value(&:clear)
|
|
421
|
+
@normalization_slots.each_value(&:clear)
|
|
422
|
+
end
|
|
423
|
+
@regex_cache.clear
|
|
424
|
+
@conditions_cache.clear
|
|
425
|
+
@segments_cache.clear
|
|
426
|
+
@attribute_conditions_cache.clear
|
|
427
|
+
end
|
|
379
428
|
end
|
|
380
429
|
|
|
381
430
|
private_constant :InstanceEvaluationDataProvider
|