featurevisor 3.0.0 → 3.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acf664663389bb7b3789a16f0e29c84aeb13e623f26bb38465e3bc8a8dcc984e
4
- data.tar.gz: 0ab04bbea29092bcfd22361bda28ab6e36879b346d579b5a204780a3bbfb2eaa
3
+ metadata.gz: 2cc7d95c6c897b09f4092c105b73b9776c6e4b6d1851dad7c2108ea8cced6b2b
4
+ data.tar.gz: 9b06c4ac0e499345af18967d161ef102b5636edd82dbf2013e15ae790ad7e832
5
5
  SHA512:
6
- metadata.gz: e123cd15ff03b6230ca26de9cdee003414b59f5b075a116b946f7b90562123980f083a4c670748d71cefcdcee0c96197e13fceb6d8a8b6bd841d0cd419167ba6
7
- data.tar.gz: ef355cc9d0f2e6660510545c9f78ccefd031c71a9a52646d36ec28fe4bac1c5d38e18c8b6f2b1d9768f54f695fa7360c5e69e2d89ff9ba2cf0fa877d91a96a56
6
+ metadata.gz: b2df8d3afe705ea850e4a77458c17d8090f4bf3b0ec27da761ef3447f8952ba0e2f970d0b1f75962c0c3402f01a2c8fe10b341a694eda2d17d5e004730f3aa51
7
+ data.tar.gz: ebd5698bbd941fbd2778350e6b7602375912a8455f0bd6af8ecd3034ca56d97b23c5803089c57a3a956fb638062838e65adeb3d38126bf4e72f5468512a54634
data/README.md CHANGED
@@ -454,7 +454,7 @@ load_datafile(f, "checkout")
454
454
 
455
455
  ### Updating datafile
456
456
 
457
- You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly.
457
+ You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile-set) event that you can listen and react to accordingly.
458
458
 
459
459
  The triggers for setting the datafile again can be:
460
460
 
@@ -520,7 +520,7 @@ f = Featurevisor.create_featurevisor(
520
520
 
521
521
  Every diagnostic has `:level`, `:code`, `:message`, and an object-shaped `:details` hash. Optional `:module`, `:moduleName`, and `:originalError` fields describe provenance. Evaluation metadata belongs in `:details`.
522
522
 
523
- Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
523
+ Diagnostic handlers are isolated from SDK behaviour. An exception in a handler does not stop other handlers or evaluations.
524
524
 
525
525
 
526
526
  ## Events
@@ -635,10 +635,12 @@ And optionally these properties depending on whether you are evaluating a featur
635
635
 
636
636
  ## Modules
637
637
 
638
- Modules allow you to intercept the evaluation process and customize SDK behavior.
638
+ Modules allow you to intercept the evaluation process and customize SDK behaviour.
639
639
 
640
640
  For feature evaluations, all `before` callbacks run in registration order, followed by all `before_evaluation` callbacks. After evaluation and caller defaults, all `after_evaluation` callbacks run, followed by all `after` callbacks. Global variable evaluations use only `before_evaluation` and `after_evaluation`. Required feature checks run through the complete module pipeline, and transformed defaults are preserved.
641
641
 
642
+ `before` and `after` remain available as deprecated feature-only compatibility callbacks. Use `before_evaluation` and `after_evaluation` for new modules so the same callbacks can handle feature and global variable evaluations.
643
+
642
644
  ### Defining a module
643
645
 
644
646
  A module is a simple hash with a unique recommended `name` and optional lifecycle functions:
@@ -663,8 +665,8 @@ my_custom_module = {
663
665
  })
664
666
  },
665
667
 
666
- # before evaluation
667
- before: ->(options) {
668
+ # before feature or global variable evaluation
669
+ before_evaluation: ->(options) {
668
670
  # update context before evaluation
669
671
  options[:context] = options[:context].merge({
670
672
  someAdditionalAttribute: 'value'
@@ -672,21 +674,15 @@ my_custom_module = {
672
674
  options
673
675
  },
674
676
 
675
- # unified callback for feature and global variable evaluations
676
- before_evaluation: ->(options) { options },
677
-
678
- # after evaluation
679
- after: ->(evaluation, options) {
677
+ # after feature or global variable evaluation
678
+ after_evaluation: ->(evaluation, options) {
680
679
  reason = evaluation[:reason]
681
680
  if reason == 'error'
682
681
  # log error
683
- return
684
682
  end
683
+ evaluation
685
684
  },
686
685
 
687
- # unified callback for feature and global variable evaluations
688
- after_evaluation: ->(evaluation, options) { evaluation },
689
-
690
686
  # configure bucket key
691
687
  bucket_key: ->(options) {
692
688
  # return custom bucket key
@@ -822,7 +818,7 @@ $ bundle exec featurevisor test \
822
818
 
823
819
  The Ruby test runner builds base datafiles and Target datafiles in memory via `npx featurevisor build --json`. When an assertion contains `target`, it is evaluated against the matching Target datafile.
824
820
 
825
- All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
821
+ All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behaviour is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
826
822
 
827
823
  ### Test against local monorepo's example-1
828
824
 
@@ -950,7 +946,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
950
946
  - Run `bundle install`
951
947
  - Push commit to `main` branch
952
948
  - Wait for CI to complete
953
- - Tag the release with the same version number, for example `v3.0.0`
949
+ - Tag the release with the same version number, for example `v3.1.0`
954
950
  - The workflow verifies that the tag matches the shared version
955
951
  - The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
956
952
 
data/bin/commands/test.rb CHANGED
@@ -223,18 +223,13 @@ module FeaturevisorCLI
223
223
  environment = assertion.key?(:environment) ? assertion[:environment] : false
224
224
  environment = false if environment.nil?
225
225
 
226
- target_key = assertion[:target] ? target_datafile_key(environment, assertion[:target]) : nil
227
- base_key = base_datafile_key(environment)
226
+ return datafiles_by_key[target_datafile_key(environment, assertion[:target])] if assertion[:target]
228
227
 
229
- if target_key && datafiles_by_key.key?(target_key)
230
- return datafiles_by_key[target_key]
231
- end
232
-
233
- datafiles_by_key[base_key]
228
+ datafiles_by_key[base_datafile_key(environment)]
234
229
  end
235
230
 
236
231
  def create_tester_instance(datafile, level, assertion)
237
- sticky = parse_sticky(assertion[:sticky])
232
+ sticky = parse_sticky(assertion[:stickyFeatures] || assertion[:sticky])
238
233
  sticky_variables = assertion[:stickyVariables].is_a?(Hash) ? assertion[:stickyVariables] : {}
239
234
 
240
235
  Featurevisor.create_featurevisor(
@@ -301,13 +296,26 @@ module FeaturevisorCLI
301
296
  puts ""
302
297
  end
303
298
 
304
- test_result = run_test_feature(assertion, test[:feature], instance, level)
299
+ begin
300
+ test_result = run_test_feature(assertion, test[:feature], instance, level)
301
+ ensure
302
+ instance.close
303
+ end
305
304
  end
306
305
  elsif test[:variable]
307
306
  datafile = resolve_datafile_for_assertion(assertion, datafiles_by_key)
308
307
  if datafile
308
+ if @options.show_datafile
309
+ puts ""
310
+ puts JSON.pretty_generate(datafile)
311
+ puts ""
312
+ end
309
313
  instance = create_tester_instance(datafile, level, assertion)
310
- test_result = run_test_variable(assertion, test[:variable], instance)
314
+ begin
315
+ test_result = run_test_variable(assertion, test[:variable], instance)
316
+ ensure
317
+ instance.close
318
+ end
311
319
  else
312
320
  test_result = { has_error: true, errors: " ✘ no datafile found for assertion target/environment combination\n", duration: 0 }
313
321
  end
@@ -358,28 +366,61 @@ module FeaturevisorCLI
358
366
  end
359
367
 
360
368
  def run_test_variable(assertion, variable_key, instance)
361
- context = parse_context(assertion[:context])
369
+ instance.set_context(parse_context(assertion[:context]), true)
362
370
  options = {}
363
371
  options[:default_variable_value] = assertion[:defaultVariableValue] if assertion.key?(:defaultVariableValue)
364
372
  started = Time.now
365
373
  errors = ""
366
374
 
367
- if assertion.key?(:expectedValue)
368
- actual = instance.get_variable(variable_key, context, options)
369
- unless compare_values(actual, assertion[:expectedValue])
370
- errors += " ✘ expectedValue: expected #{assertion[:expectedValue].inspect} but received #{actual.inspect}\n"
375
+ errors += test_variable_expectation(assertion, variable_key, instance, options)
376
+
377
+ Array(assertion[:children]).each_with_index do |child_assertion, child_index|
378
+ child = instance.spawn(
379
+ parse_context(child_assertion[:context]),
380
+ sticky_features: parse_sticky(child_assertion[:stickyFeatures]),
381
+ sticky_variables: child_assertion[:stickyVariables].is_a?(Hash) ? child_assertion[:stickyVariables] : {}
382
+ )
383
+ begin
384
+ child_options = {}
385
+ if child_assertion.key?(:defaultVariableValue)
386
+ child_options[:default_variable_value] = child_assertion[:defaultVariableValue]
387
+ end
388
+ errors += test_variable_expectation(
389
+ child_assertion,
390
+ variable_key,
391
+ child,
392
+ child_options,
393
+ "children[#{child_index}]."
394
+ )
395
+ ensure
396
+ child.close
371
397
  end
372
398
  end
373
399
 
400
+ { has_error: !errors.empty?, errors: errors, duration: Time.now - started }
401
+ end
402
+
403
+ def test_variable_expectation(assertion, variable_key, evaluator, options, prefix = "")
404
+ evaluation = evaluator.evaluate_variable(variable_key, {}, options)
405
+ errors = ""
406
+ if assertion.key?(:expectedValue) && !compare_values(evaluation[:variable_value], assertion[:expectedValue])
407
+ errors += " ✘ #{prefix}expectedValue: expected #{format_test_value(assertion[:expectedValue])} but received #{format_test_value(evaluation[:variable_value])}\n"
408
+ end
374
409
  if assertion[:expectedEvaluation].is_a?(Hash)
375
- evaluation = instance.evaluate_variable(variable_key, context, options)
376
410
  assertion[:expectedEvaluation].each do |key, expected|
377
411
  actual = get_evaluation_value(evaluation, key)
378
- errors += " ✘ expectedEvaluation.#{key}: expected #{expected.inspect} but received #{actual.inspect}\n" unless compare_values(actual, expected)
412
+ unless compare_values(actual, expected)
413
+ errors += " ✘ #{prefix}expectedEvaluation.#{key}: expected #{format_test_value(expected)} but received #{format_test_value(actual)}\n"
414
+ end
379
415
  end
380
416
  end
417
+ errors
418
+ end
381
419
 
382
- { has_error: !errors.empty?, errors: errors, duration: Time.now - started }
420
+ def format_test_value(value)
421
+ JSON.generate(value)
422
+ rescue JSON::GeneratorError
423
+ value.inspect
383
424
  end
384
425
 
385
426
  def run_test_feature(assertion, feature_key, instance, level)
@@ -518,20 +559,16 @@ module FeaturevisorCLI
518
559
  child_context = parse_context(child[:context])
519
560
 
520
561
  # Create override options for child with sticky values
521
- child_override_options = create_override_options(child)
522
-
523
- # Pass sticky values to child instance
524
- child_instance = instance.spawn(child_context, child_override_options)
525
-
526
- # Set sticky values for child if they exist
527
- # Create a local copy to ensure it's never nil
528
- child_sticky = sticky || {}
529
- if !child_sticky.empty?
530
- child_instance.set_sticky_features(child_sticky, false)
562
+ child_instance = instance.spawn(
563
+ child_context,
564
+ sticky_features: sticky || {}
565
+ )
566
+ begin
567
+ child_result = run_test_feature_child(child, feature_key, child_instance, level)
568
+ ensure
569
+ child_instance.close
531
570
  end
532
571
 
533
- child_result = run_test_feature_child(child, feature_key, child_instance, level)
534
-
535
572
  if child_result[:has_error]
536
573
  has_error = true
537
574
  errors += child_result[:errors]
@@ -1,3 +1,3 @@
1
1
  module Featurevisor
2
- VERSION = "3.0.0"
2
+ VERSION = "3.1.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: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fahad Heylaal