forwarder2 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +493 -0
- data/lib/forwarder.rb +23 -0
- data/lib/forwarder/arguments.rb +184 -0
- data/lib/forwarder/compiler.rb +63 -0
- data/lib/forwarder/evaller.rb +59 -0
- data/lib/forwarder/meta.rb +118 -0
- data/lib/forwarder/params.rb +42 -0
- data/lib/forwarder/version.rb +3 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2321eef417bac1725fd34fb39d99d5a8ce187abd
|
4
|
+
data.tar.gz: 5b4e950db43bd7a004750bbb5987746a56a9e6d3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dd4327158ec4ab363e588e7661bb87629a32275dfbc20eb1e3ab9d407f35fc2f394594d26d7c8e717cd24a2b3c0b369195feec998c5f4f21d40647ba9d679d31
|
7
|
+
data.tar.gz: 6d2754e5d8fee19b6356197c3a448a9e92cd18496dc25b246e7b622cab941b63796ebdcfd1a831a3ded3ce7bf460060c16ac4dfa267f06a347faf2378190e4cf
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Robert Dober
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
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, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,493 @@
|
|
1
|
+
# Forwarder2
|
2
|
+
|
3
|
+
This implementation is for, and needs, Ruby 2.
|
4
|
+
For Ruby 1.9 please see https://github.com/RobertDober/Forwarder19.
|
5
|
+
For Ruby 1.8.7 please see https://github.com/RobertDober/Forwarder.
|
6
|
+
|
7
|
+
## Abstract
|
8
|
+
|
9
|
+
Ruby's core Forwardable gets the job done(barely), but produces most unreadable code.
|
10
|
+
|
11
|
+
This is a nonintrusive (as is Forwardable) module that allows to delegate methods to instance variables,
|
12
|
+
objects returned by instance\_methods, other methods of the same receiver, the receiver itself, a chain of messages or
|
13
|
+
an arbitrary object. Paramters can be provided in the forwarding definition (parially or totally.
|
14
|
+
|
15
|
+
It also defines after and before filters. and some more sophisticated use cases}
|
16
|
+
|
17
|
+
## License
|
18
|
+
|
19
|
+
This software is released under the very liberal MIT license as indicated in the attached file LICENSE.
|
20
|
+
If you do not have the LICENSE file delivered the terms of the license are referred to here:
|
21
|
+
http://www.opensource.org/licenses/mit-license.html
|
22
|
+
|
23
|
+
## Performance
|
24
|
+
|
25
|
+
Execution time is that of 85~95% of Forwardable by evalling strings whenever possible.
|
26
|
+
|
27
|
+
|
28
|
+
## Simple Delegation As In Forwardable
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
|
32
|
+
forward <a_message>, to: <target>
|
33
|
+
forward <a_message>, to: <target>, as: <translation>
|
34
|
+
```
|
35
|
+
|
36
|
+
These two forms of the `forward` method, (and *only* these two forms) are directly implemented with
|
37
|
+
`def_delegator` method of `Forwardable`, as follows:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
def_delegator <target> <a_message>
|
41
|
+
def_delegator <translation> <a_message>
|
42
|
+
```
|
43
|
+
|
44
|
+
Furthermore the `forward_all` method is translated to the `def_delegators` method in the following form,
|
45
|
+
thusly
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
forward_all msg1, msg2, msg3, ..., to: target
|
49
|
+
```
|
50
|
+
is implemented as
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
def_delegators target, msg1, msg2, msg3, ...
|
54
|
+
```
|
55
|
+
|
56
|
+
### Additional Features
|
57
|
+
|
58
|
+
* Parameters (partial or total application)
|
59
|
+
* Custom And Chained Targets
|
60
|
+
* AOP Filters
|
61
|
+
* Helpers
|
62
|
+
|
63
|
+
|
64
|
+
## Parameters
|
65
|
+
|
66
|
+
|
67
|
+
### Passing One Parameter
|
68
|
+
|
69
|
+
Assuming a class `ArrayWrapper` and that their instances wrap the array object via the instance variable
|
70
|
+
`@ary` the Smalltalk method `second` can be implemented as follows.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
require 'forwarder'
|
74
|
+
class ArrayWrapper
|
75
|
+
extend Forwarder
|
76
|
+
forward :second, to: :@ary, as: :[], with: 1
|
77
|
+
...
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
The `with` keyword paramter is thus used to provide the first slice of arguments that will be provided
|
82
|
+
to the forwarded invocation. This slice will be extended by the actual parameters of the invocation
|
83
|
+
of the proxy method (e.g. the instance method defined by the `forward` method itself).
|
84
|
+
|
85
|
+
### Passing More Parameters
|
86
|
+
|
87
|
+
If `with:` is passed an array, it is splatted into the invocation, thus allowing us to pass more than
|
88
|
+
one parameter. This becomes clearer with an example.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
|
92
|
+
forward :add_whitespace_to_punctuation,
|
93
|
+
to: :name,
|
94
|
+
as: :gsub!,
|
95
|
+
with: [ /[,.]\b/, '\& ' ]
|
96
|
+
```
|
97
|
+
|
98
|
+
### A Useful Shorthand
|
99
|
+
|
100
|
+
As I found myself using the following idioms all the time
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
|
104
|
+
forward :some_method, to: :@some_hash, as: :[], with: :some_method
|
105
|
+
forward :other_method, to: :hash, as: :[], with: :other_key
|
106
|
+
|
107
|
+
```
|
108
|
+
|
109
|
+
I conceived the `to_hash` shortcut for these. Strictly spoken (and not striktly spoken
|
110
|
+
too) this is a gross generalisation of the usecase as if they target had to be a `Hash`
|
111
|
+
all the time. This is not the case of course, we are just forwarding a message with a
|
112
|
+
parameter...
|
113
|
+
|
114
|
+
Here is how the above idioms can be expressed by means of the `to_hash` target:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
|
118
|
+
forward :some_method, to_hash: :@some_hash
|
119
|
+
forward :other_method, to_hash: :hash, as: :other_key
|
120
|
+
|
121
|
+
```
|
122
|
+
|
123
|
+
Concerning jargon we are doing something a little bit confusing here. In all cases we have
|
124
|
+
an implicit translation (which is `:[]` of course). In the second case we have an explicit
|
125
|
+
translation (being `:other_key`) too. The explicit translation is transformed into the first,
|
126
|
+
and only argument, as we do not allow explicit arguments for `to_hash:` targets.
|
127
|
+
|
128
|
+
However you still can use the `forward_all` version and a `to_hash:` chain target, here is
|
129
|
+
an example:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
|
133
|
+
class Params
|
134
|
+
extend Forwarder
|
135
|
+
forward_all :count, :limit, to_hash: [:@params, :mandatory]
|
136
|
+
forward :pretend?, to_hash: [:@params, :optional], as: :dry_run
|
137
|
+
end
|
138
|
+
|
139
|
+
```
|
140
|
+
|
141
|
+
AOP is not supported for `to_hash:` targets in this version, this might change in the future
|
142
|
+
as use cases are imaginable (e.g. an after filter for the `:pretend?` method, applying !! to
|
143
|
+
the result).
|
144
|
+
|
145
|
+
### Partial Application
|
146
|
+
|
147
|
+
This example gives us the oppurtunity to look at a use case for partial applications. Let us assume that
|
148
|
+
we do not always use whitespaces, than we can leave the second paramter to be provided by the invocation
|
149
|
+
of the defined forwarder proxy.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
forward :add_something_to_punctuation,
|
153
|
+
to: :name,
|
154
|
+
as: :gsub!,
|
155
|
+
with: /([,.])\b/
|
156
|
+
```
|
157
|
+
|
158
|
+
We can achieve the same as above with the following invocation
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
o = Name.new( "the,quick, fox." )
|
162
|
+
o.add_something_to_punctuation( '\1 ' )
|
163
|
+
# name: "the, quick, fox." )
|
164
|
+
```
|
165
|
+
|
166
|
+
but we can also add a hyphen after interpunctations with this invocation
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
o = Name.new( "the,quick, fox." )
|
170
|
+
o.add_something_to_punctuation( '\1- ' )
|
171
|
+
# name: "the,- quick,- fox." )
|
172
|
+
```
|
173
|
+
|
174
|
+
But more importantely we can forward to the partial application, thus using the
|
175
|
+
partial application as a mean of composition
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
forward :add_ws_to_punctuation,
|
179
|
+
to_object: :self,
|
180
|
+
as: :add_something_to_punctuation,
|
181
|
+
with: '\1 '
|
182
|
+
|
183
|
+
forward :add_hypen_to_punctuation,
|
184
|
+
to_object: self,
|
185
|
+
as: :add_something_to_punctuation,
|
186
|
+
with_block: ->(*grps){ "#{grps.first}- " }
|
187
|
+
|
188
|
+
```
|
189
|
+
|
190
|
+
### Passing One Array
|
191
|
+
|
192
|
+
If a real array shall be passed in as one parameter it can be wrapped into an array of one element,
|
193
|
+
or the `with_ary:` keyword parameter can be used.
|
194
|
+
|
195
|
+
Example:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
forward :append_suffix, to: :@ary, as: :concat, with: [%w{ my suffix }]
|
199
|
+
forward :append_suffix, to: :@ary, as: :concat, with_ary: %w{ my suffix }
|
200
|
+
```
|
201
|
+
|
202
|
+
### Passing A Block
|
203
|
+
|
204
|
+
In case of the necessity to provide a block to the forwarded invocation, it can be specified as the
|
205
|
+
block parameter of the `forward` invocation itself.
|
206
|
+
|
207
|
+
The following example uses inject to compute a sum of elements
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
forward :sum, to: :elements, as: :inject do |s,e| s+e end
|
211
|
+
```
|
212
|
+
|
213
|
+
Please note however that common patterns like this one can benefit of the provided
|
214
|
+
helpers, in our case it is Integer.sum.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
require 'forwarder/helpers/integer/sum'
|
218
|
+
...
|
219
|
+
forward :sum, to: :elements, as: :inject &Integer.sum
|
220
|
+
# or
|
221
|
+
forward :sum, to: :elements, as: :inject, with_block: Integer.sum
|
222
|
+
...
|
223
|
+
```
|
224
|
+
|
225
|
+
Accounting for different tastes a block can be provided as a block parameter or
|
226
|
+
as a `lambda` to the `with_block:` keyword parameter. The later is taking preference
|
227
|
+
over the former, which no defined usage of the block in this case (at least for
|
228
|
+
the time being).
|
229
|
+
|
230
|
+
### Selective Helpers
|
231
|
+
|
232
|
+
As we do not want to be intrusive the helpers
|
233
|
+
have to be requested explicitly.
|
234
|
+
|
235
|
+
This can be done in three levels of granularity:
|
236
|
+
|
237
|
+
* Per helper
|
238
|
+
|
239
|
+
`require 'forwarder/helpers/integer/sum'`
|
240
|
+
|
241
|
+
* All helpers
|
242
|
+
|
243
|
+
`require 'forwarder/helpers'`
|
244
|
+
|
245
|
+
* Per monkey patched class
|
246
|
+
|
247
|
+
`require 'forwarder/helpers/integer'`
|
248
|
+
|
249
|
+
## Custom And Chained Targets
|
250
|
+
|
251
|
+
So far the `to:` keyword was followed by a symbol or string denoting a _symbolic receiver_, that is
|
252
|
+
an instance_variable or method with the denoted name. Custom and Chain Targets are implementing a
|
253
|
+
different story.
|
254
|
+
|
255
|
+
### Chain Targets
|
256
|
+
|
257
|
+
Chain Targets are also expressed with the `to:` keyword parameter, but by passing an array of _symbolic receivers_.
|
258
|
+
This array will resolve to the final target by sending each message to the result of the preceding message.
|
259
|
+
The following example should make this clearer:
|
260
|
+
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
forward :size, to: %w{@content children}
|
264
|
+
```
|
265
|
+
|
266
|
+
which could have been implemented by hand as follows:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
def size
|
270
|
+
@content.children.size
|
271
|
+
end
|
272
|
+
```
|
273
|
+
|
274
|
+
### Custom Targets
|
275
|
+
|
276
|
+
Allow the user to define a target that cannot be expressed as a _symbolic receiver_.
|
277
|
+
|
278
|
+
Custom targets are expressed by the means of the `to_object:` keyword parameter.
|
279
|
+
|
280
|
+
|
281
|
+
I want to give two examples here, the first
|
282
|
+
using `self`, which evaluates to the module in which `forward` is invoked of course, and might
|
283
|
+
thus be used to forward to class instance methods, as in the following example:
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
class Callback
|
287
|
+
def self.instances; @__instances__ ||= [] end
|
288
|
+
|
289
|
+
def self.register an_instance
|
290
|
+
instances << an_instance
|
291
|
+
end
|
292
|
+
|
293
|
+
extend Forwarder
|
294
|
+
forward :register, to_object: self
|
295
|
+
|
296
|
+
def initialize
|
297
|
+
register self
|
298
|
+
end
|
299
|
+
end
|
300
|
+
```
|
301
|
+
|
302
|
+
But when looking closely one can see that the `self.register` method is just another delegation, thus the whole code
|
303
|
+
can be rewritten even more concesily as:
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
class Callback
|
307
|
+
class << self
|
308
|
+
extend Forwarder
|
309
|
+
forward :<<, to: :instances
|
310
|
+
|
311
|
+
def instances; @__instances__ ||= [] end
|
312
|
+
end
|
313
|
+
|
314
|
+
extend Forwarder
|
315
|
+
forward :register, to_object: self, as: :<<
|
316
|
+
end
|
317
|
+
```
|
318
|
+
|
319
|
+
The second example is a forward to the instance itself, for that purpose the symbol :self
|
320
|
+
can be used. The followin is, again, an implementation of Smalltalk's `second` method. But
|
321
|
+
here we are defining it on `Array` itself, not a wrapper.
|
322
|
+
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
class Array
|
326
|
+
extend Forwarder
|
327
|
+
forward :second, to_object: :self, as: :[], with: 1
|
328
|
+
```
|
329
|
+
|
330
|
+
However the same could be accomplished by using the object/identity helper and the default
|
331
|
+
target implementation.
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
require 'forwarder/helpers/object/identity'
|
335
|
+
class Array
|
336
|
+
extend Forwarder
|
337
|
+
forward :second, to: :identity, as: :[], with: 1
|
338
|
+
```
|
339
|
+
|
340
|
+
#### Custom Targets And Closures
|
341
|
+
|
342
|
+
Another application of custom targets would be to hide a enclosed object, but as in the first
|
343
|
+
example above, such an object cannot be defined on instance level, but only on class level.
|
344
|
+
Assuming that the class itself does not need access to the object enclosed by the closure, one
|
345
|
+
could easily implement an instance count for a class as follows:
|
346
|
+
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
|
350
|
+
container = []
|
351
|
+
forward :register, to_object: container, as: :<<, with: :sentinel
|
352
|
+
forward :instance_count, to_object: container, as: :size
|
353
|
+
|
354
|
+
```
|
355
|
+
|
356
|
+
## AOP Filters
|
357
|
+
|
358
|
+
Before and After filters are implemented in this version.
|
359
|
+
|
360
|
+
The respective `before:` and
|
361
|
+
`after:` keyword parameters expect lambdas as paramters, but by specifying the `:use_block`
|
362
|
+
value the block parameter of the `forward` method can be _abused_ for this purpose.
|
363
|
+
|
364
|
+
The following examples all operate on a class wrapping a hash instance via the `hash` attribute
|
365
|
+
reader. Our first goal is to implement a `max_value` method, that will return the maxium value
|
366
|
+
of all values for given keys.
|
367
|
+
|
368
|
+
### After Filter
|
369
|
+
|
370
|
+
The lambda provided by `after:` is applied to the return value of the forwarded invocation.
|
371
|
+
The following three examples all implement the `max_value` method correctly.
|
372
|
+
|
373
|
+
|
374
|
+
```ruby
|
375
|
+
forward :max_value, to: :hash, as: :values_at, after: lambda{ |x| x.max }
|
376
|
+
forward :max_value, to: :hash, as: :values_at, after: :use_block do | x |
|
377
|
+
x.max
|
378
|
+
end
|
379
|
+
require 'forwarder/helpers/kernel/sendmsg'
|
380
|
+
forward :max_value, to: :hash, as: :values_at, after: sendmsg( :max )
|
381
|
+
```
|
382
|
+
|
383
|
+
N.B. The `Kernel#sendmsg` method is my reply to the hated - by me that is at least - `Symbol#to_proc` kludge and its
|
384
|
+
limitations, I will talk about it more in the Helpers section.
|
385
|
+
|
386
|
+
### Before Filter.
|
387
|
+
|
388
|
+
Our next goal is to implement a method `value_of_max` that returns the value for the greatest of
|
389
|
+
all provided keys.
|
390
|
+
|
391
|
+
For this we will use a before filter, its lambda is applied to the arguments
|
392
|
+
of the implemented forwarder and the result will be passed in to the forwarded invocation. The pass in
|
393
|
+
will use a splash if appropriate.
|
394
|
+
|
395
|
+
|
396
|
+
```ruby
|
397
|
+
forward :value_of_max, to: :hash, as: :[], before: lambda{ |*args| args.max }
|
398
|
+
require 'forwarder/helpers/kernel/sendmsg'
|
399
|
+
forward :value_of_max, to: :hash, as: :[], before: sendmsg( :max )
|
400
|
+
```
|
401
|
+
|
402
|
+
## Helpers
|
403
|
+
|
404
|
+
*N.B.* These are no longer part of Forwarder19, but have been moved into the gemdependency lab419_core.
|
405
|
+
|
406
|
+
Helpers define two type of methods. Firstly methods that return lambdas for frequently used
|
407
|
+
block patterns, e.g. `Integer.sum`. Secondly methods that are convenient to use inside `forward`
|
408
|
+
invocations, but not necessarily only there, e.g. `Kernel#sendmsg` or `Object#identity`.
|
409
|
+
|
410
|
+
### Functional Helpers
|
411
|
+
|
412
|
+
I see this second group, as small as it is, as an important enhancement for the functional
|
413
|
+
programming style. The possibilty to nullify a block that is necessarily used in a chain
|
414
|
+
of functional calls by passing in `{|x| x.identity}`, `sendmsg(:identity)` or even the
|
415
|
+
hated `&:identity` is a recurring pattern.
|
416
|
+
|
417
|
+
**Warning:** I will become evangelic now.
|
418
|
+
|
419
|
+
I do not like the `Symbol#to_proc` kludge, and that for two reasons. The first is pragamatic.
|
420
|
+
You cannot pass parameters, and that sucks. Why can I express `map(&:succ)` but not `map(&:+, 2)`.
|
421
|
+
Well the answer is clear, Ruby's syntax does not support it.
|
422
|
+
|
423
|
+
The second reason is on philosophical grounds. It feels wrong that Symbol shall be responsable
|
424
|
+
of transforming itself into a lambda.
|
425
|
+
|
426
|
+
Thus I created a helper in Kernel that takes the responsability, and doing so
|
427
|
+
with a clear name, expressing intent. This helper is `Kernel#sendmsg`.
|
428
|
+
|
429
|
+
|
430
|
+
```ruby
|
431
|
+
map do |ele|
|
432
|
+
ele.hello "World"
|
433
|
+
end
|
434
|
+
```
|
435
|
+
|
436
|
+
is the same as
|
437
|
+
|
438
|
+
|
439
|
+
```ruby
|
440
|
+
map( &sendmsg( :hello, "World") )
|
441
|
+
```
|
442
|
+
|
443
|
+
Furthermore it might be usuful to keep the returned `lambda` around, please compare
|
444
|
+
|
445
|
+
```ruby
|
446
|
+
adder = sendmsg( :+ )
|
447
|
+
```
|
448
|
+
versus
|
449
|
+
|
450
|
+
```ruby
|
451
|
+
adder = :+.to_proc
|
452
|
+
```
|
453
|
+
|
454
|
+
|
455
|
+
Mapping with a `Symbol` might not only be conveniently expressed as sending a message to each
|
456
|
+
element, sometimes a different meaning might be appropriate as in the example below:
|
457
|
+
|
458
|
+
|
459
|
+
```ruby
|
460
|
+
map do | ele |
|
461
|
+
some_method ele
|
462
|
+
end
|
463
|
+
```
|
464
|
+
|
465
|
+
A different helper can do this job without any ambiguity:
|
466
|
+
|
467
|
+
|
468
|
+
```ruby
|
469
|
+
map( &applying( :some_method ) )
|
470
|
+
```
|
471
|
+
|
472
|
+
### Commonly Used Pattern Helpers
|
473
|
+
|
474
|
+
This group of helpers is just to avoid to rewrite lambdas you/one/whoever/I have written zillions of times. Here is a short list of examples
|
475
|
+
the API doc should give you enough information if you look for something specific.
|
476
|
+
|
477
|
+
#### Integer.sum
|
478
|
+
|
479
|
+
```ruby
|
480
|
+
class Integer
|
481
|
+
def self.sum
|
482
|
+
->(a, b){ a + b }
|
483
|
+
end
|
484
|
+
end
|
485
|
+
```
|
486
|
+
|
487
|
+
#### Integer#inc
|
488
|
+
|
489
|
+
```ruby
|
490
|
+
class Integer
|
491
|
+
alias_method :inc, :succ # should have used forward ;)
|
492
|
+
end
|
493
|
+
```
|
data/lib/forwarder.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'forwarder/params'
|
3
|
+
|
4
|
+
module Forwarder
|
5
|
+
|
6
|
+
# How forward works:
|
7
|
+
# The parameters are analyzied by the Params object by means of the `prepare_forward`
|
8
|
+
# method. The `prepare_forward` method makes have use of the Argument object which
|
9
|
+
# implements a query API for what the given arguments allow the forwarder to do.
|
10
|
+
# And eventually the `forward!` method realises the delegation
|
11
|
+
def forward *args, &blk
|
12
|
+
params = Forwarder::Params.new self
|
13
|
+
params.prepare_forward( *args, &blk )
|
14
|
+
params.forward!
|
15
|
+
end
|
16
|
+
|
17
|
+
def forward_all *args, &blk
|
18
|
+
opts = args.pop
|
19
|
+
args.each do | arg |
|
20
|
+
forward( arg, opts, &blk)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end # module Forwarder
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'forwarder/evaller'
|
2
|
+
|
3
|
+
module Forwarder
|
4
|
+
class Arguments
|
5
|
+
attr_reader :args, :message, :target
|
6
|
+
|
7
|
+
def all?
|
8
|
+
!args? && !lambda? && @__all__
|
9
|
+
end
|
10
|
+
|
11
|
+
def after
|
12
|
+
@after ||= @params[:after]
|
13
|
+
end
|
14
|
+
|
15
|
+
def after?
|
16
|
+
after
|
17
|
+
end
|
18
|
+
|
19
|
+
def aop?
|
20
|
+
@__aop__ ||= !aop_values.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
def args?
|
24
|
+
!!args
|
25
|
+
end
|
26
|
+
|
27
|
+
def before
|
28
|
+
@before ||= @params[:before] || @params[:before_with_block]
|
29
|
+
end
|
30
|
+
|
31
|
+
def before?
|
32
|
+
before
|
33
|
+
end
|
34
|
+
|
35
|
+
def before_with_block?
|
36
|
+
!!@params[:before_with_block]
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def chain?
|
41
|
+
@params[ :to_chain ]
|
42
|
+
end
|
43
|
+
|
44
|
+
def complete_args *args
|
45
|
+
(self.args || []) + args
|
46
|
+
end
|
47
|
+
|
48
|
+
def custom_target?
|
49
|
+
@params[:to_object]
|
50
|
+
end
|
51
|
+
|
52
|
+
# def delegatable?
|
53
|
+
# !aop? && !custom_target? && !all? && !chain? && !args && !lambda?
|
54
|
+
# end
|
55
|
+
|
56
|
+
def evaluable?
|
57
|
+
!lambda? &&
|
58
|
+
!aop? &&
|
59
|
+
( !args || args.all?{|a| Evaller.evaluable? a } ) &&
|
60
|
+
( !custom_target? || Evaller.evaluable?( custom_target? ) )
|
61
|
+
end
|
62
|
+
|
63
|
+
def lambda default=nil
|
64
|
+
lambda? || default
|
65
|
+
end
|
66
|
+
|
67
|
+
def lambda?
|
68
|
+
@lambda
|
69
|
+
end
|
70
|
+
|
71
|
+
def must_not_compile?
|
72
|
+
lambda? || aop? || custom_target?
|
73
|
+
end
|
74
|
+
|
75
|
+
# This is always nil unless we are a custom_target, in which case
|
76
|
+
# default is returned if target is :self, else target is returned
|
77
|
+
def object_target default
|
78
|
+
return unless custom_target?
|
79
|
+
target == :self ? default : target
|
80
|
+
end
|
81
|
+
|
82
|
+
def serialized_params
|
83
|
+
Evaller.serialize args
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_hash?
|
87
|
+
@__to_hash__ ||= @params[ :to_hash ]
|
88
|
+
end
|
89
|
+
|
90
|
+
def translation alternative=nil, &blk
|
91
|
+
@params[ :as ].tap do | tltion |
|
92
|
+
break alternative unless tltion
|
93
|
+
break tltion unless blk
|
94
|
+
blk.( tltion )
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def aop_values
|
100
|
+
@__aop_values__ ||= @params.values_at( :after, :before, :before_with_block ).compact
|
101
|
+
end
|
102
|
+
|
103
|
+
def check_for_incompatibilities!
|
104
|
+
raise ArgumentError, "cannot provide translations for forward_all" if @__all__ && translation
|
105
|
+
raise ArgumentError, "cannot provide arguments for forward_all" if @__all__ && args?
|
106
|
+
end
|
107
|
+
|
108
|
+
def initialize *args, &blk
|
109
|
+
@message = args.shift
|
110
|
+
raise ArgumentError, "need one message and a hash of kwd params, plus an optional block" unless args.size == 1 && args.first.is_a?( Hash )
|
111
|
+
@params = args.first
|
112
|
+
set_message
|
113
|
+
set_target
|
114
|
+
set_args blk
|
115
|
+
check_for_incompatibilities!
|
116
|
+
translate_to_hash
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_args blk
|
120
|
+
set_lambda blk
|
121
|
+
hw = @params.has_key? :with
|
122
|
+
ha = @params.has_key? :with_ary
|
123
|
+
raise ArgumentError, "cannot use :with and :with_ary parameter" if hw && ha
|
124
|
+
unless to_hash?
|
125
|
+
set_args_normal if hw
|
126
|
+
set_args_ary if ha
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def set_args_ary
|
131
|
+
@args = [ @params[:with_ary] ]
|
132
|
+
raise ArgumentError, ":with_ary needs an array parameter" unless Array === @args.first
|
133
|
+
end
|
134
|
+
|
135
|
+
def set_args_normal
|
136
|
+
case arg = @params[:with]
|
137
|
+
when Array
|
138
|
+
@args = arg.dup rescue arg
|
139
|
+
else
|
140
|
+
@args = [ (arg.dup rescue arg) ]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def set_message
|
145
|
+
case @message
|
146
|
+
when Array
|
147
|
+
@__all__ = true
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_lambda blk
|
152
|
+
|
153
|
+
if use_block?
|
154
|
+
@after = blk if @params[:after] == :use_block
|
155
|
+
@before = blk if @params[:before] == :use_block || @params[:before_with_block] == :use_block
|
156
|
+
@lambda = @params[:with_block]
|
157
|
+
else
|
158
|
+
raise ArgumentError, "cannot use :with_block and a block" if
|
159
|
+
@params[:with_block] && blk
|
160
|
+
|
161
|
+
@lambda = @params.fetch :with_block, blk
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def set_target
|
166
|
+
[:to, :to_chain, :to_object, :to_hash].each do | tgt_kwd |
|
167
|
+
tgt = @params[ tgt_kwd ]
|
168
|
+
next unless tgt
|
169
|
+
|
170
|
+
raise ArgumentError, "more than one target specified." if @target
|
171
|
+
@target = tgt
|
172
|
+
end
|
173
|
+
raise ArgumentError, "no target specified." unless @target
|
174
|
+
end
|
175
|
+
|
176
|
+
def translate_to_hash
|
177
|
+
return unless @params[:to_hash]
|
178
|
+
raise ArgumentError, "cannot provide arguments for to_hash:" if @params.has_key?( :with ) || @params.has_key?( :with_ary )
|
179
|
+
end
|
180
|
+
def use_block?
|
181
|
+
aop_values.include?( :use_block )
|
182
|
+
end
|
183
|
+
end # class Arguments
|
184
|
+
end # module Forwarder
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'forwarder/evaller'
|
2
|
+
module Forwarder
|
3
|
+
# The compiler's responsability is to create a string representation of
|
4
|
+
# the delegation method, or to nil if no such string representation exists
|
5
|
+
# and delegation has to be done dynamically.
|
6
|
+
class Compiler
|
7
|
+
attr_reader :arguments
|
8
|
+
|
9
|
+
|
10
|
+
def compile
|
11
|
+
# Cannot compile because of intrinsic uncompilable traits of arguments
|
12
|
+
return if arguments.must_not_compile?
|
13
|
+
|
14
|
+
# To Hash can always compile
|
15
|
+
return compile_to_hash if arguments.to_hash?
|
16
|
+
|
17
|
+
# Cannot compile only because the arguments.args cannot be compiled
|
18
|
+
@compiled_args = Evaller.serialize arguments.args
|
19
|
+
return unless @compiled_args
|
20
|
+
|
21
|
+
# Can compile :)))
|
22
|
+
return compile_to_all if arguments.message.is_a? Array
|
23
|
+
compile_one
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def compile_one
|
29
|
+
tltion = arguments.translation arguments.message
|
30
|
+
"def #{arguments.message} *args, &blk; " +
|
31
|
+
"#{self.class.compile_target arguments.target}.#{tltion}( " +
|
32
|
+
@compiled_args +
|
33
|
+
"*args, &blk ) end"
|
34
|
+
end
|
35
|
+
|
36
|
+
def compile_to_all
|
37
|
+
arguments.message.map{ |msg|
|
38
|
+
"def #{msg} *args, &blk; #{arguments.target}.#{msg}( *args, &blk ) end"
|
39
|
+
}.join("\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
def compile_to_hash
|
43
|
+
target = arguments.to_hash?
|
44
|
+
target = target.join(".") if Array === target
|
45
|
+
[arguments.message]
|
46
|
+
.flatten
|
47
|
+
.map do | msg |
|
48
|
+
# N.B. that the expression between [] is always a Symbol
|
49
|
+
"def #{msg}; #{target}[ #{(arguments.translation||msg).to_sym.inspect} ] end"
|
50
|
+
end.join("\n")#.tap do |x| debugger end
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize args
|
54
|
+
@arguments = args
|
55
|
+
end
|
56
|
+
|
57
|
+
class << self
|
58
|
+
def compile_target target
|
59
|
+
[ target ].flatten.join( "." )
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end # class Compiler
|
63
|
+
end # module Forwarder
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Forwarder
|
2
|
+
module Evaller extend self
|
3
|
+
|
4
|
+
NotSerializable = Class.new RuntimeError
|
5
|
+
|
6
|
+
def evaluable? an_object, cache={}
|
7
|
+
!!_serialize_one( an_object, cache )
|
8
|
+
rescue NotSerializable
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
def serialize args
|
13
|
+
return "" if args.nil? || args.empty?
|
14
|
+
serialize_without_arg_suffix( args ).join(", ") + ", "
|
15
|
+
rescue NotSerializable
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def serialize_without_arg_suffix args, cache={}
|
20
|
+
args.map{ | arg |
|
21
|
+
_serialize_one arg, cache
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def _serialize_hash hsh, cache
|
27
|
+
hsh.inject [] do | r, (k, v) |
|
28
|
+
k = _serialize_one k, cache
|
29
|
+
v = _serialize_one v, cache
|
30
|
+
r << "#{k} => #{v}"
|
31
|
+
end.join( ", ")
|
32
|
+
end
|
33
|
+
|
34
|
+
def _serialize_one arg, cache
|
35
|
+
case arg
|
36
|
+
when String
|
37
|
+
"'" + arg + "'"
|
38
|
+
when Symbol, Fixnum, NilClass, FalseClass, TrueClass
|
39
|
+
arg.inspect
|
40
|
+
else
|
41
|
+
_serialize_object arg, cache
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def _serialize_object arg, cache
|
46
|
+
oid = arg.object_id
|
47
|
+
raise NotSerializable if cache[oid]
|
48
|
+
cache[oid]=true
|
49
|
+
case arg
|
50
|
+
when Array
|
51
|
+
["[ ", serialize_without_arg_suffix( arg, cache ).join(", "), " ]"].join
|
52
|
+
when Hash
|
53
|
+
[ "{ ", _serialize_hash( arg, cache ), " }" ].join
|
54
|
+
else
|
55
|
+
raise NotSerializable
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end # module Evaller
|
59
|
+
end # module Forwarder
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Forwarder
|
2
|
+
# I am the workerbee defining the methods and stuff
|
3
|
+
class Meta
|
4
|
+
|
5
|
+
attr_reader :arguments, :forwardee
|
6
|
+
|
7
|
+
# TODO: Break AOP out of this so that we do not check @ runtime
|
8
|
+
def forward
|
9
|
+
if arguments.before_with_block?
|
10
|
+
forward_with_before_with_block
|
11
|
+
elsif arguments.aop?
|
12
|
+
forward_with_aop
|
13
|
+
else
|
14
|
+
forward_without_aop
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def forward_chain
|
19
|
+
a = arguments
|
20
|
+
sr = symbolic_receiver
|
21
|
+
forwardee.module_eval do
|
22
|
+
define_method a.message do |*args, &blk|
|
23
|
+
if a.before_with_block?
|
24
|
+
args = Array(a.before.(*args,&blk)) if a.before_with_block?
|
25
|
+
blk = args.pop
|
26
|
+
elsif a.before?
|
27
|
+
args = instance_exec( *args, &a.before )
|
28
|
+
end
|
29
|
+
|
30
|
+
tgt = a.target.inject( self ){ |r, sym| sr.( r, sym ) }
|
31
|
+
tgt.send( a.translation( a.message ), *a.complete_args(*args), &a.lambda( blk ) ).tap do | result |
|
32
|
+
break a.after.( result ) if a.after?
|
33
|
+
end
|
34
|
+
#.send( a.translation( a.message ), *a.complete_args(*args), &a.lambda( blk ) )
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def forward_object
|
40
|
+
a = arguments
|
41
|
+
forwardee.module_eval do
|
42
|
+
define_method a.message do |*args, &blk|
|
43
|
+
args = instance_exec( *args, &a.before ) if a.before?
|
44
|
+
|
45
|
+
a.object_target( self )
|
46
|
+
.send( a.translation( a.message ), *a.complete_args(*args), &a.lambda( blk ) ).tap do | result |
|
47
|
+
break instance_exec( result, &a.after ) if a.after?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def forward_with_aop
|
57
|
+
a = arguments
|
58
|
+
sr = symbolic_receiver
|
59
|
+
forwardee.module_eval do
|
60
|
+
define_method a.message do |*args, &blk|
|
61
|
+
args = instance_exec( *args, &a.before ) if a.before?
|
62
|
+
sr
|
63
|
+
.( self, a.target )
|
64
|
+
.send( a.translation( a.message ), *a.complete_args(*args), &a.lambda( blk ) ).tap do | result |
|
65
|
+
break instance_exec( result, &a.after ) if a.after?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def forward_without_aop
|
72
|
+
a = arguments
|
73
|
+
sr = symbolic_receiver
|
74
|
+
forwardee.module_eval do
|
75
|
+
define_method a.message do |*args, &blk|
|
76
|
+
sr
|
77
|
+
.( self, a.target )
|
78
|
+
.send( a.translation( a.message ), *a.complete_args(*args), &a.lambda( blk ) )
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def forward_with_before_with_block
|
84
|
+
a = arguments
|
85
|
+
sr = symbolic_receiver
|
86
|
+
forwardee.module_eval do
|
87
|
+
define_method a.message do |*args, &blk|
|
88
|
+
args = Array(a.before.(*args, &blk))
|
89
|
+
new_blk = args.pop
|
90
|
+
|
91
|
+
sr
|
92
|
+
.( self, a.target )
|
93
|
+
.send( a.translation( a.message ), *a.complete_args(*args), &a.lambda( new_blk ) ).tap do | result |
|
94
|
+
break instance_exec( result, &a.after ) if a.after?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
def initialize forwardee, arguments
|
102
|
+
@forwardee = forwardee
|
103
|
+
@arguments = arguments
|
104
|
+
end
|
105
|
+
|
106
|
+
def symbolic_receiver
|
107
|
+
@__symbolic_receiver__ = ->(rec, sym) do
|
108
|
+
case "#{sym}"
|
109
|
+
when /\A@/
|
110
|
+
rec.instance_variable_get sym
|
111
|
+
else
|
112
|
+
rec.send sym
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end # class Meta
|
118
|
+
end # module Forwarder
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'forwarder/arguments'
|
2
|
+
require 'forwarder/compiler'
|
3
|
+
require 'forwarder/meta'
|
4
|
+
|
5
|
+
module Forwarder
|
6
|
+
class Params
|
7
|
+
attr_reader :forwardee, :arguments
|
8
|
+
def forward!
|
9
|
+
compiled = compile_forward
|
10
|
+
if compiled
|
11
|
+
forwardee.module_eval compiled, __FILE__, __LINE__
|
12
|
+
else
|
13
|
+
general_delegate
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def prepare_forward *args, &blk
|
18
|
+
@arguments = Arguments.new( *args, &blk )
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def compile_forward
|
24
|
+
compiler = Compiler.new arguments
|
25
|
+
compiler.compile
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize forwardee
|
29
|
+
@forwardee = forwardee
|
30
|
+
end
|
31
|
+
|
32
|
+
def general_delegate
|
33
|
+
if arguments.chain?
|
34
|
+
Meta.new( forwardee, arguments ).forward_chain
|
35
|
+
elsif arguments.custom_target?
|
36
|
+
Meta.new( forwardee, arguments ).forward_object
|
37
|
+
else
|
38
|
+
Meta.new( forwardee, arguments ).forward
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end # class Params
|
42
|
+
end # module Forwarder
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: forwarder2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert Dober
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-09-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.14'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
description: "Ruby's core Forwardable gets the job done(barely) and produces most
|
42
|
+
unreadable code. \n\n Forwarder2 not only is more readable, much more feature rich,
|
43
|
+
but also slightly faster, meaning you can use it without performance penalty.\n\n
|
44
|
+
\ Additional features include: providing arguments, (partially if needed), AOP and
|
45
|
+
custom forwarding to hashes\n "
|
46
|
+
email: robert.dober@gmail.com
|
47
|
+
executables: []
|
48
|
+
extensions: []
|
49
|
+
extra_rdoc_files: []
|
50
|
+
files:
|
51
|
+
- lib/forwarder.rb
|
52
|
+
- lib/forwarder/arguments.rb
|
53
|
+
- lib/forwarder/evaller.rb
|
54
|
+
- lib/forwarder/compiler.rb
|
55
|
+
- lib/forwarder/version.rb
|
56
|
+
- lib/forwarder/meta.rb
|
57
|
+
- lib/forwarder/params.rb
|
58
|
+
- LICENSE
|
59
|
+
- README.md
|
60
|
+
homepage: https://github.com/RobertDober/Forwarder2
|
61
|
+
licenses:
|
62
|
+
- MIT
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 2.0.0
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 2.1.3
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: Delegation And AOP Filters For It
|
84
|
+
test_files: []
|