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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bea3ddf067bcb43aee2c2c9cd380297f6f741dc358572d65903a20fd1a0e1ec
4
- data.tar.gz: a12b2559a2a647ff58c36f072d4ad0bf71180f3382cffd69d2f43e4fd2d3a040
3
+ metadata.gz: dc81717998b4dc097647c96e4b7c89664b2d0c6969100dd22d713ba9c1720619
4
+ data.tar.gz: a3998df4caeb6db3823a218fc77ac3d1d56949e3f9a66cc340f65465d9037686
5
5
  SHA512:
6
- metadata.gz: 65d7e3170d90c2388a4e2a9610a6a2d2cfeb872153bb883791bf975d7f18eae4841ce3550010cfb0f17419b9bec7beeecd0d0553d9b2c46d35e6546ff0a23377
7
- data.tar.gz: f3e79890b4d421628bbbf21680d676a83446915b34a067eb1d25fe45806ed4e78f8729be5a15f4308d1f50cfb51911c7235e05762841dd0ca264df997f3b5538
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Adornable
2
2
 
3
- Adornable provides method decorators in Ruby... 'nuff said.
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
- ...but you have a million more methods to write, and if you refactor, you'll have to screw around with a whole metric butt-load of method definitions across your app.
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
- How about this instead?
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 :memoize_for_arguments
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 few built-in decorators:
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 :memoize_for_arguments
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 :memoize_for_arguments
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
- - `decorate :memoize_for_arguments` acts like `decorate :memoize` but it namespaces that cache by the arguments passed, so it will re-compute (and cache the result) 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
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
- #### Using custom decorators explicitly
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 class method
191
- def self.blast_it(method_receiver, method_name, arguments)
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 instance method
198
- def wait_for_it(method_receiver, method_name, arguments)
199
- puts "Waiting..."
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 arguments:
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
- - `method_receiver`: the actual object that the [decorated] method is being called on (an object/class); e.g., `Foo` or an instance of `Foo`
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
- > **Note:** 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`.
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
- Contrived example of when you might want to muck around with the return value:
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
- ```rb
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
- class Foo
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
- decorate :coerce_to_int, from: FooDecorators
269
- def get_number_from_user
270
- print "Enter a number: "
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
- foo = Foo.new
245
+ ##### Yielding to the next decorator/decorated method
278
246
 
279
- foo.get_number_from_user
280
- # Enter a number
281
- # > 123
282
- # Value: "123" (class: String)
283
- # New value: 123 (class: Integer)
284
- #=> 123
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
- #### Using custom decorators implicitly
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
- # Note: this is a class method
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
- # Note: this is a class method
303
- def self.wait_for_it(method_receiver, method_name, arguments)
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:** In the case of duplicate decorator methods, later receivers registered with `::add_decorators_from` will override any duplicate decorators from earlier registered receivers.
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 testss
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
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
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 = "adornable"
8
- spec.version = Adornable::VERSION
9
- spec.authors = ["Keegan Leitz"]
10
- spec.email = ["kjleitz@gmail.com"]
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 = "Method decorators for Ruby"
13
- spec.description = "Method decorators for Ruby"
14
- spec.homepage = "https://github.com/kjleitz/adornable"
15
- spec.license = "MIT"
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 = Dir.chdir(File.expand_path('..', __FILE__)) do
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 = "exe"
23
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
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", "~> 1.17"
27
- spec.add_development_dependency "rake", "~> 10.0"
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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "adornable"
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.has_accumulated_decorators?
35
+ return unless machinery.accumulated_decorators?
36
+
31
37
  machinery.apply_accumulated_decorators_to_instance_method!(method_name)
32
- original_method = self.instance_method(method_name)
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.has_accumulated_decorators?
48
+ return unless machinery.accumulated_decorators?
49
+
43
50
  machinery.apply_accumulated_decorators_to_class_method!(method_name)
44
- original_method = self.method(method_name)
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
@@ -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(method_receiver, method_name, arguments)
4
- receiver_name, name_delimiter = if method_receiver.is_a?(Class)
5
- [method_receiver.to_s, '::']
6
- else
7
- [method_receiver.class.to_s, '#']
8
- end
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(method_receiver, method_name, arguments)
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
- existing = instance_variable_get(memo_var_name)
18
- value = existing.nil? ? yield : existing
19
- instance_variable_set(memo_var_name, value)
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(method_receiver, method_name, arguments)
23
- memo_var_name = :"@adornable_memoized_#{method_receiver.object_id}__#{method_name}__#{arguments}"
24
- existing = instance_variable_get(memo_var_name)
25
- value = existing.nil? ? yield : existing
26
- instance_variable_set(memo_var_name, value)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Adornable
2
4
  module Error
3
5
  class Base < ::StandardError
@@ -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 = { name: name, receiver: receiver }
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 has_accumulated_decorators?
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, *args)
84
- return bound_method.call(*args) if Adornable::Utils.blank?(decorators)
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
- name = decorator[:name]
87
- receiver = decorator[:receiver]
88
- validate_decorator!(name, receiver, bound_method)
89
- receiver.send(name, bound_method.receiver, bound_method.name, args) do
90
- run_decorators(remaining_decorators, bound_method, *args)
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
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Adornable
2
- VERSION = "1.0.1"
4
+ VERSION = "1.2.0"
3
5
  end
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.1
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-02-13 00:00:00.000000000 Z
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: '1.17'
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: '1.17'
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: '10.0'
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: '10.0'
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
- description: Method decorators for Ruby
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: '0'
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.8
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: []