adornable 1.0.0 → 1.1.1

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: b5866ce4d639cd7201a6467800e5ac85fa9e6b823c57496e8ac670059b5d7f5a
4
- data.tar.gz: '0418007247c26928a448fd069495b41cdd722d8902f7894185c7589ac8586d2e'
3
+ metadata.gz: 3b916559874ffdc52baa8f1b2a75a7e550ad600c501d45adeaf4c53aa0796c97
4
+ data.tar.gz: b7fd7d3eeaaae6b706fbe9f9b9f0d24770351a5ebbbd18759bd96d014466b001
5
5
  SHA512:
6
- metadata.gz: be5afe7b6f0eaadd690d4c1e62112a71d1e2f9d26481cec53bf4358a99394809d1d8f2b6a71bf464316320776753bb687bc65e71b96378aabe378da0f2065463
7
- data.tar.gz: 250c536c81d0ad0b5c7388af95623e1fc563b0fc41522902542a1e7e2ff9ca0f3f25f6e39378cc1d780ce048e4f184ab4d8a75c2a2f5d1c4ca3621e1084b645d
6
+ metadata.gz: b3d49e5efb60aec78d2b416c6d36c8c02844151a6824503f9301db45a4cd392ccea9f316500723e84a07d7ecb200b86a31c71bbc571e3294adfa637a7b92956f
7
+ data.tar.gz: a97a7142ccc356e8657421e32de71f30c6a9070e73a618e5fbaa238ae2b7e3e7193ce11782a942f75b0606d14af54f79a4fd772de83526f200c2da504f353140
data/.gitignore CHANGED
@@ -1,11 +1,70 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
1
+ # generic stuff
2
+ .env
3
+ *.gem
4
+ *.rbc
5
+ log/*.log
6
+ /.config
7
+ /InstalledFiles
6
8
  /pkg/
7
- /spec/reports/
8
9
  /tmp/
9
10
 
10
- # rspec failure tracking
11
+ # testy stuff
12
+ .rspec
11
13
  .rspec_status
14
+ *.orig
15
+ /coverage/
16
+ /coverage/
17
+ /db/*.sqlite3
18
+ /db/*.sqlite3-[0-9]*
19
+ /db/*.sqlite3-journal
20
+ /public/system
21
+ /spec/examples.txt
22
+ /spec/reports/
23
+ /spec/tmp
24
+ /test/tmp/
25
+ /test/version_tmp/
26
+ capybara-*.html
27
+ pickle-email-*.html
28
+ rerun.txt
29
+ test/dummy/db/*.sqlite3
30
+ test/dummy/db/*.sqlite3-journal
31
+ test/dummy/log/*.log
32
+ test/dummy/node_modules/
33
+ test/dummy/storage/
34
+ test/dummy/tmp/
35
+ test/dummy/yarn-error.log
36
+
37
+ # debuggy stuff
38
+ .byebug_history
39
+
40
+ ## doccy stuff
41
+ /.yardoc/
42
+ /_yardoc/
43
+ /doc/
44
+ /rdoc/
45
+
46
+ ## bundly stuff
47
+ /.bundle/
48
+ /vendor/bundle
49
+ /lib/bundler/man/
50
+
51
+ # "for a library or gem, you might want to ignore these files since the code is
52
+ # intended to run in multiple environments"
53
+ Gemfile.lock
54
+ .ruby-version
55
+ .ruby-gemset
56
+
57
+ # rvmmy stuff
58
+ .rvmrc
59
+
60
+ # editory stuff
61
+ .idea
62
+ .vscode
63
+ *.rdb
64
+
65
+ # systemy stuff
66
+ *.swm
67
+ *.swn
68
+ *.swo
69
+ *.swp
70
+ *.DS_Store
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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Keegan Leitz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,35 +1,365 @@
1
1
  # Adornable
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/adornable`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
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.
6
4
 
7
5
  ## Installation
8
6
 
9
- Add this line to your application's Gemfile:
7
+ ### Locally (to your application)
8
+
9
+ Add the gem to your application's `Gemfile`:
10
10
 
11
11
  ```ruby
12
12
  gem 'adornable'
13
13
  ```
14
14
 
15
- And then execute:
15
+ ...and then run:
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
16
20
 
17
- $ bundle
21
+ ### Globally (to your system)
18
22
 
19
- Or install it yourself as:
23
+ Alternatively, install it globally:
20
24
 
21
- $ gem install adornable
25
+ ```bash
26
+ gem install adornable
27
+ ```
22
28
 
23
29
  ## Usage
24
30
 
25
- TODO: Write usage instructions here
31
+ ### The basics
32
+
33
+ Think of a decorator as if it's just a wrapper function. You want something to happen before, around, or after a method is called, in a reusable (but dynamic) way? Maybe you want to print to a log whenever a certain method is called, or memoize its result so that additional calls don't have to re-execute the body of the method. You've tried this:
34
+
35
+ ```rb
36
+ class RandomValueGenerator
37
+ def value
38
+ # logging the method call
39
+ puts "Calling method `RandomValueGenerator#value` with no arguments"
40
+ # memoizing the result
41
+ @value ||= rand
42
+ end
43
+
44
+ def values(max)
45
+ # logging the method call
46
+ puts "Calling method `RandomValueGenerator#values` with arguments `[#{max}]`"
47
+ # memoizing the result
48
+ @values ||= {}
49
+ @values[max] ||= (1..max).map { rand }
50
+ end
51
+ end
52
+
53
+ random_value_generator = RandomValueGenerator.new
54
+
55
+ values1 = random_value_generator.values(1000)
56
+ # Calling method `RandomValueGenerator#values` with arguments `[1000]`
57
+ #=> [0.7044444114998132, 0.401953296596267, 0.3023797513191562, ...]
58
+
59
+ values1 = random_value_generator.values(1000)
60
+ # Calling method `RandomValueGenerator#values` with arguments `[1000]`
61
+ #=> [0.7044444114998132, 0.401953296596267, 0.3023797513191562, ...]
62
+
63
+ values3 = random_value_generator.values(5000)
64
+ # Calling method `RandomValueGenerator#values` with arguments `[5000]`
65
+ #=> [0.9916088057511011, 0.04466750434972333, 0.6073659341272127]
66
+
67
+ value1 = random_value_generator.value
68
+ # Calling method `RandomValueGenerator#value` with no arguments
69
+ #=> 0.4196007135344746
70
+
71
+ value2 = random_value_generator.value
72
+ # Calling method `RandomValueGenerator#value` with no arguments
73
+ #=> 0.4196007135344746
74
+ ```
75
+
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.
77
+
78
+ What if you could do this, instead, to achieve the same result?
79
+
80
+ ```rb
81
+ class RandomValueGenerator
82
+ extend Adornable
83
+
84
+ decorate :log
85
+ decorate :memoize
86
+ def value
87
+ rand
88
+ end
89
+
90
+ decorate :log
91
+ decorate :memoize
92
+ def values(max)
93
+ (1..max).map { rand }
94
+ end
95
+ end
96
+ ```
97
+
98
+ Nice, right?
99
+
100
+ > **Note:** in the case of multiple decorators decorating a method, each is executed from top to bottom.
101
+
102
+ ### Adding decorator functionality
103
+
104
+ Add the `::decorate` macro to your classes by `extend`-ing `Adornable`:
105
+
106
+ ```rb
107
+ class Foo
108
+ extend Adornable
109
+
110
+ # ...
111
+ end
112
+ ```
113
+
114
+ ### Decorating methods
115
+
116
+ Use the `decorate` macro to decorate methods.
117
+
118
+ #### Using built-in decorators
119
+
120
+ There are a couple of built-in decorators for common use-cases (these can be overridden if you so choose):
121
+
122
+ ```rb
123
+ class Foo
124
+ extend Adornable
125
+
126
+ decorate :log
127
+ def some_method
128
+ # the method name (Foo#some_method) and arguments will be logged
129
+ end
130
+
131
+ decorate :memoize
132
+ def some_other_method
133
+ # the return value will be cached
134
+ end
135
+
136
+ decorate :memoize
137
+ def yet_another_method(some_arg, some_other_arg = true, key_word_arg:, key_word_arg_with_default: 123)
138
+ # the return value will be cached based on the arguments the method receives
139
+ end
140
+
141
+ decorate :log
142
+ decorate :memoize, for_any_arguments: true
143
+ def oh_boy_another_method(some_arg, some_other_arg = true, key_word_arg:, key_word_arg_with_default: 123)
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
146
+ end
147
+
148
+ decorate :log
149
+ def self.yeah_it_works_on_class_methods_too
150
+ # the method name (Foo::yeah_it_works_on_class_methods_too) and arguments
151
+ # will be logged
152
+ end
153
+ end
154
+ ```
155
+
156
+ - `decorate :log` logs the method name and any passed arguments to the console
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
+
160
+ > **Note:** in the case of multiple decorators decorating a method, each is executed from top to bottom.
161
+
162
+ #### Writing custom decorators and using them _explicitly_
163
+
164
+ You can reference any decorator method you write, like so:
165
+
166
+ ```rb
167
+ class FooDecorators
168
+ # Note: this is defined as a CLASS method, but it can be applied to both class
169
+ # and instance methods. The only difference is in how you source the
170
+ # decorator when doing the decoration; see below for more info.
171
+ def self.blast_it(context)
172
+ puts "Blasting it!"
173
+ value = yield
174
+ "#{value}!"
175
+ end
176
+
177
+ # Note: this is defined as an INSTANCE method, but it can be applied to both
178
+ # class and instance methods. The only difference is in how you source
179
+ # the decorator when doing the decoration; see below for more info.
180
+ def wait_for_it(context, dot_count: 3)
181
+ ellipsis = dot_count.times.map { '.' }.join
182
+ puts "Waiting for it#{ellipsis}"
183
+ value = yield
184
+ "#{value}#{ellipsis}"
185
+ end
186
+ end
187
+
188
+ class Foo
189
+ extend Adornable
190
+
191
+ # Note: `from: FooDecorators` references a class (and will look for the
192
+ # `::blast_it` method on that class)
193
+ decorate :blast_it, from: FooDecorators
194
+ def some_method
195
+ "haha I'm a method"
196
+ end
197
+
198
+ # Note: `from: FooDecorators.new` references an instance (and will look for
199
+ # the `#wait_for_it` method on that instance)
200
+ decorate :wait_for_it, from: FooDecorators.new
201
+ def other_method
202
+ "haha I'm another method"
203
+ end
204
+
205
+ decorate :log
206
+ def yet_another_method(foo, bar:)
207
+ "haha I'm yet another method"
208
+ end
209
+ end
210
+
211
+ foo = Foo.new
212
+
213
+ foo.some_method
214
+ #=> "haha I'm a method!" # Note the exclamation mark
215
+
216
+ foo.other_method
217
+ #=> "haha I'm another method..." # Note the ellipsis
218
+
219
+ foo.yet_another_method(123, bloop: "bleep")
220
+ # Calling method `Foo#yet_another_method` with arguments `[123, {:bloop=>"bleep"}]`
221
+ #=> "haha I'm yet another method"
222
+ ```
223
+
224
+ 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.
225
+
226
+ 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).
227
+
228
+ ##### The required argument (`context`)
229
+
230
+ The **required argument** is an instance of `Adornable::Context`, which has some useful information about the decorated method being called
231
+
232
+ - `Adornable::Context#method_name`: the name of the decorated method being called (a symbol; e.g., `:some_method` or `:other_method`)
233
+ - `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)
234
+ - `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}]`)
235
+
236
+ ##### Custom keyword arguments (optional)
237
+
238
+ The **optional keyword arguments** are any parameters you want to be able to pass to the decorator method when decorating a method with `::decorate`:
239
+
240
+ - If you define a decorator like `def self.some_decorator(context)` then it takes no options when it is used: `decorate :some_decorator`.
241
+ - 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.
242
+ - 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.
243
+
244
+ ##### Yielding to the next decorator/decorated method
245
+
246
+ 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`.
247
+
248
+ > **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.
249
+ >
250
+ > A contrived example of when you might want to muck around with the return value:
251
+ >
252
+ > ```rb
253
+ > class FooDecorators
254
+ > def self.coerce_to_int(context)
255
+ > value = yield
256
+ > new_value = value.strip.to_i
257
+ > puts "New value: #{value.inspect} (class: #{value.class})"
258
+ > new_value
259
+ > end
260
+ > end
261
+ >
262
+ > class Foo
263
+ > extend Adornable
264
+ >
265
+ > decorate :coerce_to_int, from: FooDecorators
266
+ > def get_number_from_user
267
+ > print "Enter a number: "
268
+ > value = gets
269
+ > puts "Value: #{value.inspect} (class: #{value.class})"
270
+ > value
271
+ > end
272
+ > end
273
+ >
274
+ > foo = Foo.new
275
+ >
276
+ > foo.get_number_from_user
277
+ > # Enter a number
278
+ > # > 123
279
+ > # Value: "123" (class: String)
280
+ > # New value: 123 (class: Integer)
281
+ > #=> 123
282
+ > ```
283
+
284
+ #### Writing custom decorators and using them _implicitly_
285
+
286
+ You can also register decorator receivers so that you don't have to reference them with the `from:` option:
287
+
288
+ ```rb
289
+ class FooDecorators
290
+ def self.blast_it(context)
291
+ puts "Blasting it!"
292
+ value = yield
293
+ "#{value}!"
294
+ end
295
+ end
296
+
297
+ class MoreFooDecorators
298
+ def wait_for_it(context, dot_count: 3)
299
+ ellipsis = dot_count.times.map { '.' }.join
300
+ puts "Waiting for it#{ellipsis}"
301
+ value = yield
302
+ "#{value}#{ellipsis}"
303
+ end
304
+ end
305
+
306
+ class Foo
307
+ extend Adornable
308
+
309
+ add_decorators_from FooDecorators
310
+ add_decorators_from MoreFooDecorators.new
311
+
312
+ decorate :blast_it
313
+ decorate :wait_for_it, dot_count: 9
314
+ def some_method
315
+ "haha I'm a method"
316
+ end
317
+ end
318
+
319
+ foo = Foo.new
320
+
321
+ foo.some_method
322
+ # Blasting it!
323
+ # Waiting for it.........
324
+ #=> "haha I'm a method!........."
325
+ ```
326
+
327
+ > **Note:** All the rest of the stuff from the previous section (using decorators explicitly) also applies here (using decorators implicitly).
328
+
329
+ > **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.
330
+
331
+ > **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.
26
332
 
27
333
  ## Development
28
334
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
335
+ ### Install dependencies
336
+
337
+ ```bash
338
+ bin/setup
339
+ ```
340
+
341
+ ### Run the tests
342
+
343
+ ```bash
344
+ rake spec
345
+ ```
346
+
347
+ ### Run the linter
348
+
349
+ ```bash
350
+ rubocop
351
+ ```
30
352
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
353
+ ### Create release
354
+
355
+ ```
356
+ rake release
357
+ ```
32
358
 
33
359
  ## Contributing
34
360
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/adornable.
361
+ Bug reports and pull requests for this project are welcome at its [GitHub page](https://github.com/kjleitz/adornable). If you choose to contribute, please be nice so I don't have to run out of bubblegum, etc.
362
+
363
+ ## License
364
+
365
+ This project is open source, under the terms of the [MIT license.](https://github.com/kjleitz/adornable/blob/master/LICENSE)
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."
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,44 @@
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
27
  existing = instance_variable_get(memo_var_name)
18
28
  value = existing.nil? ? yield : existing
19
29
  instance_variable_set(memo_var_name, value)
20
30
  end
21
31
 
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)
32
+ def self.memoize_for_arguments(context)
33
+ method_receiver = context.method_receiver
34
+ method_name = context.method_name
35
+ method_args = context.method_arguments
36
+ memo_var_name = :"@adornable_memoized_for_arguments_#{method_receiver.object_id}_#{method_name}"
37
+ memo = instance_variable_get(memo_var_name) || {}
38
+ instance_variable_set(memo_var_name, memo)
39
+ args_key = method_args.inspect
40
+ memo[args_key] = yield if memo[args_key].nil?
41
+ memo[args_key]
27
42
  end
28
43
  end
29
44
  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.0"
4
+ VERSION = "1.1.1"
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.0
4
+ version: 1.1.1
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-12 00:00:00.000000000 Z
11
+ date: 2021-02-17 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,15 +137,17 @@ 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
143
+ - LICENSE
67
144
  - README.md
68
145
  - Rakefile
69
146
  - adornable.gemspec
70
147
  - bin/console
71
148
  - bin/setup
72
149
  - lib/adornable.rb
150
+ - lib/adornable/context.rb
73
151
  - lib/adornable/decorators.rb
74
152
  - lib/adornable/error.rb
75
153
  - lib/adornable/machinery.rb
@@ -79,7 +157,7 @@ homepage: https://github.com/kjleitz/adornable
79
157
  licenses:
80
158
  - MIT
81
159
  metadata: {}
82
- post_install_message:
160
+ post_install_message:
83
161
  rdoc_options: []
84
162
  require_paths:
85
163
  - lib
@@ -87,15 +165,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
87
165
  requirements:
88
166
  - - ">="
89
167
  - !ruby/object:Gem::Version
90
- version: '0'
168
+ version: 2.4.7
91
169
  required_rubygems_version: !ruby/object:Gem::Requirement
92
170
  requirements:
93
171
  - - ">="
94
172
  - !ruby/object:Gem::Version
95
173
  version: '0'
96
174
  requirements: []
97
- rubygems_version: 3.0.8
98
- signing_key:
175
+ rubygems_version: 3.1.4
176
+ signing_key:
99
177
  specification_version: 4
100
178
  summary: Method decorators for Ruby
101
179
  test_files: []
data/Gemfile.lock DELETED
@@ -1,35 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- adornable (1.0.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- diff-lcs (1.4.4)
10
- rake (10.5.0)
11
- rspec (3.10.0)
12
- rspec-core (~> 3.10.0)
13
- rspec-expectations (~> 3.10.0)
14
- rspec-mocks (~> 3.10.0)
15
- rspec-core (3.10.1)
16
- rspec-support (~> 3.10.0)
17
- rspec-expectations (3.10.1)
18
- diff-lcs (>= 1.2.0, < 2.0)
19
- rspec-support (~> 3.10.0)
20
- rspec-mocks (3.10.2)
21
- diff-lcs (>= 1.2.0, < 2.0)
22
- rspec-support (~> 3.10.0)
23
- rspec-support (3.10.2)
24
-
25
- PLATFORMS
26
- ruby
27
-
28
- DEPENDENCIES
29
- adornable!
30
- bundler (~> 1.17)
31
- rake (~> 10.0)
32
- rspec (~> 3.0)
33
-
34
- BUNDLED WITH
35
- 1.17.3