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 +1 -1
- data/lib/chainable.rb +107 -71
- data/sexp_stuff.rb +27 -0
- data/spec/chainable/merge_method_spec.rb +14 -1
- metadata +4 -2
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
|
-
|
70
|
-
|
71
|
-
|
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
|
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.
|
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-
|
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:
|