casting 0.6.7 → 0.6.8

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: 4ffec94418d01d7ecc13e0f30923c57c2bcf9eeb
4
- data.tar.gz: 448fba5864860e5e8c06f21fe9bc11e31ba9f8d6
3
+ metadata.gz: 02fe7c29c8da27adba6036307ab658548bf6a20a
4
+ data.tar.gz: 1b674c20260d1e89830e088f0db5fe2dbee43639
5
5
  SHA512:
6
- metadata.gz: 6cd3187516eb76ff46c0a31759d961ae835d65b85ffd504620fdc9bd132d55328f2f63c1ab8b925559044509a36d7a4426634fdb21fe3f8d63e48fb841c30b2c
7
- data.tar.gz: 2f93492b5429e9549fc011057cd79568624d710bf3eef66a768fe6ea2540c01a2cec2a0e5e99bb8b3a4ad821630c445560dc41f4c76da8ca1668151a242ae8d1
6
+ metadata.gz: c86a535a9ae96e3e6b1034488cda5b487c76e51a028b7fce0f907ce96b580adb46d748f0b1a8231fc287b9a164f3d71171184d80776c7bba571dca60e0b59c36
7
+ data.tar.gz: 031bfc7f0989891cb12c48097e5990bbae8d95cb2e348e8b652fa4b84f652bca2b0cfab07ea34ec49c5506cc0ac45d96e6891ace776b52cf4f35feac1593797c
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012-2014 Jim Gay
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,331 @@
1
+ # Casting
2
+
3
+ [![Build Status](https://travis-ci.org/saturnflyer/casting.png?branch=master)](https://travis-ci.org/saturnflyer/casting)
4
+ [![Code Climate](https://codeclimate.com/github/saturnflyer/casting.png)](https://codeclimate.com/github/saturnflyer/casting)
5
+ [![Coverage Status](https://coveralls.io/repos/saturnflyer/casting/badge.png)](https://coveralls.io/r/saturnflyer/casting)
6
+ [![Gem Version](https://badge.fury.io/rb/casting.png)](http://badge.fury.io/rb/casting)
7
+
8
+ ## Add behavior to your objects without using extend
9
+ Do it for the life of the object or only for the life of a block of code.
10
+
11
+ Casting gives you real delegation that flattens your object structure compared to libraries
12
+ like Delegate or Forwardable. With casting, you can implement your own decorators that
13
+ will be so much simpler than using wrappers.
14
+
15
+ Here's a quick example that you might try in a Rails project:
16
+
17
+ ```ruby
18
+ # implement a module that contains information for the request response
19
+ # and apply it to an object in your system.
20
+ def show
21
+ respond_with user.cast_as(UserRepresenter)
22
+ end
23
+ ```
24
+
25
+ To use proper delegation, your approach should preserve `self` as a reference
26
+ to the original object receiving a method. When the object receiving the forwarded
27
+ message has its own and separate notion of `self`, you're working with a wrapper (also called
28
+ consultation) and not using delegation.
29
+
30
+ The Ruby standard library includes a library called "delegate", but it is
31
+ a consultation approach. With that "delegate", all messages are forwarded to
32
+ another object, but the attendant object maintains its own identity.
33
+
34
+ With Casting, your defined methods may reference `self` and during
35
+ execution it will refer to the original client object.
36
+
37
+ Casting was created while exploring ideas for [cleaning up ruby programs](http://clean-ruby.com).
38
+
39
+ ## Usage
40
+
41
+ To use Casting, you must first extend an object as the delegation client:
42
+
43
+ ```ruby
44
+ actor = Object.new
45
+ actor.extend(Casting::Client)
46
+ ```
47
+
48
+ Or you may include the module in a particular class:
49
+
50
+ ```ruby
51
+ class Actor
52
+ include Casting::Client
53
+ end
54
+ actor = Actor.new
55
+ ```
56
+
57
+ Your objects will have a few additional methods: `delegation`, `cast`, and if you do not *already* have it defined (from another library, for example): `delegate`. The `delegate` method is aliased to `cast`.
58
+
59
+ Then you may delegate a method to an attendant object:
60
+
61
+ ```ruby
62
+ actor.delegate(:hello_world, other_actor)
63
+ ```
64
+
65
+ Or you may create an object to manage the delegation of methods to an attendant object:
66
+
67
+ ```ruby
68
+ actor.delegation(:hello_world).to(other_actor).call
69
+ ```
70
+
71
+ You may also delegate methods without an explicit attendant instance, but provide
72
+ a module containing the behavior you need to use:
73
+
74
+ ```ruby
75
+ actor.delegate(:hello_world, GreetingModule)
76
+ # or
77
+ actor.delegation(:hello_world).to(GreetingModule).call
78
+ ```
79
+
80
+ Pass arguments to your delegated method:
81
+
82
+ ```ruby
83
+ actor.delegate(:verbose_method, another_actor, arg1, arg2)
84
+
85
+ actor.delegation(:verbose_method).to(another_actor).with(arg1, arg2).call
86
+
87
+ actor.delegation(:verbose_method).to(another_actor).call(arg1, arg2)
88
+ ```
89
+
90
+ _That's great, but why do I need to do these extra steps? I just want to run the method._
91
+
92
+ Casting gives you the option to do what you want. You can run just a single method once, or alter your object to always delegate. Even better, you can alter your object to delegate temporarily...
93
+
94
+ ## Temporary Behavior
95
+
96
+ Casting also provides an option to temporarily apply behaviors to an object.
97
+
98
+ Once your class or object is a `Casting::Client` you may send the `delegate_missing_methods` message to it and your object will use `method_missing` to delegate methods to a stored attendant.
99
+
100
+ ```ruby
101
+ actor.hello_world #=> NoMethodError
102
+
103
+ Casting.delegating(actor => GreetingModule) do
104
+ actor.hello_world #=> output the value / perform the method
105
+ end
106
+
107
+ actor.hello_world #=> NoMethodError
108
+ ```
109
+
110
+ The use of `method_missing` is opt-in. If you don't want that mucking up your method calls, just don't tell it to `delegate_missing_methods`.
111
+
112
+ Before the block is run in `Casting.delegating`, a collection of delegate objects is set on the object to the provided attendant. Then the block yields, and an `ensure` block cleans up the stored attendant.
113
+
114
+ This allows you to nest your `delegating` blocks as well:
115
+
116
+ ```ruby
117
+ actor.hello_world #=> NoMethodError
118
+
119
+ Casting.delegating(actor => GreetingModule) do
120
+ actor.hello_world #=> output the value / perform the method
121
+
122
+ Casting.delegating(actor => OtherModule) do
123
+ actor.hello_world #=> still works!
124
+ actor.other_method # values/operations from the OtherModule
125
+ end
126
+
127
+ actor.other_method #=> NoMethodError
128
+ actor.hello_world #=> still works!
129
+ end
130
+
131
+ actor.hello_world #=> NoMethodError
132
+ ```
133
+
134
+ Currently, by using `delegate_missing_methods` you forever mark that object or class to use `method_missing`. This may change in the future.
135
+
136
+ ### Manual Delegate Management
137
+
138
+ If you'd rather not wrap things in the `delegating` block, you can control the delegation yourself.
139
+ For example, you can `cast_as` and `uncast` an object with a given module:
140
+
141
+ ```ruby
142
+ actor.cast_as(GreetingModule)
143
+ actor.hello_world # all subsequent calls to this method run from the module
144
+ actor.uncast # manually cleanup the delegate
145
+ actor.hello_world # => NoMethodError
146
+ ```
147
+
148
+ These methods are only defined on your `Casting::Client` object when you tell it to `delegate_missing_methods`. Because these require `method_missing`, they do not exist until you opt-in.
149
+
150
+ ## I have a Rails app, how does this help me?
151
+
152
+ Well, a common use for this behavior would be in using decorators.
153
+
154
+ When using a wrapper, your forms can behave unexpectedly
155
+
156
+ ```ruby
157
+ class UsersController
158
+ def edit
159
+ @user = UserDecorator.new(User.find(params[:id]))
160
+ end
161
+ end
162
+
163
+ <%= form_for(@user) do |f| %> #=> <form action="/user_decorators/1">
164
+ ```
165
+
166
+ Ruby allows you to hack this by defining the `class` method:
167
+
168
+ ```ruby
169
+ class UserDecorator
170
+ def class
171
+ User
172
+ end
173
+ end
174
+ ```
175
+
176
+ That would solve the problem, and it works! But having an object report that
177
+ its class is something other than what it actually is can be confusing
178
+ when you're debugging.
179
+
180
+ Instead, you could cast the object as a module and your form will generate properly:
181
+
182
+ ```ruby
183
+ class UsersController
184
+ def edit
185
+ @user = User.find(params[:id]).cast_as(UserDecorator) # as a module
186
+ end
187
+ end
188
+
189
+ <%= form_for(@user) do |f| %> #=> <form action="/users/1">
190
+ ```
191
+
192
+ This keeps your code focused on the object you care about.
193
+
194
+ ## Oh, my! Could this be used to add behavior like refinements?
195
+
196
+ You can apply methods from a delegate to all instances of a class.
197
+
198
+ ```ruby
199
+ person.hello_world #=> NoMethodError
200
+
201
+ Casting.delegating(Person => GreetingModule) do
202
+ person.hello_world #=> output the value / perform the method
203
+ end
204
+
205
+ person.hello_world #=> NoMethodError
206
+ ```
207
+
208
+ By default, the `delegate_missing_methods` method will set delegates on instances so you'll need to opt-in for this.
209
+
210
+ ```ruby
211
+ class Person
212
+ include Casting::Client
213
+ delegate_missing_methods :class
214
+ end
215
+ ```
216
+
217
+ _But what happens when you have method clashes or want a specific instance to behave differently?_
218
+
219
+ You can have your objects look to their instance delegates, their class delegates, or in a particular order:
220
+
221
+ ```ruby
222
+ class Person
223
+ include Casting::Client
224
+ # default delegation to instances
225
+ delegate_missing_methods
226
+
227
+ # delegate methods to those defined on the class
228
+ delegate_missing_methods :class
229
+
230
+ # delegate methods to those defined on the class, then those defined on the instance
231
+ delegate_missing_methods :class, :instance
232
+
233
+ # delegate methods to those defined on the instance, then those defined on the class
234
+ delegate_missing_methods :instance, :class
235
+ end
236
+ ```
237
+
238
+ ## What's happening when I use this?
239
+
240
+ Ruby allows you to access methods as objects and pass them around just like any other object.
241
+
242
+ For example, if you want a method from a class you may do this:
243
+
244
+ ```ruby
245
+ class Person
246
+ def hello
247
+ "hello"
248
+ end
249
+ end
250
+ Person.new.method(:hello).unbind #=> #<UnboundMethod: Person#hello>
251
+ # or
252
+ Person.instance_method(:hello) #=> #<UnboundMethod: Person#hello>
253
+ ```
254
+
255
+ But if you attempt to use that `UnboundMethod` on an object that is not a `Person` you'll get
256
+ an error about a type mismatch.
257
+
258
+ Casting will bind an unbound method to a client object and execute the method as though it is
259
+ defined on the client object. Any reference to `self` from the method block will refer to the
260
+ client object.
261
+
262
+ This behavior is different in Ruby 1.9 vs. 2.x.
263
+
264
+ According to [http://rubyspec.org](http://rubyspec.org) the behavior in MRI in 1.9 that allows this to happen is incorrect. In MRI (and JRuby) 1.9 you may unbind methods from an object that has been extended with a module, and bind them to another object of the same type that has *not* been extended with that module.
265
+
266
+ Casting uses this as a way to trick the interpreter into using the method where we want it and avoid forever extending the object of concern.
267
+
268
+ This changed in Ruby 2.0 and does not work. What does work (and is so much better) in 2.0 is that you may take any method from a module and apply it to any object. This means that Casting doesn't have to perform any tricks to temporarily apply behavior to an object.
269
+
270
+ For example, this fails in 1.9, but works in 2.0:
271
+
272
+ ```ruby
273
+ GreetingModule.instance_method(:hello_world).bind(actor).call
274
+ ```
275
+
276
+ Casting provides a convenience for doing this.
277
+
278
+ ## What if my modules create instance variables on the object? Can I clean them up?
279
+
280
+ Yup.
281
+
282
+ If you need to set some variables so that your module can access them, it's as easy as defining `cast_object` and `uncast_object` on your module. Here's an example:
283
+
284
+ ```ruby
285
+ module Special
286
+ def self.cast_object(obj)
287
+ obj.instance_variable_set(:@special_value, 'this is special!')
288
+ end
289
+
290
+ def self.uncast_object(obj)
291
+ obj.remove_instance_variable(:@special_value)
292
+ end
293
+
294
+ def special_behavior
295
+ "#{self.name} thinks... #{@special_value}"
296
+ end
297
+ end
298
+
299
+ object.cast_as(Special)
300
+ object.special_method
301
+ object.uncast
302
+ # object no longer has the @special_value instance variable
303
+ ```
304
+
305
+ You'll be able to leave your objects as if they were never touched by the module where you defined your behavior.
306
+
307
+ ## Installation
308
+
309
+ If you are using Bundler, add this line to your application's Gemfile:
310
+
311
+ ```ruby
312
+ gem 'casting'
313
+ ```
314
+
315
+ And then execute:
316
+
317
+ $ bundle
318
+
319
+ Or install it yourself as:
320
+
321
+ $ gem install casting
322
+
323
+ ## Contributing
324
+
325
+ 1. Fork it
326
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
327
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
328
+ 4. Push to the branch (`git push origin my-new-feature`)
329
+ 5. Create new Pull Request
330
+
331
+ Built by Jim Gay at [Saturn Flyer](http://www.saturnflyer.com)
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = FileList['test/*_test.rb']
8
+ t.ruby_opts = ["-w"]
9
+ t.verbose = true
10
+ end
11
+
12
+ task :default => :test
@@ -0,0 +1,68 @@
1
+ module Casting
2
+ module SuperDelegate
3
+
4
+ # Call the method of the same name defined in the next delegate stored in your object
5
+ #
6
+ # Because Casting creates an alternative method lookup path using a collection of delegates,
7
+ # you may use `super_delegate` to work like `super`.
8
+ #
9
+ # If you use this feature, be sure that you have created a delegate collection which does
10
+ # have the method you need or you'll see a NoMethodError.
11
+ #
12
+ # Example:
13
+ #
14
+ # module Greeter
15
+ # def greet
16
+ # "Hello"
17
+ # end
18
+ # end
19
+ #
20
+ # module FormalGreeter
21
+ # include Casting::Super
22
+ #
23
+ # def greet
24
+ # "#{super_delegate}, how do you do?"
25
+ # end
26
+ # end
27
+ #
28
+ # some_object.cast_as(Greeter, FormalGreeter)
29
+ # some_object.greet #=> 'Hello, how do you do?'
30
+ #
31
+ def super_delegate(*args, &block)
32
+ method_name = name_of_calling_method(caller)
33
+ owner = args.first || method_delegate(method_name)
34
+
35
+ super_delegate_method = unbound_method_from_next_delegate(method_name, owner)
36
+
37
+ if super_delegate_method.arity == 0
38
+ super_delegate_method.bind(self).call
39
+ else
40
+ super_delegate_method.bind(self).call(*args, &block)
41
+ end
42
+ rescue NameError
43
+ raise NoMethodError.new("super_delegate: no delegate method `#{method_name}' for #{self.inspect} from #{owner}")
44
+ end
45
+
46
+ def unbound_method_from_next_delegate(method_name, *skipped)
47
+ method_delegate_skipping(method_name, *skipped).instance_method(method_name)
48
+ end
49
+
50
+ def method_delegate_skipping(meth, skipped)
51
+ skipped_index = __delegates__.index(skipped)
52
+ __delegates__.find{|attendant|
53
+ attendant_methods(attendant).include?(meth) &&
54
+ skipped_index < __delegates__.index(attendant)
55
+ }
56
+ end
57
+
58
+ def name_of_calling_method(call_stack)
59
+ call_stack.reject{|line|
60
+ line.to_s =~ casting_library_matcher
61
+ }.first.split('`').last.sub("'","").to_sym
62
+ end
63
+
64
+ def casting_library_matcher
65
+ Regexp.new(Dir.pwd.to_s + '/lib')
66
+ end
67
+ end
68
+ end
@@ -1,3 +1,3 @@
1
1
  module Casting
2
- VERSION = '0.6.7'
2
+ VERSION = '0.6.8'
3
3
  end
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+
3
+ module Cleaner
4
+ def self.uncast_object(object)
5
+ object.send(:remove_instance_variable, :@cleaner_message)
6
+ end
7
+
8
+ def self.cast_object(object)
9
+ object.instance_variable_set(:@cleaner_message, "#{object.name} will be cleaned up")
10
+ end
11
+
12
+ def cleaner_message
13
+ @cleaner_message
14
+ end
15
+ end
16
+
17
+ class CleanupPerson
18
+ include Casting::Client
19
+ delegate_missing_methods
20
+ attr_accessor :name
21
+ end
22
+
23
+ describe 'modules with setup tasks' do
24
+ it 'allows modules to setup an object when cast_as' do
25
+ jim = CleanupPerson.new
26
+ jim.name = 'Jim'
27
+ jim.cast_as(Cleaner)
28
+ assert_equal "Jim will be cleaned up", jim.cleaner_message
29
+ assert_equal "Jim will be cleaned up", jim.instance_variable_get(:@cleaner_message)
30
+ end
31
+ end
32
+
33
+ describe 'modules with cleanup tasks' do
34
+ it 'allows modules to cleanup their required attributes when uncast' do
35
+ jim = CleanupPerson.new
36
+ jim.name = 'Jim'
37
+ jim.cast_as(Cleaner)
38
+ assert_equal "Jim will be cleaned up", jim.cleaner_message
39
+ assert_equal "Jim will be cleaned up", jim.instance_variable_get(:@cleaner_message)
40
+ jim.uncast
41
+ refute jim.instance_variable_defined?(:@cleaner_message)
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ require 'test_helper'
2
+
3
+ module AnyWay
4
+ def which_way
5
+ "any way"
6
+ end
7
+ end
8
+
9
+ module ThisWay
10
+ include Casting::SuperDelegate
11
+ def which_way
12
+ "this way or #{super_delegate(ThisWay)}"
13
+ end
14
+ end
15
+
16
+ module ThatWay
17
+ include Casting::SuperDelegate
18
+ def which_way
19
+ "#{ super_delegate } and that way!"
20
+ end
21
+ end
22
+
23
+ describe Casting, 'modules using delegate_super' do
24
+ it 'call the method from the next delegate with the same arguments' do
25
+ skip 'extending objects not used in this version of Ruby' unless test_rebinding_methods?
26
+ client = TestPerson.new.extend(Casting::Client)
27
+ client.delegate_missing_methods
28
+ client.cast_as(AnyWay, ThisWay, ThatWay)
29
+
30
+ assert_equal 'this way or any way and that way!', client.which_way
31
+ end
32
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: casting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.7
4
+ version: 0.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -27,7 +27,11 @@ files:
27
27
  - lib/casting/missing_method_client.rb
28
28
  - lib/casting/missing_method_client_class.rb
29
29
  - lib/casting/prepared_delegation.rb
30
+ - lib/casting/super_delegate.rb
30
31
  - lib/casting/version.rb
32
+ - LICENSE
33
+ - Rakefile
34
+ - README.md
31
35
  - test/test_helper.rb
32
36
  - test/casting_19_test.rb
33
37
  - test/casting_20_test.rb
@@ -37,6 +41,8 @@ files:
37
41
  - test/delegation_test.rb
38
42
  - test/method_consolidator_test.rb
39
43
  - test/missing_method_client_test.rb
44
+ - test/module_cleanup_test.rb
45
+ - test/super_test.rb
40
46
  homepage: http://github.com/saturnflyer/casting
41
47
  licenses:
42
48
  - MIT
@@ -71,3 +77,5 @@ test_files:
71
77
  - test/delegation_test.rb
72
78
  - test/method_consolidator_test.rb
73
79
  - test/missing_method_client_test.rb
80
+ - test/module_cleanup_test.rb
81
+ - test/super_test.rb