Module: Chainable
- Defined in:
- lib/chainable.rb
Overview
This mixin will be included in Module.
Class Method Summary collapse
- .call_stored_method(method_id, owner, *args, &block) ⇒ Object
-
.copy_method(source_class, target_class, name) ⇒ Object
Copies a method from one module to another.
- .could_raise(error = Exception) ⇒ Object
-
.default_options ⇒ Object
Default options for auto_chain and chain_method.
-
.mixin_for(klass, name, reuse = true) ⇒ Object
Creates mixin used by chain_method.
- .raise_potential_errors=(value) ⇒ Object
- .raise_potential_errors? ⇒ Boolean
-
.sexp_for(a, b = nil) ⇒ Object
Give a proc, class and method, string or sexp and get a sexp.
-
.sexp_walk(sexp, forbidden_locals = [], &block) ⇒ Object
Traveling a methods sexp tree.
-
.skip_chain ⇒ Object
Used internally.
- .store_method(block) ⇒ Object
-
.try_merge(klass, *names, &wrapper) ⇒ Object
Tries merge_method on all given methods for klass.
-
.unified(sexp) ⇒ Object
Unify sexp.
-
.unifier ⇒ Object
Unifier with modifications for Ruby2Ruby.
-
.wrapped_sexp(klass, name, wrapper) ⇒ Object
The sexp part of wrapped_source.
-
.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).
Instance Method Summary collapse
-
#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.
-
#chain_method(*names, &block) ⇒ Object
This will “chain” a method (read: push it to a module and include it).
-
#merge_method(*names, &block) ⇒ Object
This will try to merge into the method, instead of chaining to it (see README.rdoc).
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_options ⇒ Object
73 74 75 |
# File 'lib/chainable.rb', line 73 def self. @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
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_chain ⇒ Object
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 |
.unifier ⇒ Object
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( = {}) eigenclass = (class << self; self; end) eigenclass.class_eval do chain_method :method_added, :try_merge => false do |name| Chainable.skip_chain { chain_method name, } 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) = names.grep(Hash).inject(Chainable.) do |a, b| a.merge names.delete(b) end names = Chainable.try_merge(self, *names, &block) if [:try_merge] names.each do |name| name = name.to_s if instance_methods(false).include? name mod = Chainable.mixin_for(self, name, [: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.
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 |