modifiers 1.0.0 → 1.1.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/.travis.yml +0 -1
- data/README.md +129 -75
- data/lib/modifiers/define_modifier.rb +1 -0
- data/lib/modifiers/version.rb +1 -1
- data/spec/shared_examples_for_modifiers.rb +4 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c529f23f0c4fe7d0417b298daabad35a18c98ba
|
4
|
+
data.tar.gz: 4e26087e2b3d2a386417c3ae74bef0dfac9ef131
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cae095e6a838c2e06094242f883ad3bd18db3841bd931abee97dcd8b507990ad5a1e851d241b85ddfa1de3d90ab5a465aac3b5bed809a4830317405a2cab543
|
7
|
+
data.tar.gz: b1649f83e589491f7da170b057be4121be762efc592f06cb7c228c92a2d2d3e74a871859b5cc2b6b80b777976f575b16d7c688e7c901699dbf8ddfef314ef183
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -5,33 +5,55 @@
|
|
5
5
|
|
6
6
|
`Modifiers` is a collection of method modifiers, and a way to make more.
|
7
7
|
|
8
|
-
_Method Modifiers_, obviously,
|
8
|
+
_Method Modifiers_, obviously, modify methods. Specifically, in Ruby
|
9
9
|
terms, they are class methods which:
|
10
10
|
|
11
|
-
1. Take a symbol argument which names an instance method of the same class,
|
12
|
-
2. Return the same symbol,
|
13
|
-
3.
|
11
|
+
1. Take a symbol argument which names an instance method of the same class, _and_
|
12
|
+
2. Return the same symbol, _but_
|
13
|
+
3. Cause subsequent calls to the named method to change in some way.
|
14
14
|
|
15
|
-
|
16
|
-
modifiers (`public`, `private`, and `protected`), and `module_function`. This
|
17
|
-
library [adds a few others, and a facility for creating even more](#usage).
|
15
|
+
This library [includes a few, as well as ways to make more](#usage).
|
18
16
|
|
19
17
|
## Why is/are Modifiers?
|
20
18
|
|
21
|
-
|
22
|
-
method.
|
19
|
+
The pursuit of DRY code can involve fragments of shared behavior smaller than a
|
20
|
+
method.
|
21
|
+
|
22
|
+
Here's an example that might feel familiar:
|
23
23
|
```ruby
|
24
|
-
# old and busted
|
25
24
|
def count_ducks
|
26
25
|
@count_ducks ||= DuckFlock.all.map(&size).inject(0, &:+)
|
27
26
|
end
|
28
27
|
```
|
29
28
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
This method is quite small, but it still complects the concerns of counting
|
30
|
+
ducks and of saving and reusing the result of a calculation, and that latter
|
31
|
+
concern might be repeated any number of times in your codebase.
|
32
|
+
|
33
|
+
With modifiers, we can encapsulate the implementation of the memoization, and
|
34
|
+
keep the intent:
|
35
|
+
```ruby
|
36
|
+
def count_ducks
|
37
|
+
DuckFlock.all.map(&size).inject(0, &:+)
|
38
|
+
end
|
39
|
+
memoized :count_ducks
|
40
|
+
```
|
41
|
+
|
42
|
+
## Requirements
|
43
|
+
|
44
|
+
Behind the scenes, `modifiers` uses `Module#prepend`, so it requires Ruby
|
45
|
+
version 2.0.0 or higher.
|
46
|
+
|
47
|
+
If you have at least version 2.1.0, you can call them in-line with your method
|
48
|
+
definitions, which looks completely baller imo:
|
49
|
+
```ruby
|
50
|
+
# requires Ruby 2.1.0 or higher, cool kids only
|
51
|
+
memoized query def fetch_from_api(params)
|
52
|
+
ApiFetcher.new(params).call
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
(All other code examples in this document work on 2.0.)
|
35
57
|
|
36
58
|
## Installation
|
37
59
|
|
@@ -47,6 +69,36 @@ And then execute:
|
|
47
69
|
|
48
70
|
### built-in modifiers
|
49
71
|
|
72
|
+
#### memoized
|
73
|
+
|
74
|
+
Every now and then, you start to care how long it takes for a method to run.
|
75
|
+
You may find yourself wishing it just re-used some hard-won values, rather than
|
76
|
+
throwing them away and rebuilding them anew every time you call it.
|
77
|
+
|
78
|
+
(You may recognize the example from earlier, but this one is more complete.)
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
require 'modifiers/memoized'
|
82
|
+
|
83
|
+
class DuckService
|
84
|
+
extend Modifiers
|
85
|
+
|
86
|
+
def count_ducks
|
87
|
+
DuckFlock.all.map(&size).inject(0, &:+)
|
88
|
+
end
|
89
|
+
memoized :count_ducks
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
A method modified by `memoized` will run once normally, per unique combination
|
94
|
+
of arguments, after which it will simply return the same result for the
|
95
|
+
lifetime of the receiving object. Dazzle your friends with your terse, yet
|
96
|
+
performant, fibonnaci implementations!
|
97
|
+
|
98
|
+
(If you want all this and more, you can use
|
99
|
+
[memoist](https://github.com/matthewrudy/memoist) (formerly
|
100
|
+
`ActiveSupport::Memoizable`) instead, but I warn you: it involves `eval`.)
|
101
|
+
|
50
102
|
#### deprecated
|
51
103
|
|
52
104
|
Sometimes there's a method, and you want it to die, but not a clean, swift
|
@@ -60,9 +112,10 @@ require 'modifiers/deprecated'
|
|
60
112
|
class BadHacks
|
61
113
|
extend Modifiers
|
62
114
|
|
63
|
-
|
115
|
+
def awful_method
|
64
116
|
# some ugly hack, probably involving define_method and ObjectSpace
|
65
117
|
end
|
118
|
+
deprecated :awful_method
|
66
119
|
end
|
67
120
|
```
|
68
121
|
|
@@ -73,36 +126,6 @@ BadHacks#awful_method called from app/controllers/ducks_controller.rb:782`
|
|
73
126
|
(Please note that the `deprecated` method is deprecated, and you should
|
74
127
|
definitely use `Gem.deprecate` instead.)
|
75
128
|
|
76
|
-
#### memoized
|
77
|
-
|
78
|
-
Every now and then, you will come to care how long it takes for a method to
|
79
|
-
run. You may find yourself wishing it just re-used some hard-won values,
|
80
|
-
rather than throwing them away and rebuilding them anew every time you call it.
|
81
|
-
|
82
|
-
To demonstrate this, on multiple levels, I will re-use the case from a previous
|
83
|
-
section.
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
require 'modifiers/memoized'
|
87
|
-
|
88
|
-
class DuckService
|
89
|
-
extend Modifiers
|
90
|
-
|
91
|
-
memoized def count_ducks
|
92
|
-
DuckFlock.all.map(&size).inject(0, &:+)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
```
|
96
|
-
|
97
|
-
A method modified by `memoized` will run once normally, per unique combination
|
98
|
-
of arguments, after which it will simply return the same result for the
|
99
|
-
lifetime of the receiving object. Dazzle your friends with your terse, yet
|
100
|
-
performant, fibonnaci implementations!
|
101
|
-
|
102
|
-
(If you want all this and more, you can use
|
103
|
-
[memoist](https://github.com/matthewrudy/memoist) (formerly
|
104
|
-
`ActiveSupport::Memoizable`) instead, but I warn you: it involves `eval`.)
|
105
|
-
|
106
129
|
#### commands and queries
|
107
130
|
|
108
131
|
You may have heard of 'Command-Query Separation`, and the claim that code
|
@@ -118,15 +141,17 @@ sounds.
|
|
118
141
|
Conversely (?), a method modified by `query` will never change the state of
|
119
142
|
anything non-global and in-process. This is also trivial, but it might seem
|
120
143
|
more impressive.
|
144
|
+
|
121
145
|
```ruby
|
122
146
|
require 'modifiers/command_query'
|
123
147
|
|
124
148
|
class DuckFarmer < Struct.new(:hutches)
|
125
149
|
extend Modifiers
|
126
150
|
|
127
|
-
|
151
|
+
def fullest_hutch
|
128
152
|
hutches.max { |h1,h2| h1.count_eggs - h2.count_eggs }
|
129
153
|
end
|
154
|
+
query :fullest_hutch
|
130
155
|
end
|
131
156
|
|
132
157
|
class DuckHutch < Struct.new(:num_eggs)
|
@@ -151,8 +176,6 @@ If this was an infomercial, now is when I would say something like "It's just
|
|
151
176
|
that easy, Michael!", and you (your name is Michael in this scenario) would say
|
152
177
|
"Now _that's_ incredible!" and the audience would applaud.
|
153
178
|
|
154
|
-
I'm mildly proud of this library.
|
155
|
-
|
156
179
|
### defining new modifiers
|
157
180
|
|
158
181
|
New modifiers can be defined in your own modules using the `define_modifier` method.
|
@@ -164,59 +187,90 @@ require 'modifiers/define_modifier'
|
|
164
187
|
|
165
188
|
module DuckFarmModifiers
|
166
189
|
extend Modifiers
|
167
|
-
define_modifier(:duck)
|
190
|
+
define_modifier(:duck) do |*args, &block|
|
191
|
+
super(*args, &block)
|
192
|
+
end
|
168
193
|
end
|
169
194
|
|
170
195
|
class DuckFarm
|
171
196
|
extend DuckFarmModifiers
|
197
|
+
def farm
|
198
|
+
# raise, tend, cultivate
|
199
|
+
end
|
172
200
|
|
173
|
-
duck :
|
201
|
+
duck :farm # => unchanged
|
174
202
|
end
|
175
203
|
```
|
176
204
|
|
177
|
-
|
205
|
+
Much as with `define_method`, the first argument to `define_modifier` gives us
|
206
|
+
the name of the new modifier, and the block gives us the implementation of a
|
207
|
+
given *modification*: a method which intercepts calls to the original method
|
208
|
+
(in this case, `DuckFarm#farm`), does whatever it likes, then invokes the
|
209
|
+
original method using `super`.
|
210
|
+
|
211
|
+
(Sadly, just as with `define_method`, you have to use explicit arguments when
|
212
|
+
calling `super`. Sorry, doing otherwise involved too much oddity.)
|
213
|
+
|
214
|
+
But maybe you don't want to call the original method at all!
|
178
215
|
```ruby
|
179
216
|
module DuckFarmModifiers
|
180
|
-
define_modifier(:
|
181
|
-
method_invocation.invoke
|
182
|
-
end
|
217
|
+
define_modifier(:x) { }
|
183
218
|
end
|
184
219
|
```
|
185
220
|
|
186
|
-
|
187
|
-
modified by the modifier, kinda like with `define_method`.
|
188
|
-
|
189
|
-
The argument passed to that block will be an object representing a particular
|
190
|
-
call to the modified method. As I showed you above, all you have to do
|
191
|
-
continue that call as normal is `#invoke` it.
|
192
|
-
|
193
|
-
Or not!
|
221
|
+
Or maybe you don't want to call it with the same arguments!
|
194
222
|
```ruby
|
195
223
|
module DuckFarmModifiers
|
196
|
-
define_modifier(:
|
197
|
-
|
224
|
+
define_modifier(:int) do |*args, &block|
|
225
|
+
super(*args.map(&:to_i), &block)
|
198
226
|
end
|
199
227
|
end
|
200
|
-
```
|
201
228
|
|
202
229
|
You can do things before, after, or even "around" the invocation.
|
203
230
|
```ruby
|
204
231
|
module DuckFarmModifiers
|
205
|
-
define_modifier(:perf_logged) do |
|
232
|
+
define_modifier(:perf_logged) do |*args, &block|
|
206
233
|
start = Time.now
|
207
|
-
|
208
|
-
Rails.logger "#{
|
234
|
+
super(*args, &block)
|
235
|
+
Rails.logger "#{self.class.name}##{__method__} finished in #{Time.now - start}s"
|
209
236
|
end
|
210
237
|
end
|
211
238
|
```
|
212
239
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
240
|
+
### Extending modifiers
|
241
|
+
|
242
|
+
The body of a modifier will be evaluated in the context of the receiver
|
243
|
+
instance of the modified method, so you can refer to any other instance
|
244
|
+
methods. However, given that you probably wrote your modifier to be used from
|
245
|
+
_any_ object, there isn't much you can rely on, so you may find yourself
|
246
|
+
writing everything you need right there in the method, resulting in a long and
|
247
|
+
ugly method body.
|
218
248
|
|
219
|
-
|
249
|
+
Luckily, there's a way out: if a given modification requires additional
|
250
|
+
behavior, simply pass a module with all the other methods you need as the
|
251
|
+
second argument to `define_modifier`.
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
module DuckFarmModifiers
|
255
|
+
module Gracefully
|
256
|
+
private
|
257
|
+
|
258
|
+
def logger
|
259
|
+
Rails.logger
|
260
|
+
end
|
261
|
+
|
262
|
+
def log_exception(method_name, exception)
|
263
|
+
logger.warn("#{method_name} raised #{exception}")
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
define_modifier(:gracefully, Gracefully) do |*args, &block|
|
268
|
+
super(*args, &block)
|
269
|
+
rescue => e
|
270
|
+
log_exception(__method__, e)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
```
|
220
274
|
|
221
275
|
## Contributing
|
222
276
|
|
data/lib/modifiers/version.rb
CHANGED
@@ -24,6 +24,10 @@ RSpec.shared_examples 'a modifier' do |modifier, changes_return_value: false|
|
|
24
24
|
let(:random_class_name) { (0...10).map { ('A'..'Z').to_a[rand(26)] }.join }
|
25
25
|
subject(:instance) { test_class.new }
|
26
26
|
|
27
|
+
it 'returns the symbol it is passed' do
|
28
|
+
expect(test_class.send(modifier, :public_method)).to be :public_method
|
29
|
+
end
|
30
|
+
|
27
31
|
unless changes_return_value
|
28
32
|
it 'does not change the return value of the method' do
|
29
33
|
test_class.send(modifier, :public_method)
|