decors 0.1.0 → 0.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
  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