adornable 1.0.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +96 -0
- data/Gemfile +2 -0
- data/README.md +100 -95
- data/Rakefile +3 -1
- data/adornable.gemspec +20 -13
- data/bin/console +1 -0
- data/lib/adornable.rb +13 -6
- data/lib/adornable/context.rb +23 -0
- data/lib/adornable/decorators.rb +35 -17
- data/lib/adornable/error.rb +2 -0
- data/lib/adornable/machinery.rb +39 -12
- data/lib/adornable/utils.rb +12 -1
- data/lib/adornable/version.rb +3 -1
- metadata +90 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc81717998b4dc097647c96e4b7c89664b2d0c6969100dd22d713ba9c1720619
|
4
|
+
data.tar.gz: a3998df4caeb6db3823a218fc77ac3d1d56949e3f9a66cc340f65465d9037686
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4aaf8fe2928bb4652dc5a7b1f235fadc7114580cfc29d91a9fdf508edbba15cccc09627b6a097552774c73babede934ca77a9388fa7ce1bfdbfb5975ab0661da
|
7
|
+
data.tar.gz: cc2ec1780014bfb5043a44857a2d9fc6eb7957ec245fef03accbe64436c11cc9f2596105d25d5e888b136ca27d57565ec414f9fbb466f72fc425744d6fe69af2
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
- rubocop-rspec
|
4
|
+
- rubocop-rake
|
5
|
+
|
6
|
+
# Globals
|
7
|
+
|
8
|
+
AllCops:
|
9
|
+
NewCops: enable
|
10
|
+
|
11
|
+
# Layout
|
12
|
+
|
13
|
+
Layout/LineLength:
|
14
|
+
Max: 120
|
15
|
+
Exclude:
|
16
|
+
- 'spec/**/*_spec.rb'
|
17
|
+
|
18
|
+
Layout/EndAlignment:
|
19
|
+
EnforcedStyleAlignWith: variable
|
20
|
+
|
21
|
+
Layout/FirstArrayElementIndentation:
|
22
|
+
EnforcedStyle: consistent
|
23
|
+
|
24
|
+
# Metrics
|
25
|
+
|
26
|
+
Metrics/AbcSize:
|
27
|
+
CountRepeatedAttributes: false
|
28
|
+
Exclude:
|
29
|
+
- 'spec/**/*_spec.rb'
|
30
|
+
|
31
|
+
Metrics/BlockLength:
|
32
|
+
Exclude:
|
33
|
+
- 'spec/**/*_spec.rb'
|
34
|
+
|
35
|
+
Metrics/ClassLength:
|
36
|
+
Max: 150
|
37
|
+
CountComments: false
|
38
|
+
CountAsOne:
|
39
|
+
- array
|
40
|
+
- hash
|
41
|
+
- heredoc
|
42
|
+
Exclude:
|
43
|
+
- 'spec/**/*_spec.rb'
|
44
|
+
|
45
|
+
Metrics/MethodLength:
|
46
|
+
Max: 20
|
47
|
+
CountComments: false
|
48
|
+
CountAsOne:
|
49
|
+
- array
|
50
|
+
- hash
|
51
|
+
- heredoc
|
52
|
+
|
53
|
+
Metrics/ModuleLength:
|
54
|
+
Max: 150
|
55
|
+
CountComments: false
|
56
|
+
CountAsOne:
|
57
|
+
- array
|
58
|
+
- hash
|
59
|
+
- heredoc
|
60
|
+
Exclude:
|
61
|
+
- 'spec/**/*_spec.rb'
|
62
|
+
|
63
|
+
# Rspec
|
64
|
+
|
65
|
+
RSpec/ExampleLength:
|
66
|
+
Max: 25
|
67
|
+
|
68
|
+
RSpec/MessageSpies:
|
69
|
+
Enabled: false
|
70
|
+
|
71
|
+
RSpec/MultipleExpectations:
|
72
|
+
Enabled: false
|
73
|
+
|
74
|
+
RSpec/NestedGroups:
|
75
|
+
Max: 10
|
76
|
+
|
77
|
+
# Style
|
78
|
+
|
79
|
+
Style/DoubleNegation:
|
80
|
+
Enabled: false
|
81
|
+
|
82
|
+
Style/ExpandPathArguments:
|
83
|
+
Exclude:
|
84
|
+
- 'adornable.gemspec'
|
85
|
+
|
86
|
+
Style/StringLiterals:
|
87
|
+
Enabled: false
|
88
|
+
|
89
|
+
Style/TrailingCommaInArguments:
|
90
|
+
EnforcedStyleForMultiline: consistent_comma
|
91
|
+
|
92
|
+
Style/TrailingCommaInArrayLiteral:
|
93
|
+
EnforcedStyleForMultiline: consistent_comma
|
94
|
+
|
95
|
+
Style/TrailingCommaInHashLiteral:
|
96
|
+
EnforcedStyleForMultiline: consistent_comma
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Adornable
|
2
2
|
|
3
|
-
Adornable provides
|
3
|
+
Adornable provides the ability to cleanly decorate methods in Ruby. You can make and use your own decorators, and you can also use some of the built-in ones that the gem provides. _Decorating_ methods is as simple as slapping a `decorate :some_decorator` above your method definition. _Defining_ decorators can be as simple as defining a method that yields to a block, or as complex as manipulating the decorated method's receiver and arguments, and/or changing the functionality of the decorator based on custom options supplied to it when initially applying the decorator.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -26,8 +26,6 @@ Alternatively, install it globally:
|
|
26
26
|
gem install adornable
|
27
27
|
```
|
28
28
|
|
29
|
-
...but why would you do that?
|
30
|
-
|
31
29
|
## Usage
|
32
30
|
|
33
31
|
### The basics
|
@@ -75,9 +73,9 @@ value2 = random_value_generator.value
|
|
75
73
|
#=> 0.4196007135344746
|
76
74
|
```
|
77
75
|
|
78
|
-
|
76
|
+
However, you have a million more methods to write, and if you refactor, you'll have to screw around with a slew of method definitions across your app.
|
79
77
|
|
80
|
-
|
78
|
+
What if you could do this, instead, to achieve the same result?
|
81
79
|
|
82
80
|
```rb
|
83
81
|
class RandomValueGenerator
|
@@ -90,33 +88,11 @@ class RandomValueGenerator
|
|
90
88
|
end
|
91
89
|
|
92
90
|
decorate :log
|
93
|
-
decorate :
|
91
|
+
decorate :memoize
|
94
92
|
def values(max)
|
95
93
|
(1..max).map { rand }
|
96
94
|
end
|
97
95
|
end
|
98
|
-
|
99
|
-
random_value_generator = RandomValueGenerator.new
|
100
|
-
|
101
|
-
values1 = random_value_generator.values(1000)
|
102
|
-
# Calling method `RandomValueGenerator#values` with arguments `[1000]`
|
103
|
-
#=> [0.7044444114998132, 0.401953296596267, 0.3023797513191562, ...]
|
104
|
-
|
105
|
-
values1 = random_value_generator.values(1000)
|
106
|
-
# Calling method `RandomValueGenerator#values` with arguments `[1000]`
|
107
|
-
#=> [0.7044444114998132, 0.401953296596267, 0.3023797513191562, ...]
|
108
|
-
|
109
|
-
values3 = random_value_generator.values(5000)
|
110
|
-
# Calling method `RandomValueGenerator#values` with arguments `[5000]`
|
111
|
-
#=> [0.9916088057511011, 0.04466750434972333, 0.6073659341272127]
|
112
|
-
|
113
|
-
value1 = random_value_generator.value
|
114
|
-
# Calling method `RandomValueGenerator#value` with no arguments
|
115
|
-
#=> 0.4196007135344746
|
116
|
-
|
117
|
-
value2 = random_value_generator.value
|
118
|
-
# Calling method `RandomValueGenerator#value` with no arguments
|
119
|
-
#=> 0.4196007135344746
|
120
96
|
```
|
121
97
|
|
122
98
|
Nice, right?
|
@@ -141,7 +117,7 @@ Use the `decorate` macro to decorate methods.
|
|
141
117
|
|
142
118
|
#### Using built-in decorators
|
143
119
|
|
144
|
-
There are a
|
120
|
+
There are a couple of built-in decorators for common use-cases (these can be overridden if you so choose):
|
145
121
|
|
146
122
|
```rb
|
147
123
|
class Foo
|
@@ -149,56 +125,64 @@ class Foo
|
|
149
125
|
|
150
126
|
decorate :log
|
151
127
|
def some_method
|
152
|
-
#
|
128
|
+
# the method name (Foo#some_method) and arguments will be logged
|
153
129
|
end
|
154
130
|
|
155
131
|
decorate :memoize
|
156
132
|
def some_other_method
|
157
|
-
#
|
133
|
+
# the return value will be cached
|
158
134
|
end
|
159
135
|
|
160
|
-
decorate :
|
136
|
+
decorate :memoize
|
161
137
|
def yet_another_method(some_arg, some_other_arg = true, key_word_arg:, key_word_arg_with_default: 123)
|
162
|
-
#
|
138
|
+
# the return value will be cached based on the arguments the method receives
|
163
139
|
end
|
164
140
|
|
165
141
|
decorate :log
|
166
|
-
decorate :
|
142
|
+
decorate :memoize, for_any_arguments: true
|
167
143
|
def oh_boy_another_method(some_arg, some_other_arg = true, key_word_arg:, key_word_arg_with_default: 123)
|
168
|
-
#
|
144
|
+
# the method name (Foo#oh_boy_another_method) and arguments will be logged
|
145
|
+
# the return value will be cached regardless of the arguments received
|
169
146
|
end
|
170
147
|
|
171
148
|
decorate :log
|
172
149
|
def self.yeah_it_works_on_class_methods_too
|
173
|
-
#
|
150
|
+
# the method name (Foo::yeah_it_works_on_class_methods_too) and arguments
|
151
|
+
# will be logged
|
174
152
|
end
|
175
153
|
end
|
176
154
|
```
|
177
155
|
|
178
156
|
- `decorate :log` logs the method name and any passed arguments to the console
|
179
|
-
- `decorate :memoize` caches the result of the first call and returns that initial result (and does not execute the method again) for any additional calls
|
180
|
-
- `
|
157
|
+
- `decorate :memoize` caches the result of the first call and returns that initial result (and does not execute the method again) for any additional calls. By default, it namespaces the cache by the arguments passed to the method, so it will re-compute only if the arguments change; if the arguments are the same as any previous time the method was called, it will return the cached result instead.
|
158
|
+
- pass the `for_any_arguments: true` option (e.g., `decorate :memoize, for_any_arguments: true`) to ignore the arguments in the caching process and simply memoize the result no matter what
|
159
|
+
- a `nil` value returned from a memoized method will still be cached like any other value
|
181
160
|
|
182
161
|
> **Note:** in the case of multiple decorators decorating a method, each is executed from top to bottom.
|
183
162
|
|
184
|
-
####
|
163
|
+
#### Writing custom decorators and using them _explicitly_
|
185
164
|
|
186
165
|
You can reference any decorator method you write, like so:
|
187
166
|
|
188
167
|
```rb
|
189
168
|
class FooDecorators
|
190
|
-
# Note: this is a
|
191
|
-
|
169
|
+
# Note: this is defined as a CLASS method, but it can be applied to both class
|
170
|
+
# and instance methods. The only difference is in how you source the
|
171
|
+
# decorator when doing the decoration; see below for more info.
|
172
|
+
def self.blast_it(context)
|
192
173
|
puts "Blasting it!"
|
193
174
|
value = yield
|
194
175
|
"#{value}!"
|
195
176
|
end
|
196
177
|
|
197
|
-
# Note: this is an
|
198
|
-
|
199
|
-
|
178
|
+
# Note: this is defined as an INSTANCE method, but it can be applied to both
|
179
|
+
# class and instance methods. The only difference is in how you source
|
180
|
+
# the decorator when doing the decoration; see below for more info.
|
181
|
+
def wait_for_it(context, dot_count: 3)
|
182
|
+
ellipsis = dot_count.times.map { '.' }.join
|
183
|
+
puts "Waiting for it#{ellipsis}"
|
200
184
|
value = yield
|
201
|
-
"#{value}
|
185
|
+
"#{value}#{ellipsis}"
|
202
186
|
end
|
203
187
|
end
|
204
188
|
|
@@ -238,60 +222,73 @@ foo.yet_another_method(123, bloop: "bleep")
|
|
238
222
|
#=> "haha I'm yet another method"
|
239
223
|
```
|
240
224
|
|
241
|
-
Use the `from:` option to specify what should receive the decorator method. Keep in mind that the decorator method will be called on the thing specified by `from:`... so, if you provide a class, it better be a class method, and if you supply an instance, it better be an instance method.
|
225
|
+
Use the `from:` option to specify what should receive the decorator method. Keep in mind that the decorator method will be called on the thing specified by `from:`... so, if you provide a class, it better be a class method on that thing, and if you supply an instance, it better be an instance method on that thing.
|
242
226
|
|
243
|
-
Every decorator method must take the following
|
227
|
+
Every custom decorator method that you define must take one required argument (`context`) and any number of keyword arguments. It should also `yield` (or take a block argument and invoke it) at some point in the body of the method. The point at which you `yield` will be the point at which the decorated method will execute (or, if there are multiple decorators on the method, each following decorator will be invoked until the decorators have been exhausted and the decorated method is finally executed).
|
244
228
|
|
245
|
-
|
246
|
-
- `method_name`: the name of the [decorated] method being called on `method_receiver` (a symbol); e.g., `:some_method` or `:other_method`
|
247
|
-
- `arguments`: an array of arguments passed to the [decorated] method, including keyword arguments; e.g., if `:yet_another_method` was called like `Foo.new.yet_another_method(123, bar: true)` then `arguments` would be `[123, {:bar=>true}]`
|
229
|
+
##### The required argument (`context`)
|
248
230
|
|
249
|
-
|
250
|
-
>
|
251
|
-
> **Note:** the return value of your decorator **will replace the return value of the decorated method,** so _also_ you should probably return whatever value `yield` returned. Again, it is a valid use case to return something _else,_ but 99% of the time you probably want to return the value returned by the wrapped method.
|
231
|
+
The **required argument** is an instance of `Adornable::Context`, which has some useful information about the decorated method being called
|
252
232
|
|
253
|
-
|
233
|
+
- `Adornable::Context#method_name`: the name of the decorated method being called (a symbol; e.g., `:some_method` or `:other_method`)
|
234
|
+
- `Adornable::Context#method_receiver`: the actual object that the decorated method (the `#method_name`) belongs to/is being called on (an object/class; e.g., the class `Foo` if it's a decorated class method, or an instance of `Foo` if it's a decorated instance method)
|
235
|
+
- `Adornable::Context#method_arguments`: an array of arguments passed to the decorated method, including keyword arguments as a final hash (e.g., if `:yet_another_method` was called like `Foo.new.yet_another_method(123, bar: true)` then `arguments` would be `[123, {:bar=>true}]`)
|
254
236
|
|
255
|
-
|
256
|
-
class FooDecorators
|
257
|
-
def self.coerce_to_int(method_receiver, method_name, arguments)
|
258
|
-
value = yield
|
259
|
-
new_value = value.strip.to_i
|
260
|
-
puts "New value: #{value.inspect} (class: #{value.class})"
|
261
|
-
new_value
|
262
|
-
end
|
263
|
-
end
|
237
|
+
##### Custom keyword arguments (optional)
|
264
238
|
|
265
|
-
|
266
|
-
extend Adornable
|
239
|
+
The **optional keyword arguments** are any parameters you want to be able to pass to the decorator method when decorating a method with `::decorate`:
|
267
240
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
value = gets
|
272
|
-
puts "Value: #{value.inspect} (class: #{value.class})"
|
273
|
-
value
|
274
|
-
end
|
275
|
-
end
|
241
|
+
- If you define a decorator like `def self.some_decorator(context)` then it takes no options when it is used: `decorate :some_decorator`.
|
242
|
+
- If you define a decorator like `def self.some_decorator(context, some_option:)` then it takes one _required_ keyword argument when it is used: `decorate :some_decorator, some_option: 123` (so that `::some_decorator` will receive `123` as the `some_option` parameter every time the decorated method is called). You can customize functionality of the decorator this way.
|
243
|
+
- Similarly, if you define a decorator like `def self.some_decorator(context, some_option: 456)`, then it takes one _optional_ keyword argument when it is used: `decorate :some_decorator` is valid (and implies `some_option: 456` since it has a default), and `decorate :some_decorator, some_option: 789` is valid as well.
|
276
244
|
|
277
|
-
|
245
|
+
##### Yielding to the next decorator/decorated method
|
278
246
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
```
|
247
|
+
Every decorator method **should also probably `yield`** at some point in the method body. I say _"should"_ because, technically, you don't have to, but if you don't then the original method will never be called. That's a valid use-case, but 99% of the time you're gonna want to `yield`.
|
248
|
+
|
249
|
+
> **Note:** the return value of your decorator **will replace the return value of the decorated method,** so _also_ you should probably return whatever value `yield` returned. Again, it is a valid use case to return something _else,_ but 99% of the time you probably want to return the value returned by the wrapped method.
|
250
|
+
>
|
251
|
+
> A contrived example of when you might want to muck around with the return value:
|
252
|
+
>
|
253
|
+
> ```rb
|
254
|
+
> class FooDecorators
|
255
|
+
> def self.coerce_to_int(context)
|
256
|
+
> value = yield
|
257
|
+
> new_value = value.strip.to_i
|
258
|
+
> puts "New value: #{value.inspect} (class: #{value.class})"
|
259
|
+
> new_value
|
260
|
+
> end
|
261
|
+
> end
|
262
|
+
>
|
263
|
+
> class Foo
|
264
|
+
> extend Adornable
|
265
|
+
>
|
266
|
+
> decorate :coerce_to_int, from: FooDecorators
|
267
|
+
> def get_number_from_user
|
268
|
+
> print "Enter a number: "
|
269
|
+
> value = gets
|
270
|
+
> puts "Value: #{value.inspect} (class: #{value.class})"
|
271
|
+
> value
|
272
|
+
> end
|
273
|
+
> end
|
274
|
+
>
|
275
|
+
> foo = Foo.new
|
276
|
+
>
|
277
|
+
> foo.get_number_from_user
|
278
|
+
> # Enter a number
|
279
|
+
> # > 123
|
280
|
+
> # Value: "123" (class: String)
|
281
|
+
> # New value: 123 (class: Integer)
|
282
|
+
> #=> 123
|
283
|
+
> ```
|
286
284
|
|
287
|
-
####
|
285
|
+
#### Writing custom decorators and using them _implicitly_
|
288
286
|
|
289
287
|
You can also register decorator receivers so that you don't have to reference them with the `from:` option:
|
290
288
|
|
291
289
|
```rb
|
292
290
|
class FooDecorators
|
293
|
-
|
294
|
-
def self.blast_it(method_receiver, method_name, arguments)
|
291
|
+
def self.blast_it(context)
|
295
292
|
puts "Blasting it!"
|
296
293
|
value = yield
|
297
294
|
"#{value}!"
|
@@ -299,11 +296,11 @@ class FooDecorators
|
|
299
296
|
end
|
300
297
|
|
301
298
|
class MoreFooDecorators
|
302
|
-
|
303
|
-
|
304
|
-
puts "Waiting for it
|
299
|
+
def wait_for_it(context, dot_count: 3)
|
300
|
+
ellipsis = dot_count.times.map { '.' }.join
|
301
|
+
puts "Waiting for it#{ellipsis}"
|
305
302
|
value = yield
|
306
|
-
"#{value}
|
303
|
+
"#{value}#{ellipsis}"
|
307
304
|
end
|
308
305
|
end
|
309
306
|
|
@@ -311,10 +308,10 @@ class Foo
|
|
311
308
|
extend Adornable
|
312
309
|
|
313
310
|
add_decorators_from FooDecorators
|
314
|
-
add_decorators_from MoreFooDecorators
|
311
|
+
add_decorators_from MoreFooDecorators.new
|
315
312
|
|
316
313
|
decorate :blast_it
|
317
|
-
decorate :wait_for_it
|
314
|
+
decorate :wait_for_it, dot_count: 9
|
318
315
|
def some_method
|
319
316
|
"haha I'm a method"
|
320
317
|
end
|
@@ -324,12 +321,14 @@ foo = Foo.new
|
|
324
321
|
|
325
322
|
foo.some_method
|
326
323
|
# Blasting it!
|
327
|
-
# Waiting for it
|
328
|
-
#=> "haha I'm a method
|
324
|
+
# Waiting for it.........
|
325
|
+
#=> "haha I'm a method!........."
|
329
326
|
```
|
330
327
|
|
331
|
-
> **Note:**
|
332
|
-
|
328
|
+
> **Note:** All the rest of the stuff from the previous section (using decorators explicitly) also applies here (using decorators implicitly).
|
329
|
+
|
330
|
+
> **Note:** In the case of duplicate decorator methods, later receivers registered with `::add_decorators_from` will override any decorators by the same name from earlier registered receivers.
|
331
|
+
|
333
332
|
> **Note:** in the case of multiple decorators decorating a method, each is executed from top to bottom; i.e., the top wraps the next, which wraps the next, and so on, until the method itself is wrapped.
|
334
333
|
|
335
334
|
## Development
|
@@ -340,12 +339,18 @@ foo.some_method
|
|
340
339
|
bin/setup
|
341
340
|
```
|
342
341
|
|
343
|
-
### Run
|
342
|
+
### Run the tests
|
344
343
|
|
345
344
|
```bash
|
346
345
|
rake spec
|
347
346
|
```
|
348
347
|
|
348
|
+
### Run the linter
|
349
|
+
|
350
|
+
```bash
|
351
|
+
rubocop
|
352
|
+
```
|
353
|
+
|
349
354
|
### Create release
|
350
355
|
|
351
356
|
```
|
data/Rakefile
CHANGED
data/adornable.gemspec
CHANGED
@@ -1,29 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
3
|
lib = File.expand_path("../lib", __FILE__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require "adornable/version"
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
8
|
+
spec.name = "adornable"
|
9
|
+
spec.version = Adornable::VERSION
|
10
|
+
spec.authors = ["Keegan Leitz"]
|
11
|
+
spec.email = ["kjleitz@gmail.com"]
|
11
12
|
|
12
|
-
spec.summary
|
13
|
-
spec.description
|
14
|
-
spec.homepage
|
15
|
-
spec.license
|
13
|
+
spec.summary = "Method decorators for Ruby"
|
14
|
+
spec.description = "Adornable provides the ability to cleanly decorate methods in Ruby. You can make and use your own decorators, and you can also use some of the built-in ones that the gem provides. _Decorating_ methods is as simple as slapping a `decorate :some_decorator` above your method definition. _Defining_ decorators can be as simple as defining a method that yields to a block, or as complex as manipulating the decorated method's receiver and arguments, and/or changing the functionality of the decorator based on custom options supplied to it when initially applying the decorator." # rubocop:disable Layout/LineLength
|
15
|
+
spec.homepage = "https://github.com/kjleitz/adornable"
|
16
|
+
spec.license = "MIT"
|
17
|
+
spec.required_ruby_version = ">= 2.4.7"
|
16
18
|
|
17
19
|
# Specify which files should be added to the gem when it is released.
|
18
20
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
|
-
spec.files
|
21
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
20
22
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
23
|
end
|
22
|
-
spec.bindir
|
23
|
-
spec.executables
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
26
|
spec.require_paths = ["lib"]
|
25
27
|
|
26
|
-
spec.add_development_dependency "bundler", "~>
|
27
|
-
spec.add_development_dependency "rake", "~>
|
28
|
+
spec.add_development_dependency "bundler", "~> 2.2"
|
29
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
28
30
|
spec.add_development_dependency "rspec", "~> 3.0"
|
31
|
+
spec.add_development_dependency "rubocop", "~> 1.10"
|
32
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.9"
|
33
|
+
spec.add_development_dependency "rubocop-rake", "~> 0.5"
|
34
|
+
spec.add_development_dependency "rubocop-rspec", "~> 2.2"
|
35
|
+
spec.add_development_dependency "solargraph"
|
29
36
|
end
|
data/bin/console
CHANGED
data/lib/adornable.rb
CHANGED
@@ -1,15 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "adornable/version"
|
2
4
|
require "adornable/utils"
|
3
5
|
require "adornable/error"
|
4
6
|
require "adornable/decorators"
|
5
7
|
require "adornable/machinery"
|
6
8
|
|
9
|
+
# Extend the `Adornable` module in your class in order to have access to the
|
10
|
+
# `decorate` and `add_decorators_from` macros.
|
7
11
|
module Adornable
|
8
12
|
def adornable_machinery
|
9
13
|
@adornable_machinery ||= Adornable::Machinery.new
|
10
14
|
end
|
11
15
|
|
12
|
-
def decorate(decorator_name, from: nil, defer_validation: false)
|
16
|
+
def decorate(decorator_name, from: nil, defer_validation: false, **decorator_options)
|
13
17
|
if Adornable::Utils.blank?(name)
|
14
18
|
raise Adornable::Error::InvalidDecoratorArguments, "Decorator name must be provided."
|
15
19
|
end
|
@@ -17,7 +21,8 @@ module Adornable
|
|
17
21
|
adornable_machinery.accumulate_decorator!(
|
18
22
|
name: decorator_name,
|
19
23
|
receiver: from,
|
20
|
-
defer_validation: !!defer_validation
|
24
|
+
defer_validation: !!defer_validation,
|
25
|
+
decorator_options: decorator_options,
|
21
26
|
)
|
22
27
|
end
|
23
28
|
|
@@ -27,9 +32,10 @@ module Adornable
|
|
27
32
|
|
28
33
|
def method_added(method_name)
|
29
34
|
machinery = adornable_machinery # for local variable
|
30
|
-
return unless machinery.
|
35
|
+
return unless machinery.accumulated_decorators?
|
36
|
+
|
31
37
|
machinery.apply_accumulated_decorators_to_instance_method!(method_name)
|
32
|
-
original_method =
|
38
|
+
original_method = instance_method(method_name)
|
33
39
|
define_method(method_name) do |*args|
|
34
40
|
bound_method = original_method.bind(self)
|
35
41
|
machinery.run_decorated_instance_method(bound_method, *args)
|
@@ -39,9 +45,10 @@ module Adornable
|
|
39
45
|
|
40
46
|
def singleton_method_added(method_name)
|
41
47
|
machinery = adornable_machinery # for local variable
|
42
|
-
return unless machinery.
|
48
|
+
return unless machinery.accumulated_decorators?
|
49
|
+
|
43
50
|
machinery.apply_accumulated_decorators_to_class_method!(method_name)
|
44
|
-
original_method =
|
51
|
+
original_method = method(method_name)
|
45
52
|
define_singleton_method(method_name) do |*args|
|
46
53
|
machinery.run_decorated_class_method(original_method, *args)
|
47
54
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Adornable
|
4
|
+
# A context object is passed to the decorator method, and contains information
|
5
|
+
# about the decorated method being called.
|
6
|
+
class Context
|
7
|
+
attr_reader(*%i[
|
8
|
+
method_receiver
|
9
|
+
method_name
|
10
|
+
method_arguments
|
11
|
+
decorator_name
|
12
|
+
decorator_options
|
13
|
+
])
|
14
|
+
|
15
|
+
def initialize(method_receiver:, method_name:, method_arguments:, decorator_name:, decorator_options:)
|
16
|
+
@method_receiver = method_receiver
|
17
|
+
@method_name = method_name
|
18
|
+
@method_arguments = method_arguments
|
19
|
+
@decorator_name = decorator_name
|
20
|
+
@decorator_options = decorator_options
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/adornable/decorators.rb
CHANGED
@@ -1,29 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'adornable/utils'
|
4
|
+
|
1
5
|
module Adornable
|
6
|
+
# `Adornable::Decorators` is used as the default namespace for decorator
|
7
|
+
# methods when a decorator method that is neither explicitly sourced (via the
|
8
|
+
# `decorate from: <receiver>` option) nor implicitly sourced (via the
|
9
|
+
# `add_decorators_from <receiver>` macro).
|
2
10
|
class Decorators
|
3
|
-
def self.log(
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
full_name = "`#{receiver_name}#{name_delimiter}#{method_name}`"
|
10
|
-
arguments_desc = arguments.empty? ? "no arguments" : "arguments `#{arguments}`"
|
11
|
+
def self.log(context)
|
12
|
+
method_receiver = context.method_receiver
|
13
|
+
method_name = context.method_name
|
14
|
+
method_args = context.method_arguments
|
15
|
+
full_name = Adornable::Utils.formal_method_name(method_receiver, method_name)
|
16
|
+
arguments_desc = method_args.empty? ? "no arguments" : "arguments `#{method_args.inspect}`"
|
11
17
|
puts "Calling method #{full_name} with #{arguments_desc}"
|
12
18
|
yield
|
13
19
|
end
|
14
20
|
|
15
|
-
def self.memoize(
|
21
|
+
def self.memoize(context, for_any_arguments: false, &block)
|
22
|
+
return memoize_for_arguments(context, &block) unless for_any_arguments
|
23
|
+
|
24
|
+
method_receiver = context.method_receiver
|
25
|
+
method_name = context.method_name
|
16
26
|
memo_var_name = :"@adornable_memoized_#{method_receiver.object_id}_#{method_name}"
|
17
|
-
|
18
|
-
|
19
|
-
|
27
|
+
|
28
|
+
if instance_variable_defined?(memo_var_name)
|
29
|
+
instance_variable_get(memo_var_name)
|
30
|
+
else
|
31
|
+
instance_variable_set(memo_var_name, yield)
|
32
|
+
end
|
20
33
|
end
|
21
34
|
|
22
|
-
def self.memoize_for_arguments(
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
35
|
+
def self.memoize_for_arguments(context)
|
36
|
+
method_receiver = context.method_receiver
|
37
|
+
method_name = context.method_name
|
38
|
+
method_args = context.method_arguments
|
39
|
+
memo_var_name = :"@adornable_memoized_for_arguments_#{method_receiver.object_id}_#{method_name}"
|
40
|
+
memo = instance_variable_get(memo_var_name) || {}
|
41
|
+
instance_variable_set(memo_var_name, memo)
|
42
|
+
args_key = method_args.inspect
|
43
|
+
memo[args_key] = yield unless memo.key?(args_key)
|
44
|
+
memo[args_key]
|
27
45
|
end
|
28
46
|
end
|
29
47
|
end
|
data/lib/adornable/error.rb
CHANGED
data/lib/adornable/machinery.rb
CHANGED
@@ -1,22 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'adornable/utils'
|
2
4
|
require 'adornable/error'
|
5
|
+
require 'adornable/context'
|
3
6
|
|
4
7
|
module Adornable
|
5
|
-
class Machinery
|
8
|
+
class Machinery # :nodoc:
|
6
9
|
def register_decorator_receiver!(receiver)
|
7
10
|
registered_decorator_receivers.unshift(receiver)
|
8
11
|
end
|
9
12
|
|
10
|
-
def accumulate_decorator!(name:, receiver:, defer_validation:)
|
13
|
+
def accumulate_decorator!(name:, receiver:, defer_validation:, decorator_options:)
|
11
14
|
name = name.to_sym
|
12
15
|
receiver ||= find_suitable_receiver_for(name)
|
13
16
|
validate_decorator!(name, receiver) unless defer_validation
|
14
17
|
|
15
|
-
decorator = {
|
18
|
+
decorator = {
|
19
|
+
name: name,
|
20
|
+
receiver: receiver,
|
21
|
+
options: decorator_options || {},
|
22
|
+
}
|
23
|
+
|
16
24
|
accumulated_decorators << decorator
|
17
25
|
end
|
18
26
|
|
19
|
-
def
|
27
|
+
def accumulated_decorators?
|
20
28
|
Adornable::Utils.present?(accumulated_decorators)
|
21
29
|
end
|
22
30
|
|
@@ -80,14 +88,31 @@ module Adornable
|
|
80
88
|
@class_method_decorators[name] = decorators || []
|
81
89
|
end
|
82
90
|
|
83
|
-
def run_decorators(decorators, bound_method, *
|
84
|
-
return bound_method.call(*
|
91
|
+
def run_decorators(decorators, bound_method, *method_arguments)
|
92
|
+
return bound_method.call(*method_arguments) if Adornable::Utils.blank?(decorators)
|
93
|
+
|
85
94
|
decorator, *remaining_decorators = decorators
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
95
|
+
decorator_name = decorator[:name]
|
96
|
+
decorator_receiver = decorator[:receiver]
|
97
|
+
decorator_options = decorator[:options]
|
98
|
+
validate_decorator!(decorator_name, decorator_receiver, bound_method)
|
99
|
+
|
100
|
+
context = Adornable::Context.new(
|
101
|
+
method_receiver: bound_method.receiver,
|
102
|
+
method_name: bound_method.name,
|
103
|
+
method_arguments: method_arguments,
|
104
|
+
decorator_name: decorator_name,
|
105
|
+
decorator_options: decorator_options,
|
106
|
+
)
|
107
|
+
|
108
|
+
send_parameters = if Adornable::Utils.present?(decorator_options)
|
109
|
+
[decorator_name, context, decorator_options]
|
110
|
+
else
|
111
|
+
[decorator_name, context]
|
112
|
+
end
|
113
|
+
|
114
|
+
decorator_receiver.send(*send_parameters) do
|
115
|
+
run_decorators(remaining_decorators, bound_method, *method_arguments)
|
91
116
|
end
|
92
117
|
end
|
93
118
|
|
@@ -97,6 +122,7 @@ module Adornable
|
|
97
122
|
end
|
98
123
|
end
|
99
124
|
|
125
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Layout/LineLength
|
100
126
|
def validate_decorator!(decorator_name, decorator_receiver, bound_method = nil)
|
101
127
|
return if decorator_receiver.respond_to?(decorator_name)
|
102
128
|
|
@@ -106,7 +132,7 @@ module Adornable
|
|
106
132
|
method_location = bound_method.source_location
|
107
133
|
"Cannot decorate `#{method_full_name}` (defined at `#{method_location.first}:#{method_location.second})."
|
108
134
|
end
|
109
|
-
|
135
|
+
|
110
136
|
base_message = "Decorator method `#{decorator_name.inspect}` cannot be found on `#{decorator_receiver.inspect}`."
|
111
137
|
|
112
138
|
definition_hint = if decorator_receiver.is_a?(Class) && decorator_receiver.instance_methods.include?(decorator_name)
|
@@ -120,5 +146,6 @@ module Adornable
|
|
120
146
|
message = [location_hint, base_message, definition_hint].compact.join(" ")
|
121
147
|
raise Adornable::Error::InvalidDecoratorArguments, message
|
122
148
|
end
|
149
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Layout/LineLength
|
123
150
|
end
|
124
151
|
end
|
data/lib/adornable/utils.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Adornable
|
2
|
-
class Utils
|
4
|
+
class Utils # :nodoc:
|
3
5
|
class << self
|
4
6
|
def blank?(value)
|
5
7
|
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
@@ -12,6 +14,15 @@ module Adornable
|
|
12
14
|
def presence(value)
|
13
15
|
value if present?(value)
|
14
16
|
end
|
17
|
+
|
18
|
+
def formal_method_name(method_receiver, method_name)
|
19
|
+
receiver_name, name_delimiter = if method_receiver.is_a?(Class)
|
20
|
+
[method_receiver.to_s, '::']
|
21
|
+
else
|
22
|
+
[method_receiver.class.to_s, '#']
|
23
|
+
end
|
24
|
+
"`#{receiver_name}#{name_delimiter}#{method_name}`"
|
25
|
+
end
|
15
26
|
end
|
16
27
|
end
|
17
28
|
end
|
data/lib/adornable/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: adornable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Keegan Leitz
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-04-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.2'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '2.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '13.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '13.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,7 +52,83 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
|
-
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-performance
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.9'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.5'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.5'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.2'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.2'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: solargraph
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Adornable provides the ability to cleanly decorate methods in Ruby. You
|
126
|
+
can make and use your own decorators, and you can also use some of the built-in
|
127
|
+
ones that the gem provides. _Decorating_ methods is as simple as slapping a `decorate
|
128
|
+
:some_decorator` above your method definition. _Defining_ decorators can be as simple
|
129
|
+
as defining a method that yields to a block, or as complex as manipulating the decorated
|
130
|
+
method's receiver and arguments, and/or changing the functionality of the decorator
|
131
|
+
based on custom options supplied to it when initially applying the decorator.
|
56
132
|
email:
|
57
133
|
- kjleitz@gmail.com
|
58
134
|
executables: []
|
@@ -61,9 +137,9 @@ extra_rdoc_files: []
|
|
61
137
|
files:
|
62
138
|
- ".gitignore"
|
63
139
|
- ".rspec"
|
140
|
+
- ".rubocop.yml"
|
64
141
|
- ".travis.yml"
|
65
142
|
- Gemfile
|
66
|
-
- Gemfile.lock
|
67
143
|
- LICENSE
|
68
144
|
- README.md
|
69
145
|
- Rakefile
|
@@ -71,6 +147,7 @@ files:
|
|
71
147
|
- bin/console
|
72
148
|
- bin/setup
|
73
149
|
- lib/adornable.rb
|
150
|
+
- lib/adornable/context.rb
|
74
151
|
- lib/adornable/decorators.rb
|
75
152
|
- lib/adornable/error.rb
|
76
153
|
- lib/adornable/machinery.rb
|
@@ -80,7 +157,7 @@ homepage: https://github.com/kjleitz/adornable
|
|
80
157
|
licenses:
|
81
158
|
- MIT
|
82
159
|
metadata: {}
|
83
|
-
post_install_message:
|
160
|
+
post_install_message:
|
84
161
|
rdoc_options: []
|
85
162
|
require_paths:
|
86
163
|
- lib
|
@@ -88,15 +165,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
165
|
requirements:
|
89
166
|
- - ">="
|
90
167
|
- !ruby/object:Gem::Version
|
91
|
-
version:
|
168
|
+
version: 2.4.7
|
92
169
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
170
|
requirements:
|
94
171
|
- - ">="
|
95
172
|
- !ruby/object:Gem::Version
|
96
173
|
version: '0'
|
97
174
|
requirements: []
|
98
|
-
rubygems_version: 3.0.
|
99
|
-
signing_key:
|
175
|
+
rubygems_version: 3.0.9
|
176
|
+
signing_key:
|
100
177
|
specification_version: 4
|
101
178
|
summary: Method decorators for Ruby
|
102
179
|
test_files: []
|