featurevisor 0.2.0 → 1.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.
@@ -5,7 +5,7 @@ require "json"
5
5
  module Featurevisor
6
6
  # DatafileReader class for reading and processing Featurevisor datafiles
7
7
  class DatafileReader
8
- attr_reader :schema_version, :revision, :segments, :features, :logger, :regex_cache
8
+ attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :logger, :regex_cache
9
9
 
10
10
  # Initialize a new DatafileReader
11
11
  # @param options [Hash] Options hash containing datafile and logger
@@ -17,6 +17,7 @@ module Featurevisor
17
17
 
18
18
  @schema_version = datafile[:schemaVersion]
19
19
  @revision = datafile[:revision]
20
+ @featurevisor_version = datafile[:featurevisorVersion]
20
21
  @segments = (datafile[:segments] || {}).transform_keys(&:to_sym)
21
22
  @features = (datafile[:features] || {}).transform_keys(&:to_sym)
22
23
 
@@ -65,6 +66,16 @@ module Featurevisor
65
66
  @schema_version
66
67
  end
67
68
 
69
+ def get_datafile
70
+ {
71
+ schemaVersion: @schema_version,
72
+ revision: @revision,
73
+ featurevisorVersion: @featurevisor_version,
74
+ segments: @segments,
75
+ features: @features
76
+ }.compact
77
+ end
78
+
68
79
  # Get a segment by key
69
80
  # @param segment_key [String] Segment key to retrieve
70
81
  # @return [Hash, nil] Segment data or nil if not found
@@ -172,15 +183,11 @@ module Featurevisor
172
183
  end
173
184
 
174
185
  if conditions.is_a?(Hash) && conditions[:not] && conditions[:not].is_a?(Array)
175
- return conditions[:not].all? do
176
- all_conditions_are_matched({ and: conditions[:not] }, context) == false
177
- end
186
+ return !all_conditions_are_matched({ and: conditions[:not] }, context)
178
187
  end
179
188
 
180
189
  if conditions.is_a?(Hash) && conditions["not"] && conditions["not"].is_a?(Array)
181
- return conditions["not"].all? do
182
- all_conditions_are_matched({ "and" => conditions["not"] }, context) == false
183
- end
190
+ return !all_conditions_are_matched({ "and" => conditions["not"] }, context)
184
191
  end
185
192
 
186
193
 
@@ -237,15 +244,11 @@ module Featurevisor
237
244
  end
238
245
 
239
246
  if group_segments[:not] && group_segments[:not].is_a?(Array)
240
- return group_segments[:not].all? do
241
- all_segments_are_matched({ and: group_segments[:not] }, context) == false
242
- end
247
+ return !all_segments_are_matched({ and: group_segments[:not] }, context)
243
248
  end
244
249
 
245
250
  if group_segments["not"] && group_segments["not"].is_a?(Array)
246
- return group_segments["not"].all? do
247
- all_segments_are_matched({ "and" => group_segments["not"] }, context) == false
248
- end
251
+ return !all_segments_are_matched({ "and" => group_segments["not"] }, context)
249
252
  end
250
253
 
251
254
 
@@ -351,4 +354,6 @@ module Featurevisor
351
354
  segments
352
355
  end
353
356
  end
357
+
358
+ private_constant :DatafileReader
354
359
  end
@@ -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 sticky_set].freeze
5
+ EVENT_NAMES = %w[datafile_set context_set sticky_set error].freeze
6
6
 
7
7
  # Event emitter class for handling event subscriptions and triggers
8
8
  class Emitter
@@ -42,7 +42,7 @@ module Featurevisor
42
42
 
43
43
  return unless listeners
44
44
 
45
- listeners.each do |listener|
45
+ listeners.dup.each do |listener|
46
46
  begin
47
47
  listener.call(details)
48
48
  rescue => err
@@ -17,7 +17,8 @@ module Featurevisor
17
17
  VARIABLE_NOT_FOUND = "variable_not_found" # variable's schema is not defined in the feature
18
18
  VARIABLE_DEFAULT = "variable_default" # default variable value used
19
19
  VARIABLE_DISABLED = "variable_disabled" # feature is disabled, and variable's disabledValue is used
20
- VARIABLE_OVERRIDE = "variable_override" # variable overridden from inside a variation
20
+ VARIABLE_OVERRIDE_VARIATION = "variable_override_variation" # variable overridden from inside a variation
21
+ VARIABLE_OVERRIDE_RULE = "variable_override_rule" # variable overridden from inside a rule
21
22
 
22
23
  # Common
23
24
  NO_MATCH = "no_match" # no rules matched
@@ -35,44 +36,39 @@ module Featurevisor
35
36
  # Evaluation module for feature flag evaluation
36
37
  module Evaluate
37
38
 
38
- # Evaluate with hooks
39
+ # Evaluate with modules
39
40
  # @param options [Hash] Evaluation options
40
41
  # @return [Hash] Evaluation result
41
- def self.evaluate_with_hooks(options)
42
+ def self.evaluate_with_modules(options)
42
43
  begin
43
- hooks_manager = options[:hooks_manager]
44
- hooks = hooks_manager.get_all
44
+ modules_manager = options[:modules_manager]
45
45
 
46
- # Run before hooks
46
+ # Run before modules
47
47
  result_options = options
48
- hooks.each do |hook|
49
- if hook.respond_to?(:call_before)
50
- result_options = hook.call_before(result_options)
51
- end
48
+ if modules_manager
49
+ result_options = modules_manager.run_before_modules(result_options)
52
50
  end
53
51
 
54
52
  # Evaluate
55
53
  evaluation = evaluate(result_options)
56
54
 
57
55
  # Default: variation
58
- if options[:default_variation_value] &&
56
+ if options.key?(:default_variation_value) &&
59
57
  evaluation[:type] == "variation" &&
60
58
  evaluation[:variation_value].nil?
61
59
  evaluation[:variation_value] = options[:default_variation_value]
62
60
  end
63
61
 
64
62
  # Default: variable
65
- if options[:default_variable_value] &&
63
+ if options.key?(:default_variable_value) &&
66
64
  evaluation[:type] == "variable" &&
67
65
  evaluation[:variable_value].nil?
68
66
  evaluation[:variable_value] = options[:default_variable_value]
69
67
  end
70
68
 
71
- # Run after hooks
72
- hooks.each do |hook|
73
- if hook.respond_to?(:call_after)
74
- evaluation = hook.call_after(evaluation, result_options)
75
- end
69
+ # Run after modules
70
+ if modules_manager
71
+ evaluation = modules_manager.run_after_modules(evaluation, result_options)
76
72
  end
77
73
 
78
74
  evaluation
@@ -107,9 +103,7 @@ module Featurevisor
107
103
  logger = options[:logger]
108
104
  datafile_reader = options[:datafile_reader]
109
105
  sticky = options[:sticky]
110
- hooks_manager = options[:hooks_manager]
111
-
112
- hooks = hooks_manager.get_all
106
+ modules_manager = options[:modules_manager]
113
107
  evaluation = nil
114
108
 
115
109
  begin
@@ -132,10 +126,10 @@ module Featurevisor
132
126
  if type == "variable"
133
127
  if feature && variable_key &&
134
128
  feature[:variablesSchema] &&
135
- (feature[:variablesSchema][variable_key] || feature[:variablesSchema][variable_key.to_sym])
136
- variable_schema = feature[:variablesSchema][variable_key] || feature[:variablesSchema][variable_key.to_sym]
129
+ has_key?(feature[:variablesSchema], variable_key)
130
+ variable_schema = fetch_with_symbol_key(feature[:variablesSchema], variable_key)
137
131
 
138
- if variable_schema[:disabledValue]
132
+ if variable_schema.key?(:disabledValue)
139
133
  # disabledValue: <value>
140
134
  evaluation = {
141
135
  type: type,
@@ -179,8 +173,8 @@ module Featurevisor
179
173
  end
180
174
 
181
175
  # Sticky
182
- if sticky && (sticky[feature_key] || sticky[feature_key.to_sym])
183
- sticky_feature = sticky[feature_key] || sticky[feature_key.to_sym]
176
+ if sticky && has_key?(sticky, feature_key)
177
+ sticky_feature = fetch_with_symbol_key(sticky, feature_key)
184
178
 
185
179
  # flag
186
180
  if type == "flag" && sticky_feature.key?(:enabled)
@@ -201,7 +195,7 @@ module Featurevisor
201
195
  if type == "variation"
202
196
  variation_value = sticky_feature[:variation]
203
197
 
204
- if variation_value
198
+ unless variation_value.nil?
205
199
  evaluation = {
206
200
  type: type,
207
201
  feature_key: feature_key,
@@ -219,8 +213,8 @@ module Featurevisor
219
213
  if type == "variable" && variable_key
220
214
  variables = sticky_feature[:variables]
221
215
 
222
- if variables && (variables[variable_key] || variables[variable_key.to_sym])
223
- variable_value = variables[variable_key] || variables[variable_key.to_sym]
216
+ if variables && has_key?(variables, variable_key)
217
+ variable_value = fetch_with_symbol_key(variables, variable_key)
224
218
  evaluation = {
225
219
  type: type,
226
220
  feature_key: feature_key,
@@ -261,8 +255,8 @@ module Featurevisor
261
255
  variable_schema = nil
262
256
 
263
257
  if variable_key
264
- if feature[:variablesSchema] && (feature[:variablesSchema][variable_key] || feature[:variablesSchema][variable_key.to_sym])
265
- variable_schema = feature[:variablesSchema][variable_key] || feature[:variablesSchema][variable_key.to_sym]
258
+ if feature[:variablesSchema] && has_key?(feature[:variablesSchema], variable_key)
259
+ variable_schema = fetch_with_symbol_key(feature[:variablesSchema], variable_key)
266
260
  end
267
261
 
268
262
  # variable schema not found
@@ -343,8 +337,8 @@ module Featurevisor
343
337
  end
344
338
 
345
339
  # variable
346
- if variable_key && force[:variables] && (force[:variables][variable_key] || force[:variables][variable_key.to_sym])
347
- variable_value = force[:variables][variable_key] || force[:variables][variable_key.to_sym]
340
+ if variable_key && force[:variables] && has_key?(force[:variables], variable_key)
341
+ variable_value = fetch_with_symbol_key(force[:variables], variable_key)
348
342
  evaluation = {
349
343
  type: type,
350
344
  feature_key: feature_key,
@@ -385,8 +379,8 @@ module Featurevisor
385
379
 
386
380
  required_variation_value = nil
387
381
 
388
- if required_variation_evaluation[:variation_value]
389
- required_variation_value = required_variation_evaluation[:variation_value]
382
+ if has_key?(required_variation_evaluation, :variation_value)
383
+ required_variation_value = fetch_with_symbol_key(required_variation_evaluation, :variation_value)
390
384
  elsif required_variation_evaluation[:variation]
391
385
  required_variation_value = required_variation_evaluation[:variation][:value]
392
386
  end
@@ -421,24 +415,24 @@ module Featurevisor
421
415
  logger: logger
422
416
  })
423
417
 
424
- # Run bucket key hooks
425
- bucket_key = hooks_manager.run_bucket_key_hooks({
418
+ # Run bucket key modules
419
+ bucket_key = modules_manager.run_bucket_key_modules({
426
420
  feature_key: feature_key,
427
421
  context: context,
428
422
  bucket_by: feature[:bucketBy],
429
423
  bucket_key: bucket_key
430
- })
424
+ }) if modules_manager
431
425
 
432
426
  # bucketValue
433
427
  bucket_value = Featurevisor::Bucketer.get_bucketed_number(bucket_key)
434
428
 
435
- # Run bucket value hooks
436
- bucket_value = hooks_manager.run_bucket_value_hooks({
429
+ # Run bucket value modules
430
+ bucket_value = modules_manager.run_bucket_value_modules({
437
431
  feature_key: feature_key,
438
432
  bucket_key: bucket_key,
439
433
  context: context,
440
434
  bucket_value: bucket_value
441
- })
435
+ }) if modules_manager
442
436
 
443
437
  matched_traffic = nil
444
438
  matched_allocation = nil
@@ -601,10 +595,50 @@ module Featurevisor
601
595
  # variable
602
596
  if type == "variable" && variable_key
603
597
  # override from rule
598
+ if matched_traffic &&
599
+ matched_traffic[:variableOverrides] &&
600
+ has_key?(matched_traffic[:variableOverrides], variable_key)
601
+ overrides = fetch_with_symbol_key(matched_traffic[:variableOverrides], variable_key)
602
+
603
+ override_index = overrides.find_index do |o|
604
+ if o[:conditions]
605
+ conditions = o[:conditions].is_a?(String) && o[:conditions] != "*" ? JSON.parse(o[:conditions]) : o[:conditions]
606
+ datafile_reader.all_conditions_are_matched(conditions, context)
607
+ elsif o[:segments]
608
+ segments = datafile_reader.parse_segments_if_stringified(o[:segments])
609
+ datafile_reader.all_segments_are_matched(segments, context)
610
+ else
611
+ false
612
+ end
613
+ end
614
+
615
+ unless override_index.nil?
616
+ override = overrides[override_index]
617
+
618
+ evaluation = {
619
+ type: type,
620
+ feature_key: feature_key,
621
+ reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE_RULE,
622
+ bucket_key: bucket_key,
623
+ bucket_value: bucket_value,
624
+ rule_key: matched_traffic[:key],
625
+ traffic: matched_traffic,
626
+ variable_key: variable_key,
627
+ variable_schema: variable_schema,
628
+ variable_value: override[:value],
629
+ variable_override_index: override_index
630
+ }
631
+
632
+ logger.debug("variable override from rule", evaluation)
633
+
634
+ return evaluation
635
+ end
636
+ end
637
+
604
638
  if matched_traffic &&
605
639
  matched_traffic[:variables] &&
606
- (matched_traffic[:variables][variable_key] || matched_traffic[:variables][variable_key.to_sym])
607
- variable_value = matched_traffic[:variables][variable_key] || matched_traffic[:variables][variable_key.to_sym]
640
+ has_key?(matched_traffic[:variables], variable_key)
641
+ variable_value = fetch_with_symbol_key(matched_traffic[:variables], variable_key)
608
642
  evaluation = {
609
643
  type: type,
610
644
  feature_key: feature_key,
@@ -637,81 +671,38 @@ module Featurevisor
637
671
  if variation_value && feature[:variations].is_a?(Array)
638
672
  variation = feature[:variations].find { |v| v[:value] == variation_value }
639
673
 
640
- if variation && variation[:variableOverrides] && (variation[:variableOverrides][variable_key] || variation[:variableOverrides][variable_key.to_sym])
641
- overrides = variation[:variableOverrides][variable_key] || variation[:variableOverrides][variable_key.to_sym]
642
-
643
- logger.debug("checking variableOverrides", {
644
- feature_key: feature_key,
645
- variable_key: variable_key,
646
- overrides: overrides,
647
- context: context
648
- })
674
+ if variation && variation[:variableOverrides] && has_key?(variation[:variableOverrides], variable_key)
675
+ overrides = fetch_with_symbol_key(variation[:variableOverrides], variable_key)
649
676
 
650
- override = overrides.find do |o|
651
- logger.debug("evaluating override", {
652
- feature_key: feature_key,
653
- variable_key: variable_key,
654
- override: o,
655
- context: context
656
- })
657
-
658
- result = if o[:conditions]
659
- matched = datafile_reader.all_conditions_are_matched(
660
- o[:conditions].is_a?(String) && o[:conditions] != "*" ?
661
- JSON.parse(o[:conditions]) : o[:conditions],
662
- context
663
- )
664
- logger.debug("conditions match result", {
665
- feature_key: feature_key,
666
- variable_key: variable_key,
667
- conditions: o[:conditions],
668
- matched: matched
669
- })
670
- matched
677
+ override_index = overrides.find_index do |o|
678
+ if o[:conditions]
679
+ conditions = o[:conditions].is_a?(String) && o[:conditions] != "*" ? JSON.parse(o[:conditions]) : o[:conditions]
680
+ datafile_reader.all_conditions_are_matched(conditions, context)
671
681
  elsif o[:segments]
672
682
  segments = datafile_reader.parse_segments_if_stringified(o[:segments])
673
- matched = datafile_reader.all_segments_are_matched(segments, context)
674
- logger.debug("segments match result", {
675
- feature_key: feature_key,
676
- variable_key: variable_key,
677
- segments: o[:segments],
678
- parsed_segments: segments,
679
- matched: matched
680
- })
681
- matched
683
+ datafile_reader.all_segments_are_matched(segments, context)
682
684
  else
683
- logger.debug("override has no conditions or segments", {
684
- feature_key: feature_key,
685
- variable_key: variable_key,
686
- override: o
687
- })
688
685
  false
689
686
  end
690
-
691
- logger.debug("override evaluation result", {
692
- feature_key: feature_key,
693
- variable_key: variable_key,
694
- result: result
695
- })
696
-
697
- result
698
687
  end
699
688
 
700
- if override
689
+ unless override_index.nil?
690
+ override = overrides[override_index]
701
691
  evaluation = {
702
692
  type: type,
703
693
  feature_key: feature_key,
704
- reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE,
694
+ reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE_VARIATION,
705
695
  bucket_key: bucket_key,
706
696
  bucket_value: bucket_value,
707
697
  rule_key: matched_traffic&.[](:key),
708
698
  traffic: matched_traffic,
709
699
  variable_key: variable_key,
710
700
  variable_schema: variable_schema,
711
- variable_value: override[:value]
701
+ variable_value: override[:value],
702
+ variable_override_index: override_index
712
703
  }
713
704
 
714
- logger.debug("variable override", evaluation)
705
+ logger.debug("variable override from variation", evaluation)
715
706
 
716
707
  return evaluation
717
708
  end
@@ -719,8 +710,8 @@ module Featurevisor
719
710
 
720
711
  if variation &&
721
712
  variation[:variables] &&
722
- (variation[:variables][variable_key] || variation[:variables][variable_key.to_sym])
723
- variable_value = variation[:variables][variable_key] || variation[:variables][variable_key.to_sym]
713
+ has_key?(variation[:variables], variable_key)
714
+ variable_value = fetch_with_symbol_key(variation[:variables], variable_key)
724
715
  evaluation = {
725
716
  type: type,
726
717
  feature_key: feature_key,
@@ -814,5 +805,20 @@ module Featurevisor
814
805
  evaluation
815
806
  end
816
807
  end
808
+
809
+ def self.fetch_with_symbol_key(obj, key)
810
+ return obj[key] if obj.is_a?(Hash) && obj.key?(key)
811
+
812
+ symbol_key = key.to_sym
813
+ return obj[symbol_key] if obj.is_a?(Hash) && obj.key?(symbol_key)
814
+
815
+ nil
816
+ end
817
+
818
+ def self.has_key?(obj, key)
819
+ return false unless obj.is_a?(Hash)
820
+
821
+ obj.key?(key) || obj.key?(key.to_sym)
822
+ end
817
823
  end
818
824
  end
@@ -24,7 +24,7 @@ module Featurevisor
24
24
  # @param previous_reader [DatafileReader] Previous datafile reader
25
25
  # @param new_reader [DatafileReader] New datafile reader
26
26
  # @return [Hash] Event parameters
27
- def self.get_params_for_datafile_set_event(previous_reader, new_reader)
27
+ def self.get_params_for_datafile_set_event(previous_reader, new_reader, replace = false)
28
28
  previous_revision = previous_reader.get_revision
29
29
  previous_feature_keys = previous_reader.get_feature_keys
30
30
 
@@ -67,9 +67,10 @@ module Featurevisor
67
67
 
68
68
  {
69
69
  revision: new_revision,
70
- previous_revision: previous_revision,
71
- revision_changed: previous_revision != new_revision,
72
- features: all_affected_features
70
+ previousRevision: previous_revision,
71
+ revisionChanged: previous_revision != new_revision,
72
+ features: all_affected_features,
73
+ replaced: replace
73
74
  }
74
75
  end
75
76
  end