chainable 0.3.1 → 0.3.2

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
@@ -1,6 +1,6 @@
1
1
  <b>A word of warning:</b>
2
2
 
3
- This is heavy ruby abuse. It even got the Evil of the Day Award from zenspider.
3
+ This is heavy ruby abuse. It even got the <b>Evil of the Day Award™</b> from zenspider.
4
4
 
5
5
  Forks are welcome!
6
6
 
@@ -170,8 +170,13 @@ chain_method tends do produce slightly faster methods than alias_method_chain:
170
170
  alias_method_chain (define_method) 3.470000 0.590000 4.060000 ( 4.096245)
171
171
 
172
172
  == Installation
173
- Add github gems, if you haven't already:
173
+ From RubyForge:
174
+ gem install chainable
175
+
176
+ From GitHub:
174
177
  gem sources -a http://gems.github.com
178
+ gem install rkh-chainable
179
+
180
+ == Running test
175
181
 
176
- Install gem:
177
- gem install rkh-chainable
182
+ The specs should work with rspec, mspec and bacon.
data/lib/chainable.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "ruby2ruby"
2
2
 
3
+ # This mixin will be included in Module.
3
4
  module Chainable
4
5
 
5
6
  # This will "chain" a method (read: push it to a module and include it).
@@ -7,13 +8,25 @@ module Chainable
7
8
  # Maybe that is not what you want, as methods defined by def tend to be
8
9
  # faster. If that is the case, simply don't pass the block and call def
9
10
  # after chain_method instead.
11
+ #
12
+ # It takes the following options:
13
+ #
14
+ # === try_merge
15
+ # try_merge will try merge_method for every method given and only chain if
16
+ # that fails. Default is false.
17
+ #
18
+ # === module_reuse
19
+ # Will try to reuse the last mixin to keep the inheritance chain short.
20
+ # Default is true.
10
21
  def chain_method(*names, &block)
11
- options = names.grep(Hash).inject({}) { |a, b| a.merge names.delete(b) }
22
+ options = names.grep(Hash).inject(Chainable.default_options) do |a, b|
23
+ a.merge names.delete(b)
24
+ end
12
25
  names = Chainable.try_merge(self, *names, &block) if options[:try_merge]
13
26
  names.each do |name|
14
27
  name = name.to_s
15
28
  if instance_methods(false).include? name
16
- mod = Module.new
29
+ mod = Chainable.mixin_for(self, name, options[:module_reuse])
17
30
  Chainable.copy_method(self, mod, name)
18
31
  include mod
19
32
  end
@@ -27,6 +40,7 @@ module Chainable
27
40
  # chain_method(:some_method, :try_merge => true) { ... }
28
41
  # instead, which will fall back to chain_method if merge fails.
29
42
  def merge_method(*names, &block)
43
+ raise ArgumentError, "no block given" unless block
30
44
  names.each do |name|
31
45
  name = name.to_s
32
46
  raise ArgumentError, "cannot merge #{name}" unless instance_methods(false).include? name
@@ -38,19 +52,38 @@ module Chainable
38
52
  # will be called on that method right after it has been defined. This will
39
53
  # only affect methods defined for the class (or module) auto_chain has been
40
54
  # send to. See README.rdoc or spec/chainable/auto_chain_spec.rb for examples.
41
- def auto_chain
42
- class << self
43
- chain_method :method_added do |name|
44
- Chainable.skip_chain { chain_method name }
55
+ #
56
+ # auto_chain takes a hash of options, just like chain_method does.
57
+ def auto_chain options = {}
58
+ eigenclass = (class << self; self; end)
59
+ eigenclass.class_eval do
60
+ chain_method :method_added, :try_merge => false do |name|
61
+ Chainable.skip_chain { chain_method name, options }
45
62
  end
46
63
  end
47
64
  result = yield
48
- class << self
49
- remove_method :method_added
50
- end
65
+ eigenclass.class_eval { remove_method :method_added }
51
66
  result
52
67
  end
53
68
 
69
+ # Default options for auto_chain and chain_method.
70
+ #
71
+ # Example usage:
72
+ # Chainable.default_options[:try_merge] = true
73
+ def self.default_options
74
+ @default_options ||= { :try_merge => false, :module_reuse => true }
75
+ end
76
+
77
+ # Creates mixin used by chain_method.
78
+ def self.mixin_for(klass, name, reuse = true)
79
+ @last_mixin ||= {}
80
+ if reuse and klass.ancestors[1] == @last_mixin[klass]
81
+ im = @last_mixin[klass].instance_methods(false)
82
+ return @last_mixin[klass] unless im.include?(name.to_s)
83
+ end
84
+ @last_mixin[klass] = Module.new
85
+ end
86
+
54
87
  # Used internally. See source of Chainbale#auto_chain.
55
88
  def self.skip_chain
56
89
  return if @auto_chain
@@ -73,6 +106,8 @@ module Chainable
73
106
  end
74
107
  end
75
108
 
109
+ # The sexp part of wrapped_source. Note: In rubinius, we could use this directly
110
+ # rather than generating the source again.
76
111
  def self.wrapped_sexp(klass, name, wrapper)
77
112
  inner = unified sexp_for(klass, name)
78
113
  outer = unified sexp_for(wrapper)
@@ -82,7 +117,9 @@ module Chainable
82
117
  s(:defn, name, s(:args), s(:scope, s(:block, outer[3])))
83
118
  end
84
119
 
85
- def self.sexp_walk(sexp, forbidden_locals = [], &block)
120
+ # Traveling a methods sexp tree. Block will be called for every super.
121
+ def self.sexp_walk(sexp, forbidden_locals = [], &block) # :yield: sexp
122
+ # TODO: Refactor me, I'm ugly!
86
123
  return [] unless sexp.is_a? Sexp
87
124
  local = nil
88
125
  case sexp[0]
@@ -101,10 +138,12 @@ module Chainable
101
138
  sexp.inject(locals) { |l, e| l + sexp_walk(e, forbidden_locals, &block) }
102
139
  end
103
140
 
141
+ # Unify sexp.
104
142
  def self.unified sexp
105
143
  unifier.process sexp
106
144
  end
107
145
 
146
+ # Give a proc, class and method, string or sexp and get a sexp.
108
147
  def self.sexp_for a, b = nil
109
148
  require "parse_tree"
110
149
  case a
@@ -115,10 +154,10 @@ module Chainable
115
154
  end
116
155
  end
117
156
 
157
+ # Unifier with modifications for Ruby2Ruby. (Stolen from Ruby2Ruby.)
118
158
  def self.unifier
119
159
  return @unifier if @unifier
120
160
  @unifier = Unifier.new
121
- # HACK (stolen from ruby2ruby)
122
161
  @unifier.processors.each { |p| p.unsupported.delete :cfunc }
123
162
  @unifier
124
163
  end
@@ -136,14 +175,37 @@ module Chainable
136
175
  end
137
176
  end
138
177
 
178
+ def self.store_method(block)
179
+ @method_store ||= []
180
+ @method_store << block
181
+ @method_store.length - 1
182
+ end
183
+
184
+ def self.call_stored_method(method_id, owner, *args, &block)
185
+ @method_store[method_id].bind(owner).call(*args, &block)
186
+ end
187
+
139
188
  # Copies a method from one module to another.
189
+ # TODO: This could be solved totally different in Rubinius.
140
190
  def self.copy_method(source_class, target_class, name)
141
191
  begin
142
192
  target_class.class_eval Ruby2Ruby.translate(source_class, name)
143
193
  rescue NameError
194
+ # If we get here, the method is written in C or something. So let's do
195
+ # some evil magic.
144
196
  m = source_class.instance_method name
145
197
  target_class.class_eval do
146
- define_method(name) { |*a, &b| m.bind(self).call(*a, &b) }
198
+ begin
199
+ eval "define_method(name) { |*a, &b| m.bind(self).call(*a, &b) }"
200
+ rescue SyntaxError # Ruby < 1.8.7, JRuby
201
+ # Really really evil, have to change it.
202
+ method_id = Chainable.store_method(m)
203
+ eval %[
204
+ def #{name}(*a, &b)
205
+ Chainable.call_stored_method(#{method_id}, self, *a, &b)
206
+ end
207
+ ]
208
+ end
147
209
  end
148
210
  end
149
211
  end
@@ -2,7 +2,7 @@ require "lib/chainable"
2
2
 
3
3
  describe "Chainable#chain_method" do
4
4
 
5
- before :each do
5
+ before do
6
6
  @a_class = Class.new do
7
7
  def foo
8
8
  :foo
@@ -2,7 +2,7 @@ require "lib/chainable"
2
2
 
3
3
  describe "Chainable#chain_method(..., :try_merge => true)" do
4
4
 
5
- before :each do
5
+ before do
6
6
  @a_class = Class.new do
7
7
  def foo
8
8
  :foo
@@ -46,7 +46,14 @@ describe "Chainable#merge_method" do
46
46
  superclass = Class.new { define_method(:inherited_method) { } }
47
47
  Class.new(superclass) { merge_method(:inherited_method) { } }
48
48
  end
49
- forbidden.each { |block| block.should raise_error(ArgumentError) }
49
+ forbidden.each do |block|
50
+ # just for fun
51
+ if defined? Bacon
52
+ block.should.raise(ArgumentError)
53
+ else
54
+ block.should raise_error(ArgumentError)
55
+ end
56
+ end
50
57
  end
51
58
 
52
59
  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.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Haase
@@ -37,7 +37,6 @@ 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
41
40
  files:
42
41
  - LICENSE
43
42
  - Rakefile
@@ -48,7 +47,6 @@ files:
48
47
  - spec/chainable/chain_method_spec.rb
49
48
  - spec/chainable/chain_method_try_merge_spec.rb
50
49
  - benchmark/chainable.rb
51
- - sexp_stuff.rb
52
50
  has_rdoc: true
53
51
  homepage: http://rkh.github.com/chainable
54
52
  post_install_message:
data/sexp_stuff.rb DELETED
@@ -1,27 +0,0 @@
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