adornable 1.2.0 → 1.3.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: dc81717998b4dc097647c96e4b7c89664b2d0c6969100dd22d713ba9c1720619
4
- data.tar.gz: a3998df4caeb6db3823a218fc77ac3d1d56949e3f9a66cc340f65465d9037686
3
+ metadata.gz: f70e9c6dc00ff446a979d776bb8effbe0063b8e3cb1269081fe3dd78749b1a6d
4
+ data.tar.gz: dd2ee8ef02b67f5c1c48f83d3f04e9869bac1705c8eb12101c5e90e06274e6d8
5
5
  SHA512:
6
- metadata.gz: 4aaf8fe2928bb4652dc5a7b1f235fadc7114580cfc29d91a9fdf508edbba15cccc09627b6a097552774c73babede934ca77a9388fa7ce1bfdbfb5975ab0661da
7
- data.tar.gz: cc2ec1780014bfb5043a44857a2d9fc6eb7957ec245fef03accbe64436c11cc9f2596105d25d5e888b136ca27d57565ec414f9fbb466f72fc425744d6fe69af2
6
+ metadata.gz: 4cc9f4866c9e130ca8fc7d8cf2988a43c252604bf9030b919526d289badc3554d91c2eb4f825462ff6ac491f0f6519aaacff2e2bdd5c5c8a16dda33bd865c735
7
+ data.tar.gz: 1ecc141ccfff6820a76811bef1427ff90ca4072c35690b732dfbae24a45c6f7c901b8aa83e8172e57d69ad3eca84a50736f8835cfaf3339b56b980a1b72dd1c6
@@ -0,0 +1,48 @@
1
+ name: Test and lint
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ pull_request:
9
+ branches:
10
+ - main
11
+
12
+ # Allows you to run this workflow manually from the Actions tab
13
+ workflow_dispatch:
14
+
15
+ jobs:
16
+ build:
17
+ name: Run RSpec tests and RuboCop lints
18
+
19
+ runs-on: ubuntu-latest
20
+
21
+ strategy:
22
+ matrix:
23
+ ruby-version:
24
+ - 3.2
25
+ - 3.1
26
+ - 3.0
27
+ - 2.7
28
+ - 2.6
29
+ - 2.5
30
+
31
+ steps:
32
+ - name: Checkout the repo
33
+ uses: actions/checkout@v2
34
+
35
+ - name: Set up Ruby v${{ matrix.ruby-version }}
36
+ uses: ruby/setup-ruby@v1
37
+ with:
38
+ ruby-version: ${{ matrix.ruby-version }}
39
+ bundler-cache: true
40
+
41
+ - name: Install dependencies
42
+ run: bundle install
43
+
44
+ - name: Run RSpec tests
45
+ run: bundle exec rake spec
46
+
47
+ - name: Run RuboCop lints
48
+ run: bundle exec rubocop
data/.rubocop.yml CHANGED
@@ -7,6 +7,12 @@ require:
7
7
 
8
8
  AllCops:
9
9
  NewCops: enable
10
+ TargetRubyVersion: 2.5
11
+
12
+ # Gemspec
13
+
14
+ Gemspec/RequiredRubyVersion:
15
+ Enabled: false
10
16
 
11
17
  # Layout
12
18
 
@@ -24,6 +30,7 @@ Layout/FirstArrayElementIndentation:
24
30
  # Metrics
25
31
 
26
32
  Metrics/AbcSize:
33
+ Max: 20
27
34
  CountRepeatedAttributes: false
28
35
  Exclude:
29
36
  - 'spec/**/*_spec.rb'
@@ -60,10 +67,13 @@ Metrics/ModuleLength:
60
67
  Exclude:
61
68
  - 'spec/**/*_spec.rb'
62
69
 
70
+ Metrics/ParameterLists:
71
+ CountKeywordArgs: false
72
+
63
73
  # Rspec
64
74
 
65
75
  RSpec/ExampleLength:
66
- Max: 25
76
+ Max: 40
67
77
 
68
78
  RSpec/MessageSpies:
69
79
  Enabled: false
data/Gemfile CHANGED
@@ -6,3 +6,15 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
7
  # Specify your gem's dependencies in adornable.gemspec
8
8
  gemspec
9
+
10
+ group :development, :test do
11
+ gem "bundler", "~> 2.2"
12
+ gem "pry"
13
+ gem "rake", "~> 13.0"
14
+ gem "rspec", "~> 3.0"
15
+ gem "rubocop", "~> 1.10"
16
+ gem "rubocop-performance", "~> 1.9"
17
+ gem "rubocop-rake", "~> 0.5"
18
+ gem "rubocop-rspec", "~> 2.2"
19
+ gem "solargraph"
20
+ end
data/README.md CHANGED
@@ -4,6 +4,8 @@ Adornable provides the ability to cleanly decorate methods in Ruby. You can make
4
4
 
5
5
  ## Installation
6
6
 
7
+ **NOTE:** This library is tested with Ruby versions 2.5.x through 3.2.x.
8
+
7
9
  ### Locally (to your application)
8
10
 
9
11
  Add the gem to your application's `Gemfile`:
@@ -232,7 +234,9 @@ The **required argument** is an instance of `Adornable::Context`, which has some
232
234
 
233
235
  - `Adornable::Context#method_name`: the name of the decorated method being called (a symbol; e.g., `:some_method` or `:other_method`)
234
236
  - `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}]`)
237
+ - `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, baz: 456)` then `method_arguments` would be `[123, {:bar=>true,:baz=>456}]`)
238
+ - `Adornable::Context#method_positional_args`: an array of just the positional arguments passed to the decorated method, excluding keyword arguments (e.g., if `:yet_another_method` was called like `Foo.new.yet_another_method(123, bar: true, baz: 456)` then `method_positional_args` would be `[123]`)
239
+ - `Adornable::Context#method_kwargs`: a hash of just the keyword arguments passed to the decorated method (e.g., if `:yet_another_method` was called like `Foo.new.yet_another_method(123, { bam: "hi" }, bar: true, baz: 456)` then `method_kwargs` would be `{:bar=>true,:baz=>456}`)
236
240
 
237
241
  ##### Custom keyword arguments (optional)
238
242
 
@@ -351,12 +355,6 @@ rake spec
351
355
  rubocop
352
356
  ```
353
357
 
354
- ### Create release
355
-
356
- ```
357
- rake release
358
- ```
359
-
360
358
  ## Contributing
361
359
 
362
360
  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.
data/adornable.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
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
15
  spec.homepage = "https://github.com/kjleitz/adornable"
16
16
  spec.license = "MIT"
17
- spec.required_ruby_version = ">= 2.4.7"
17
+ spec.required_ruby_version = ">= 2.5.0"
18
18
 
19
19
  # Specify which files should be added to the gem when it is released.
20
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -24,13 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.bindir = "exe"
25
25
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
26
  spec.require_paths = ["lib"]
27
-
28
- spec.add_development_dependency "bundler", "~> 2.2"
29
- spec.add_development_dependency "rake", "~> 13.0"
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"
27
+ spec.metadata['rubygems_mfa_required'] = 'true'
36
28
  end
data/bin/asdf_switch ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # First argument should be desired ruby version. If a partial version is given
4
+ # (e.g., 2.6) and multiple versions are found to be installed (e.g., 2.6.9 and
5
+ # 2.6.10), then the highest version (in semver version order) will be selected.
6
+ SELECTED_RUBY_VERSION=$(asdf list ruby | sort -V | sed -e 's/^[[:space:]]*//' | grep "^$1" | tail -n 1)
7
+
8
+ if [ -z "$SELECTED_RUBY_VERSION" ]; then
9
+ echo "Ruby version $1 is not installed. Try running:"
10
+ echo " asdf install ruby $1"
11
+ exit 1
12
+ fi
13
+
14
+ echo "Using Ruby version $SELECTED_RUBY_VERSION"
15
+
16
+ echo "(setting global Ruby)"
17
+ asdf global ruby $SELECTED_RUBY_VERSION
18
+
19
+ echo "(reshimming for this version)"
20
+ asdf reshim ruby $SELECTED_RUBY_VERSION
21
+
22
+ echo "(reinstalling bundled gems)"
23
+ bundle install --redownload
24
+
@@ -8,14 +8,26 @@ module Adornable
8
8
  method_receiver
9
9
  method_name
10
10
  method_arguments
11
+ method_positional_args
12
+ method_kwargs
11
13
  decorator_name
12
14
  decorator_options
13
15
  ])
14
16
 
15
- def initialize(method_receiver:, method_name:, method_arguments:, decorator_name:, decorator_options:)
17
+ def initialize(
18
+ method_receiver:,
19
+ method_name:,
20
+ method_arguments:,
21
+ method_positional_args:,
22
+ method_kwargs:,
23
+ decorator_name:,
24
+ decorator_options:
25
+ )
16
26
  @method_receiver = method_receiver
17
27
  @method_name = method_name
18
28
  @method_arguments = method_arguments
29
+ @method_positional_args = method_positional_args
30
+ @method_kwargs = method_kwargs
19
31
  @decorator_name = decorator_name
20
32
  @decorator_options = decorator_options
21
33
  end
@@ -38,14 +38,14 @@ module Adornable
38
38
  clear_accumulated_decorators!
39
39
  end
40
40
 
41
- def run_decorated_instance_method(bound_method, *args)
41
+ def run_decorated_instance_method(bound_method, *args, **kwargs)
42
42
  decorators = get_instance_method_decorators(bound_method.name)
43
- run_decorators(decorators, bound_method, *args)
43
+ run_decorators(decorators, bound_method, *args, **kwargs)
44
44
  end
45
45
 
46
- def run_decorated_class_method(bound_method, *args)
46
+ def run_decorated_class_method(bound_method, *args, **kwargs)
47
47
  decorators = get_class_method_decorators(bound_method.name)
48
- run_decorators(decorators, bound_method, *args)
48
+ run_decorators(decorators, bound_method, *args, **kwargs)
49
49
  end
50
50
 
51
51
  private
@@ -88,8 +88,10 @@ module Adornable
88
88
  @class_method_decorators[name] = decorators || []
89
89
  end
90
90
 
91
- def run_decorators(decorators, bound_method, *method_arguments)
92
- return bound_method.call(*method_arguments) if Adornable::Utils.blank?(decorators)
91
+ def run_decorators(decorators, bound_method, *method_positional_args, **method_kwargs)
92
+ if Adornable::Utils.blank?(decorators)
93
+ return Adornable::Utils.empty_aware_send(bound_method, :call, method_positional_args, method_kwargs)
94
+ end
93
95
 
94
96
  decorator, *remaining_decorators = decorators
95
97
  decorator_name = decorator[:name]
@@ -97,22 +99,30 @@ module Adornable
97
99
  decorator_options = decorator[:options]
98
100
  validate_decorator!(decorator_name, decorator_receiver, bound_method)
99
101
 
102
+ # This is for backwards-compatibility between Ruby 2.x and Ruby 3.x; in v3
103
+ # keyword arguments are treated differently than in v2 with respect to
104
+ # hash parameter equivalency. Previously, it was easy to just assume
105
+ # `method_arguments` could be an array with a hash at the end representing
106
+ # any given keyword arguments. However, in Ruby 3.x, we have to be able to
107
+ # distinguish between kwargs and trailing positional args of type `Hash`,
108
+ # so we'll shim `Adornable::Context#method_arguments` to look like it used
109
+ # to and then provide two new properties, `#method_positional_args` and
110
+ # `#method_kwargs`, to `Adornable::Context` for explicitness.
111
+ method_arguments = method_positional_args.dup
112
+ method_arguments << method_kwargs if Adornable::Utils.present?(method_kwargs)
113
+
100
114
  context = Adornable::Context.new(
101
115
  method_receiver: bound_method.receiver,
102
116
  method_name: bound_method.name,
103
117
  method_arguments: method_arguments,
118
+ method_positional_args: method_positional_args,
119
+ method_kwargs: method_kwargs,
104
120
  decorator_name: decorator_name,
105
121
  decorator_options: decorator_options,
106
122
  )
107
123
 
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)
124
+ Adornable::Utils.empty_aware_send(decorator_receiver, decorator_name, [context], decorator_options) do
125
+ run_decorators(remaining_decorators, bound_method, *method_positional_args, **method_kwargs)
116
126
  end
117
127
  end
118
128
 
@@ -23,6 +23,29 @@ module Adornable
23
23
  end
24
24
  "`#{receiver_name}#{name_delimiter}#{method_name}`"
25
25
  end
26
+
27
+ # This craziness is here because Ruby 2.6 and below don't like when you
28
+ # pass even _empty_ arguments to `#call` or `#send` or any other method
29
+ # with a splat, for callables that take no arguments. For example, this
30
+ # takes the place of:
31
+ #
32
+ # receiver.send(method_name, *splat_args, **splat_kwargs)
33
+ #
34
+ # ...or:
35
+ #
36
+ # receiver.some_method(*splat_args, **splat_kwargs)
37
+ #
38
+ # ...which is not cool <= 2.6.x apparently, if `#some_method` takes zero
39
+ # arguments even if both `splat_args` and `splat_kwargs` are empty (thus
40
+ # passing it zero arguments in actuality). Oh well.
41
+ #
42
+ def empty_aware_send(receiver, method_name, splat_args, splat_kwargs, &block)
43
+ return receiver.send(method_name, &block) if splat_args.empty? && splat_kwargs.empty?
44
+ return receiver.send(method_name, *splat_args, &block) if splat_kwargs.empty?
45
+ return receiver.send(method_name, **splat_kwargs, &block) if splat_args.empty?
46
+
47
+ receiver.send(method_name, *splat_args, **splat_kwargs, &block)
48
+ end
26
49
  end
27
50
  end
28
51
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Adornable
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/adornable.rb CHANGED
@@ -14,7 +14,7 @@ module Adornable
14
14
  end
15
15
 
16
16
  def decorate(decorator_name, from: nil, defer_validation: false, **decorator_options)
17
- if Adornable::Utils.blank?(name)
17
+ if Adornable::Utils.blank?(decorator_name)
18
18
  raise Adornable::Error::InvalidDecoratorArguments, "Decorator name must be provided."
19
19
  end
20
20
 
@@ -36,10 +36,16 @@ module Adornable
36
36
 
37
37
  machinery.apply_accumulated_decorators_to_instance_method!(method_name)
38
38
  original_method = instance_method(method_name)
39
- define_method(method_name) do |*args|
39
+
40
+ # NB: If you only supply `*args` to the block, you get kwargs as a trailing
41
+ # Hash member in the `args` array. If you supply both `*args, **kwargs` to
42
+ # the block, kwargs are excluded from the `args` array and only appear in
43
+ # the `kwargs` argument as a Hash.
44
+ define_method(method_name) do |*args, **kwargs|
40
45
  bound_method = original_method.bind(self)
41
- machinery.run_decorated_instance_method(bound_method, *args)
46
+ machinery.run_decorated_instance_method(bound_method, *args, **kwargs)
42
47
  end
48
+
43
49
  super
44
50
  end
45
51
 
@@ -49,9 +55,15 @@ module Adornable
49
55
 
50
56
  machinery.apply_accumulated_decorators_to_class_method!(method_name)
51
57
  original_method = method(method_name)
52
- define_singleton_method(method_name) do |*args|
53
- machinery.run_decorated_class_method(original_method, *args)
58
+
59
+ # NB: If you only supply `*args` to the block, you get kwargs as a trailing
60
+ # Hash member in the `args` array. If you supply both `*args, **kwargs` to
61
+ # the block, kwargs are excluded from the `args` array and only appear in
62
+ # the `kwargs` argument as a Hash.
63
+ define_singleton_method(method_name) do |*args, **kwargs|
64
+ machinery.run_decorated_class_method(original_method, *args, **kwargs)
54
65
  end
66
+
55
67
  super
56
68
  end
57
69
  end
metadata CHANGED
@@ -1,127 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adornable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keegan Leitz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-19 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '2.2'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '2.2'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '13.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '13.0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
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'
11
+ date: 2023-07-10 00:00:00.000000000 Z
12
+ dependencies: []
125
13
  description: Adornable provides the ability to cleanly decorate methods in Ruby. You
126
14
  can make and use your own decorators, and you can also use some of the built-in
127
15
  ones that the gem provides. _Decorating_ methods is as simple as slapping a `decorate
@@ -135,6 +23,7 @@ executables: []
135
23
  extensions: []
136
24
  extra_rdoc_files: []
137
25
  files:
26
+ - ".github/workflows/main.yml"
138
27
  - ".gitignore"
139
28
  - ".rspec"
140
29
  - ".rubocop.yml"
@@ -144,6 +33,7 @@ files:
144
33
  - README.md
145
34
  - Rakefile
146
35
  - adornable.gemspec
36
+ - bin/asdf_switch
147
37
  - bin/console
148
38
  - bin/setup
149
39
  - lib/adornable.rb
@@ -156,7 +46,8 @@ files:
156
46
  homepage: https://github.com/kjleitz/adornable
157
47
  licenses:
158
48
  - MIT
159
- metadata: {}
49
+ metadata:
50
+ rubygems_mfa_required: 'true'
160
51
  post_install_message:
161
52
  rdoc_options: []
162
53
  require_paths:
@@ -165,14 +56,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
165
56
  requirements:
166
57
  - - ">="
167
58
  - !ruby/object:Gem::Version
168
- version: 2.4.7
59
+ version: 2.5.0
169
60
  required_rubygems_version: !ruby/object:Gem::Requirement
170
61
  requirements:
171
62
  - - ">="
172
63
  - !ruby/object:Gem::Version
173
64
  version: '0'
174
65
  requirements: []
175
- rubygems_version: 3.0.9
66
+ rubygems_version: 3.4.1
176
67
  signing_key:
177
68
  specification_version: 4
178
69
  summary: Method decorators for Ruby