featurevisor 2.0.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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module Featurevisor
4
6
  # Events module for generating event parameters
5
7
  module Events
@@ -8,7 +10,7 @@ module Featurevisor
8
10
  # @param new_sticky [Hash] New sticky features
9
11
  # @param replace [Boolean] Whether features were replaced
10
12
  # @return [Hash] Event parameters
11
- def self.get_params_for_sticky_set_event(previous_sticky = {}, new_sticky = {}, replace = false)
13
+ def self.get_params_for_sticky_features_set_event(previous_sticky = {}, new_sticky = {}, replace = false)
12
14
  keys_before = previous_sticky.keys
13
15
  keys_after = new_sticky.keys
14
16
 
@@ -20,6 +22,13 @@ module Featurevisor
20
22
  }
21
23
  end
22
24
 
25
+ def self.get_params_for_sticky_variables_set_event(previous_sticky = {}, new_sticky = {}, replace = false)
26
+ {
27
+ variables: (previous_sticky.keys + new_sticky.keys).uniq,
28
+ replaced: replace
29
+ }
30
+ end
31
+
23
32
  # Get parameters for datafile set event
24
33
  # @param previous_reader [InstanceEvaluationDataProvider] Previous datafile
25
34
  # @param new_reader [InstanceEvaluationDataProvider] New datafile
@@ -30,6 +39,8 @@ module Featurevisor
30
39
 
31
40
  new_revision = new_reader.get_revision
32
41
  new_feature_keys = new_reader.get_feature_keys
42
+ previous_variable_keys = previous_reader.get_variable_keys
43
+ new_variable_keys = new_reader.get_variable_keys
33
44
 
34
45
  # results
35
46
  removed_features = []
@@ -48,7 +59,8 @@ module Featurevisor
48
59
  previous_feature = previous_reader.get_feature(previous_feature_key)
49
60
  new_feature = new_reader.get_feature(previous_feature_key)
50
61
 
51
- if previous_feature && new_feature && previous_feature[:hash] != new_feature[:hash]
62
+ if previous_feature && new_feature &&
63
+ (previous_feature[:hash].nil? || new_feature[:hash].nil? || previous_feature[:hash] != new_feature[:hash])
52
64
  # feature was changed in new datafile
53
65
  changed_features << previous_feature_key
54
66
  end
@@ -64,14 +76,118 @@ module Featurevisor
64
76
 
65
77
  # combine all affected feature keys
66
78
  all_affected_features = (removed_features + changed_features + added_features).uniq
79
+ all_affected_variables = (previous_variable_keys + new_variable_keys).uniq.select do |key|
80
+ previous = previous_reader.get_global_variable(key)
81
+ current = new_reader.get_global_variable(key)
82
+ previous.nil? || current.nil? || previous[:hash].nil? || current[:hash].nil? || previous[:hash] != current[:hash]
83
+ end
84
+
85
+
86
+ changed_segments = (previous_reader.get_segment_keys + new_reader.get_segment_keys).uniq.select do |key|
87
+ previous_reader.get_segment(key) != new_reader.get_segment(key)
88
+ end.map(&:to_s)
89
+ feature_keys = (previous_feature_keys + new_feature_keys).uniq
90
+ loop do
91
+ before = all_affected_features.length
92
+ feature_keys.each do |key|
93
+ next if all_affected_features.include?(key)
94
+
95
+ candidates = [previous_reader.get_feature(key), new_reader.get_feature(key)].compact
96
+ all_affected_features << key if candidates.any? do |feature|
97
+ dependency = feature_dependencies(feature)
98
+ !(dependency[:segments].map(&:to_s) & changed_segments).empty? ||
99
+ !(dependency[:features].map(&:to_s) & all_affected_features.map(&:to_s)).empty?
100
+ end
101
+ end
102
+ break if before == all_affected_features.length
103
+ end
104
+
105
+ (previous_variable_keys + new_variable_keys).uniq.each do |key|
106
+ next if all_affected_variables.include?(key)
107
+
108
+ candidates = [previous_reader.get_global_variable(key), new_reader.get_global_variable(key)].compact
109
+ all_affected_variables << key if candidates.any? do |variable|
110
+ dependency = global_variable_dependencies(variable)
111
+ !(dependency[:segments].map(&:to_s) & changed_segments).empty? ||
112
+ !(dependency[:features].map(&:to_s) & all_affected_features.map(&:to_s)).empty?
113
+ end
114
+ end
67
115
 
68
116
  {
69
117
  revision: new_revision,
70
118
  previousRevision: previous_revision,
71
119
  revisionChanged: previous_revision != new_revision,
72
120
  features: all_affected_features,
121
+ variables: all_affected_variables,
73
122
  replaced: replace
74
123
  }
75
124
  end
125
+
126
+ def self.required_feature_keys(values)
127
+ items = values.is_a?(Array) ? values : [values].compact
128
+ items.filter_map do |required|
129
+ required.is_a?(String) ? required : (required[:feature] || required[:key])
130
+ end
131
+ end
132
+ private_class_method :required_feature_keys
133
+
134
+ def self.segment_keys(value)
135
+ return [] if value.nil? || value == "*"
136
+ if value.is_a?(String)
137
+ if value.start_with?("{", "[")
138
+ begin
139
+ return segment_keys(JSON.parse(value, symbolize_names: true))
140
+ rescue JSON::ParserError
141
+ return []
142
+ end
143
+ end
144
+ return [value]
145
+ end
146
+ return value.flat_map { |item| segment_keys(item) }.uniq if value.is_a?(Array)
147
+ return [] unless value.is_a?(Hash)
148
+
149
+ %i[and or not].flat_map { |key| segment_keys(value[key] || value[key.to_s]) }.uniq
150
+ end
151
+ private_class_method :segment_keys
152
+
153
+ def self.override_dependencies(groups)
154
+ overrides = groups.is_a?(Hash) ? groups.values.flatten : []
155
+ {
156
+ segments: overrides.flat_map { |override| segment_keys(override[:segments]) }.uniq,
157
+ features: overrides.flat_map { |override| required_feature_keys(override[:requiredFeatures]) }.uniq
158
+ }
159
+ end
160
+ private_class_method :override_dependencies
161
+
162
+ def self.feature_dependencies(feature)
163
+ requirements = feature.key?(:requiredFeatures) ? feature[:requiredFeatures] : feature[:required]
164
+ segments = []
165
+ features = required_feature_keys(requirements)
166
+ Array(feature[:traffic]).each do |traffic|
167
+ segments.concat(segment_keys(traffic[:segments]))
168
+ nested = override_dependencies(traffic[:variableOverrides])
169
+ segments.concat(nested[:segments])
170
+ features.concat(nested[:features])
171
+ end
172
+ Array(feature[:force]).each { |force| segments.concat(segment_keys(force[:segments])) }
173
+ Array(feature[:variations]).each do |variation|
174
+ nested = override_dependencies(variation[:variableOverrides])
175
+ segments.concat(nested[:segments])
176
+ features.concat(nested[:features])
177
+ end
178
+ { segments: segments.uniq, features: features.uniq }
179
+ end
180
+ private_class_method :feature_dependencies
181
+
182
+ def self.global_variable_dependencies(variable)
183
+ segments = []
184
+ features = required_feature_keys(variable[:requiredFeatures])
185
+ Array(variable[:overrides]).each do |override|
186
+ segments.concat(segment_keys(override[:segments]))
187
+ features.concat(required_feature_keys(override[:requiredFeatures]))
188
+ end
189
+ { segments: segments.uniq, features: features.uniq }
190
+ end
191
+ private_class_method :global_variable_dependencies
76
192
  end
77
193
  end
@@ -11,7 +11,8 @@ module Featurevisor
11
11
  schemaVersion: "2",
12
12
  revision: "unknown",
13
13
  segments: {},
14
- features: {}
14
+ features: {},
15
+ variables: {}
15
16
  }.freeze
16
17
 
17
18
  # Initialize a new Featurevisor instance
@@ -19,7 +20,7 @@ module Featurevisor
19
20
  # @option options [Hash, String] :datafile Datafile content or JSON string
20
21
  # @option options [Hash] :context Initial context
21
22
  # @option options [String] :log_level Log level
22
- # @option options [Hash] :sticky Sticky features
23
+ # @option options [Hash] :sticky_features Sticky features
23
24
  # @option options [Array<Hash, FeaturevisorModule>] :modules Array of modules
24
25
  # @option options [Proc] :on_diagnostic Diagnostic handler
25
26
  def initialize(options = {})
@@ -31,7 +32,8 @@ module Featurevisor
31
32
  )
32
33
  @on_diagnostic = options[:on_diagnostic] || options[:onDiagnostic]
33
34
  @emitter = Featurevisor::Emitter.new
34
- @sticky = options[:sticky] || {}
35
+ @sticky_features = options[:sticky_features] || options[:stickyFeatures] || {}
36
+ @sticky_variables = options[:sticky_variables] || options[:stickyVariables] || {}
35
37
  @closed = false
36
38
  @module_diagnostic_subscriptions = []
37
39
 
@@ -108,7 +110,8 @@ module Featurevisor
108
110
  end
109
111
  unless parsed_datafile.is_a?(Hash) && parsed_datafile[:schemaVersion].is_a?(String) &&
110
112
  parsed_datafile[:revision].is_a?(String) && parsed_datafile[:segments].is_a?(Hash) &&
111
- parsed_datafile[:features].is_a?(Hash)
113
+ parsed_datafile[:features].is_a?(Hash) &&
114
+ (!parsed_datafile.key?(:variables) || parsed_datafile[:variables].is_a?(Hash))
112
115
  raise ArgumentError, "Invalid datafile"
113
116
  end
114
117
  next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile.get_datafile, parsed_datafile)
@@ -140,27 +143,35 @@ module Featurevisor
140
143
  # Set sticky features
141
144
  # @param sticky [Hash] Sticky features
142
145
  # @param replace [Boolean] Whether to replace existing sticky features
143
- def set_sticky(sticky, replace = false)
144
- previous_sticky_features = @sticky || {}
146
+ def set_sticky_features(sticky, replace = false)
147
+ previous_sticky_features = @sticky_features || {}
145
148
 
146
149
  if replace
147
- @sticky = sticky
150
+ @sticky_features = sticky
148
151
  else
149
- @sticky = {
150
- **@sticky,
152
+ @sticky_features = {
153
+ **@sticky_features,
151
154
  **sticky
152
155
  }
153
156
  end
154
157
 
155
- params = Featurevisor::Events.get_params_for_sticky_set_event(previous_sticky_features, @sticky, replace)
158
+ params = Featurevisor::Events.get_params_for_sticky_features_set_event(previous_sticky_features, @sticky_features, replace)
156
159
 
157
160
  report_diagnostic(
158
161
  level: "info",
159
- code: "sticky_set",
162
+ code: "sticky_features_set",
160
163
  message: "Sticky features set",
161
164
  details: params
162
165
  )
163
- @emitter.trigger("sticky_set", params)
166
+ @emitter.trigger("sticky_features_set", params)
167
+ end
168
+
169
+ def set_sticky_variables(sticky, replace = false)
170
+ previous = @sticky_variables || {}
171
+ @sticky_variables = replace ? sticky : { **@sticky_variables, **sticky }
172
+ params = Featurevisor::Events.get_params_for_sticky_variables_set_event(previous, @sticky_variables, replace)
173
+ report_diagnostic(level: "info", code: "sticky_variables_set", message: "Sticky variables set", details: params)
174
+ @emitter.trigger("sticky_variables_set", params)
164
175
  end
165
176
 
166
177
  # Get the revision
@@ -181,7 +192,7 @@ module Featurevisor
181
192
  @datafile.get_feature_keys
182
193
  end
183
194
 
184
- def get_variable_keys(feature_key)
195
+ def get_variable_keys(feature_key = nil)
185
196
  @datafile.get_variable_keys(feature_key)
186
197
  end
187
198
 
@@ -275,7 +286,8 @@ module Featurevisor
275
286
  Featurevisor::ChildInstance.new(
276
287
  parent: self,
277
288
  context: get_context(context),
278
- sticky: options[:sticky]
289
+ sticky_features: options[:sticky_features] || options[:stickyFeatures],
290
+ sticky_variables: options[:sticky_variables] || options[:stickyVariables]
279
291
  )
280
292
  end
281
293
 
@@ -350,12 +362,17 @@ module Featurevisor
350
362
  # @param context [Hash] Context
351
363
  # @param options [Hash] Override options
352
364
  # @return [Hash] Evaluation result
353
- def evaluate_variable(feature_key, variable_key, context = {}, options = {})
365
+ def evaluate_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
366
+ if variable_key_or_context.nil? || variable_key_or_context.is_a?(Hash)
367
+ context = variable_key_or_context || {}
368
+ return evaluate_variable_without_feature(feature_or_variable_key, context, context_or_options)
369
+ end
370
+
354
371
  Featurevisor::Evaluate.evaluate_with_modules(
355
- get_evaluation_dependencies(context, options).merge(
372
+ get_evaluation_dependencies(context_or_options, options).merge(
356
373
  type: "variable",
357
- feature_key: feature_key,
358
- variable_key: variable_key
374
+ feature_key: feature_or_variable_key,
375
+ variable_key: variable_key_or_context
359
376
  )
360
377
  )
361
378
  end
@@ -366,13 +383,13 @@ module Featurevisor
366
383
  # @param context [Hash] Context
367
384
  # @param options [Hash] Override options
368
385
  # @return [Object, nil] Variable value or nil
369
- def get_variable(feature_key, variable_key, context = {}, options = {})
386
+ def get_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
370
387
  begin
371
- evaluation = evaluate_variable(feature_key, variable_key, context, options)
388
+ evaluation = evaluate_variable(feature_or_variable_key, variable_key_or_context, context_or_options, options)
372
389
 
373
390
  if evaluation.key?(:variable_value)
374
- if evaluation[:variable_schema] &&
375
- evaluation[:variable_schema][:type] == "json" &&
391
+ variable_type = evaluation.dig(:variable_schema, :type) || evaluation.dig(:variable, :type)
392
+ if variable_type == "json" &&
376
393
  evaluation[:variable_value].is_a?(String)
377
394
  JSON.parse(evaluation[:variable_value], symbolize_names: true)
378
395
  else
@@ -382,7 +399,7 @@ module Featurevisor
382
399
  nil
383
400
  end
384
401
  rescue => e
385
- report_diagnostic(level: "error", code: "evaluation_error", message: "getVariable failed", originalError: e, details: { featureKey: feature_key, variableKey: variable_key })
402
+ report_diagnostic(level: "error", code: "evaluation_error", message: "getVariable failed", originalError: e, details: { variableKey: variable_key_or_context || feature_or_variable_key })
386
403
  nil
387
404
  end
388
405
  end
@@ -393,8 +410,8 @@ module Featurevisor
393
410
  # @param context [Hash] Context
394
411
  # @param options [Hash] Override options
395
412
  # @return [Boolean, nil] Boolean value or nil
396
- def get_variable_boolean(feature_key, variable_key, context = {}, options = {})
397
- variable_value = get_variable(feature_key, variable_key, context, options)
413
+ def get_variable_boolean(*args)
414
+ variable_value = get_variable(*args)
398
415
  get_value_by_type(variable_value, "boolean")
399
416
  end
400
417
 
@@ -404,8 +421,8 @@ module Featurevisor
404
421
  # @param context [Hash] Context
405
422
  # @param options [Hash] Override options
406
423
  # @return [String, nil] String value or nil
407
- def get_variable_string(feature_key, variable_key, context = {}, options = {})
408
- variable_value = get_variable(feature_key, variable_key, context, options)
424
+ def get_variable_string(*args)
425
+ variable_value = get_variable(*args)
409
426
  get_value_by_type(variable_value, "string")
410
427
  end
411
428
 
@@ -415,8 +432,8 @@ module Featurevisor
415
432
  # @param context [Hash] Context
416
433
  # @param options [Hash] Override options
417
434
  # @return [Integer, nil] Integer value or nil
418
- def get_variable_integer(feature_key, variable_key, context = {}, options = {})
419
- variable_value = get_variable(feature_key, variable_key, context, options)
435
+ def get_variable_integer(*args)
436
+ variable_value = get_variable(*args)
420
437
  get_value_by_type(variable_value, "integer")
421
438
  end
422
439
 
@@ -426,8 +443,8 @@ module Featurevisor
426
443
  # @param context [Hash] Context
427
444
  # @param options [Hash] Override options
428
445
  # @return [Float, nil] Float value or nil
429
- def get_variable_double(feature_key, variable_key, context = {}, options = {})
430
- variable_value = get_variable(feature_key, variable_key, context, options)
446
+ def get_variable_double(*args)
447
+ variable_value = get_variable(*args)
431
448
  get_value_by_type(variable_value, "double")
432
449
  end
433
450
 
@@ -437,8 +454,8 @@ module Featurevisor
437
454
  # @param context [Hash] Context
438
455
  # @param options [Hash] Override options
439
456
  # @return [Array, nil] Array value or nil
440
- def get_variable_array(feature_key, variable_key, context = {}, options = {})
441
- variable_value = get_variable(feature_key, variable_key, context, options)
457
+ def get_variable_array(*args)
458
+ variable_value = get_variable(*args)
442
459
  get_value_by_type(variable_value, "array")
443
460
  end
444
461
 
@@ -448,8 +465,8 @@ module Featurevisor
448
465
  # @param context [Hash] Context
449
466
  # @param options [Hash] Override options
450
467
  # @return [Hash, nil] Object value or nil
451
- def get_variable_object(feature_key, variable_key, context = {}, options = {})
452
- variable_value = get_variable(feature_key, variable_key, context, options)
468
+ def get_variable_object(*args)
469
+ variable_value = get_variable(*args)
453
470
  get_value_by_type(variable_value, "object")
454
471
  end
455
472
 
@@ -459,8 +476,8 @@ module Featurevisor
459
476
  # @param context [Hash] Context
460
477
  # @param options [Hash] Override options
461
478
  # @return [Object, nil] JSON value or nil
462
- def get_variable_json(feature_key, variable_key, context = {}, options = {})
463
- variable_value = get_variable(feature_key, variable_key, context, options)
479
+ def get_variable_json(*args)
480
+ variable_value = get_variable(*args)
464
481
  get_value_by_type(variable_value, "json")
465
482
  end
466
483
 
@@ -469,7 +486,7 @@ module Featurevisor
469
486
  # @param feature_keys [Array<String>] Feature keys to evaluate
470
487
  # @param options [Hash] Override options
471
488
  # @return [Hash] All evaluations
472
- def get_all_evaluations(context = {}, feature_keys = [], options = {})
489
+ def get_feature_evaluations(context = {}, feature_keys = [], options = {})
473
490
  result = {}
474
491
 
475
492
  keys = feature_keys.size > 0 ? feature_keys : @datafile.get_feature_keys
@@ -510,8 +527,100 @@ module Featurevisor
510
527
  result
511
528
  end
512
529
 
530
+ def get_variable_evaluations(context = {}, variable_keys = [], options = {})
531
+ keys = variable_keys.empty? ? @datafile.get_variable_keys : variable_keys
532
+ keys.to_h { |key| [key, get_variable(key.to_s, context, options)] }
533
+ end
534
+
513
535
  private
514
536
 
537
+ def evaluate_variable_without_feature(variable_key, context = {}, options = {})
538
+ evaluation_options = {
539
+ type: "variable",
540
+ variable_key: variable_key.to_s,
541
+ context: get_context(context)
542
+ }
543
+ evaluation_options[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
544
+ begin
545
+ evaluation_options = @modules_manager.run_before_evaluation_modules(evaluation_options)
546
+ resolved_key = evaluation_options[:variable_key]
547
+ variable = @datafile.get_global_variable(resolved_key)
548
+ sticky = options[:__featurevisor_child_sticky_variables] || @sticky_variables
549
+ evaluation = { type: "variable", variable_key: resolved_key, reason: Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND }
550
+
551
+ if sticky.key?(resolved_key) || sticky.key?(resolved_key.to_sym)
552
+ sticky_value = sticky.key?(resolved_key) ? sticky[resolved_key] : sticky[resolved_key.to_sym]
553
+ evaluation.merge!(reason: Featurevisor::EvaluationReason::STICKY, variable: variable,
554
+ variable_value: sticky_value)
555
+ elsif variable
556
+ unless required_features_are_matched(variable[:requiredFeatures], evaluation_options[:context], options)
557
+ value_key = variable[:useDefaultWhenDisabled] ? :defaultValue : :disabledValue
558
+ evaluation.merge!(reason: Featurevisor::EvaluationReason::REQUIRED_FEATURES_UNMET,
559
+ variable: variable)
560
+ evaluation[:variable_value] = variable[value_key] if variable.key?(value_key)
561
+ else
562
+ (variable[:overrides] || []).each_with_index do |override, index|
563
+ next unless required_features_are_matched(override[:requiredFeatures], evaluation_options[:context], options)
564
+ conditions_match = !override[:conditions] || @datafile.all_conditions_are_matched(
565
+ @datafile.parse_conditions_if_stringified(override[:conditions]), evaluation_options[:context]
566
+ )
567
+ segments_match = !override[:segments] || @datafile.all_segments_are_matched(
568
+ @datafile.parse_segments_if_stringified(override[:segments]), evaluation_options[:context]
569
+ )
570
+ next unless conditions_match && segments_match
571
+
572
+ evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE_RULE,
573
+ variable: variable,
574
+ variable_override_index: index, variable_override_key: override[:key],
575
+ variable_override_path: override[:keyPath])
576
+ evaluation[:variable_value] = override[:value] if override.key?(:value)
577
+ break
578
+ end
579
+ if evaluation[:reason] == Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND
580
+ evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_DEFAULT,
581
+ variable: variable)
582
+ evaluation[:variable_value] = variable[:defaultValue] if variable.key?(:defaultValue)
583
+ end
584
+ end
585
+ report_diagnostic(level: "warn", code: "variable_deprecated", message: "Variable \"#{resolved_key}\" is deprecated",
586
+ details: { variableKey: resolved_key, evaluation: evaluation }) if variable[:deprecated]
587
+ end
588
+
589
+ if !evaluation.key?(:variable_value) && evaluation_options.key?(:default_variable_value)
590
+ evaluation[:variable_value] = evaluation_options[:default_variable_value]
591
+ end
592
+ evaluation = @modules_manager.run_global_after_modules(evaluation, evaluation_options)
593
+ report_diagnostic(level: "debug", code: evaluation[:reason], message: "Global variable evaluated", details: evaluation)
594
+ evaluation
595
+ rescue => e
596
+ evaluation = { type: "variable", variable_key: evaluation_options[:variable_key],
597
+ reason: Featurevisor::EvaluationReason::ERROR, error: e }
598
+ report_diagnostic(level: "error", code: "evaluation_error", message: "Global variable evaluation failed",
599
+ originalError: e, details: evaluation)
600
+ evaluation
601
+ end
602
+ end
603
+
604
+ def required_features_are_matched(requirements, context, options)
605
+ return true if requirements.nil?
606
+
607
+ items = requirements.is_a?(Array) ? requirements : [requirements]
608
+ clean_options = options.reject { |key, _| %i[default_variation_value default_variable_value].include?(key) }
609
+ items.all? do |required|
610
+ if required.is_a?(String)
611
+ key = required
612
+ expected_enabled = true
613
+ expected_variation = nil
614
+ else
615
+ key = required[:feature]
616
+ expected_enabled = required.key?(:enabled) ? required[:enabled] : true
617
+ expected_variation = required[:variation]
618
+ end
619
+ next false unless is_enabled(key, context, clean_options) == expected_enabled
620
+ expected_variation.nil? || get_variation(key, context, clean_options) == expected_variation
621
+ end
622
+ end
623
+
515
624
  # Get evaluation dependencies
516
625
  # @param context [Hash] Context
517
626
  # @param options [Hash] Override options
@@ -522,7 +631,8 @@ module Featurevisor
522
631
  diagnostics: @diagnostics,
523
632
  modules_manager: @modules_manager,
524
633
  datafile: @datafile,
525
- sticky: options[:__featurevisor_child_sticky] || @sticky,
634
+ sticky: options[:__featurevisor_child_sticky_features] || @sticky_features,
635
+ sticky_variables: options[:__featurevisor_child_sticky_variables] || @sticky_variables,
526
636
  }.tap do |dependencies|
527
637
  dependencies[:default_variation_value] = options[:default_variation_value] if options.key?(:default_variation_value)
528
638
  dependencies[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
@@ -573,6 +683,10 @@ module Featurevisor
573
683
  features: {
574
684
  **(previous[:features] || {}),
575
685
  **(incoming[:features] || {})
686
+ },
687
+ variables: {
688
+ **(previous[:variables] || {}),
689
+ **(incoming[:variables] || {})
576
690
  }
577
691
  }.compact
578
692
  end
@@ -13,9 +13,11 @@ module Featurevisor
13
13
  @name = options[:name]
14
14
  @setup = options[:setup]
15
15
  @before = options[:before]
16
+ @before_evaluation = options[:before_evaluation] || options[:beforeEvaluation]
16
17
  @bucket_key = options[:bucket_key]
17
18
  @bucket_value = options[:bucket_value]
18
19
  @after = options[:after]
20
+ @after_evaluation = options[:after_evaluation] || options[:afterEvaluation]
19
21
  @close = options[:close]
20
22
  end
21
23
 
@@ -29,6 +31,12 @@ module Featurevisor
29
31
  @before.call(options)
30
32
  end
31
33
 
34
+ def call_before_evaluation(options)
35
+ return options unless @before_evaluation
36
+
37
+ @before_evaluation.call(options)
38
+ end
39
+
32
40
  def call_bucket_key(options)
33
41
  return options[:bucket_key] unless @bucket_key
34
42
 
@@ -47,6 +55,12 @@ module Featurevisor
47
55
  @after.call(evaluation, options)
48
56
  end
49
57
 
58
+ def call_after_evaluation(evaluation, options)
59
+ return evaluation unless @after_evaluation
60
+
61
+ @after_evaluation.call(evaluation, options)
62
+ end
63
+
50
64
  def call_close
51
65
  @close.call if @close
52
66
  end
@@ -124,6 +138,43 @@ module Featurevisor
124
138
  end
125
139
 
126
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
+ def run_before_evaluation_modules(options)
150
+ @modules.reduce(options) do |result, mod|
151
+ mod.call_before_evaluation(result)
152
+ end
153
+ end
154
+
155
+ def run_after_evaluation_modules(evaluation, options)
156
+ @modules.reduce(evaluation) do |result, mod|
157
+ mod.call_after_evaluation(result, options)
158
+ end
159
+ end
160
+
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)
127
178
  @modules.reduce(options) do |result, mod|
128
179
  mod.call_before(result)
129
180
  end
@@ -145,12 +196,6 @@ module Featurevisor
145
196
  bucket_value
146
197
  end
147
198
 
148
- def run_after_modules(evaluation, options)
149
- @modules.reduce(evaluation) do |result, mod|
150
- mod.call_after(result, options)
151
- end
152
- end
153
-
154
199
  def close_all
155
200
  @modules.each do |mod|
156
201
  @clear_module_diagnostic_subscriptions.call(mod) if @clear_module_diagnostic_subscriptions
@@ -1,3 +1,3 @@
1
1
  module Featurevisor
2
- VERSION = "2.0.0"
2
+ VERSION = "3.0.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: featurevisor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fahad Heylaal