decors 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c33d4924184f251e5ee9c3a372251355e09efe7f
4
- data.tar.gz: 9ddc59be81ccc266b78d36b3b9dc656b37c97bc0
3
+ metadata.gz: 7759f5e7d47d9e741ae03ea06cb02acaff1f984e
4
+ data.tar.gz: 2b41184cffff4ba470f4730fe68c3fe52e779518
5
5
  SHA512:
6
- metadata.gz: 3b965a9750314f1f8e921510c9e6275eae49ec18c3f8a2285b796700acfb813e9db6aa4f95e9c68d179d1cc2665bf8891c093a9617a94971f44cf4098f1f801e
7
- data.tar.gz: 6f6e62a25f06fe9e2d474aa1ec45cc43c85f4e598d070e2b0da5ee9b42d854b8b0a44ab96f5e2d4b4567921dd9ac5a4ee75d8a4b766c46a0820b8b6b008132e4
6
+ metadata.gz: 7abe88ecad1024a83fefd0629af2120689063b0335d0fcb8f5711a071213c5919177804ddd9d377f38faffc7e0bf0aaa7cd9772b42e4df15376326850328c219
7
+ data.tar.gz: 66d71eea9cd047431f76e349f4a95e15cdf06d60bcaaaa7103831f4eb12539fe3622ead623755a9e68ba820b77c6c10fc09708ab1e09fbe7a131f1644f46c7ec
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  .rspec
3
3
  .rubocop.yml
4
4
  *.gem
5
+ README.html
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.1
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ install:
3
+ - bundle install
4
+ script: bundle exec rspec spec
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Decors changelog
2
2
 
3
+ ## Version 0.2 (Released May 10, 2017)
4
+
5
+ - (bugfix) nested singleton handling (https://github.com/getbannerman/decors/issues/2)
6
+ - (bugfix) keep method visibility (https://github.com/getbannerman/decors/issues/3)
7
+
8
+
3
9
  ## Version 0.1 (Released April 24, 2017)
4
10
 
5
11
  - First implementation
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ ruby File.open('./.ruby-version').readline.split("\n").first
2
+
3
+ source 'https://rubygems.org' do
4
+ gemspec
5
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,43 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ decors (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.1)
10
+ diff-lcs (1.3)
11
+ method_source (0.8.2)
12
+ pry (0.10.4)
13
+ coderay (~> 1.1.0)
14
+ method_source (~> 0.8.1)
15
+ slop (~> 3.4)
16
+ rspec (3.5.0)
17
+ rspec-core (~> 3.5.0)
18
+ rspec-expectations (~> 3.5.0)
19
+ rspec-mocks (~> 3.5.0)
20
+ rspec-core (3.5.4)
21
+ rspec-support (~> 3.5.0)
22
+ rspec-expectations (3.5.0)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.5.0)
25
+ rspec-mocks (3.5.0)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.5.0)
28
+ rspec-support (3.5.0)
29
+ slop (3.6.0)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ decors!
36
+ pry!
37
+ rspec (~> 3)!
38
+
39
+ RUBY VERSION
40
+ ruby 2.3.1p112
41
+
42
+ BUNDLED WITH
43
+ 1.14.6
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+
2
+ Copyright (c) 2012 Michael Fairley
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Decors
2
2
 
3
- #### Yet another implementation of Python Decorators/Java annotation in ruby
3
+ #### Yet another implementation of Python Decorators/Java annotations in ruby
4
4
 
5
5
  Here are other implementations:
6
6
  - https://github.com/wycats/ruby_decorators
@@ -8,11 +8,191 @@ Here are other implementations:
8
8
  - https://github.com/fredwu/ruby_decorators
9
9
  - https://gist.github.com/reu/2762650
10
10
 
11
+ [See why](#faq) we decided to implement this differently.
12
+
11
13
  ## Install
12
14
 
13
- Add `gem 'decors'` to your Gemfile and then `bundle`
15
+ Add `gem 'decors'` to your Gemfile
16
+ Then run `bundle`
14
17
 
15
18
  ## Usage
16
19
 
20
+ #### Basic usage
21
+
22
+ ```ruby
23
+ # Create a new decorator
24
+ module DB
25
+ class WithinTransaction < ::Decors::DecoratorBase
26
+ def call(*)
27
+ DB.transaction { super }
28
+ end
29
+ end
30
+ end
31
+
32
+ # Define the alias on the class/module you want to use it
33
+ class Item
34
+ define_decorator :WithinTransaction, DB::WithinTransaction
35
+
36
+ # From now on you can use the decorator to decorate any method/singleton method
37
+
38
+ WithinTransaction()
39
+ def do_stuff_in_db
40
+ # do_stuff_in_db within a db transaction
41
+ end
42
+
43
+ WithinTransaction()
44
+ def self.do_stuff_in_db
45
+ # do_stuff_in_db within a db transaction
46
+ end
47
+ end
48
+ ```
49
+
50
+ #### Mixin usage
51
+
52
+ ```ruby
53
+ # Create a new decorator
54
+ module DbDecorator
55
+ class WithinTransaction < ::Decors::DecoratorBase; end
56
+ class LogBeforeDbHit < ::Decors::DecoratorBase; end
57
+
58
+ # define decorator that can be mixed with other classes/modules
59
+ define_mixin_decorator :WithinTransaction, WithinTransaction
60
+ define_mixin_decorator :LogBeforeDbHit, LogBeforeDbHit
61
+ end
62
+
63
+ class Item
64
+ extend DbDecorator
65
+
66
+ WithinTransaction()
67
+ LogBeforeDbHit()
68
+ def do_stuff_in_db
69
+ # do_stuff_in_db within a db transaction
70
+ end
71
+ end
72
+ ```
73
+
74
+ #### Argument modification
75
+
76
+ Sometimes you want the decorator to modify your method arguments. For example a generic way to define an api that handles pagination and filtering.
77
+
78
+ ```ruby
79
+ module Api
80
+ class SimpleApi < ::Decors::DecoratorBase
81
+ attr_reader :model_store, :pagination, :filtering
82
+
83
+ def initialize(*, model_store, pagination:, filtering:)
84
+ @model_store = model_store
85
+ @pagination = pagination
86
+ @filtering = filtering
87
+ super
88
+ end
89
+
90
+ def call(this, *)
91
+ models, page, per_page = paginate(filter(model_store.all))
92
+
93
+ super(this, models, page: page, per_page: per_page)
94
+ end
95
+
96
+ ...
97
+ end
98
+ end
99
+
100
+ class ItemController
101
+ SimpleApi(Item, pagination: true, filtering: true)
102
+ def index(items, page:, per_page:)
103
+ render json: { items: items.map(&:as_json), page: page, per_page: per_page }
104
+ end
105
+ end
106
+ ```
107
+
108
+ ## FAQ
109
+
110
+ #### What is going on under the wood?
111
+ ```ruby
112
+ class User
113
+ define_decorator :Delay, Async::Delay
114
+
115
+ Delay(priority: :high)
116
+ def delayed_computation(*args, &blk)
117
+ # CODE here
118
+ end
119
+ end
120
+
121
+ # Is strictly equivalent to
122
+
123
+ class User
124
+ def secret_computation(*args, &blk)
125
+ Async::Delay.new(User, User.instance_method(:secret_computation), priority: :high)
126
+ .call(self, *args, &blk) do
127
+ # CODE here
128
+ end
129
+ end
130
+ end
131
+ ```
132
+
133
+
134
+
135
+ #### What is the difference between this and method modifiers?
136
+
137
+ Method modifiers modify methods, specifically those are class methods which:
138
+
139
+ 1. Take the instance method name (symbol) as first argument
140
+ 2. Change subsequent calls to that method
141
+ 3. return the same symbol
142
+
143
+ The limitation of those are:
144
+ - They often come after the method which hurts lisibility
145
+ - If they come before the method you cannot have other argument than the method_name
146
+ - nesting those is a pain
147
+
148
+ with decorator you can do the following
149
+
150
+ ```ruby
151
+ class User
152
+ ...
153
+
154
+ Delay(priority: :high)
155
+ Retry(3)
156
+ DbTransaction()
157
+ def secret_computation
158
+ # computation inside transaction retried up to 3 times if an error occurs.
159
+ # All that performed async
160
+ end
161
+ end
162
+
163
+ # VS
164
+
165
+ class User
166
+ ...
167
+
168
+ db_transaction def secret_computation
169
+ end
170
+
171
+ retry_when_failing :secret_computation, 3
172
+ handle_async :secret_computation, priority: :high
173
+
174
+ end
175
+ ```
176
+
177
+ #### Why another implementation of this ?
178
+
179
+ Most of the implementation use the unary operator `+@` syntax to define the decorator which leads to a lost reference to the caller instance. This imply a global state of what are the current decorators to apply to the next method definition (which is not always threadsafe depending on the implementation).
180
+
181
+ Current implementations wasn't handling nested singleton (see https://github.com/getbannerman/decors/issues/2)
182
+
183
+ Current implementations have a rather large footprint. A class that needs a decorated method have all of its method definition hooked on the `method_added` callback. In our case it's only added at the decorator call. And we'll soon support callback removal after method definition.
184
+
185
+ The obvious drawback of our approach are: The syntax that might be less comprehensive for a new comer, and the namespacing which we handle via aliasing at decorator declaration.
186
+
187
+ #### What is that weird syntax?
188
+
189
+ By convention we use ConstantName case for the decorators in order not to mix them with other method call/DSL.
190
+ This is not enforced and you can have the text case you like.
191
+
192
+ Note that, this syntax is also used in other gems such as [Contract](https://github.com/egonSchiele/contracts.ruby)
193
+
17
194
  ## License
18
195
 
196
+ Copyright (c) 2017 [Bannerman](https://github.com/getbannerman), [Vivien Meyet](https://github.com/vmeyet)
197
+
198
+ Licensed under the [MIT license](http://fredwu.mit-license.org/).
data/decors.gemspec CHANGED
@@ -6,6 +6,7 @@ require 'decors'
6
6
  Gem::Specification.new do |gem|
7
7
  gem.name = "decors"
8
8
  gem.version = ::Decors::VERSION
9
+ gem.licenses = ['MIT']
9
10
  gem.authors = ['Vivien Meyet']
10
11
  gem.email = ['vivien@getbannerman.com']
11
12
  gem.description = "Ruby implementation of Python method decorators / Java annotations"
@@ -17,6 +18,6 @@ Gem::Specification.new do |gem|
17
18
  gem.test_files = gem.files.grep(%r{^spec/})
18
19
  gem.require_paths = ['lib']
19
20
 
20
- gem.add_development_dependency 'pry-byebug', '~> 0'
21
- gem.add_development_dependency 'rspec', '~> 0'
21
+ gem.add_development_dependency 'pry', '~> 0'
22
+ gem.add_development_dependency 'rspec', '~> 3'
22
23
  end
@@ -8,9 +8,9 @@ module Decors
8
8
  method_definer = mixin ? :define_method : :define_singleton_method
9
9
 
10
10
  send(method_definer, decorator_name) do |*params, &blk|
11
- ctx = self.singleton_class? ? ObjectSpace.each_object(self).first : self
12
- ctx.send(:extend, MethodAddedListener)
13
- ctx.declared_decorators << [decorator_class, params, blk]
11
+ extend(singleton_class? ? Decors::MethodAdded::SingletonListener : Decors::MethodAdded::StandardListener)
12
+
13
+ declared_decorators << [decorator_class, params, blk]
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,63 @@
1
+ require 'decors/utils'
2
+
3
+ module Decors
4
+ module MethodAdded
5
+ module Handler
6
+ private
7
+
8
+ def declared_decorators
9
+ @declared_decorators ||= []
10
+ end
11
+
12
+ # method addition handling is the same for singleton and instance method
13
+ def handle_method_addition(clazz, method_name)
14
+ # @_ignore_additions allows to temporarily disable the hook
15
+ return if @_ignore_additions || declared_decorators.empty?
16
+ decorator_class, params, blk = declared_decorators.pop
17
+
18
+ decorated_method = clazz.instance_method(method_name)
19
+
20
+ @_ignore_additions = true
21
+ decorator = decorator_class.new(clazz, decorated_method, *params, &blk)
22
+ @_ignore_additions = false
23
+
24
+ clazz.send(:define_method, method_name) { |*args, &block| decorator.call(self, *args, &block) }
25
+ clazz.send(Decors::Utils.method_visibility(decorated_method), method_name)
26
+ end
27
+ end
28
+
29
+ module StandardListener
30
+ include ::Decors::MethodAdded::Handler
31
+
32
+ def method_added(meth)
33
+ super
34
+ handle_method_addition(self, meth)
35
+ end
36
+
37
+ def singleton_method_added(meth)
38
+ super
39
+ handle_method_addition(singleton_class, meth)
40
+ end
41
+ end
42
+
43
+ module SingletonListener
44
+ include ::Decors::MethodAdded::Handler
45
+
46
+ def singleton_method_added(meth)
47
+ super
48
+ handle_method_addition(singleton_class, meth)
49
+ end
50
+
51
+ def self.extended(base)
52
+ ObjectSpace.each_object(base).first.send(:extend, ForwardToSingletonListener)
53
+ end
54
+
55
+ module ForwardToSingletonListener
56
+ def singleton_method_added(meth)
57
+ super
58
+ singleton_class.send(:handle_method_addition, singleton_class, meth)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ module Decors
2
+ module Utils
3
+ class << self
4
+ def method_visibility(method)
5
+ if method.owner.private_method_defined?(method.name)
6
+ :private
7
+ elsif method.owner.protected_method_defined?(method.name)
8
+ :protected
9
+ elsif method.owner.public_method_defined?(method.name)
10
+ :public
11
+ else
12
+ fail ArgumentError, 'Unkwnown method'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/decors.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'decors/decorator_base'
2
2
  require 'decors/decorator_definition'
3
- require 'decors/method_added_listener'
3
+ require 'decors/method_added'
4
4
 
5
5
  module Decors
6
- VERSION = '0.1.0'
6
+ VERSION = '0.2.0'
7
7
  end
data/spec/decors_spec.rb CHANGED
@@ -64,6 +64,25 @@ describe Decors do
64
64
  instance.test_method_not_decorated
65
65
  }
66
66
  end
67
+
68
+ context 'It keep the method visibility' do
69
+ before {
70
+ TestClass.class_eval {
71
+ SimpleDecorator()
72
+ public def public_test_method(*); end
73
+
74
+ SimpleDecorator()
75
+ private def private_test_method(*); end
76
+
77
+ SimpleDecorator()
78
+ protected def protected_test_method(*); end
79
+ }
80
+ }
81
+
82
+ it { expect(TestClass).to be_public_method_defined(:public_test_method) }
83
+ it { expect(TestClass).to be_protected_method_defined(:protected_test_method) }
84
+ it { expect(TestClass).to be_private_method_defined(:private_test_method) }
85
+ end
67
86
  end
68
87
 
69
88
  context 'when decorator is defining a method during initialization' do
@@ -310,6 +329,48 @@ describe Decors do
310
329
  after { TestClass.test_method }
311
330
  end
312
331
 
332
+ context 'when mixin extended on the class (singleton method in singleton class)' do
333
+ before {
334
+ TestClass.class_eval {
335
+ class << self
336
+ extend Decors::DecoratorDefinition
337
+
338
+ define_decorator :Deco, Deco
339
+
340
+ Deco()
341
+ def self.test_method
342
+ :ok
343
+ end
344
+ end
345
+ }
346
+ }
347
+
348
+ it { expect(Spy).to receive(:called) }
349
+ after { TestClass.singleton_class.test_method }
350
+ end
351
+
352
+ context 'when mixin extended on the class (method in singleton class of singleton class)' do
353
+ before {
354
+ TestClass.class_eval {
355
+ class << self
356
+ class << self
357
+ extend Decors::DecoratorDefinition
358
+
359
+ define_decorator :Deco, Deco
360
+
361
+ Deco()
362
+ def test_method
363
+ :ok
364
+ end
365
+ end
366
+ end
367
+ }
368
+ }
369
+
370
+ it { expect(Spy).to receive(:called) }
371
+ after { TestClass.singleton_class.test_method }
372
+ end
373
+
313
374
  context 'when mixin extended on the class (method in singleton class)' do
314
375
  before {
315
376
  TestClass.class_eval {
@@ -366,4 +427,3 @@ describe Decors do
366
427
  end
367
428
  end
368
429
  end
369
-
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decors
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vivien Meyet
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-25 00:00:00.000000000 Z
11
+ date: 2017-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: pry-byebug
14
+ name: pry
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '3'
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: '0'
40
+ version: '3'
41
41
  description: Ruby implementation of Python method decorators / Java annotations
42
42
  email:
43
43
  - vivien@getbannerman.com
@@ -46,17 +46,24 @@ extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
48
  - ".gitignore"
49
+ - ".ruby-version"
50
+ - ".travis.yml"
49
51
  - CHANGELOG.md
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - LICENSE
50
55
  - README.md
51
56
  - decors.gemspec
52
57
  - lib/decors.rb
53
58
  - lib/decors/decorator_base.rb
54
59
  - lib/decors/decorator_definition.rb
55
- - lib/decors/method_added_listener.rb
60
+ - lib/decors/method_added.rb
61
+ - lib/decors/utils.rb
56
62
  - spec/decors_spec.rb
57
63
  - spec/spec_helper.rb
58
64
  homepage: https://github.com/getbannerman/decors
59
- licenses: []
65
+ licenses:
66
+ - MIT
60
67
  metadata: {}
61
68
  post_install_message:
62
69
  rdoc_options: []
@@ -74,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
81
  version: '0'
75
82
  requirements: []
76
83
  rubyforge_project:
77
- rubygems_version: 2.6.8
84
+ rubygems_version: 2.5.1
78
85
  signing_key:
79
86
  specification_version: 4
80
87
  summary: Ruby implementation of Python method decorators / Java annotations
@@ -1,35 +0,0 @@
1
- module Decors
2
- # Mixin that add method_added hooks
3
- module MethodAddedListener
4
- def method_added(meth)
5
- super
6
- handle_method_addition(self, meth)
7
- end
8
-
9
- def singleton_method_added(meth)
10
- super
11
- handle_method_addition(singleton_class, meth)
12
- end
13
-
14
- def declared_decorators
15
- @declared_decorators ||= []
16
- end
17
-
18
- private
19
-
20
- # method addition handling is the same for singleton and instance method
21
- def handle_method_addition(clazz, method_name)
22
- # @_ignore_additions allows to temporarily disable the hook
23
- return if @_ignore_additions || declared_decorators.empty?
24
- decorator_class, params, blk = declared_decorators.pop
25
-
26
- decorated_method = clazz.instance_method(method_name)
27
-
28
- @_ignore_additions = true
29
- decorator = decorator_class.new(clazz, decorated_method, *params, &blk)
30
- @_ignore_additions = false
31
-
32
- clazz.send(:define_method, method_name) { |*args, &block| decorator.call(self, *args, &block) }
33
- end
34
- end
35
- end