chainable 0.3.0 → 0.3.1

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
@@ -139,7 +139,7 @@ throwing such errors at you.
139
139
  Enter "try_merge":
140
140
 
141
141
  SomeEvilClassWithoutHooks.class_eval do
142
- chain_method instance_methods(false), :try_merge => true do
142
+ chain_method *instance_methods(false), :try_merge => true do
143
143
  old_value = self.value.dup
144
144
  super.tap { observer.notify if old_value != value }
145
145
  end
data/lib/chainable.rb CHANGED
@@ -2,50 +2,6 @@ require "ruby2ruby"
2
2
 
3
3
  module Chainable
4
4
 
5
- def self.skip_chain
6
- return if @auto_chain
7
- @auto_chain = true
8
- yield
9
- @auto_chain = false
10
- end
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
-
49
5
  # This will "chain" a method (read: push it to a module and include it).
50
6
  # If a block is given, it will do a define_method(name, &block).
51
7
  # Maybe that is not what you want, as methods defined by def tend to be
@@ -53,49 +9,31 @@ module Chainable
53
9
  # after chain_method instead.
54
10
  def chain_method(*names, &block)
55
11
  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
12
+ names = Chainable.try_merge(self, *names, &block) if options[:try_merge]
66
13
  names.each do |name|
67
14
  name = name.to_s
68
15
  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
16
+ mod = Module.new
17
+ Chainable.copy_method(self, mod, name)
18
+ include mod
76
19
  end
77
20
  block ||= Proc.new { super }
78
21
  define_method(name, &block)
79
22
  end
80
23
  end
81
24
 
25
+ # This will try to merge into the method, instead of chaining to it (see
26
+ # README.rdoc). You probably don't want to use this directly but try
27
+ # chain_method(:some_method, :try_merge => true) { ... }
28
+ # instead, which will fall back to chain_method if merge fails.
82
29
  def merge_method(*names, &block)
83
30
  names.each do |name|
84
31
  name = name.to_s
85
- unless instance_methods(false).include? name
86
- define_method(name, &block)
87
- next
88
- end
32
+ raise ArgumentError, "cannot merge #{name}" unless instance_methods(false).include? name
89
33
  class_eval Chainable.wrapped_source(self, name, block)
90
34
  end
91
35
  end
92
36
 
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 }
97
- end
98
-
99
37
  # If you define a method inside a block passed to auto_chain, chain_method
100
38
  # will be called on that method right after it has been defined. This will
101
39
  # only affect methods defined for the class (or module) auto_chain has been
@@ -112,9 +50,107 @@ module Chainable
112
50
  end
113
51
  result
114
52
  end
53
+
54
+ # Used internally. See source of Chainbale#auto_chain.
55
+ def self.skip_chain
56
+ return if @auto_chain
57
+ @auto_chain = true
58
+ yield
59
+ @auto_chain = false
60
+ end
61
+
62
+ # Given a class, a method name and a proc, it will try to merge the sexp
63
+ # of the method into the sexp of the proc and return the source code (as
64
+ # method definition). While doing so, it tries to prevent harm.
65
+ #
66
+ # Raises an ArgumentError on failure.
67
+ def self.wrapped_source(klass, name, wrapper)
68
+ begin
69
+ src = Ruby2Ruby.new.process wrapped_sexp(klass, name, wrapper)
70
+ src.gsub "# do nothing", "nil"
71
+ rescue Exception
72
+ raise ArgumentError, "cannot merge #{name}"
73
+ end
74
+ end
75
+
76
+ def self.wrapped_sexp(klass, name, wrapper)
77
+ inner = unified sexp_for(klass, name)
78
+ outer = unified sexp_for(wrapper)
79
+ raise if inner[2] != s(:args) or outer[2]
80
+ inner_locals = sexp_walk(inner) { raise }
81
+ sexp_walk(outer, inner_locals) { |e| e.replace inner[3][1] }
82
+ s(:defn, name, s(:args), s(:scope, s(:block, outer[3])))
83
+ end
84
+
85
+ def self.sexp_walk(sexp, forbidden_locals = [], &block)
86
+ return [] unless sexp.is_a? Sexp
87
+ local = nil
88
+ case sexp[0]
89
+ when :lvar then local = sexp[1]
90
+ when :lasgn then local = sexp[1] if sexp[1].to_s =~ /^\w+$/
91
+ when :zsuper, :super
92
+ raise if sexp.length > 1
93
+ yield(sexp)
94
+ when :call then raise if sexp[2] == :eval
95
+ end
96
+ locals = []
97
+ if local
98
+ raise if forbidden_locals.include? local
99
+ locals << local
100
+ end
101
+ sexp.inject(locals) { |l, e| l + sexp_walk(e, forbidden_locals, &block) }
102
+ end
103
+
104
+ def self.unified sexp
105
+ unifier.process sexp
106
+ end
107
+
108
+ def self.sexp_for a, b = nil
109
+ require "parse_tree"
110
+ case a
111
+ when Class, String then ParseTree.translate(a, b)
112
+ when Proc then ParseTree.new.parse_tree_for_proc(a)
113
+ when Sexp then a
114
+ else raise ArgumentError, "no sexp for #{a.inspect}"
115
+ end
116
+ end
117
+
118
+ def self.unifier
119
+ return @unifier if @unifier
120
+ @unifier = Unifier.new
121
+ # HACK (stolen from ruby2ruby)
122
+ @unifier.processors.each { |p| p.unsupported.delete :cfunc }
123
+ @unifier
124
+ end
125
+
126
+ # Tries merge_method on all given methods for klass.
127
+ # Returns names of the methods that could not be merged.
128
+ def self.try_merge(klass, *names, &wrapper)
129
+ names.reject do |name|
130
+ begin
131
+ klass.class_eval { merge_method(name, &wrapper) }
132
+ true
133
+ rescue ArgumentError
134
+ false
135
+ end
136
+ end
137
+ end
138
+
139
+ # Copies a method from one module to another.
140
+ def self.copy_method(source_class, target_class, name)
141
+ begin
142
+ target_class.class_eval Ruby2Ruby.translate(source_class, name)
143
+ rescue NameError
144
+ m = source_class.instance_method name
145
+ target_class.class_eval do
146
+ define_method(name) { |*a, &b| m.bind(self).call(*a, &b) }
147
+ end
148
+ end
149
+ end
115
150
 
116
151
  end
117
152
 
118
153
  Module.class_eval do
119
154
  include Chainable
155
+ private *Chainable.instance_methods(false)
120
156
  end
data/sexp_stuff.rb ADDED
@@ -0,0 +1,27 @@
1
+ require "parse_tree"
2
+ require "ruby2ruby"
3
+
4
+ module FastSexp
5
+
6
+ def r2r(a, b = nil)
7
+ return Ruby2Ruby.new.process a if a.is_a? Sexp
8
+ r2r u(a, b = nil)
9
+ end
10
+
11
+ def u(a, b = nil)
12
+ unless @unifier
13
+ @unifier = Unifier.new
14
+ @unifier.processors.each { |p| p.unsupported.delete :cfunc }
15
+ end
16
+ case a
17
+ when Sexp then sexp = a
18
+ when String, Class then sexp = ParseTree.translate(a, b)
19
+ when Proc then sexp = ParseTree.new.parse_tree_for_proc(a)
20
+ else raise ArgumentError, "dunno how to handle #{a.inspect}"
21
+ end
22
+ @unifier.process sexp
23
+ end
24
+
25
+ end
26
+
27
+ include FastSexp
@@ -17,10 +17,16 @@ describe "Chainable#merge_method" do
17
17
  forbidden = []
18
18
  forbidden << lambda do
19
19
  Class.new do
20
- define_method(:same_lvars) { x = 10; x * 2 }
20
+ define_method(:same_lvars) { x = 10 }
21
21
  merge_method(:same_lvars) { x = 5; super; puts x }
22
22
  end
23
23
  end
24
+ forbidden << lambda do
25
+ Class.new do
26
+ define_method(:do_eval) { eval "x = 10" }
27
+ merge_method(:do_eval) { x = 5; super; puts x }
28
+ end
29
+ end
24
30
  forbidden << lambda do
25
31
  Class.new do
26
32
  define_method(:args) { |x| x }
@@ -33,6 +39,13 @@ describe "Chainable#merge_method" do
33
39
  merge_method(:args) { |x| x.to_s; super; puts x }
34
40
  end
35
41
  end
42
+ forbidden << lambda do
43
+ Class.new { merge_method(:i_dont_exist) { } }
44
+ end
45
+ forbidden << lambda do
46
+ superclass = Class.new { define_method(:inherited_method) { } }
47
+ Class.new(superclass) { merge_method(:inherited_method) { } }
48
+ end
36
49
  forbidden.each { |block| block.should raise_error(ArgumentError) }
37
50
  end
38
51
 
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.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Haase
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-22 00:00:00 +01:00
12
+ date: 2009-03-25 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -37,6 +37,7 @@ extra_rdoc_files:
37
37
  - spec/chainable/chain_method_spec.rb
38
38
  - spec/chainable/chain_method_try_merge_spec.rb
39
39
  - benchmark/chainable.rb
40
+ - sexp_stuff.rb
40
41
  files:
41
42
  - LICENSE
42
43
  - Rakefile
@@ -47,6 +48,7 @@ files:
47
48
  - spec/chainable/chain_method_spec.rb
48
49
  - spec/chainable/chain_method_try_merge_spec.rb
49
50
  - benchmark/chainable.rb
51
+ - sexp_stuff.rb
50
52
  has_rdoc: true
51
53
  homepage: http://rkh.github.com/chainable
52
54
  post_install_message: