Module: Chainable

Defined in:
lib/chainable.rb

Overview

This mixin will be included in Module.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.call_stored_method(method_id, owner, *args, &block) ⇒ Object



184
185
186
# File 'lib/chainable.rb', line 184

def self.call_stored_method(method_id, owner, *args, &block)
  @method_store[method_id].bind(owner).call(*args, &block)
end

.copy_method(source_class, target_class, name) ⇒ Object

Copies a method from one module to another. TODO: This could be solved totally different in Rubinius.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/chainable.rb', line 202

def self.copy_method(source_class, target_class, name)
  begin
    target_class.class_eval Ruby2Ruby.translate(source_class, name)
  rescue NameError
    # If we get here, the method is written in C or something. So let's do
    # some evil magic.
    m = source_class.instance_method name
    target_class.class_eval do
      begin
        Chainable.could_raise(SyntaxError) # for specs
        eval "define_method(name) { |*a, &b| m.bind(self).call(*a, &b) }"
      rescue SyntaxError # Ruby < 1.8.7, JRuby
        # Really really evil, have to change it.
        method_id = Chainable.store_method(m)
        eval %[
          def #{name}(*a, &b)
            Chainable.call_stored_method(#{method_id}, self, *a, &b)
          end
        ]
      end
    end
  end
end

.could_raise(error = Exception) ⇒ Object



196
197
198
# File 'lib/chainable.rb', line 196

def self.could_raise(error = Exception)
  raise error if raise_potential_errors?
end

.default_optionsObject

Default options for auto_chain and chain_method.

Example usage:

Chainable.default_options[:try_merge] = true


73
74
75
# File 'lib/chainable.rb', line 73

def self.default_options
  @default_options ||= { :try_merge => false, :module_reuse => true }
end

.mixin_for(klass, name, reuse = true) ⇒ Object

Creates mixin used by chain_method.



78
79
80
81
82
83
84
85
# File 'lib/chainable.rb', line 78

def self.mixin_for(klass, name, reuse = true)
  @last_mixin ||= {}
  if reuse and klass.ancestors[1] == @last_mixin[klass]
    im = @last_mixin[klass].instance_methods(false)
    return @last_mixin[klass] unless im.include?(name.to_s)
  end
  @last_mixin[klass] = Module.new
end

.raise_potential_errors=(value) ⇒ Object



188
189
190
# File 'lib/chainable.rb', line 188

def self.raise_potential_errors=(value)
  @raise_potential_errors = value
end

.raise_potential_errors?Boolean

Returns:

  • (Boolean)


192
193
194
# File 'lib/chainable.rb', line 192

def self.raise_potential_errors?
  @raise_potential_errors ||= false
end

.sexp_for(a, b = nil) ⇒ Object

Give a proc, class and method, string or sexp and get a sexp.



147
148
149
150
151
152
153
154
155
# File 'lib/chainable.rb', line 147

def self.sexp_for(a, b = nil)
  require "parse_tree"
  case a
  when Class, String then ParseTree.translate(a, b)
  when Proc then ParseTree.new.parse_tree_for_proc(a)
  when Sexp then a
  else raise ArgumentError, "no sexp for #{a.inspect}"
  end
end

.sexp_walk(sexp, forbidden_locals = [], &block) ⇒ Object

Traveling a methods sexp tree. Block will be called for every super.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/chainable.rb', line 121

def self.sexp_walk(sexp, forbidden_locals = [], &block) # :yield: sexp
  # TODO: Refactor me, I'm ugly!
  return [] unless sexp.is_a? Sexp
  local = nil
  case sexp[0]
  when :lvar then local = sexp[1]
  when :lasgn then local = sexp[1] if sexp[1].to_s =~ /^\w+$/
  when :zsuper, :super
    raise if sexp.length > 1
    yield(sexp)
  when :call then raise if sexp[2] == :eval
  end
  locals = []
  if local
    raise if forbidden_locals.include? local
    locals << local
  end
  sexp.inject(locals) { |l, e| l + sexp_walk(e, forbidden_locals, &block) }
end

.skip_chainObject

Used internally. See source of Chainbale#auto_chain.



88
89
90
91
92
93
# File 'lib/chainable.rb', line 88

def self.skip_chain
  return if @auto_chain
  @auto_chain = true
  yield
  @auto_chain = false
end

.store_method(block) ⇒ Object



178
179
180
181
182
# File 'lib/chainable.rb', line 178

def self.store_method(block)
  @method_store ||= []
  @method_store << block
  @method_store.length - 1
end

.try_merge(klass, *names, &wrapper) ⇒ Object

Tries merge_method on all given methods for klass. Returns names of the methods that could not be merged.



167
168
169
170
171
172
173
174
175
176
# File 'lib/chainable.rb', line 167

def self.try_merge(klass, *names, &wrapper)
  names.reject do |name|
    begin
      klass.class_eval { merge_method(name, &wrapper) }
      true
    rescue ArgumentError
      false
    end
  end
end

.unified(sexp) ⇒ Object

Unify sexp.



142
143
144
# File 'lib/chainable.rb', line 142

def self.unified(sexp)
  unifier.process sexp
end

.unifierObject

Unifier with modifications for Ruby2Ruby. (Stolen from Ruby2Ruby.)



158
159
160
161
162
163
# File 'lib/chainable.rb', line 158

def self.unifier
  return @unifier if @unifier
  @unifier = Unifier.new
  @unifier.processors.each { |p| p.unsupported.delete :cfunc }
  @unifier
end

.wrapped_sexp(klass, name, wrapper) ⇒ Object

The sexp part of wrapped_source. Note: In rubinius, we could use this directly rather than generating the source again.



111
112
113
114
115
116
117
118
# File 'lib/chainable.rb', line 111

def self.wrapped_sexp(klass, name, wrapper)
  inner = unified sexp_for(klass, name)
  outer = unified sexp_for(wrapper)
  raise if inner[2] != s(:args) or outer[2]
  inner_locals = sexp_walk(inner) { raise }
  sexp_walk(outer, inner_locals) { |e| e.replace inner[3][1] }
  s(:defn, name, s(:args), s(:scope, s(:block, outer[3])))
end

.wrapped_source(klass, name, wrapper) ⇒ Object

Given a class, a method name and a proc, it will try to merge the sexp of the method into the sexp of the proc and return the source code (as method definition). While doing so, it tries to prevent harm.

Raises an ArgumentError on failure.



100
101
102
103
104
105
106
107
# File 'lib/chainable.rb', line 100

def self.wrapped_source(klass, name, wrapper)
  begin
    src = Ruby2Ruby.new.process wrapped_sexp(klass, name, wrapper)
    src.gsub "# do nothing", "nil"
  rescue Exception
    raise ArgumentError, "cannot merge #{name}"
  end
end

Instance Method Details

#auto_chain(options = {}) ⇒ Object

If you define a method inside a block passed to auto_chain, chain_method will be called on that method right after it has been defined. This will only affect methods defined for the class (or module) auto_chain has been send to. See README.rdoc or spec/chainable/auto_chain_spec.rb for examples.

auto_chain takes a hash of options, just like chain_method does.



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/chainable.rb', line 57

def auto_chain(options = {})
  eigenclass = (class << self; self; end)
  eigenclass.class_eval do
    chain_method :method_added, :try_merge => false do |name|
      Chainable.skip_chain { chain_method name, options }
    end
  end
  result = yield
  eigenclass.class_eval { remove_method :method_added }
  result
end

#chain_method(*names, &block) ⇒ Object

This will “chain” a method (read: push it to a module and include it). If a block is given, it will do a define_method(name, &block). Maybe that is not what you want, as methods defined by def tend to be faster. If that is the case, simply don’t pass the block and call def after chain_method instead.

It takes the following options:

try_merge

try_merge will try merge_method for every method given and only chain if that fails. Default is false.

module_reuse

Will try to reuse the last mixin to keep the inheritance chain short. Default is true.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/chainable.rb', line 21

def chain_method(*names, &block)
  options = names.grep(Hash).inject(Chainable.default_options) do |a, b|
    a.merge names.delete(b)
  end
  names = Chainable.try_merge(self, *names, &block) if options[:try_merge]
  names.each do |name|
    name = name.to_s
    if instance_methods(false).include? name
      mod = Chainable.mixin_for(self, name, options[:module_reuse])
      Chainable.copy_method(self, mod, name)
      include mod
    end
    block ||= Proc.new { super }
    define_method(name, &block)
  end
end

#merge_method(*names, &block) ⇒ Object

This will try to merge into the method, instead of chaining to it (see README.rdoc). You probably don’t want to use this directly but try

chain_method(:some_method, :try_merge => true) { ... }

instead, which will fall back to chain_method if merge fails.

Raises:

  • (ArgumentError)


42
43
44
45
46
47
48
49
# File 'lib/chainable.rb', line 42

def merge_method(*names, &block)
  raise ArgumentError, "no block given" unless block
  names.each do |name|
    name = name.to_s
    raise ArgumentError, "cannot merge #{name}" unless instance_methods(false).include? name
    class_eval Chainable.wrapped_source(self, name, block)
  end
end