featurevisor 1.1.0 → 2.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.
@@ -55,14 +55,15 @@ module Featurevisor
55
55
  # Default: variation
56
56
  if options.key?(:default_variation_value) &&
57
57
  evaluation[:type] == "variation" &&
58
- evaluation[:variation_value].nil?
58
+ !evaluation.key?(:variation_value) &&
59
+ !evaluation.key?(:variation)
59
60
  evaluation[:variation_value] = options[:default_variation_value]
60
61
  end
61
62
 
62
63
  # Default: variable
63
64
  if options.key?(:default_variable_value) &&
64
65
  evaluation[:type] == "variable" &&
65
- evaluation[:variable_value].nil?
66
+ !evaluation.key?(:variable_value)
66
67
  evaluation[:variable_value] = options[:default_variable_value]
67
68
  end
68
69
 
@@ -76,7 +77,7 @@ module Featurevisor
76
77
  type = options[:type]
77
78
  feature_key = options[:feature_key]
78
79
  variable_key = options[:variable_key]
79
- logger = options[:logger]
80
+ diagnostics = options[:diagnostics]
80
81
 
81
82
  evaluation = {
82
83
  type: type,
@@ -86,7 +87,7 @@ module Featurevisor
86
87
  error: e
87
88
  }
88
89
 
89
- logger.error("error during evaluation", evaluation)
90
+ diagnostics.error("Error during evaluation", evaluation)
90
91
 
91
92
  evaluation
92
93
  end
@@ -100,8 +101,8 @@ module Featurevisor
100
101
  feature_key = options[:feature_key]
101
102
  variable_key = options[:variable_key]
102
103
  context = options[:context]
103
- logger = options[:logger]
104
- datafile_reader = options[:datafile_reader]
104
+ diagnostics = options[:diagnostics]
105
+ datafile = options[:datafile]
105
106
  sticky = options[:sticky]
106
107
  modules_manager = options[:modules_manager]
107
108
  evaluation = nil
@@ -120,7 +121,7 @@ module Featurevisor
120
121
  reason: Featurevisor::EvaluationReason::DISABLED
121
122
  }
122
123
 
123
- feature = datafile_reader.get_feature(feature_key)
124
+ feature = datafile.get_feature(feature_key)
124
125
 
125
126
  # serve variable default value if feature is disabled (if explicitly specified)
126
127
  if type == "variable"
@@ -156,7 +157,7 @@ module Featurevisor
156
157
  end
157
158
 
158
159
  # serve disabled variation value if feature is disabled (if explicitly specified)
159
- if type == "variation" && feature && feature[:disabledVariationValue]
160
+ if type == "variation" && feature && feature.key?(:disabledVariationValue)
160
161
  evaluation = {
161
162
  type: type,
162
163
  feature_key: feature_key,
@@ -166,7 +167,7 @@ module Featurevisor
166
167
  }
167
168
  end
168
169
 
169
- logger.debug("feature is disabled", evaluation)
170
+ diagnostics.debug("feature is disabled", evaluation)
170
171
 
171
172
  return evaluation
172
173
  end
@@ -186,7 +187,7 @@ module Featurevisor
186
187
  enabled: sticky_feature[:enabled]
187
188
  }
188
189
 
189
- logger.debug("using sticky enabled", evaluation)
190
+ diagnostics.debug("using sticky enabled", evaluation)
190
191
 
191
192
  return evaluation
192
193
  end
@@ -195,7 +196,7 @@ module Featurevisor
195
196
  if type == "variation"
196
197
  variation_value = sticky_feature[:variation]
197
198
 
198
- unless variation_value.nil?
199
+ if sticky_feature.key?(:variation)
199
200
  evaluation = {
200
201
  type: type,
201
202
  feature_key: feature_key,
@@ -203,7 +204,7 @@ module Featurevisor
203
204
  variation_value: variation_value
204
205
  }
205
206
 
206
- logger.debug("using sticky variation", evaluation)
207
+ diagnostics.debug("using sticky variation", evaluation)
207
208
 
208
209
  return evaluation
209
210
  end
@@ -223,7 +224,7 @@ module Featurevisor
223
224
  variable_value: variable_value
224
225
  }
225
226
 
226
- logger.debug("using sticky variable", evaluation)
227
+ diagnostics.debug("using sticky variable", evaluation)
227
228
 
228
229
  return evaluation
229
230
  end
@@ -231,7 +232,7 @@ module Featurevisor
231
232
  end
232
233
 
233
234
  # Feature
234
- feature = feature_key.is_a?(String) ? datafile_reader.get_feature(feature_key) : feature_key
235
+ feature = feature_key.is_a?(String) ? datafile.get_feature(feature_key) : feature_key
235
236
 
236
237
  # feature: not found
237
238
  unless feature
@@ -241,14 +242,14 @@ module Featurevisor
241
242
  reason: Featurevisor::EvaluationReason::FEATURE_NOT_FOUND
242
243
  }
243
244
 
244
- logger.warn("feature not found", evaluation)
245
+ diagnostics.warn("feature not found", evaluation)
245
246
 
246
247
  return evaluation
247
248
  end
248
249
 
249
250
  # feature: deprecated
250
251
  if type == "flag" && feature[:deprecated]
251
- logger.warn("feature is deprecated", { feature_key: feature_key })
252
+ diagnostics.warn("feature is deprecated", { feature_key: feature_key })
252
253
  end
253
254
 
254
255
  # variableSchema
@@ -268,13 +269,13 @@ module Featurevisor
268
269
  variable_key: variable_key
269
270
  }
270
271
 
271
- logger.warn("variable schema not found", evaluation)
272
+ diagnostics.warn("variable schema not found", evaluation)
272
273
 
273
274
  return evaluation
274
275
  end
275
276
 
276
277
  if variable_schema[:deprecated]
277
- logger.warn("variable is deprecated", {
278
+ diagnostics.warn("variable is deprecated", {
278
279
  feature_key: feature_key,
279
280
  variable_key: variable_key
280
281
  })
@@ -289,13 +290,13 @@ module Featurevisor
289
290
  reason: Featurevisor::EvaluationReason::NO_VARIATIONS
290
291
  }
291
292
 
292
- logger.warn("no variations", evaluation)
293
+ diagnostics.warn("no variations", evaluation)
293
294
 
294
295
  return evaluation
295
296
  end
296
297
 
297
298
  # Forced
298
- force_result = datafile_reader.get_matched_force(feature, context)
299
+ force_result = datafile.get_matched_force(feature, context)
299
300
  force = force_result[:force]
300
301
  force_index = force_result[:forceIndex]
301
302
 
@@ -311,7 +312,7 @@ module Featurevisor
311
312
  enabled: force[:enabled]
312
313
  }
313
314
 
314
- logger.debug("forced enabled found", evaluation)
315
+ diagnostics.debug("forced enabled found", evaluation)
315
316
 
316
317
  return evaluation
317
318
  end
@@ -330,7 +331,7 @@ module Featurevisor
330
331
  variation: variation
331
332
  }
332
333
 
333
- logger.debug("forced variation found", evaluation)
334
+ diagnostics.debug("forced variation found", evaluation)
334
335
 
335
336
  return evaluation
336
337
  end
@@ -350,7 +351,7 @@ module Featurevisor
350
351
  variable_value: variable_value
351
352
  }
352
353
 
353
- logger.debug("forced variable", evaluation)
354
+ diagnostics.debug("forced variable", evaluation)
354
355
 
355
356
  return evaluation
356
357
  end
@@ -400,7 +401,7 @@ module Featurevisor
400
401
  enabled: required_features_are_enabled
401
402
  }
402
403
 
403
- logger.debug("required features not enabled", evaluation)
404
+ diagnostics.debug("required features not enabled", evaluation)
404
405
 
405
406
  return evaluation
406
407
  end
@@ -412,7 +413,7 @@ module Featurevisor
412
413
  feature_key: feature_key,
413
414
  bucket_by: feature[:bucketBy],
414
415
  context: context,
415
- logger: logger
416
+ diagnostics: diagnostics
416
417
  })
417
418
 
418
419
  # Run bucket key modules
@@ -438,13 +439,13 @@ module Featurevisor
438
439
  matched_allocation = nil
439
440
 
440
441
  if type != "flag"
441
- matched_traffic = datafile_reader.get_matched_traffic(feature[:traffic], context)
442
+ matched_traffic = datafile.get_matched_traffic(feature[:traffic], context)
442
443
 
443
444
  if matched_traffic
444
- matched_allocation = datafile_reader.get_matched_allocation(matched_traffic, bucket_value)
445
+ matched_allocation = datafile.get_matched_allocation(matched_traffic, bucket_value)
445
446
  end
446
447
  else
447
- matched_traffic = datafile_reader.get_matched_traffic(feature[:traffic], context)
448
+ matched_traffic = datafile.get_matched_traffic(feature[:traffic], context)
448
449
  end
449
450
 
450
451
  if matched_traffic
@@ -461,7 +462,7 @@ module Featurevisor
461
462
  enabled: false
462
463
  }
463
464
 
464
- logger.debug("matched rule with 0 percentage", evaluation)
465
+ diagnostics.debug("matched rule with 0 percentage", evaluation)
465
466
 
466
467
  return evaluation
467
468
  end
@@ -487,7 +488,7 @@ module Featurevisor
487
488
  enabled: matched_traffic[:enabled].nil? ? true : matched_traffic[:enabled]
488
489
  }
489
490
 
490
- logger.debug("matched", evaluation)
491
+ diagnostics.debug("matched", evaluation)
491
492
 
492
493
  return evaluation
493
494
  end
@@ -502,7 +503,7 @@ module Featurevisor
502
503
  enabled: false
503
504
  }
504
505
 
505
- logger.debug("not matched", evaluation)
506
+ diagnostics.debug("not matched", evaluation)
506
507
 
507
508
  return evaluation
508
509
  end
@@ -520,7 +521,7 @@ module Featurevisor
520
521
  enabled: matched_traffic[:enabled]
521
522
  }
522
523
 
523
- logger.debug("override from rule", evaluation)
524
+ diagnostics.debug("override from rule", evaluation)
524
525
 
525
526
  return evaluation
526
527
  end
@@ -538,7 +539,7 @@ module Featurevisor
538
539
  enabled: true
539
540
  }
540
541
 
541
- logger.debug("matched traffic", evaluation)
542
+ diagnostics.debug("matched traffic", evaluation)
542
543
 
543
544
  return evaluation
544
545
  end
@@ -562,7 +563,7 @@ module Featurevisor
562
563
  variation: variation
563
564
  }
564
565
 
565
- logger.debug("override from rule", evaluation)
566
+ diagnostics.debug("override from rule", evaluation)
566
567
 
567
568
  return evaluation
568
569
  end
@@ -584,7 +585,7 @@ module Featurevisor
584
585
  variation: variation
585
586
  }
586
587
 
587
- logger.debug("allocated variation", evaluation)
588
+ diagnostics.debug("allocated variation", evaluation)
588
589
 
589
590
  return evaluation
590
591
  end
@@ -603,10 +604,10 @@ module Featurevisor
603
604
  override_index = overrides.find_index do |o|
604
605
  if o[:conditions]
605
606
  conditions = o[:conditions].is_a?(String) && o[:conditions] != "*" ? JSON.parse(o[:conditions]) : o[:conditions]
606
- datafile_reader.all_conditions_are_matched(conditions, context)
607
+ datafile.all_conditions_are_matched(conditions, context)
607
608
  elsif o[:segments]
608
- segments = datafile_reader.parse_segments_if_stringified(o[:segments])
609
- datafile_reader.all_segments_are_matched(segments, context)
609
+ segments = datafile.parse_segments_if_stringified(o[:segments])
610
+ datafile.all_segments_are_matched(segments, context)
610
611
  else
611
612
  false
612
613
  end
@@ -629,7 +630,7 @@ module Featurevisor
629
630
  variable_override_index: override_index
630
631
  }
631
632
 
632
- logger.debug("variable override from rule", evaluation)
633
+ diagnostics.debug("variable override from rule", evaluation)
633
634
 
634
635
  return evaluation
635
636
  end
@@ -652,7 +653,7 @@ module Featurevisor
652
653
  variable_value: variable_value
653
654
  }
654
655
 
655
- logger.debug("override from rule", evaluation)
656
+ diagnostics.debug("override from rule", evaluation)
656
657
 
657
658
  return evaluation
658
659
  end
@@ -677,10 +678,10 @@ module Featurevisor
677
678
  override_index = overrides.find_index do |o|
678
679
  if o[:conditions]
679
680
  conditions = o[:conditions].is_a?(String) && o[:conditions] != "*" ? JSON.parse(o[:conditions]) : o[:conditions]
680
- datafile_reader.all_conditions_are_matched(conditions, context)
681
+ datafile.all_conditions_are_matched(conditions, context)
681
682
  elsif o[:segments]
682
- segments = datafile_reader.parse_segments_if_stringified(o[:segments])
683
- datafile_reader.all_segments_are_matched(segments, context)
683
+ segments = datafile.parse_segments_if_stringified(o[:segments])
684
+ datafile.all_segments_are_matched(segments, context)
684
685
  else
685
686
  false
686
687
  end
@@ -702,7 +703,7 @@ module Featurevisor
702
703
  variable_override_index: override_index
703
704
  }
704
705
 
705
- logger.debug("variable override from variation", evaluation)
706
+ diagnostics.debug("variable override from variation", evaluation)
706
707
 
707
708
  return evaluation
708
709
  end
@@ -725,7 +726,7 @@ module Featurevisor
725
726
  variable_value: variable_value
726
727
  }
727
728
 
728
- logger.debug("allocated variable", evaluation)
729
+ diagnostics.debug("allocated variable", evaluation)
729
730
 
730
731
  return evaluation
731
732
  end
@@ -742,7 +743,7 @@ module Featurevisor
742
743
  bucket_value: bucket_value
743
744
  }
744
745
 
745
- logger.debug("no matched variation", evaluation)
746
+ diagnostics.debug("no matched variation", evaluation)
746
747
 
747
748
  return evaluation
748
749
  end
@@ -760,7 +761,7 @@ module Featurevisor
760
761
  variable_value: variable_schema[:defaultValue]
761
762
  }
762
763
 
763
- logger.debug("using default value", evaluation)
764
+ diagnostics.debug("using default value", evaluation)
764
765
 
765
766
  return evaluation
766
767
  end
@@ -774,7 +775,7 @@ module Featurevisor
774
775
  bucket_value: bucket_value
775
776
  }
776
777
 
777
- logger.debug("variable not found", evaluation)
778
+ diagnostics.debug("variable not found", evaluation)
778
779
 
779
780
  return evaluation
780
781
  end
@@ -788,7 +789,7 @@ module Featurevisor
788
789
  enabled: false
789
790
  }
790
791
 
791
- logger.debug("nothing matched", evaluation)
792
+ diagnostics.debug("nothing matched", evaluation)
792
793
 
793
794
  evaluation
794
795
  rescue => e
@@ -800,7 +801,7 @@ module Featurevisor
800
801
  error: e
801
802
  }
802
803
 
803
- logger.error("error", evaluation)
804
+ diagnostics.error("Error during evaluation", evaluation)
804
805
 
805
806
  evaluation
806
807
  end
@@ -3,17 +3,17 @@
3
3
  require "json"
4
4
 
5
5
  module Featurevisor
6
- # DatafileReader class for reading and processing Featurevisor datafiles
7
- class DatafileReader
8
- attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :logger, :regex_cache
6
+ # Private datafile and matching adapter used by the evaluator.
7
+ class InstanceEvaluationDataProvider
8
+ attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :diagnostics, :regex_cache
9
9
 
10
- # Initialize a new DatafileReader
11
- # @param options [Hash] Options hash containing datafile and logger
10
+ # Initialize a new evaluation data provider.
11
+ # @param options [Hash] Options hash containing datafile and diagnostics
12
12
  # @option options [Hash] :datafile Datafile content
13
- # @option options [Logger] :logger Logger instance
13
+ # @option options [DiagnosticReporter] :diagnostics Diagnostic reporter
14
14
  def initialize(options)
15
15
  datafile = options[:datafile]
16
- @logger = options[:logger]
16
+ @diagnostics = options[:diagnostics]
17
17
 
18
18
  @schema_version = datafile[:schemaVersion]
19
19
  @revision = datafile[:revision]
@@ -133,7 +133,15 @@ module Featurevisor
133
133
 
134
134
  return @regex_cache[cache_key] if @regex_cache[cache_key]
135
135
 
136
- regex = Regexp.new(regex_string, flags)
136
+ ruby_flags = 0
137
+ ruby_flags |= Regexp::IGNORECASE if flags.include?("i")
138
+ # Ruby's MULTILINE option controls dot matching newlines, which is the
139
+ # JavaScript `s` behavior. Ruby anchors already operate per line.
140
+ ruby_flags |= Regexp::MULTILINE if flags.include?("s")
141
+ invalid_flags = flags.delete("gimsuy")
142
+ raise ArgumentError, "invalid regular expression flags: #{invalid_flags}" unless invalid_flags.empty?
143
+
144
+ regex = Regexp.new(regex_string, ruby_flags)
137
145
  @regex_cache[cache_key] = regex
138
146
  @regex_cache[cache_key]
139
147
  end
@@ -155,12 +163,11 @@ module Featurevisor
155
163
  result = Conditions.condition_is_matched(conditions, context, get_regex_proc)
156
164
  return result
157
165
  rescue => e
158
- @logger.warn("Error in condition evaluation: #{e.message}", {
159
- error: e.class.name,
160
- details: {
161
- condition: conditions,
162
- context: context
163
- }
166
+ @diagnostics.warn(e.message, {
167
+ code: "condition_match_error",
168
+ error: e,
169
+ condition: conditions,
170
+ context: context
164
171
  })
165
172
  return false
166
173
  end
@@ -183,10 +190,12 @@ module Featurevisor
183
190
  end
184
191
 
185
192
  if conditions.is_a?(Hash) && conditions[:not] && conditions[:not].is_a?(Array)
193
+ return false if conditions[:not].empty?
186
194
  return !all_conditions_are_matched({ and: conditions[:not] }, context)
187
195
  end
188
196
 
189
197
  if conditions.is_a?(Hash) && conditions["not"] && conditions["not"].is_a?(Array)
198
+ return false if conditions["not"].empty?
190
199
  return !all_conditions_are_matched({ "and" => conditions["not"] }, context)
191
200
  end
192
201
 
@@ -244,10 +253,12 @@ module Featurevisor
244
253
  end
245
254
 
246
255
  if group_segments[:not] && group_segments[:not].is_a?(Array)
256
+ return false if group_segments[:not].empty?
247
257
  return !all_segments_are_matched({ and: group_segments[:not] }, context)
248
258
  end
249
259
 
250
260
  if group_segments["not"] && group_segments["not"].is_a?(Array)
261
+ return false if group_segments["not"].empty?
251
262
  return !all_segments_are_matched({ "and" => group_segments["not"] }, context)
252
263
  end
253
264
 
@@ -333,7 +344,7 @@ module Featurevisor
333
344
  begin
334
345
  JSON.parse(conditions)
335
346
  rescue => e
336
- @logger.error("Error parsing conditions", {
347
+ @diagnostics.error("Error parsing conditions", {
337
348
  error: e,
338
349
  details: {
339
350
  conditions: conditions
@@ -355,5 +366,5 @@ module Featurevisor
355
366
  end
356
367
  end
357
368
 
358
- private_constant :DatafileReader
369
+ private_constant :InstanceEvaluationDataProvider
359
370
  end
@@ -21,8 +21,8 @@ module Featurevisor
21
21
  end
22
22
 
23
23
  # Get parameters for datafile set event
24
- # @param previous_reader [DatafileReader] Previous datafile reader
25
- # @param new_reader [DatafileReader] New datafile reader
24
+ # @param previous_reader [InstanceEvaluationDataProvider] Previous datafile
25
+ # @param new_reader [InstanceEvaluationDataProvider] New datafile
26
26
  # @return [Hash] Event parameters
27
27
  def self.get_params_for_datafile_set_event(previous_reader, new_reader, replace = false)
28
28
  previous_revision = previous_reader.get_revision