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 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