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 +4 -4
- data/CHANGELOG.md +10 -1
- data/README.md +30 -34
- data/lib/both_is_good/configuration.rb +5 -5
- data/lib/both_is_good/context/error.rb +25 -0
- data/lib/both_is_good/context/names.rb +17 -0
- data/lib/both_is_good/context/result.rb +32 -0
- data/lib/both_is_good/context/switching.rb +21 -0
- data/lib/both_is_good/context.rb +4 -0
- data/lib/both_is_good/invocation.rb +20 -17
- data/lib/both_is_good/version.rb +1 -1
- data/lib/both_is_good.rb +1 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d102cfa072abe3f151cdb8ab22d34735fe6652c44742f3cb97042f0f09e9696
|
|
4
|
+
data.tar.gz: cf94ded67dab441497ec2b6bd3d95c06b79a4adbecbe3b20f9e904c563d5df2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6eb5a6c598cd9fe76a7596a5c2012d61f04b3c9c7bf289624c8ee52f5f928329f2502e4710fa8023f917c215b44e76e9f94752f76ed6e3a0129e9f215c741ab0
|
|
7
|
+
data.tar.gz: 989cfb3caa9f459b869829388e5256da660c7d729604f0024cb295a9f634aa623822b89438e9a25fa78bc35e6bf5a9a7d46bfbf5bf71e6ecb93454666e4d879e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
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: ->(
|
|
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: ->(
|
|
39
|
-
on_compare: ->(
|
|
40
|
-
on_primary_error: ->(
|
|
41
|
-
on_secondary_error: ->(
|
|
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
|
|
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
|
|
66
|
-
receives
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
79
|
-
It
|
|
80
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
active, "primary" is the replacement
|
|
93
|
-
* The `
|
|
94
|
-
|
|
95
|
-
With `switch` active, "
|
|
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 = ->(
|
|
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,
|
|
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, [
|
|
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, [
|
|
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
|
|
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
|
|
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
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
data/lib/both_is_good/version.rb
CHANGED
data/lib/both_is_good.rb
CHANGED
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
|
+
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-
|
|
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
|