chainable 0.2.1 → 0.3.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.
- data/README.rdoc +90 -5
- data/benchmark/chainable.rb +54 -35
- data/lib/chainable.rb +77 -12
- data/spec/chainable/auto_chain_spec.rb +4 -4
- data/spec/chainable/chain_method_spec.rb +2 -2
- data/spec/chainable/chain_method_try_merge_spec.rb +72 -0
- data/spec/chainable/merge_method_spec.rb +39 -0
- metadata +5 -1
data/README.rdoc
CHANGED
@@ -2,12 +2,16 @@
|
|
2
2
|
|
3
3
|
This is heavy ruby abuse. It even got the Evil of the Day Award™ from zenspider.
|
4
4
|
|
5
|
+
Forks are welcome!
|
6
|
+
|
5
7
|
== Thou shalt not use alias_method_chain!
|
6
8
|
- http://yehudakatz.com/2009/03/06/alias_method_chain-in-models
|
7
9
|
- http://yehudakatz.com/2009/01/18/other-ways-to-wrap-a-method
|
8
10
|
- http://www.codefluency.com/articles/2009/01/03/wrapping-a-method-in-ruby
|
9
11
|
|
10
12
|
== What it does
|
13
|
+
|
14
|
+
=== Chaining Methods
|
11
15
|
Chainable is an alternative to alias_method_chain, that uses inheritance, rather
|
12
16
|
than aliasing. It does the following when "chaining" a method:
|
13
17
|
|
@@ -74,15 +78,96 @@ Of course you can do this with any class (or module):
|
|
74
78
|
|
75
79
|
Note that there is a speed advantage when using chain_method without a block
|
76
80
|
and doing a "def", since chain_method will use define_method if a block is
|
77
|
-
given, which produces slower methods
|
81
|
+
given, which produces slower methods.
|
82
|
+
|
83
|
+
=== Merging Methods
|
84
|
+
|
85
|
+
But wait, there is more:
|
86
|
+
|
87
|
+
class Foo
|
88
|
+
def foo
|
89
|
+
10
|
90
|
+
end
|
91
|
+
merge_method :foo do
|
92
|
+
super * 3
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
puts Ruby2Ruby.translate Foo, :foo
|
97
|
+
|
98
|
+
The output:
|
99
|
+
|
100
|
+
def foo
|
101
|
+
(10) * 3
|
102
|
+
end
|
103
|
+
|
104
|
+
<b>Before you yell at me about how insane I am, read on!</b>
|
105
|
+
|
106
|
+
The library will only allow merging, if it thinks, it is possible:
|
107
|
+
|
108
|
+
class Foo
|
109
|
+
def foo
|
110
|
+
x = 10
|
111
|
+
end
|
112
|
+
merge_method :foo do
|
113
|
+
x = 20
|
114
|
+
super
|
115
|
+
puts x
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
Will give you:
|
120
|
+
|
121
|
+
ArgumentError: cannot merge foo.
|
122
|
+
|
123
|
+
Same goes for this one:
|
124
|
+
|
125
|
+
class Foo
|
126
|
+
def foo x
|
127
|
+
puts x
|
128
|
+
end
|
129
|
+
merge_method :foo do
|
130
|
+
super
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# => ArgumentError: cannot merge foo.
|
135
|
+
|
136
|
+
But where is the fun in that one? You probably don't want your ruby script
|
137
|
+
throwing such errors at you.
|
138
|
+
|
139
|
+
Enter "try_merge":
|
140
|
+
|
141
|
+
SomeEvilClassWithoutHooks.class_eval do
|
142
|
+
chain_method instance_methods(false), :try_merge => true do
|
143
|
+
old_value = self.value.dup
|
144
|
+
super.tap { observer.notify if old_value != value }
|
145
|
+
end
|
146
|
+
attr_accessor :observer
|
147
|
+
end
|
148
|
+
|
149
|
+
some_evil_instance.observer = MyObserver.new
|
150
|
+
|
151
|
+
== When to use it?
|
152
|
+
|
153
|
+
As with alias_method_chain, you should use this as seldom as possible. Prefer
|
154
|
+
clean inheritance over evil hacks. There actually is only one case one may use
|
155
|
+
chainable (or alias_method_chain, for that matter): If there is a class you
|
156
|
+
need to modify that is not part of your own code and the instances you deal with
|
157
|
+
may already exists when you modify the class. In case you can modify the class
|
158
|
+
before instance creation, just create another class inheriting from the first
|
159
|
+
one and overwrite new to return instances of the latter.
|
78
160
|
|
79
161
|
== Benchmark
|
80
162
|
chain_method tends do produce slightly faster methods than alias_method_chain:
|
163
|
+
$ rake benchmark
|
81
164
|
user system total real
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
165
|
+
no wrappers 0.000000 0.000000 0.000000 ( 0.004887)
|
166
|
+
merge_method 0.000000 0.000000 0.000000 ( 0.004830)
|
167
|
+
chain_method (def) 1.040000 0.350000 1.390000 ( 1.392329)
|
168
|
+
chain_method (define_method) 1.150000 0.240000 1.390000 ( 1.396007)
|
169
|
+
alias_method_chain (def) 1.210000 0.260000 1.470000 ( 1.472633)
|
170
|
+
alias_method_chain (define_method) 3.470000 0.590000 4.060000 ( 4.096245)
|
86
171
|
|
87
172
|
== Installation
|
88
173
|
Add github gems, if you haven't already:
|
data/benchmark/chainable.rb
CHANGED
@@ -2,50 +2,69 @@ require "benchmark"
|
|
2
2
|
require "lib/chainable"
|
3
3
|
require "active_support"
|
4
4
|
|
5
|
-
class
|
6
|
-
|
7
|
-
CALL_TIMES =
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
5
|
+
class BenchMe
|
6
|
+
|
7
|
+
CALL_TIMES = 5000
|
8
|
+
DEF_TIMES = 500
|
9
|
+
BENCH_ME = []
|
10
|
+
|
11
|
+
def self.report
|
12
|
+
@report || "unknown"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.inherited klass
|
16
|
+
BENCH_ME << klass
|
17
|
+
klass.class_eval "def a_method; nil; end"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.run
|
21
|
+
Benchmark.bmbm do |x|
|
22
|
+
BENCH_ME.each do |klass|
|
23
|
+
object = klass.new
|
24
|
+
x.report(klass.report) { CALL_TIMES.times { object.a_method } }
|
25
|
+
end
|
16
26
|
end
|
17
27
|
end
|
18
|
-
|
19
|
-
def bm2; end
|
28
|
+
|
20
29
|
end
|
21
30
|
|
22
|
-
class
|
23
|
-
@
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
31
|
+
class NoWrappers < BenchMe
|
32
|
+
@report = "no wrappers"
|
33
|
+
end
|
34
|
+
|
35
|
+
class MergeMethod < BenchMe
|
36
|
+
@report = "merge_method"
|
37
|
+
DEF_TIMES.times { merge_method(:a_method) { super } }
|
38
|
+
end
|
39
|
+
|
40
|
+
class ChainMethodDef < BenchMe
|
41
|
+
@report = "chain_method (def)"
|
42
|
+
DEF_TIMES.times do
|
43
|
+
chain_method(:a_method)
|
44
|
+
def a_method; super; end
|
28
45
|
end
|
29
46
|
end
|
30
47
|
|
31
|
-
class
|
32
|
-
@
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
48
|
+
class ChainMethod < BenchMe
|
49
|
+
@report = "chain_method (define_method)"
|
50
|
+
DEF_TIMES.times { chain_method(:a_method) { super } }
|
51
|
+
end
|
52
|
+
|
53
|
+
class AliasMethodChainDef < BenchMe
|
54
|
+
@report = "alias_method_chain (def)"
|
55
|
+
DEF_TIMES.times do |i|
|
56
|
+
eval "def a_method_with_#{i}; a_method_without_#{i}; end"
|
57
|
+
alias_method_chain :a_method, i
|
39
58
|
end
|
40
59
|
end
|
41
60
|
|
42
|
-
|
43
|
-
|
61
|
+
class AliasMethodChain < BenchMe
|
62
|
+
@report = "alias_method_chain (define_method)"
|
63
|
+
DEF_TIMES.times do |i|
|
64
|
+
without = "a_method_without_#{i}".to_sym
|
65
|
+
define_method("a_method_with_#{i}") { send without }
|
66
|
+
alias_method_chain :a_method, i
|
67
|
+
end
|
44
68
|
end
|
45
69
|
|
46
|
-
|
47
|
-
BenchmarkChainable.bm1(x)
|
48
|
-
BenchmarkAliasMethodChain.bm1(x)
|
49
|
-
BenchmarkChainable.bm2(x)
|
50
|
-
BenchmarkAliasMethodChain.bm2(x)
|
51
|
-
end
|
70
|
+
BenchMe.run
|
data/lib/chainable.rb
CHANGED
@@ -9,26 +9,91 @@ module Chainable
|
|
9
9
|
@auto_chain = false
|
10
10
|
end
|
11
11
|
|
12
|
+
def self.wrapped_source klass, name, wrapper
|
13
|
+
begin
|
14
|
+
inner = unifier.process(parse_tree.parse_tree_for_method(klass, name))
|
15
|
+
outer = unifier.process(parse_tree.parse_tree_for_proc(wrapper))
|
16
|
+
rescue Exception
|
17
|
+
raise ArgumentError, "cannot merge #{name}"
|
18
|
+
end
|
19
|
+
raise ArgumentError, "cannot merge #{name}" if inner[2] != s(:args) or outer[2]
|
20
|
+
inner_locals = []
|
21
|
+
sexp_walk(inner) do |e|
|
22
|
+
raise ArgumentError, "cannot merge #{name}" if [:zsuper, :super].include? e[0]
|
23
|
+
inner_locals << e if e[0] == :lvar
|
24
|
+
end
|
25
|
+
sexp_walk(outer) do |e|
|
26
|
+
if inner_locals.include? e or (e[0] == :super and e.length > 1)
|
27
|
+
raise ArgumentError, "cannot merge #{name}"
|
28
|
+
end
|
29
|
+
e.replace inner[3][1] if [:zsuper, :super].include? e[0]
|
30
|
+
end
|
31
|
+
src = Ruby2Ruby.new.process s(:defn, name, s(:args), s(:scope, s(:block, outer[3])))
|
32
|
+
src.gsub "# do nothing", "nil"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.unifier
|
36
|
+
return @unifier if @unifier
|
37
|
+
@unifier = Unifier.new
|
38
|
+
# HACK (stolen from ruby2ruby)
|
39
|
+
@unifier.processors.each { |p| p.unsupported.delete :cfunc }
|
40
|
+
@unifier
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.parse_tree
|
44
|
+
return @parse_tree if @parse_tree
|
45
|
+
require "parse_tree"
|
46
|
+
@parse_tree = ParseTree.new
|
47
|
+
end
|
48
|
+
|
12
49
|
# This will "chain" a method (read: push it to a module and include it).
|
13
50
|
# If a block is given, it will do a define_method(name, &block).
|
14
51
|
# Maybe that is not what you want, as methods defined by def tend to be
|
15
52
|
# faster. If that is the case, simply don't pass the block and call def
|
16
53
|
# after chain_method instead.
|
17
54
|
def chain_method(*names, &block)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
55
|
+
options = names.grep(Hash).inject({}) { |a, b| a.merge names.delete(b) }
|
56
|
+
if options[:try_merge]
|
57
|
+
names.reject! do |name|
|
58
|
+
begin
|
59
|
+
merge_method(name, &block)
|
60
|
+
true
|
61
|
+
rescue ArgumentError
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
names.each do |name|
|
67
|
+
name = name.to_s
|
68
|
+
if instance_methods(false).include? name
|
69
|
+
begin
|
70
|
+
code = Ruby2Ruby.translate self, name
|
71
|
+
include Module.new { eval code }
|
72
|
+
rescue NameError
|
73
|
+
m = instance_method name
|
74
|
+
include Module.new { define_method(name) { |*a, &b| m.bind(self).call(*a, &b) } }
|
75
|
+
end
|
27
76
|
end
|
77
|
+
block ||= Proc.new { super }
|
78
|
+
define_method(name, &block)
|
28
79
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
80
|
+
end
|
81
|
+
|
82
|
+
def merge_method(*names, &block)
|
83
|
+
names.each do |name|
|
84
|
+
name = name.to_s
|
85
|
+
unless instance_methods(false).include? name
|
86
|
+
define_method(name, &block)
|
87
|
+
next
|
88
|
+
end
|
89
|
+
class_eval Chainable.wrapped_source(self, name, block)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.sexp_walk sexp, &block
|
94
|
+
return unless sexp.is_a? Sexp
|
95
|
+
yield sexp
|
96
|
+
sexp.each { |e| sexp_walk e, &block }
|
32
97
|
end
|
33
98
|
|
34
99
|
# If you define a method inside a block passed to auto_chain, chain_method
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require "lib/chainable"
|
2
2
|
|
3
|
-
describe Chainable do
|
3
|
+
describe "Chainable#auto_chain" do
|
4
4
|
|
5
|
-
it "should chain all methods defined inside
|
5
|
+
it "should chain all methods defined inside given block" do
|
6
6
|
a_class = Class.new do
|
7
7
|
auto_chain do
|
8
8
|
define_method(:foo) { 100 }
|
@@ -13,7 +13,7 @@ describe Chainable do
|
|
13
13
|
a_class.new.foo.should == 222
|
14
14
|
end
|
15
15
|
|
16
|
-
it "should allow defining methods both inside and outside of
|
16
|
+
it "should allow defining methods both inside and outside of the block" do
|
17
17
|
a_class = Class.new do
|
18
18
|
define_method(:foo) { 100 }
|
19
19
|
chain_method :foo
|
@@ -23,7 +23,7 @@ describe Chainable do
|
|
23
23
|
a_class.new.foo.should == 222
|
24
24
|
end
|
25
25
|
|
26
|
-
it "should
|
26
|
+
it "should work with core functions" do
|
27
27
|
# We screw with String#reverse, this could mess up other specs.
|
28
28
|
String.class_eval do
|
29
29
|
chain_method :reverse # or we would overwrite the original
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require "lib/chainable"
|
2
2
|
|
3
|
-
describe Chainable do
|
3
|
+
describe "Chainable#chain_method" do
|
4
4
|
|
5
5
|
before :each do
|
6
6
|
@a_class = Class.new do
|
@@ -60,7 +60,7 @@ describe Chainable do
|
|
60
60
|
@an_instance.to_i.should == @original_results["to_i"] + 20
|
61
61
|
end
|
62
62
|
|
63
|
-
it "should allow passing multiple method names
|
63
|
+
it "should allow passing multiple method names" do
|
64
64
|
@a_class.class_eval do
|
65
65
|
chain_method :random, :foo2
|
66
66
|
chain_method(:foo, :to_i) { super.to_s }
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "lib/chainable"
|
2
|
+
|
3
|
+
describe "Chainable#chain_method(..., :try_merge => true)" do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@a_class = Class.new do
|
7
|
+
def foo
|
8
|
+
:foo
|
9
|
+
end
|
10
|
+
def foo2
|
11
|
+
foo.to_s.upcase
|
12
|
+
end
|
13
|
+
def to_i
|
14
|
+
42
|
15
|
+
end
|
16
|
+
def inspect
|
17
|
+
random
|
18
|
+
super
|
19
|
+
end
|
20
|
+
define_method(:random) { @some_value ||= rand(1000) }
|
21
|
+
end
|
22
|
+
@an_instance = @a_class.new
|
23
|
+
@original_results = @a_class.instance_methods(false).inject({}) do |h, m|
|
24
|
+
h.merge m => @an_instance.send(m)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should not change the behaviour of the original methods" do
|
29
|
+
5.times do
|
30
|
+
@original_results.each do |method_name, method_result|
|
31
|
+
@a_class.class_eval { chain_method method_name, :try_merge => true }
|
32
|
+
@an_instance.send(method_name).should == method_result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should work for core methods" do
|
38
|
+
String.class_eval do
|
39
|
+
chain_method(:upcase, :try_merge => true) do |*x|
|
40
|
+
return super if x.empty?
|
41
|
+
x.first
|
42
|
+
end
|
43
|
+
end
|
44
|
+
"foo".upcase("bar").should == "bar"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should define a new method, when block given" do
|
48
|
+
@a_class.class_eval do
|
49
|
+
chain_method(:to_i, :try_merge => true) { super - 5 }
|
50
|
+
chain_method(:foo2, :try_merge => true) { "foo2" }
|
51
|
+
end
|
52
|
+
@an_instance.to_i.should == @original_results["to_i"] - 5
|
53
|
+
@an_instance.foo2.should == "foo2"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should allow passing multiple method names" do
|
57
|
+
@a_class.class_eval do
|
58
|
+
chain_method(:foo, :to_i, :try_merge => true) { super.to_s }
|
59
|
+
end
|
60
|
+
@an_instance.foo.should == @original_results["foo"].to_s
|
61
|
+
@an_instance.to_i.should == @original_results["to_i"].to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should merge, if possible" do
|
65
|
+
old_ancestors = @a_class.ancestors
|
66
|
+
@a_class.class_eval do
|
67
|
+
chain_method(:to_i, :foo, :try_merge => true) { super.to_s }
|
68
|
+
end
|
69
|
+
@a_class.ancestors.should == old_ancestors
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "lib/chainable"
|
2
|
+
|
3
|
+
describe "Chainable#merge_method" do
|
4
|
+
|
5
|
+
it "should be able to merge \"simple\" methods" do
|
6
|
+
Class.new do
|
7
|
+
old_ancestors = ancestors
|
8
|
+
define_method(:simple) { 100 + 100 }
|
9
|
+
new.simple.should == 200
|
10
|
+
merge_method(:simple) { super - 100 }
|
11
|
+
new.simple.should == 100
|
12
|
+
ancestors.should == old_ancestors
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should refuse merging methods, if the merge would cause harm" do
|
17
|
+
forbidden = []
|
18
|
+
forbidden << lambda do
|
19
|
+
Class.new do
|
20
|
+
define_method(:same_lvars) { x = 10; x * 2 }
|
21
|
+
merge_method(:same_lvars) { x = 5; super; puts x }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
forbidden << lambda do
|
25
|
+
Class.new do
|
26
|
+
define_method(:args) { |x| x }
|
27
|
+
merge_method(:args) { super }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
forbidden << lambda do
|
31
|
+
Class.new do
|
32
|
+
define_method(:args) { |x| x = 0 }
|
33
|
+
merge_method(:args) { |x| x.to_s; super; puts x }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
forbidden.each { |block| block.should raise_error(ArgumentError) }
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chainable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Konstantin Haase
|
@@ -33,7 +33,9 @@ extra_rdoc_files:
|
|
33
33
|
- LICENSE
|
34
34
|
- lib/chainable.rb
|
35
35
|
- spec/chainable/auto_chain_spec.rb
|
36
|
+
- spec/chainable/merge_method_spec.rb
|
36
37
|
- spec/chainable/chain_method_spec.rb
|
38
|
+
- spec/chainable/chain_method_try_merge_spec.rb
|
37
39
|
- benchmark/chainable.rb
|
38
40
|
files:
|
39
41
|
- LICENSE
|
@@ -41,7 +43,9 @@ files:
|
|
41
43
|
- README.rdoc
|
42
44
|
- lib/chainable.rb
|
43
45
|
- spec/chainable/auto_chain_spec.rb
|
46
|
+
- spec/chainable/merge_method_spec.rb
|
44
47
|
- spec/chainable/chain_method_spec.rb
|
48
|
+
- spec/chainable/chain_method_try_merge_spec.rb
|
45
49
|
- benchmark/chainable.rb
|
46
50
|
has_rdoc: true
|
47
51
|
homepage: http://rkh.github.com/chainable
|