both_is_good 0.3.0 → 0.4.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 +20 -0
- data/README.md +86 -68
- data/lib/both_is_good/configuration.rb +6 -0
- data/lib/both_is_good/implemented_twice.rb +2 -2
- data/lib/both_is_good/invocation.rb +14 -4
- data/lib/both_is_good/local_configuration.rb +5 -5
- data/lib/both_is_good/target.rb +3 -0
- data/lib/both_is_good/version.rb +1 -1
- data/lib/both_is_good.rb +26 -25
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51b74df537030d143e21234302934cf0893cf9cb5e476c1eaf11d7451454b986
|
|
4
|
+
data.tar.gz: bda32fe8c40fe83abcbb8e416f1f3e2ce4f465ec6f125b9f5a28264305df20c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a368aa4dccf34706bbb6d0ca5b563cedd9af13920580775cf44cb7c5c135439e67b0b0fb306f6c9f0e467d21c19779ebb0c2ba69be15de89aec1a050c7e10fd7
|
|
7
|
+
data.tar.gz: 5d682bcfd654cae15d627aa53a51c0819a73874b70c6336df0177177e75ad1478e5ecb06f17724ce8537b71f7bec15f35452ea2180fe23115b34e170093e5949
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
### Breaking changes
|
|
6
|
+
|
|
7
|
+
* `primary:` and `secondary:` are renamed to `original:` and `replacement:`
|
|
8
|
+
throughout the public API.
|
|
9
|
+
* The internal alias prefixes change from
|
|
10
|
+
`_bothisgood_primary_` / `_bothisgood_secondary_` to
|
|
11
|
+
`_bothisgood_original_` / `_bothisgood_replacement_`.
|
|
12
|
+
|
|
13
|
+
### New features
|
|
14
|
+
|
|
15
|
+
* Added `switch:` parameter, for swapping the implementation based on
|
|
16
|
+
feature-flags.
|
|
17
|
+
|
|
18
|
+
## 0.3.1
|
|
19
|
+
|
|
20
|
+
* Allow the supplied `original` / `replacement` methods to be private.
|
data/README.md
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# BothIsGood
|
|
2
2
|
|
|
3
|
-
This gem adds a module to include into classes, supplying a convenient, concise
|
|
4
|
-
to implement multiple versions of the same method, and _run them both_. Then
|
|
5
|
-
can still _use_ the old implementation, but get an alert or log message if
|
|
6
|
-
version ever produces a different result.
|
|
3
|
+
This gem adds a module to include into classes, supplying a convenient, concise
|
|
4
|
+
way to implement multiple versions of the same method, and _run them both_. Then
|
|
5
|
+
you can still _use_ the old implementation, but get an alert or log message if
|
|
6
|
+
the new version ever produces a different result.
|
|
7
7
|
|
|
8
|
-
This is not a new concept; `scientist` pioneered the approach in 2016. But
|
|
9
|
-
is moderately _heavy_, and takes significant effort to use, so I've
|
|
10
|
-
lightweight dual-implementation libraries multiple times;
|
|
11
|
-
it so I won't have to do so again later!
|
|
8
|
+
This is not a new concept; `scientist` pioneered the approach in 2016. But
|
|
9
|
+
scientist is moderately _heavy_, and takes significant effort to use, so I've
|
|
10
|
+
ended up implementing lightweight dual-implementation libraries multiple times;
|
|
11
|
+
this time I'm publishing it so I won't have to do so again later!
|
|
12
12
|
|
|
13
13
|
## Inline Invocation
|
|
14
14
|
|
|
15
|
-
The "simplest" way to use BothIsGood is 'inline' - no configuration object,
|
|
16
|
-
supply all of the needed options on the `implemented_twice` call in
|
|
15
|
+
The "simplest" way to use BothIsGood is 'inline' - no configuration object,
|
|
16
|
+
you just supply all of the needed options on the `implemented_twice` call in
|
|
17
|
+
place.
|
|
17
18
|
|
|
18
19
|
```ruby
|
|
19
20
|
include BothIsGood
|
|
@@ -21,21 +22,23 @@ include BothIsGood
|
|
|
21
22
|
def foo_one = implementation(details)
|
|
22
23
|
def foo_two = more_implementation(details)
|
|
23
24
|
|
|
24
|
-
# A minimal call. Note that with no global configuration this is not very
|
|
25
|
-
# since if the implementations disagree, there's no hook implemented
|
|
26
|
-
|
|
25
|
+
# A minimal call. Note that with no global configuration this is not very
|
|
26
|
+
# valuable, since if the implementations disagree, there's no hook implemented
|
|
27
|
+
# to _tell you_ that.
|
|
28
|
+
implemented_twice(:foo, original: :foo_one, replacement: :foo_two)
|
|
27
29
|
|
|
28
30
|
# A complex call using all of the available options:
|
|
29
31
|
implemented_twice(
|
|
30
32
|
:foo,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
original: :foo_one,
|
|
34
|
+
replacement: :foo_two,
|
|
33
35
|
rate: 0.01,
|
|
36
|
+
switch: ->(klass, name) { FeatureFlags.enabled?(:"enable_#{name}") },
|
|
34
37
|
comparator: ->(val_one, val_two) { Math.abs(val_one - val_two) < 0.01 },
|
|
35
|
-
on_mismatch: ->(val_one, val_two) { LOGGER.warn("
|
|
38
|
+
on_mismatch: ->(val_one, val_two) { LOGGER.warn("mismatch: #{val_one} | #{val_two}") },
|
|
36
39
|
on_compare: ->(val_one, val_two) { LOGGER.warn("comparing #{val_one} to #{val_two}") },
|
|
37
|
-
on_primary_error: ->(err, args) { LOGGER.warn("
|
|
38
|
-
on_secondary_error: ->(err, args) { LOGGER.warn("
|
|
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}") },
|
|
39
42
|
on_hook_error: ->(err) { LOGGER.warn("OH NO! #{err.class.name}: #{err.message}") }
|
|
40
43
|
)
|
|
41
44
|
```
|
|
@@ -43,48 +46,63 @@ implemented_twice(
|
|
|
43
46
|
The method takes these parameters:
|
|
44
47
|
|
|
45
48
|
* The (only) positional parameter is the name of the method it will implement.
|
|
46
|
-
This _can_ match the
|
|
47
|
-
and if it does, `implemented_twice` will alias the existing method out of
|
|
48
|
-
way (to `
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
This _can_ match the `original:` or `replacement:` name (but not both),
|
|
50
|
+
and if it does, `implemented_twice` will alias the existing method out of
|
|
51
|
+
the way (to `_bothisgood_original_#{name}` or
|
|
52
|
+
`_bothisgood_replacement_#{name}`).
|
|
53
|
+
* The `original:` parameter specifies a method name that will be called _and
|
|
54
|
+
have its result used as the return value_ regardless of the comparison
|
|
55
|
+
outcome. Errors from the original method are bubbled up as usual.
|
|
56
|
+
* The `replacement:` parameter specifies a method name that will be called for
|
|
57
|
+
comparison's sake (though not necessarily every time). Errors raised from
|
|
58
|
+
the replacement method are swallowed.
|
|
59
|
+
* The `rate:` parameter (default 1.0) specifies what fraction of the calls
|
|
60
|
+
should bother evaluating the shadow implementation for comparison. If the
|
|
61
|
+
implementation is costly (makes significant database calls, for example)
|
|
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
|
|
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.
|
|
75
|
+
* The `comparator:` parameter takes a callable, and yields two arguments to
|
|
76
|
+
it (the results of the two implementations); its result is truthy or falsey.
|
|
61
77
|
By default, comparison is done using `==`.
|
|
62
|
-
* The `on_mismatch:` parameter takes a callable (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
87
|
+
time both implementations are evaluated (every time unless `rate` is set).
|
|
70
88
|
* The `on_primary_error:` parameter takes a callable and yields 1, 2, or 3
|
|
71
|
-
arguments
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
primary
|
|
75
|
-
* The `on_secondary_error:` parameter behaves identically (yielding the
|
|
76
|
-
method name), but secondary exceptions are _not_ re-raised.
|
|
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
96
|
* The `on_hook_error:` parameter is a callable that will be yielded _one_
|
|
78
|
-
parameter (the StandardError instance), and is invoked if an error is
|
|
79
|
-
during one of the other hooks.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
bubbled otherwise.
|
|
97
|
+
parameter (the StandardError instance), and is invoked if an error is
|
|
98
|
+
_raised_ during one of the other hooks. Those errors will be swallowed if
|
|
99
|
+
`on_hook_error` is supplied (unless your hook re-raises!), and bubbled
|
|
100
|
+
otherwise.
|
|
83
101
|
|
|
84
|
-
`implemented_twice` can additionally be called with three positional
|
|
85
|
-
the second
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
`implemented_twice` can additionally be called with three positional
|
|
103
|
+
parameters; the second is used as the `original` method name, and the third
|
|
104
|
+
as `replacement`. That means that, if you use a configuration object, you can
|
|
105
|
+
just:
|
|
88
106
|
|
|
89
107
|
```ruby
|
|
90
108
|
include BothIsGood
|
|
@@ -92,12 +110,12 @@ include BothIsGood
|
|
|
92
110
|
def foo_one = implementation(details)
|
|
93
111
|
def foo_two = more_implementation(details)
|
|
94
112
|
|
|
95
|
-
# defines `foo`, using `foo_one` as
|
|
113
|
+
# defines `foo`, using `foo_one` as original and `foo_two` as replacement.
|
|
96
114
|
implemented_twice :foo, :foo_one, :foo_two
|
|
97
115
|
```
|
|
98
116
|
|
|
99
|
-
If
|
|
100
|
-
|
|
117
|
+
If called with _two_ positional parameters, the first is used as both the
|
|
118
|
+
final method name _and_ the original implementation.
|
|
101
119
|
|
|
102
120
|
```ruby
|
|
103
121
|
include BothIsGood
|
|
@@ -105,21 +123,22 @@ include BothIsGood
|
|
|
105
123
|
def foo = implementation(details)
|
|
106
124
|
def foo_two = more_implementation(details)
|
|
107
125
|
|
|
108
|
-
# Defines `foo`, using `foo` as
|
|
109
|
-
#
|
|
126
|
+
# Defines `foo`, using `foo` as original and `foo_two` as replacement.
|
|
127
|
+
# The original `foo` method is aliased to `_bothisgood_original_foo`.
|
|
110
128
|
implemented_twice :foo, :foo_two
|
|
111
129
|
```
|
|
112
130
|
|
|
113
131
|
## Configuration
|
|
114
132
|
|
|
115
|
-
All
|
|
116
|
-
can be configured globally, or onto a BothIsGood::Configuration object, to
|
|
117
|
-
having to supply them constantly.
|
|
133
|
+
All parameters aside from the positional, `original:`, and `replacement:` ones
|
|
134
|
+
can be configured globally, or onto a `BothIsGood::Configuration` object, to
|
|
135
|
+
avoid having to supply them constantly.
|
|
118
136
|
|
|
119
137
|
```ruby
|
|
120
138
|
# Global configuration
|
|
121
139
|
BothIsGood.configure do |config|
|
|
122
140
|
config.rate = 0.5
|
|
141
|
+
config.switch = ->(klass, name) { FeatureFlags.enabled?(:"enable_#{name}") }
|
|
123
142
|
config.on_compare = ->(a, b) { LOGGER.puts "compared!" }
|
|
124
143
|
config.on_hook_error = ->(e) { LOGGER.puts "bad -.-" }
|
|
125
144
|
end
|
|
@@ -127,16 +146,15 @@ end
|
|
|
127
146
|
# Local configuration - starting values are taken from the global config
|
|
128
147
|
MY_BIG_CONFIG = BothIsGood::Configuration.new
|
|
129
148
|
MY_BIG_CONFIG.rate = 0.7
|
|
130
|
-
MY_BIG_CONFIG.on_secondary_error = ->(
|
|
149
|
+
MY_BIG_CONFIG.on_secondary_error = ->(e) { LOGGER.puts "No" }
|
|
131
150
|
|
|
132
151
|
module MyFoo
|
|
133
152
|
include BothIsGood
|
|
134
153
|
self.both_is_good_configure(MY_BIG_CONFIG)
|
|
135
154
|
end
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
#
|
|
139
|
-
# supplied config object if one is given.
|
|
156
|
+
# In-class configuration - starting values are taken from the global config,
|
|
157
|
+
# or the supplied config object if one is given.
|
|
140
158
|
module MyBar
|
|
141
159
|
include BothIsGood
|
|
142
160
|
self.both_is_good_configure(rate: 0.02)
|
|
@@ -2,6 +2,7 @@ module BothIsGood
|
|
|
2
2
|
class Configuration
|
|
3
3
|
DEFAULTS = {
|
|
4
4
|
rate: 1.0,
|
|
5
|
+
switch: nil,
|
|
5
6
|
on_mismatch: nil,
|
|
6
7
|
on_compare: nil,
|
|
7
8
|
on_primary_error: nil,
|
|
@@ -32,6 +33,11 @@ module BothIsGood
|
|
|
32
33
|
@rate = value
|
|
33
34
|
end
|
|
34
35
|
|
|
36
|
+
def switch=(value)
|
|
37
|
+
validate_hook!(:switch, value, [0, 2])
|
|
38
|
+
@switch = value
|
|
39
|
+
end
|
|
40
|
+
|
|
35
41
|
def on_mismatch=(value)
|
|
36
42
|
validate_hook!(:on_mismatch, value, [2, 3, 4])
|
|
37
43
|
@on_mismatch = value
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module BothIsGood
|
|
2
2
|
class ImplementedTwice
|
|
3
|
-
def initialize(owner,
|
|
3
|
+
def initialize(owner, original:, replacement:, **opts)
|
|
4
4
|
base = owner.both_is_good_configuration
|
|
5
|
-
@local_config = LocalConfiguration.new(base, owner:,
|
|
5
|
+
@local_config = LocalConfiguration.new(base, owner:, original:, replacement:, **opts)
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def call(target, *args, **kwargs)
|
|
@@ -18,8 +18,18 @@ module BothIsGood
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
-
memoize def
|
|
22
|
-
|
|
21
|
+
memoize def switched?
|
|
22
|
+
if @config.switch.nil?
|
|
23
|
+
false
|
|
24
|
+
elsif @config.switch.arity == 0
|
|
25
|
+
@config.switch.call
|
|
26
|
+
else
|
|
27
|
+
@config.switch.call(@target.target_class, @target.method_name)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
memoize def primary = switched? ? @config.replacement : @config.original
|
|
32
|
+
memoize def secondary = switched? ? @config.original : @config.replacement
|
|
23
33
|
|
|
24
34
|
memoize def trigger? = rand < @config.rate
|
|
25
35
|
|
|
@@ -38,8 +48,8 @@ module BothIsGood
|
|
|
38
48
|
on_secondary_success
|
|
39
49
|
end
|
|
40
50
|
|
|
41
|
-
memoize def primary_result = @target.send(
|
|
42
|
-
memoize def secondary_result = @target.send(
|
|
51
|
+
memoize def primary_result = @target.instance.send(primary, *@args, **@kwargs)
|
|
52
|
+
memoize def secondary_result = @target.instance.send(secondary, *@args, **@kwargs)
|
|
43
53
|
|
|
44
54
|
def on_primary_error(error)
|
|
45
55
|
hook = @config.on_primary_error
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
module BothIsGood
|
|
2
2
|
class LocalConfiguration < Configuration
|
|
3
|
-
LOCAL_ATTRIBUTES = %i[
|
|
3
|
+
LOCAL_ATTRIBUTES = %i[original replacement comparator].freeze
|
|
4
4
|
|
|
5
5
|
attr_reader(*LOCAL_ATTRIBUTES)
|
|
6
6
|
|
|
7
|
-
def initialize(base_config, owner:,
|
|
7
|
+
def initialize(base_config, owner:, original:, replacement:, **opts)
|
|
8
8
|
comparator = opts.delete(:comparator)
|
|
9
9
|
super(base_config, **opts)
|
|
10
|
-
@
|
|
11
|
-
@
|
|
10
|
+
@original = validated_method(owner, :original, original)
|
|
11
|
+
@replacement = validated_method(owner, :replacement, replacement)
|
|
12
12
|
@comparator = validated_comparator(comparator)
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ module BothIsGood
|
|
|
16
16
|
|
|
17
17
|
def validated_method(owner, role, name)
|
|
18
18
|
raise ArgumentError, "#{role} must not be nil" if name.nil?
|
|
19
|
-
unless owner.method_defined?(name)
|
|
19
|
+
unless owner.method_defined?(name) || owner.private_method_defined?(name)
|
|
20
20
|
raise ArgumentError, "#{role} method #{name.inspect} is not defined on #{owner}"
|
|
21
21
|
end
|
|
22
22
|
name
|
data/lib/both_is_good/version.rb
CHANGED
data/lib/both_is_good.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "both_is_good/memoization"
|
|
2
|
+
require_relative "both_is_good/target"
|
|
2
3
|
|
|
3
4
|
module BothIsGood
|
|
4
5
|
def self.configuration = Configuration.global
|
|
@@ -23,13 +24,13 @@ module BothIsGood
|
|
|
23
24
|
@both_is_good_configuration || BothIsGood.configuration
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def implemented_twice(*positional,
|
|
27
|
-
implementer = DualImplementer.new(*positional, target: self,
|
|
27
|
+
def implemented_twice(*positional, original: nil, replacement: nil, **opts)
|
|
28
|
+
implementer = DualImplementer.new(*positional, target: self, original:, replacement:, **opts)
|
|
28
29
|
implementer.apply_aliases!
|
|
29
30
|
runner = implementer.implementation
|
|
30
31
|
|
|
31
32
|
define_method(implementer.name) do |*args, **kwargs|
|
|
32
|
-
runner.call(self, *args, **kwargs)
|
|
33
|
+
runner.call(BothIsGood::Target.new(self, implementer.name, self.class), *args, **kwargs)
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
end
|
|
@@ -37,11 +38,11 @@ module BothIsGood
|
|
|
37
38
|
class DualImplementer
|
|
38
39
|
include Memoization
|
|
39
40
|
|
|
40
|
-
def initialize(*positional, target:,
|
|
41
|
+
def initialize(*positional, target:, original:, replacement:, **opts)
|
|
41
42
|
@target = target
|
|
42
43
|
@positional = positional
|
|
43
|
-
@
|
|
44
|
-
@
|
|
44
|
+
@kw_original = original
|
|
45
|
+
@kw_replacement = replacement
|
|
45
46
|
@opts = opts
|
|
46
47
|
|
|
47
48
|
validate!
|
|
@@ -50,28 +51,28 @@ module BothIsGood
|
|
|
50
51
|
attr_reader :positional, :target, :opts
|
|
51
52
|
|
|
52
53
|
memoize def name = positional.first
|
|
53
|
-
memoize def
|
|
54
|
-
memoize def
|
|
54
|
+
memoize def original = aliased_original? ? :"_bothisgood_original_#{name}" : supplied_original
|
|
55
|
+
memoize def replacement = aliased_replacement? ? :"_bothisgood_replacement_#{name}" : supplied_replacement
|
|
55
56
|
|
|
56
57
|
def apply_aliases!
|
|
57
|
-
target.alias_method(
|
|
58
|
-
target.alias_method(
|
|
58
|
+
target.alias_method(original, name) if aliased_original?
|
|
59
|
+
target.alias_method(replacement, name) if aliased_replacement?
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
memoize def implementation = ImplementedTwice.new(target,
|
|
62
|
+
memoize def implementation = ImplementedTwice.new(target, original:, replacement:, **opts)
|
|
62
63
|
|
|
63
64
|
private
|
|
64
65
|
|
|
65
|
-
memoize def
|
|
66
|
-
memoize def
|
|
66
|
+
memoize def supplied_original = @kw_original || positional[-2]
|
|
67
|
+
memoize def supplied_replacement = @kw_replacement || positional.last
|
|
67
68
|
|
|
68
|
-
memoize def
|
|
69
|
-
memoize def
|
|
69
|
+
memoize def aliased_original? = supplied_original == name
|
|
70
|
+
memoize def aliased_replacement? = supplied_replacement == name
|
|
70
71
|
|
|
71
72
|
def validate!
|
|
72
73
|
validate_name_supplied!
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
validate_replacement_supplied!
|
|
75
|
+
validate_no_original_replacement_match!
|
|
75
76
|
validate_no_mixing!
|
|
76
77
|
validate_no_extra_positional!
|
|
77
78
|
end
|
|
@@ -81,20 +82,20 @@ module BothIsGood
|
|
|
81
82
|
raise(ArgumentError, "the 'name' positional parameter is required")
|
|
82
83
|
end
|
|
83
84
|
|
|
84
|
-
def
|
|
85
|
-
return if @
|
|
86
|
-
raise ArgumentError, "
|
|
85
|
+
def validate_replacement_supplied!
|
|
86
|
+
return if @kw_replacement || positional.length >= 2
|
|
87
|
+
raise ArgumentError, "replacement is required, either as a positional argument or as a keyword argument"
|
|
87
88
|
end
|
|
88
89
|
|
|
89
|
-
def
|
|
90
|
-
return if
|
|
91
|
-
raise ArgumentError, "
|
|
90
|
+
def validate_no_original_replacement_match!
|
|
91
|
+
return if supplied_original != supplied_replacement
|
|
92
|
+
raise ArgumentError, "original and replacement cannot be the same method"
|
|
92
93
|
end
|
|
93
94
|
|
|
94
95
|
def validate_no_mixing!
|
|
95
96
|
return if positional.length <= 1
|
|
96
|
-
return if @
|
|
97
|
-
raise ArgumentError, "cannot mix positional and keyword
|
|
97
|
+
return if @kw_original.nil? && @kw_replacement.nil?
|
|
98
|
+
raise ArgumentError, "cannot mix positional and keyword original:/replacement:"
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
def validate_no_extra_positional!
|
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.4.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-
|
|
11
|
+
date: 2026-04-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -126,6 +126,7 @@ files:
|
|
|
126
126
|
- ".rspec"
|
|
127
127
|
- ".rubocop.yml"
|
|
128
128
|
- ".standard.yml"
|
|
129
|
+
- CHANGELOG.md
|
|
129
130
|
- LICENSE
|
|
130
131
|
- README.md
|
|
131
132
|
- both_is_good.gemspec
|
|
@@ -135,6 +136,7 @@ files:
|
|
|
135
136
|
- lib/both_is_good/invocation.rb
|
|
136
137
|
- lib/both_is_good/local_configuration.rb
|
|
137
138
|
- lib/both_is_good/memoization.rb
|
|
139
|
+
- lib/both_is_good/target.rb
|
|
138
140
|
- lib/both_is_good/version.rb
|
|
139
141
|
homepage: https://github.com/nevinera/both_is_good
|
|
140
142
|
licenses:
|