chainable 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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 (but makes the method a real closure).
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
- chainable (define_method) 0.160000 0.010000 0.170000 ( 0.183363)
83
- chainable (def & eval) 0.170000 0.010000 0.180000 ( 0.177084)
84
- alias_method_chain (define_method) 0.570000 0.030000 0.600000 ( 0.607330)
85
- alias_method_chain (def & eval) 0.170000 0.020000 0.190000 ( 0.190048)
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:
@@ -2,50 +2,69 @@ require "benchmark"
2
2
  require "lib/chainable"
3
3
  require "active_support"
4
4
 
5
- class BenchmarkChain
6
- CHAIN_LENGTH = 1000
7
- CALL_TIMES = 1000
8
- class << self
9
- def bm1(x)
10
- obj = new
11
- x.report("#{@name} (define_method)") { CALL_TIMES.times { obj.bm1 } }
12
- end
13
- def bm2(x)
14
- obj = new
15
- x.report("#{@name} (def & eval)") { CALL_TIMES.times { obj.bm2 } }
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
- define_method(:bm1) { }
19
- def bm2; end
28
+
20
29
  end
21
30
 
22
- class BenchmarkChainable < BenchmarkChain
23
- @name = "chainable"
24
- CHAIN_LENGTH.times do
25
- chain_method(:bm1) { super }
26
- chain_method(:bm2)
27
- def bm2; super; end
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 BenchmarkAliasMethodChain < BenchmarkChain
32
- @name = "alias_method_chain"
33
- CHAIN_LENGTH.times do |i|
34
- method_without = "bm1_without_#{i}"
35
- define_method("bm1_with_#{i}") { send(method_without) }
36
- alias_method_chain :bm1, i.to_s
37
- eval "def bm2_with_#{i}; bm2_without_#{i}; end"
38
- alias_method_chain :bm2, i.to_s
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
- def bench(x, klass)
43
- obj = klass.new
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
- Benchmark.bmbm do |x|
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
- raise ArgumentError, "no method name given" if names.empty?
19
- name = names.shift.to_s
20
- if instance_methods(false).include? name
21
- begin
22
- code = Ruby2Ruby.translate self, name
23
- include Module.new { eval code }
24
- rescue NameError
25
- m = instance_method name
26
- include Module.new { define_method(name) { |*a, &b| m.bind(self).call(*a, &b) } }
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
- block ||= Proc.new { super }
30
- define_method(name, &block)
31
- chain_method(*names, &block) unless names.empty?
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 auto_chain" do
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 auto_chain" do
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 allow auto_chain with core functions" do
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 to chain_method" do
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.2.1
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