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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 202758e9fe9a2c81f67c3ea7456be5d068c1d9aa8ccf35f721a4ca447a94196a
4
- data.tar.gz: bf2cae186ce5b6d3b483f4eab14768553cc5d2e09148c16ebbaf3ac60af7d7a0
3
+ metadata.gz: e7745fb77b8646de51fe54b58c32274829b60b046e6a47d81872e3148107eac6
4
+ data.tar.gz: ef5f69f50e1af01af4b45aece70603a67dc77f3af08da98b10cf6e50dd3d77df
5
5
  SHA512:
6
- metadata.gz: aa565646ae2e3b54d018044a79e92d9172e0ac8478dbb2dbee1699605a9457fbea6b694dc42c9027b40d4d2a3358c3146be946cfe307319119ab6ba2d03657ed
7
- data.tar.gz: e8e689efcc96ac4ca3bd0ec7469895fbb1c9b4e70737d0f56f87fa9f0733395c0e0de6938a7b6c7c4c6bd7daef9f9818daa9163e508b0242836b750aa83e82f4
6
+ metadata.gz: 745e81a4af47bc19e0a0bfe550badafb52e7541a067f461bdbc8b73be259399da5ca3fd519e7458ee0afa3ee6f810e0d7e28446add35ee13e970f25ecc7a0d2d
7
+ data.tar.gz: 44947e427ceb1cf6b87e2e0784e7c77dddcbd8e27677c6794dd885a3a83dec65a4a2a1cec1815198abbe3e47187399700e9ec7e4342fd729428fafb21ab2dadd
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
@@ -89,6 +89,8 @@ f = Featurevisor.create_featurevisor(
89
89
 
90
90
  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
91
 
92
+ 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`, `add_module`, `remove_module`, and `close`. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
93
+
92
94
  ## Initialization
93
95
 
94
96
  The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
@@ -697,6 +699,8 @@ f.remove_module('my-custom-module')
697
699
 
698
700
  ## Child instance
699
701
 
702
+ 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.
703
+
700
704
  When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
701
705
 
702
706
  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.
@@ -722,8 +726,11 @@ Similar to parent SDK, child instances also support several additional methods:
722
726
 
723
727
  - `set_context`
724
728
  - `set_sticky`
729
+ - `evaluate_flag`
725
730
  - `is_enabled`
731
+ - `evaluate_variation`
726
732
  - `get_variation`
733
+ - `evaluate_variable`
727
734
  - `get_variable`
728
735
  - `get_variable_boolean`
729
736
  - `get_variable_string`
@@ -821,7 +828,7 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
821
828
  Install the provider:
822
829
 
823
830
  ```ruby
824
- gem "featurevisor-openfeature", "~> 1.1"
831
+ gem "featurevisor-openfeature", "~> 2.0"
825
832
  ```
826
833
 
827
834
  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.
@@ -901,7 +908,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
901
908
  - Run `bundle install`
902
909
  - Push commit to `main` branch
903
910
  - Wait for CI to complete
904
- - Tag the release with the same version number, for example `v1.1.0`
911
+ - Tag the release with the same version number, for example `v2.0.0`
905
912
  - The workflow verifies that the tag matches the shared version
906
913
  - The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
907
914
 
data/bin/commands/test.rb CHANGED
@@ -671,7 +671,7 @@ module FeaturevisorCLI
671
671
 
672
672
  if assertion.key?(:expectedToMatch)
673
673
  expected_to_match = assertion[:expectedToMatch]
674
- actual = instance.instance_variable_get(:@datafile_reader).all_conditions_are_matched(conditions, context)
674
+ actual = instance.instance_variable_get(:@datafile).all_conditions_are_matched(conditions, context)
675
675
 
676
676
  if actual != expected_to_match
677
677
  has_error = true
@@ -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
@@ -13,6 +13,7 @@ module Featurevisor
13
13
  @context = options[:context] || {}
14
14
  @sticky = options[:sticky] || {}
15
15
  @emitter = Featurevisor::Emitter.new
16
+ @parent_unsubscribers = []
16
17
  end
17
18
 
18
19
  # Subscribe to an event
@@ -25,12 +26,25 @@ module Featurevisor
25
26
  if event_name == "context_set" || event_name == "sticky_set"
26
27
  @emitter.on(event_name, callback)
27
28
  else
28
- @parent.on(event_name, callback)
29
+ parent_unsubscribe = @parent.on(event_name, callback)
30
+ active = true
31
+ unsubscribe = nil
32
+ unsubscribe = proc do
33
+ next unless active
34
+
35
+ active = false
36
+ parent_unsubscribe.call
37
+ @parent_unsubscribers.delete(unsubscribe)
38
+ end
39
+ @parent_unsubscribers << unsubscribe
40
+ unsubscribe
29
41
  end
30
42
  end
31
43
 
32
44
  # Close the child instance
33
45
  def close
46
+ @parent_unsubscribers.dup.each(&:call)
47
+ @parent_unsubscribers.clear
34
48
  @emitter.clear_all
35
49
  end
36
50
 
@@ -98,6 +112,15 @@ module Featurevisor
98
112
  )
99
113
  end
100
114
 
115
+ # Evaluate a feature flag and return its full evaluation details.
116
+ def evaluate_flag(feature_key, context = {}, options = {})
117
+ @parent.evaluate_flag(
118
+ feature_key,
119
+ { **@context, **context },
120
+ { **options, __featurevisor_child_sticky: @sticky }
121
+ )
122
+ end
123
+
101
124
  # Get variation value
102
125
  # @param feature_key [String] Feature key
103
126
  # @param context [Hash] Context
@@ -117,6 +140,15 @@ module Featurevisor
117
140
  )
118
141
  end
119
142
 
143
+ # Evaluate a variation and return its full evaluation details.
144
+ def evaluate_variation(feature_key, context = {}, options = {})
145
+ @parent.evaluate_variation(
146
+ feature_key,
147
+ { **@context, **context },
148
+ { **options, __featurevisor_child_sticky: @sticky }
149
+ )
150
+ end
151
+
120
152
  # Get variable value
121
153
  # @param feature_key [String] Feature key
122
154
  # @param variable_key [String] Variable key
@@ -138,6 +170,16 @@ module Featurevisor
138
170
  )
139
171
  end
140
172
 
173
+ # Evaluate a variable and return its full evaluation details.
174
+ def evaluate_variable(feature_key, variable_key, context = {}, options = {})
175
+ @parent.evaluate_variable(
176
+ feature_key,
177
+ variable_key,
178
+ { **@context, **context },
179
+ { **options, __featurevisor_child_sticky: @sticky }
180
+ )
181
+ end
182
+
141
183
  # Get variable as boolean
142
184
  # @param feature_key [String] Feature key
143
185
  # @param variable_key [String] Variable key
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "date"
4
+ require "time"
4
5
 
5
6
  module Featurevisor
6
7
  # Conditions module for evaluating feature flags and segments
7
8
  module Conditions
9
+ MISSING = Object.new.freeze
10
+ private_constant :MISSING
11
+
8
12
  # Get value from context object using dot notation path
9
13
  # @param obj [Hash] Context object
10
14
  # @param path [String] Dot-separated path to the value
@@ -12,12 +16,36 @@ module Featurevisor
12
16
  def self.get_value_from_context(obj, path)
13
17
  return nil if obj.nil? || path.nil?
14
18
 
15
- if path.index(".") == -1
16
- return obj[path.to_sym] || obj[path]
19
+ value = get_value_with_presence(obj, path)
20
+ value.equal?(MISSING) ? nil : value
21
+ end
22
+
23
+ def self.get_value_with_presence(obj, path)
24
+ return MISSING unless obj.is_a?(Hash) && path.is_a?(String)
25
+
26
+ path.split(".").reduce(obj) do |current, key|
27
+ break MISSING unless current.is_a?(Hash)
28
+
29
+ if current.key?(key.to_sym)
30
+ current[key.to_sym]
31
+ elsif current.key?(key)
32
+ current[key]
33
+ else
34
+ break MISSING
35
+ end
17
36
  end
37
+ end
38
+ private_class_method :get_value_with_presence
39
+
40
+ def self.strict_equal?(left, right)
41
+ return true if left.nil? && right.nil?
42
+ return left.to_f == right.to_f if left.is_a?(Numeric) && right.is_a?(Numeric)
43
+ return left == right if left.is_a?(String) && right.is_a?(String)
44
+ return left == right if (left == true || left == false) && (right == true || right == false)
18
45
 
19
- path.split(".").reduce(obj) { |o, i| o&.[](i.to_sym) || o&.[](i) }
46
+ false
20
47
  end
48
+ private_class_method :strict_equal?
21
49
 
22
50
  # Check if a condition is matched against context
23
51
  # @param condition [Hash] Condition to evaluate
@@ -30,18 +58,19 @@ module Featurevisor
30
58
  value = condition["value"] || condition[:value]
31
59
  regex_flags = condition["regexFlags"] || condition[:regexFlags]
32
60
 
33
- context_value_from_path = get_value_from_context(context, attribute)
61
+ raw_context_value = get_value_with_presence(context, attribute)
62
+ context_value_from_path = raw_context_value.equal?(MISSING) ? nil : raw_context_value
63
+ attribute_exists = !raw_context_value.equal?(MISSING)
34
64
 
35
65
  case operator
36
66
  when "equals"
37
- context_value_from_path == value
67
+ attribute_exists && strict_equal?(context_value_from_path, value)
38
68
  when "notEquals"
39
- context_value_from_path != value
69
+ !attribute_exists || !strict_equal?(context_value_from_path, value)
40
70
  when "before", "after"
41
- # date comparisons
42
- value_in_context = context_value_from_path
43
- date_in_context = value_in_context.is_a?(Date) ? value_in_context : Date.parse(value_in_context.to_s)
44
- date_in_condition = value.is_a?(Date) ? value : Date.parse(value.to_s)
71
+ date_in_context = portable_date(context_value_from_path)
72
+ date_in_condition = portable_date(value)
73
+ return false unless date_in_context && date_in_condition
45
74
 
46
75
  if operator == "before"
47
76
  date_in_context < date_in_condition
@@ -50,21 +79,13 @@ module Featurevisor
50
79
  end
51
80
  when "in", "notIn"
52
81
  # in / notIn (where condition value is an array)
53
- if value.is_a?(Array) && (context_value_from_path.is_a?(String) || context_value_from_path.is_a?(Numeric) || context_value_from_path.nil?)
54
- # Check if the attribute key actually exists in the context
55
- key_exists = context.key?(attribute.to_sym) || context.key?(attribute.to_s)
56
-
57
- # If key doesn't exist, notIn should fail (return false), in should also fail
58
- if !key_exists
59
- return false
60
- end
61
-
62
- value_in_context = context_value_from_path.to_s
63
-
82
+ if attribute_exists && value.is_a?(Array) &&
83
+ (context_value_from_path.is_a?(String) || context_value_from_path.is_a?(Numeric) || context_value_from_path.nil?)
84
+ matched = value.any? { |candidate| strict_equal?(candidate, context_value_from_path) }
64
85
  if operator == "in"
65
- value.include?(value_in_context)
86
+ matched
66
87
  else # notIn
67
- !value.include?(value_in_context)
88
+ !matched
68
89
  end
69
90
  else
70
91
  false
@@ -124,18 +145,18 @@ module Featurevisor
124
145
  false
125
146
  end
126
147
  when "exists"
127
- context_value_from_path != nil
148
+ attribute_exists
128
149
  when "notExists"
129
- context_value_from_path.nil?
150
+ !attribute_exists
130
151
  when "includes", "notIncludes"
131
152
  # includes / notIncludes (where context value is an array)
132
- if context_value_from_path.is_a?(Array) && value.is_a?(String)
133
- value_in_context = context_value_from_path
134
-
153
+ if context_value_from_path.is_a?(Array) &&
154
+ (value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false || value.nil?)
155
+ matched = context_value_from_path.any? { |candidate| strict_equal?(candidate, value) }
135
156
  if operator == "includes"
136
- value_in_context.include?(value)
157
+ matched
137
158
  else # notIncludes
138
- !value_in_context.include?(value)
159
+ !matched
139
160
  end
140
161
  else
141
162
  false
@@ -143,10 +164,18 @@ module Featurevisor
143
164
  else
144
165
  false
145
166
  end
146
- rescue => e
147
- # Log error but don't stop execution
148
- warn "Error in condition evaluation: #{e.message}"
149
- false
150
167
  end
168
+
169
+ def self.portable_date(value)
170
+ return value if value.is_a?(Time) || value.is_a?(DateTime)
171
+ return value.to_time if value.is_a?(Date)
172
+ return nil unless value.is_a?(String)
173
+ return nil unless value.match?(/T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:\d{2})\z/)
174
+
175
+ Time.iso8601(value)
176
+ rescue ArgumentError
177
+ nil
178
+ end
179
+ private_class_method :portable_date
151
180
  end
152
181
  end
@@ -1,21 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Featurevisor
4
- # Log levels for the logger
4
+ # Diagnostic severity levels
5
5
  LOG_LEVELS = %w[fatal error warn info debug].freeze
6
6
  DEFAULT_LOG_LEVEL = "info".freeze
7
- LOGGER_PREFIX = "[Featurevisor]".freeze
7
+ DIAGNOSTIC_PREFIX = "[Featurevisor]".freeze
8
8
 
9
- # Logger class for handling different log levels
10
- class Logger
9
+ # Private evaluator adapter for structured diagnostics.
10
+ class DiagnosticReporter
11
11
  attr_reader :level, :handler
12
12
 
13
- # Initialize a new logger
14
- # @param options [Hash] Logger options
13
+ # Initialize a diagnostic reporter.
14
+ # @param options [Hash] Reporter options
15
15
  # @option options [String] :level Log level (default: "info")
16
- # @option options [Proc] :handler Custom log handler (default: default_log_handler)
16
+ # @option options [Proc] :handler Internal structured diagnostic sink
17
17
  def initialize(options = {})
18
18
  @level = options[:level] || DEFAULT_LOG_LEVEL
19
+ @filter = !options.key?(:handler)
19
20
  @handler = options[:handler] || method(:default_log_handler)
20
21
  end
21
22
 
@@ -25,12 +26,13 @@ module Featurevisor
25
26
  @level = level
26
27
  end
27
28
 
28
- # Log a message at a specific level
29
- # @param level [String] Log level
30
- # @param message [String] Log message
29
+ # Forward an evaluator diagnostic. Filtering is performed centrally by
30
+ # Instance so module subscriptions and error events remain independent.
31
+ # @param level [String] Diagnostic level
32
+ # @param message [String] Diagnostic message
31
33
  # @param details [Hash, nil] Additional details
32
34
  def log(level, message, details = nil)
33
- return unless should_handle?(level)
35
+ return if @filter && !should_handle?(level)
34
36
 
35
37
  @handler.call(level, message, details)
36
38
  end
@@ -72,16 +74,8 @@ module Featurevisor
72
74
 
73
75
  private
74
76
 
75
- # Check if the current level should handle the given log level
76
- # @param log_level [String] Log level to check
77
- # @return [Boolean] True if should handle
78
- def should_handle?(log_level)
79
- current_index = LOG_LEVELS.index(@level)
80
- target_index = LOG_LEVELS.index(log_level)
81
-
82
- return false if current_index.nil? || target_index.nil?
83
-
84
- current_index >= target_index
77
+ def should_handle?(level)
78
+ LOG_LEVELS.index(level).to_i <= LOG_LEVELS.index(@level).to_i
85
79
  end
86
80
 
87
81
  # Default log handler that outputs to console
@@ -99,45 +93,18 @@ module Featurevisor
99
93
  case method_name
100
94
  when "puts"
101
95
  if details && !details.empty?
102
- Kernel.puts("#{LOGGER_PREFIX} #{message} #{details.inspect}")
96
+ Kernel.puts("#{DIAGNOSTIC_PREFIX} #{message} #{details.inspect}")
103
97
  else
104
- Kernel.puts("#{LOGGER_PREFIX} #{message}")
98
+ Kernel.puts("#{DIAGNOSTIC_PREFIX} #{message}")
105
99
  end
106
100
  when "warn"
107
101
  if details && !details.empty?
108
- Kernel.warn("#{LOGGER_PREFIX} #{message} #{details.inspect}")
102
+ Kernel.warn("#{DIAGNOSTIC_PREFIX} #{message} #{details.inspect}")
109
103
  else
110
- Kernel.warn("#{LOGGER_PREFIX} #{message}")
104
+ Kernel.warn("#{DIAGNOSTIC_PREFIX} #{message}")
111
105
  end
112
106
  end
113
107
  end
114
108
  end
115
109
 
116
- # Default log handler function
117
- # @param level [String] Log level
118
- # @param message [String] Log message
119
- # @param details [Hash, nil] Additional details
120
- def self.default_log_handler(level, message, details = nil)
121
- method_name = case level
122
- when "info" then "puts"
123
- when "warn" then "warn"
124
- when "error", "fatal" then "warn"
125
- else "puts"
126
- end
127
-
128
- case method_name
129
- when "puts"
130
- if details && !details.empty?
131
- Kernel.puts("#{LOGGER_PREFIX} #{message} #{details.inspect}")
132
- else
133
- Kernel.puts("#{LOGGER_PREFIX} #{message}")
134
- end
135
- when "warn"
136
- if details && !details.empty?
137
- Kernel.warn("#{LOGGER_PREFIX} #{message} #{details.inspect}")
138
- else
139
- Kernel.warn("#{LOGGER_PREFIX} #{message}")
140
- end
141
- end
142
- end
143
110
  end