featurevisor 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 202758e9fe9a2c81f67c3ea7456be5d068c1d9aa8ccf35f721a4ca447a94196a
4
- data.tar.gz: bf2cae186ce5b6d3b483f4eab14768553cc5d2e09148c16ebbaf3ac60af7d7a0
3
+ metadata.gz: acf664663389bb7b3789a16f0e29c84aeb13e623f26bb38465e3bc8a8dcc984e
4
+ data.tar.gz: 0ab04bbea29092bcfd22361bda28ab6e36879b346d579b5a204780a3bbfb2eaa
5
5
  SHA512:
6
- metadata.gz: aa565646ae2e3b54d018044a79e92d9172e0ac8478dbb2dbee1699605a9457fbea6b694dc42c9027b40d4d2a3358c3146be946cfe307319119ab6ba2d03657ed
7
- data.tar.gz: e8e689efcc96ac4ca3bd0ec7469895fbb1c9b4e70737d0f56f87fa9f0733395c0e0de6938a7b6c7c4c6bd7daef9f9818daa9163e508b0242836b750aa83e82f4
6
+ metadata.gz: e123cd15ff03b6230ca26de9cdee003414b59f5b075a116b946f7b90562123980f083a4c670748d71cefcdcee0c96197e13fceb6d8a8b6bd841d0cd419167ba6
7
+ data.tar.gz: ef355cc9d0f2e6660510545c9f78ccefd031c71a9a52646d36ec28fe4bac1c5d38e18c8b6f2b1d9768f54f695fa7360c5e69e2d89ff9ba2cf0fa877d91a96a56
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2025 Fahad Heylaal (https://fahad19.com)
3
+ Copyright (c) 2026 Fahad Heylaal (https://fahad19.com)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -19,8 +19,9 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
19
19
  - [Getting variation](#getting-variation)
20
20
  - [Getting variables](#getting-variables)
21
21
  - [Type specific methods](#type-specific-methods)
22
- - [Getting all evaluations](#getting-all-evaluations)
23
- - [Sticky](#sticky)
22
+ - [Getting global variables](#getting-global-variables)
23
+ - [Getting aggregate evaluations](#getting-aggregate-evaluations)
24
+ - [Sticky features and variables](#sticky-features-and-variables)
24
25
  - [Initialize with sticky](#initialize-with-sticky)
25
26
  - [Set sticky afterwards](#set-sticky-afterwards)
26
27
  - [Setting datafile](#setting-datafile)
@@ -36,7 +37,7 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
36
37
  - [Events](#events)
37
38
  - [`datafile_set`](#datafile_set)
38
39
  - [`context_set`](#context_set)
39
- - [`sticky_set`](#sticky_set)
40
+ - [`sticky_features_set` and `sticky_variables_set`](#sticky_features_set-and-sticky_variables_set)
40
41
  - [`error`](#error)
41
42
  - [Modules](#modules)
42
43
  - [Defining a module](#defining-a-module)
@@ -89,6 +90,8 @@ f = Featurevisor.create_featurevisor(
89
90
 
90
91
  Most applications only need this factory and the returned `Featurevisor::Instance`. Public extension and observability APIs include modules, diagnostics, events, and the datafile structures accepted by the factory.
91
92
 
93
+ Concurrent evaluations are safe after an instance is configured. Do not mutate or close the same instance concurrently with evaluations. Serialize calls to `set_datafile`, `set_context`, `set_sticky_features`, `set_sticky_variables`, `add_module`, `remove_module`, and `close`. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
94
+
92
95
  ## Initialization
93
96
 
94
97
  The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
@@ -293,12 +296,24 @@ f.get_variable_json(feature_key, variable_key, context = {})
293
296
 
294
297
  Type specific methods do not coerce values. `get_variable_integer` returns `nil` for the string `"1"`, and boolean getters return `nil` for non-boolean values.
295
298
 
296
- ## Getting all evaluations
299
+ ## Getting global variables
300
+
301
+ Global variables use the same methods with a single variable key:
302
+
303
+ ```ruby
304
+ message = f.get_variable_string('welcomeMessage', context)
305
+ value = f.get_variable('checkoutSettings', context)
306
+ evaluation = f.evaluate_variable('checkoutSettings', context)
307
+ ```
308
+
309
+ Global variables resolve sticky values first, then required features, then the first matching override, and finally their default value.
310
+
311
+ ## Getting aggregate evaluations
297
312
 
298
313
  You can get evaluations of all features available in the SDK instance:
299
314
 
300
315
  ```ruby
301
- all_evaluations = f.get_all_evaluations({})
316
+ all_evaluations = f.get_feature_evaluations({})
302
317
 
303
318
  puts all_evaluations
304
319
  # {
@@ -319,11 +334,17 @@ puts all_evaluations
319
334
 
320
335
  This is handy especially when you want to pass all evaluations from a backend application to the frontend.
321
336
 
322
- ## Sticky
337
+ Global variable values are available separately:
338
+
339
+ ```ruby
340
+ global_variables = f.get_variable_evaluations({})
341
+ ```
342
+
343
+ ## Sticky features and variables
323
344
 
324
345
  For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/):
325
346
 
326
- Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn(context, sticky: ...)` when a child needs its own sticky state.
347
+ Sticky values belong to an SDK or child instance. Feature and global variable sticky values are independent.
327
348
 
328
349
  ### Initialize with sticky
329
350
 
@@ -331,7 +352,7 @@ Sticky values belong to an SDK or child instance. Evaluation options do not acce
331
352
  require 'featurevisor'
332
353
 
333
354
  f = Featurevisor.create_featurevisor(
334
- sticky: {
355
+ sticky_features: {
335
356
  myFeatureKey: {
336
357
  enabled: true,
337
358
  # optional
@@ -343,6 +364,9 @@ f = Featurevisor.create_featurevisor(
343
364
  anotherFeatureKey: {
344
365
  enabled: false
345
366
  }
367
+ },
368
+ sticky_variables: {
369
+ welcomeMessage: 'Welcome back'
346
370
  }
347
371
  )
348
372
  ```
@@ -354,7 +378,7 @@ Once initialized with sticky features, the SDK will look for values there first
354
378
  You can also set sticky features after the SDK is initialized:
355
379
 
356
380
  ```ruby
357
- f.set_sticky({
381
+ f.set_sticky_features({
358
382
  myFeatureKey: {
359
383
  enabled: true,
360
384
  variation: 'treatment',
@@ -366,6 +390,8 @@ f.set_sticky({
366
390
  enabled: false
367
391
  }
368
392
  }, true) # replace existing sticky features (false by default)
393
+
394
+ f.set_sticky_variables({ welcomeMessage: 'Welcome back' }, true)
369
395
  ```
370
396
 
371
397
  ## Setting datafile
@@ -391,7 +417,7 @@ By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's
391
417
  - `segments` are merged, with incoming entries overriding existing ones
392
418
  - `features` are merged, with incoming entries overriding existing ones
393
419
 
394
- This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together.
420
+ This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features, segments, and global variables together.
395
421
 
396
422
  ### Replacing
397
423
 
@@ -543,15 +569,19 @@ unsubscribe = f.on('context_set') do |event|
543
569
  end
544
570
  ```
545
571
 
546
- ### `sticky_set`
572
+ ### `sticky_features_set` and `sticky_variables_set`
547
573
 
548
574
  ```ruby
549
- unsubscribe = f.on('sticky_set') do |event|
575
+ feature_unsubscribe = f.on('sticky_features_set') do |event|
550
576
  replaced = event[:replaced] # true if sticky features got replaced
551
577
  features = event[:features] # list of all affected feature keys
552
578
 
553
579
  puts 'Sticky features set'
554
580
  end
581
+
582
+ variable_unsubscribe = f.on('sticky_variables_set') do |event|
583
+ variables = event[:variables]
584
+ end
555
585
  ```
556
586
 
557
587
  ### `error`
@@ -581,11 +611,14 @@ evaluation = f.evaluate_variation(feature_key, context = {})
581
611
 
582
612
  # variable
583
613
  evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
614
+
615
+ # global variable
616
+ global_evaluation = f.evaluate_variable(variable_key, context = {})
584
617
  ```
585
618
 
586
619
  The returned object will always contain the following properties:
587
620
 
588
- - `feature_key`: the feature key
621
+ - `feature_key`: the feature key when evaluating a feature
589
622
  - `reason`: the reason how the value was evaluated
590
623
 
591
624
  And optionally these properties depending on whether you are evaluating a feature variation or a variable:
@@ -604,6 +637,8 @@ And optionally these properties depending on whether you are evaluating a featur
604
637
 
605
638
  Modules allow you to intercept the evaluation process and customize SDK behavior.
606
639
 
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
+
607
642
  ### Defining a module
608
643
 
609
644
  A module is a simple hash with a unique recommended `name` and optional lifecycle functions:
@@ -637,6 +672,9 @@ my_custom_module = {
637
672
  options
638
673
  },
639
674
 
675
+ # unified callback for feature and global variable evaluations
676
+ before_evaluation: ->(options) { options },
677
+
640
678
  # after evaluation
641
679
  after: ->(evaluation, options) {
642
680
  reason = evaluation[:reason]
@@ -646,6 +684,9 @@ my_custom_module = {
646
684
  end
647
685
  },
648
686
 
687
+ # unified callback for feature and global variable evaluations
688
+ after_evaluation: ->(evaluation, options) { evaluation },
689
+
649
690
  # configure bucket key
650
691
  bucket_key: ->(options) {
651
692
  # return custom bucket key
@@ -697,6 +738,8 @@ f.remove_module('my-custom-module')
697
738
 
698
739
  ## Child instance
699
740
 
741
+ A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling `close` removes both child-owned listeners and subscriptions delegated to the parent.
742
+
700
743
  When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
701
744
 
702
745
  But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request.
@@ -716,14 +759,19 @@ Now you can pass the child instance where your individual request is being handl
716
759
  is_enabled = child_f.is_enabled('my_feature')
717
760
  variation = child_f.get_variation('my_feature')
718
761
  variable_value = child_f.get_variable('my_feature', 'my_variable')
762
+ global_value = child_f.get_variable('welcomeMessage')
719
763
  ```
720
764
 
721
765
  Similar to parent SDK, child instances also support several additional methods:
722
766
 
723
767
  - `set_context`
724
- - `set_sticky`
768
+ - `set_sticky_features`
769
+ - `set_sticky_variables`
770
+ - `evaluate_flag`
725
771
  - `is_enabled`
772
+ - `evaluate_variation`
726
773
  - `get_variation`
774
+ - `evaluate_variable`
727
775
  - `get_variable`
728
776
  - `get_variable_boolean`
729
777
  - `get_variable_string`
@@ -732,7 +780,8 @@ Similar to parent SDK, child instances also support several additional methods:
732
780
  - `get_variable_array`
733
781
  - `get_variable_object`
734
782
  - `get_variable_json`
735
- - `get_all_evaluations`
783
+ - `get_feature_evaluations`
784
+ - `get_variable_evaluations`
736
785
  - `on`
737
786
  - `close`
738
787
 
@@ -821,7 +870,7 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
821
870
  Install the provider:
822
871
 
823
872
  ```ruby
824
- gem "featurevisor-openfeature", "~> 1.1"
873
+ gem "featurevisor-openfeature", "~> 3.0"
825
874
  ```
826
875
 
827
876
  It installs the matching `featurevisor` gem and the official `openfeature-sdk` dependency. The provider and base SDK deliberately share the same version, and the provider requires that exact Featurevisor version.
@@ -845,9 +894,9 @@ enabled = client.fetch_boolean_value(
845
894
  )
846
895
  ```
847
896
 
848
- Use `checkout` for a flag, `checkout:variation` for its variation, and `checkout:title` for its `title` variable. Boolean variables use the boolean resolver. Arrays, hashes, and JSON variables use the object resolver.
897
+ Use `checkout` for a flag, `checkout:variation` for its variation, `checkout:title` for its `title` variable, and `variable:welcomeMessage` for a global variable. Boolean variables use the boolean resolver. Arrays, hashes, and JSON variables use the object resolver.
849
898
 
850
- OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, and `variation_key` can customize the mapping.
899
+ OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, `variation_key`, and `global_variable_prefix` can customize the mapping. The global variable prefix defaults to `variable` and cannot contain the separator.
851
900
 
852
901
  You can pass any Featurevisor initialization options directly to the provider. These options are used to create the Featurevisor instance owned by the provider:
853
902
 
@@ -901,7 +950,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
901
950
  - Run `bundle install`
902
951
  - Push commit to `main` branch
903
952
  - Wait for CI to complete
904
- - Tag the release with the same version number, for example `v1.1.0`
953
+ - Tag the release with the same version number, for example `v3.0.0`
905
954
  - The workflow verifies that the tag matches the shared version
906
955
  - The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
907
956
 
data/bin/cli.rb CHANGED
@@ -43,7 +43,7 @@ module FeaturevisorCLI
43
43
  options.environment = v
44
44
  end
45
45
 
46
- opts.on("--feature=FEATURE", "Feature key (required for benchmark)") do |v|
46
+ opts.on("--feature=FEATURE", "Feature key") do |v|
47
47
  options.feature = v
48
48
  end
49
49
 
@@ -150,6 +150,6 @@ module FeaturevisorCLI
150
150
  puts " featurevisor benchmark --feature=myFeature --environment=dev --n=10000"
151
151
  puts " featurevisor assess-distribution --feature=myFeature --n=10000"
152
152
  puts ""
153
- puts "Note: benchmark command requires --environment and --feature options"
153
+ puts "Note: benchmark requires --environment and either --feature or --variable"
154
154
  end
155
155
  end
@@ -22,8 +22,8 @@ module FeaturevisorCLI
22
22
  exit 1
23
23
  end
24
24
 
25
- unless @options.feature
26
- puts "Error: --feature is required for benchmark command"
25
+ unless @options.feature || @options.variable
26
+ puts "Error: --feature or --variable is required for benchmark command"
27
27
  exit 1
28
28
  end
29
29
 
@@ -41,7 +41,8 @@ module FeaturevisorCLI
41
41
  datafile_build_duration_ms = (datafile_build_duration * 1000).round
42
42
 
43
43
  puts "\nBenchmark Featurevisor feature"
44
- puts " Feature: #{@options.feature}"
44
+ puts " Feature: #{@options.feature}" if @options.feature
45
+ puts " Global variable: #{@options.variable}" if @options.variable && !@options.feature
45
46
  puts " Environment: #{@options.environment}"
46
47
  puts " Target: #{target}" if target
47
48
  puts " Iterations: #{@options.n}"
@@ -59,7 +60,10 @@ module FeaturevisorCLI
59
60
  puts "Against context: #{context.to_json}"
60
61
 
61
62
  # Run the appropriate benchmark
62
- if @options.variation
63
+ if @options.variable && !@options.feature
64
+ puts "Evaluating global variable \"#{@options.variable}\" #{@options.n} times..."
65
+ output = benchmark_global_variable(instance, @options.variable, context, @options.n)
66
+ elsif @options.variation
63
67
  puts "Evaluating variation #{@options.n} times..."
64
68
  output = benchmark_feature_variation(instance, @options.feature, context, @options.n)
65
69
  elsif @options.variable
@@ -223,6 +227,12 @@ module FeaturevisorCLI
223
227
  end
224
228
  end
225
229
 
230
+ def benchmark_global_variable(instance, variable_key, context, n)
231
+ benchmark_evaluation(n) do
232
+ instance.get_variable(variable_key, context)
233
+ end
234
+ end
235
+
226
236
  def format_value(value)
227
237
  if value.nil?
228
238
  "null"
data/bin/commands/test.rb CHANGED
@@ -235,10 +235,12 @@ module FeaturevisorCLI
235
235
 
236
236
  def create_tester_instance(datafile, level, assertion)
237
237
  sticky = parse_sticky(assertion[:sticky])
238
+ sticky_variables = assertion[:stickyVariables].is_a?(Hash) ? assertion[:stickyVariables] : {}
238
239
 
239
240
  Featurevisor.create_featurevisor(
240
241
  datafile: datafile,
241
- sticky: sticky,
242
+ sticky_features: sticky,
243
+ sticky_variables: sticky_variables,
242
244
  log_level: level,
243
245
  modules: [
244
246
  {
@@ -265,7 +267,7 @@ module FeaturevisorCLI
265
267
  tests.each do |test|
266
268
  test_key = test[:key]
267
269
  assertions = test[:assertions] || []
268
- if test[:feature] && !@options.targets.empty?
270
+ if (test[:feature] || test[:variable]) && !@options.targets.empty?
269
271
  assertions = assertions.select do |assertion|
270
272
  assertion[:target].nil? || @options.targets.include?(assertion[:target])
271
273
  end
@@ -301,6 +303,14 @@ module FeaturevisorCLI
301
303
 
302
304
  test_result = run_test_feature(assertion, test[:feature], instance, level)
303
305
  end
306
+ elsif test[:variable]
307
+ datafile = resolve_datafile_for_assertion(assertion, datafiles_by_key)
308
+ if datafile
309
+ instance = create_tester_instance(datafile, level, assertion)
310
+ test_result = run_test_variable(assertion, test[:variable], instance)
311
+ else
312
+ test_result = { has_error: true, errors: " ✘ no datafile found for assertion target/environment combination\n", duration: 0 }
313
+ end
304
314
  elsif test[:segment]
305
315
  segment_key = test[:segment]
306
316
  segment = segments_by_key[segment_key]
@@ -347,6 +357,31 @@ module FeaturevisorCLI
347
357
  end
348
358
  end
349
359
 
360
+ def run_test_variable(assertion, variable_key, instance)
361
+ context = parse_context(assertion[:context])
362
+ options = {}
363
+ options[:default_variable_value] = assertion[:defaultVariableValue] if assertion.key?(:defaultVariableValue)
364
+ started = Time.now
365
+ errors = ""
366
+
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"
371
+ end
372
+ end
373
+
374
+ if assertion[:expectedEvaluation].is_a?(Hash)
375
+ evaluation = instance.evaluate_variable(variable_key, context, options)
376
+ assertion[:expectedEvaluation].each do |key, expected|
377
+ actual = get_evaluation_value(evaluation, key)
378
+ errors += " ✘ expectedEvaluation.#{key}: expected #{expected.inspect} but received #{actual.inspect}\n" unless compare_values(actual, expected)
379
+ end
380
+ end
381
+
382
+ { has_error: !errors.empty?, errors: errors, duration: Time.now - started }
383
+ end
384
+
350
385
  def run_test_feature(assertion, feature_key, instance, level)
351
386
  context = parse_context(assertion[:context])
352
387
  sticky = parse_sticky(assertion[:sticky])
@@ -354,7 +389,7 @@ module FeaturevisorCLI
354
389
  # Set context and sticky for this assertion
355
390
  instance.set_context(context, false)
356
391
  if sticky && !sticky.empty?
357
- instance.set_sticky(sticky, false)
392
+ instance.set_sticky_features(sticky, false)
358
393
  end
359
394
 
360
395
  # Create override options
@@ -492,7 +527,7 @@ module FeaturevisorCLI
492
527
  # Create a local copy to ensure it's never nil
493
528
  child_sticky = sticky || {}
494
529
  if !child_sticky.empty?
495
- child_instance.set_sticky(child_sticky, false)
530
+ child_instance.set_sticky_features(child_sticky, false)
496
531
  end
497
532
 
498
533
  child_result = run_test_feature_child(child, feature_key, child_instance, level)
@@ -671,7 +706,7 @@ module FeaturevisorCLI
671
706
 
672
707
  if assertion.key?(:expectedToMatch)
673
708
  expected_to_match = assertion[:expectedToMatch]
674
- actual = instance.instance_variable_get(:@datafile_reader).all_conditions_are_matched(conditions, context)
709
+ actual = instance.instance_variable_get(:@datafile).all_conditions_are_matched(conditions, context)
675
710
 
676
711
  if actual != expected_to_match
677
712
  has_error = true
@@ -795,6 +830,8 @@ module FeaturevisorCLI
795
830
  evaluation[:force]
796
831
  when :required
797
832
  evaluation[:required]
833
+ when :requiredFeatures
834
+ evaluation[:required_features]
798
835
  when :sticky
799
836
  evaluation[:sticky]
800
837
  when :variation
@@ -809,6 +846,10 @@ module FeaturevisorCLI
809
846
  evaluation[:variable_schema]
810
847
  when :variableOverrideIndex
811
848
  evaluation[:variable_override_index]
849
+ when :variableOverrideKey
850
+ evaluation[:variable_override_key]
851
+ when :variableOverridePath
852
+ evaluation[:variable_override_path]
812
853
  else
813
854
  nil
814
855
  end
@@ -30,16 +30,16 @@ module Featurevisor
30
30
  # - feature_key [String] The feature key
31
31
  # - bucket_by [String, Array<String>, Hash] Bucketing strategy
32
32
  # - context [Hash] User context
33
- # - logger [Logger] Logger instance
33
+ # - diagnostics [DiagnosticReporter] Diagnostic reporter
34
34
  # @return [String] The bucket key
35
35
  # @raise [StandardError] If bucket_by is invalid
36
36
  def self.get_bucket_key(options)
37
37
  feature_key = options[:feature_key]
38
38
  bucket_by = options[:bucket_by]
39
39
  context = options[:context]
40
- logger = options[:logger]
40
+ diagnostics = options[:diagnostics]
41
41
 
42
- type, attribute_keys = parse_bucket_by(bucket_by, logger, feature_key)
42
+ type, attribute_keys = parse_bucket_by(bucket_by, diagnostics, feature_key)
43
43
 
44
44
  bucket_key = build_bucket_key(attribute_keys, context, type, feature_key)
45
45
 
@@ -50,10 +50,10 @@ module Featurevisor
50
50
 
51
51
  # Parse bucket_by configuration to determine type and attribute keys
52
52
  # @param bucket_by [String, Array<String>, Hash] Bucketing strategy
53
- # @param logger [Logger] Logger instance
53
+ # @param diagnostics [DiagnosticReporter] Diagnostic reporter
54
54
  # @param feature_key [String] Feature key for error logging
55
55
  # @return [Array] Tuple of [type, attribute_keys]
56
- def self.parse_bucket_by(bucket_by, logger, feature_key)
56
+ def self.parse_bucket_by(bucket_by, diagnostics, feature_key)
57
57
  if bucket_by.is_a?(String)
58
58
  ["plain", [bucket_by]]
59
59
  elsif bucket_by.is_a?(Array)
@@ -61,7 +61,7 @@ module Featurevisor
61
61
  elsif bucket_by.is_a?(Hash) && bucket_by[:or].is_a?(Array)
62
62
  ["or", bucket_by[:or]]
63
63
  else
64
- logger.error("invalid bucketBy", { feature_key: feature_key, bucket_by: bucket_by })
64
+ diagnostics.error("invalid bucketBy", { feature_key: feature_key, bucket_by: bucket_by })
65
65
  raise StandardError, "invalid bucketBy"
66
66
  end
67
67
  end
@@ -78,7 +78,7 @@ module Featurevisor
78
78
  attribute_keys.each do |attribute_key|
79
79
  attribute_value = Featurevisor::Conditions.get_value_from_context(context, attribute_key)
80
80
 
81
- next if attribute_value.nil?
81
+ next if attribute_value.nil? && !context_path_exists?(context, attribute_key)
82
82
 
83
83
  if type == "plain" || type == "and"
84
84
  bucket_key << attribute_value
@@ -89,7 +89,61 @@ module Featurevisor
89
89
  end
90
90
 
91
91
  bucket_key << feature_key
92
- bucket_key
92
+ bucket_key.map { |value| javascript_string(value) }
93
93
  end
94
+
95
+ def self.context_path_exists?(context, path)
96
+ path.split(".").reduce(context) do |current, key|
97
+ return false unless current.is_a?(Hash)
98
+ return false unless current.key?(key.to_sym) || current.key?(key)
99
+
100
+ current.key?(key.to_sym) ? current[key.to_sym] : current[key]
101
+ end
102
+ true
103
+ end
104
+
105
+ def self.javascript_string(value)
106
+ return "" if value.nil?
107
+ return value ? "true" : "false" if value == true || value == false
108
+ if value.is_a?(Float)
109
+ return "NaN" if value.nan?
110
+ return value.positive? ? "Infinity" : "-Infinity" if value.infinite?
111
+ return "0" if value.zero?
112
+ return value.to_i.to_s if value == value.to_i && value.abs < 1e21
113
+ if value.abs >= 1e-6 && value.abs < 1e21
114
+ return scientific_to_fixed(value.to_s)
115
+ end
116
+
117
+ coefficient, exponent = value.to_s.downcase.split("e")
118
+ return "#{coefficient.delete_suffix('.0')}e#{Integer(exponent) >= 0 ? '+' : ''}#{Integer(exponent)}"
119
+ end
120
+ return value.map { |item| javascript_string(item) }.join(",") if value.is_a?(Array)
121
+ return "[object Object]" if value.is_a?(Hash)
122
+
123
+ value.to_s
124
+ end
125
+
126
+ def self.scientific_to_fixed(value)
127
+ return value unless value.downcase.include?("e")
128
+
129
+ coefficient, exponent_text = value.downcase.split("e")
130
+ exponent = Integer(exponent_text)
131
+ negative = coefficient.start_with?("-")
132
+ digits = coefficient.delete_prefix("-").delete(".")
133
+ decimal_position = coefficient.delete_prefix("-").index(".") || coefficient.delete_prefix("-").length
134
+ decimal_position += exponent
135
+
136
+ result = if decimal_position <= 0
137
+ "0.#{"0" * -decimal_position}#{digits}"
138
+ elsif decimal_position >= digits.length
139
+ "#{digits}#{"0" * (decimal_position - digits.length)}"
140
+ else
141
+ "#{digits[0...decimal_position]}.#{digits[decimal_position..]}"
142
+ end
143
+
144
+ result = result.sub(/(\.\d*?)0+\z/, '\1').delete_suffix(".")
145
+ negative ? "-#{result}" : result
146
+ end
147
+ private_class_method :scientific_to_fixed
94
148
  end
95
149
  end