both_is_good 0.4.0 → 0.5.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: 51b74df537030d143e21234302934cf0893cf9cb5e476c1eaf11d7451454b986
4
- data.tar.gz: bda32fe8c40fe83abcbb8e416f1f3e2ce4f465ec6f125b9f5a28264305df20c8
3
+ metadata.gz: 2d102cfa072abe3f151cdb8ab22d34735fe6652c44742f3cb97042f0f09e9696
4
+ data.tar.gz: cf94ded67dab441497ec2b6bd3d95c06b79a4adbecbe3b20f9e904c563d5df2a
5
5
  SHA512:
6
- metadata.gz: a368aa4dccf34706bbb6d0ca5b563cedd9af13920580775cf44cb7c5c135439e67b0b0fb306f6c9f0e467d21c19779ebb0c2ba69be15de89aec1a050c7e10fd7
7
- data.tar.gz: 5d682bcfd654cae15d627aa53a51c0819a73874b70c6336df0177177e75ad1478e5ecb06f17724ce8537b71f7bec15f35452ea2180fe23115b34e170093e5949
6
+ metadata.gz: 6eb5a6c598cd9fe76a7596a5c2012d61f04b3c9c7bf289624c8ee52f5f928329f2502e4710fa8023f917c215b44e76e9f94752f76ed6e3a0129e9f215c741ab0
7
+ data.tar.gz: 989cfb3caa9f459b869829388e5256da660c7d729604f0024cb295a9f634aa623822b89438e9a25fa78bc35e6bf5a9a7d46bfbf5bf71e6ecb93454666e4d879e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.4.0 (unreleased)
3
+ ## 0.5.0
4
+
5
+ ### Breaking changes
6
+
7
+ * All of the callables (except `on_hook_error`) are now passed a single
8
+ "context" object exposing the available data, instead of being yielded
9
+ complex lists of values depending on their arity. This also exposes
10
+ some additional data to those hooks for use.
11
+
12
+ ## 0.4.0
4
13
 
5
14
  ### Breaking changes
6
15
 
data/README.md CHANGED
@@ -33,12 +33,12 @@ implemented_twice(
33
33
  original: :foo_one,
34
34
  replacement: :foo_two,
35
35
  rate: 0.01,
36
- switch: ->(klass, name) { FeatureFlags.enabled?(:"enable_#{name}") },
36
+ switch: ->(ctx) { FeatureFlags.enabled?(:"enable_#{ctx.tag}") },
37
37
  comparator: ->(val_one, val_two) { Math.abs(val_one - val_two) < 0.01 },
38
- on_mismatch: ->(val_one, val_two) { LOGGER.warn("mismatch: #{val_one} | #{val_two}") },
39
- on_compare: ->(val_one, val_two) { LOGGER.warn("comparing #{val_one} to #{val_two}") },
40
- on_primary_error: ->(err, args) { LOGGER.warn("primary error #{err.class.name}") },
41
- on_secondary_error: ->(err, args) { LOGGER.warn("secondary error #{err.class.name}") },
38
+ on_mismatch: ->(ctx) { LOGGER.warn("mismatch: #{ctx.primary_result} | #{ctx.secondary_result}") },
39
+ on_compare: ->(ctx) { LOGGER.warn("comparing #{ctx.primary_result} to #{ctx.secondary_result}") },
40
+ on_primary_error: ->(ctx) { LOGGER.warn("primary error #{ctx.error.class.name}") },
41
+ on_secondary_error: ->(ctx) { LOGGER.warn("secondary error #{ctx.error.class.name}") },
42
42
  on_hook_error: ->(err) { LOGGER.warn("OH NO! #{err.class.name}: #{err.message}") }
43
43
  )
44
44
  ```
@@ -60,39 +60,35 @@ The method takes these parameters:
60
60
  should bother evaluating the shadow implementation for comparison. If the
61
61
  implementation is costly (makes significant database calls, for example)
62
62
  and/or invoked frequently, you probably want a lower rate in production.
63
- * The `switch:` parameter takes a callable with arity 0 or 2. When it returns
63
+ * The `switch:` parameter takes a callable with arity 0 or 1. When it returns
64
64
  a truthy value, the roles swap: replacement becomes the return value and
65
- original becomes the shadow (called at `rate` for comparison). Arity 2
66
- receives the target's actual class and the method name as a symbol, making
67
- it straightforward to drive from a feature-flag system:
68
-
69
- ```ruby
70
- switch: ->(klass, name) { FeatureFlags.enabled?(:"enable_#{klass}_#{name}") }
71
- ```
72
-
73
- `switch` can be set on a shared `Configuration` object so you only have to
74
- define it once.
65
+ original becomes the shadow (called at `rate` for comparison). Arity 1
66
+ receives a `BothIsGood::Context::Switching` object, making it straightforward
67
+ to drive from a feature-flag system. The context exposes `target_class`,
68
+ `method_name`, `target_class_name`, `target_class_string` (underscored,
69
+ like `"my_module/my_class"`), and `tag` (like `"my_mod/my_class--my_method"`)
75
70
  * The `comparator:` parameter takes a callable, and yields two arguments to
76
71
  it (the results of the two implementations); its result is truthy or falsey.
77
72
  By default, comparison is done using `==`.
78
- * The `on_mismatch:` parameter takes a callable (lambda or Proc generally).
79
- It supports arities 2, 3, and 4. The first two arguments are always the
80
- _primary_ result (the one being returned) and the _secondary_ result (the
81
- shadow). **When `switch` is active, these are `(replacement, original)`,
82
- not `(original, replacement)`.** Arity 3 also receives a names hash like
83
- `{primary: :foo_one, secondary: :foo_two}` reflecting the current role
84
- assignment. Arity 4 additionally receives the call args array before the
85
- names hash. It fires any time the results _differ_.
86
- * The `on_compare:` parameter takes the same shaped argument, but fires any
73
+ * The `on_mismatch:` parameter takes a callable that receives a
74
+ `BothIsGood::Context::Result`. It fires any time the results _differ_.
75
+ * The `on_compare:` parameter takes the same shaped callable, but fires any
87
76
  time both implementations are evaluated (every time unless `rate` is set).
88
- * The `on_primary_error:` parameter takes a callable and yields 1, 2, or 3
89
- arguments: the StandardError rescued, the args supplied (as an array,
90
- potentially with a Hash at the end for kwargs), and the name of the primary
91
- method. The exception will be re-raised after handling. With `switch`
92
- active, "primary" is the replacement method.
93
- * The `on_secondary_error:` parameter behaves identically (yielding the
94
- secondary method name), but secondary exceptions are _not_ re-raised.
95
- With `switch` active, "secondary" is the original method.
77
+
78
+ The result context exposes `primary_result`, `secondary_result`,
79
+ `primary_name`, `secondary_name`, `args`, `target_class`, `method_name`,
80
+ `target_class_name`, `target_class_string`, and `tag`. **When `switch` is
81
+ active, "primary" is the replacement and "secondary" is the original.**
82
+ * The `on_primary_error:` parameter takes a callable that receives a
83
+ `BothIsGood::Context::Error`. The exception will be re-raised after
84
+ handling. With `switch` active, "primary" is the replacement method.
85
+ * The `on_secondary_error:` parameter takes the same shaped callable, but
86
+ secondary exceptions are _not_ re-raised. With `switch` active, "secondary"
87
+ is the original method.
88
+
89
+ The error context exposes `error`, `args`, `dispatched_name` (the actual
90
+ method called - primary or secondary), `target_class`, `method_name`,
91
+ `target_class_name`, `target_class_string`, and `tag`.
96
92
  * The `on_hook_error:` parameter is a callable that will be yielded _one_
97
93
  parameter (the StandardError instance), and is invoked if an error is
98
94
  _raised_ during one of the other hooks. Those errors will be swallowed if
@@ -138,7 +134,7 @@ avoid having to supply them constantly.
138
134
  # Global configuration
139
135
  BothIsGood.configure do |config|
140
136
  config.rate = 0.5
141
- config.switch = ->(klass, name) { FeatureFlags.enabled?(:"enable_#{name}") }
137
+ config.switch = ->(ctx) { FeatureFlags.enabled?(:"enable_#{ctx.tag}") }
142
138
  config.on_compare = ->(a, b) { LOGGER.puts "compared!" }
143
139
  config.on_hook_error = ->(e) { LOGGER.puts "bad -.-" }
144
140
  end
@@ -34,27 +34,27 @@ module BothIsGood
34
34
  end
35
35
 
36
36
  def switch=(value)
37
- validate_hook!(:switch, value, [0, 2])
37
+ validate_hook!(:switch, value, [0, 1])
38
38
  @switch = value
39
39
  end
40
40
 
41
41
  def on_mismatch=(value)
42
- validate_hook!(:on_mismatch, value, [2, 3, 4])
42
+ validate_hook!(:on_mismatch, value, [1])
43
43
  @on_mismatch = value
44
44
  end
45
45
 
46
46
  def on_compare=(value)
47
- validate_hook!(:on_compare, value, [2, 3, 4])
47
+ validate_hook!(:on_compare, value, [1])
48
48
  @on_compare = value
49
49
  end
50
50
 
51
51
  def on_primary_error=(value)
52
- validate_hook!(:on_primary_error, value, [1, 2, 3])
52
+ validate_hook!(:on_primary_error, value, [1])
53
53
  @on_primary_error = value
54
54
  end
55
55
 
56
56
  def on_secondary_error=(value)
57
- validate_hook!(:on_secondary_error, value, [1, 2, 3])
57
+ validate_hook!(:on_secondary_error, value, [1])
58
58
  @on_secondary_error = value
59
59
  end
60
60
 
@@ -0,0 +1,25 @@
1
+ module BothIsGood
2
+ module Context
3
+ class Error
4
+ include BothIsGood::Memoization
5
+ include BothIsGood::Context::Names
6
+
7
+ attr_reader :error, :args, :dispatched_name
8
+
9
+ def initialize(target:, args:, error:, dispatched_name:)
10
+ @target = target
11
+ @args = args
12
+ @error = error
13
+ @dispatched_name = dispatched_name
14
+ end
15
+
16
+ def target_class = @target.target_class
17
+
18
+ def method_name = @target.method_name
19
+
20
+ memoize def target_class_name = target_class.name
21
+ memoize def target_class_string = class_to_tag(target_class)
22
+ memoize def tag = method_to_tag(target_class, method_name)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ module BothIsGood
2
+ module Context
3
+ module Names
4
+ def underscore(supplied_name)
5
+ supplied_name
6
+ .gsub("::", "/")
7
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
8
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
9
+ .downcase
10
+ end
11
+
12
+ def class_to_tag(klass) = underscore(klass.name)
13
+
14
+ def method_to_tag(klass, method_name) = "#{class_to_tag(klass)}--#{method_name}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ module BothIsGood
2
+ module Context
3
+ class Result
4
+ include BothIsGood::Memoization
5
+ include BothIsGood::Context::Names
6
+
7
+ attr_reader :args, :primary_result, :secondary_result
8
+
9
+ def initialize(target:, args:, primary_result:, secondary_result:, names:)
10
+ @target = target
11
+ @args = args
12
+ @primary_result = primary_result
13
+ @secondary_result = secondary_result
14
+ @names = names
15
+ end
16
+
17
+ memoize def target_class = @target.target_class
18
+
19
+ memoize def method_name = @target.method_name
20
+
21
+ memoize def primary_name = @names[:primary]
22
+
23
+ memoize def secondary_name = @names[:secondary]
24
+
25
+ memoize def target_class_name = target_class.name
26
+
27
+ memoize def target_class_string = class_to_tag(target_class)
28
+
29
+ memoize def tag = method_to_tag(target_class, method_name)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ module BothIsGood
2
+ module Context
3
+ class Switching
4
+ include BothIsGood::Memoization
5
+ include BothIsGood::Context::Names
6
+
7
+ def initialize(target_class, method_name)
8
+ @target_class = target_class
9
+ @method_name = method_name
10
+ end
11
+
12
+ attr_reader :target_class, :method_name
13
+
14
+ memoize def target_class_name = target_class.name
15
+
16
+ memoize def target_class_string = class_to_tag(target_class)
17
+
18
+ memoize def tag = method_to_tag(target_class, method_name)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ require_relative "context/names"
2
+ require_relative "context/switching"
3
+ require_relative "context/result"
4
+ require_relative "context/error"
@@ -24,7 +24,7 @@ module BothIsGood
24
24
  elsif @config.switch.arity == 0
25
25
  @config.switch.call
26
26
  else
27
- @config.switch.call(@target.target_class, @target.method_name)
27
+ @config.switch.call(BothIsGood::Context::Switching.new(@target.target_class, @target.method_name))
28
28
  end
29
29
  end
30
30
 
@@ -53,14 +53,14 @@ module BothIsGood
53
53
 
54
54
  def on_primary_error(error)
55
55
  hook = @config.on_primary_error
56
- with_hook_error_handling { invoke_error_hook(hook, error, primary) } if hook
56
+ with_hook_error_handling { hook.call(error_context(error, primary)) } if hook
57
57
  end
58
58
 
59
59
  def on_secondary_error(error)
60
60
  return unless @config.on_secondary_error
61
61
 
62
62
  with_hook_error_handling do
63
- invoke_error_hook(@config.on_secondary_error, error, secondary)
63
+ @config.on_secondary_error.call(error_context(error, secondary))
64
64
  end
65
65
  end
66
66
 
@@ -74,7 +74,7 @@ module BothIsGood
74
74
  return unless @config.on_compare
75
75
 
76
76
  with_hook_error_handling do
77
- invoke_result_hook(@config.on_compare, primary_result, secondary_result)
77
+ @config.on_compare.call(result_context)
78
78
  end
79
79
  end
80
80
 
@@ -82,7 +82,7 @@ module BothIsGood
82
82
  return unless @config.on_mismatch
83
83
 
84
84
  with_hook_error_handling do
85
- invoke_result_hook(@config.on_mismatch, primary_result, secondary_result)
85
+ @config.on_mismatch.call(result_context)
86
86
  end
87
87
  end
88
88
 
@@ -104,20 +104,23 @@ module BothIsGood
104
104
  memoize def names = {primary:, secondary:}
105
105
  memoize def call_args = @kwargs.empty? ? @args : [*@args, @kwargs]
106
106
 
107
- def invoke_result_hook(hook, primary_result, secondary_result)
108
- case hook.arity
109
- when 2 then hook.call(primary_result, secondary_result)
110
- when 3 then hook.call(primary_result, secondary_result, names)
111
- else hook.call(primary_result, secondary_result, call_args, names)
112
- end
107
+ memoize def result_context
108
+ BothIsGood::Context::Result.new(
109
+ target: @target,
110
+ args: call_args,
111
+ primary_result:,
112
+ secondary_result:,
113
+ names:
114
+ )
113
115
  end
114
116
 
115
- def invoke_error_hook(hook, error, method_name)
116
- case hook.arity
117
- when 1 then hook.call(error)
118
- when 2 then hook.call(error, call_args)
119
- else hook.call(error, call_args, method_name)
120
- end
117
+ def error_context(error, dispatched_name)
118
+ BothIsGood::Context::Error.new(
119
+ target: @target,
120
+ args: call_args,
121
+ error:,
122
+ dispatched_name:
123
+ )
121
124
  end
122
125
  end
123
126
  end
@@ -1,3 +1,3 @@
1
1
  module BothIsGood
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/both_is_good.rb CHANGED
@@ -105,6 +105,7 @@ module BothIsGood
105
105
  end
106
106
  end
107
107
 
108
+ require_relative "both_is_good/context"
108
109
  require_relative "both_is_good/version"
109
110
  require_relative "both_is_good/configuration"
110
111
  require_relative "both_is_good/local_configuration"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: both_is_good
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Mueller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-01 00:00:00.000000000 Z
11
+ date: 2026-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -132,6 +132,11 @@ files:
132
132
  - both_is_good.gemspec
133
133
  - lib/both_is_good.rb
134
134
  - lib/both_is_good/configuration.rb
135
+ - lib/both_is_good/context.rb
136
+ - lib/both_is_good/context/error.rb
137
+ - lib/both_is_good/context/names.rb
138
+ - lib/both_is_good/context/result.rb
139
+ - lib/both_is_good/context/switching.rb
135
140
  - lib/both_is_good/implemented_twice.rb
136
141
  - lib/both_is_good/invocation.rb
137
142
  - lib/both_is_good/local_configuration.rb