EagerBeaver provides an interface for adding #method_missing-related abilities
to a class or module.
- Method matchers can be added dynamically and cumulatively, reducing the risk of accidentally altering or removing previously-added functionality.
- Matched methods are automatically reflected in calls to
#respond_to?and#method, DRY-ing up code by centralizing method-matching logic. - Matched methods are automatically added to the including class/module and
invoked. Subsequent calls won't trigger
#method_missing. - When a method cannot be matched,
super's#method_missingis automatically invoked.
Add this line to your application's Gemfile:
gem 'eager_beaver'
And then execute:
$ bundle
Or install it yourself as:
$ gem install eager_beaver
Any class or module which includes EagerBeaver will gain the add_method_matcher
pseudo-keyword, which [indirectly] yields an EagerBeaver::MethodMatcher to the
given block:
require 'eager_beaver'
class NeedsMethods
include EagerBeaver
add_method_matcher do |mm|
...
end
endIn this case, the resulting MethodMatcher is added to the end of a MethodMatcher list
associated with NeedsMethods.
Each MethodMatcher needs two things: a lambda for matching missing method names
and a lambda for creating the code for any method names it matches:
add_method_matcher do |mm|
mm.match = lambda { ... }
mm.new_method_code = lambda { ...}
end
endThe match lambda should return a true value if the missing method name is one
can be handled by the MethodMatcher. The following example will match
missing methods of the form #make_<attr_name>:
mm.match = lambda {
/\Amake_(\w+)\z/ =~ context.missing_method_name
context.attr_name = Regexp.last_match ? Regexp.last_match[1] : nil
return Regexp.last_match
}As the example shows, each MethodMatcher contains a context which provides:
- the name of the missing method (
context.missing_method_name) - the original method receiver instance (
context.original_receiver) - a place to stash information (dynamically-generated accessors
context.<attr_name>andcontext.<attr_name>=)
This context is shared between the match and new_method_code lambdas, and
is reset between uses of each MethodMatcher.
The new_method_code lambda should return a string which will create the
missing method in NeedsMethods:
mm.new_method_code = lambda {
code = %Q{
def #{context.missing_method_name}(arg)
puts "method \##{context.missing_method_name} has been called"
puts "\##{context.missing_method_name} was originally called on #{context.original_receiver}"
puts "#{context.attr_name} was passed from matching to code generation"
puts "the current call has arguments: \#{arg}"
return "result = \#{arg}"
end
}
return code
}As the example shows, it is perfectly reasonable to take advantage of work done
by the match lambda (in this case, the parsing of <attr_name>).
After the generated code is inserted into NeedsMethods, the missing method
call is resent to the original receiver.
require 'eager_beaver'
class NeedsMethods
include EagerBeaver
add_method_matcher do |mm|
mm.match = lambda {
/\Amake_(\w+)\z/ =~ context.missing_method_name
context.attr_name = Regexp.last_match ? Regexp.last_match[1] : nil
return Regexp.last_match
}
mm.new_method_code = lambda {
code = %Q{
def #{context.missing_method_name}(arg)
puts "method \##{context.missing_method_name} has been called"
puts "\##{context.missing_method_name} was originally called on #{context.original_receiver}"
puts "#{context.attr_name} was passed from matching to code generation"
puts "the current call has arguments: \#{arg}"
return "result = \#{arg}"
end
}
return code
}
end
endGiven the NeedsMethods class in the example above, let's work through the
following code:
nm1 = NeedsMethods.new
puts nm1.make_thingy(10)
puts nm1.make_widget("hi")
nm2 = NeedsMethods.new
puts nm2.make_thingy(20)
puts nm2.make_widget("hello")
nm2.dont_make_thisAs instances of NeedsMethods, nm1 and nm2 will automatically handle
methods of the form #make_<attr_name>.
The line:
puts nm1.make_thingy(10)will trigger nm1's #method_missing, which NeedsMethods implements thanks to
EagerBeaver. Each MethodMatcher associated with EagerBeaver is run against
the method name make_thingy, and sure enough one matches. This causes the
following methods to be inserted to NeedsMethods:
def make_thingy(arg)
puts "method #make_thingy has been called"
puts "#make_thingy was originally called on #<NeedsMethods:0x007fa1bc17f498>"
puts "thingy was passed from matching to code generation"
puts "the current call has arguments: #{arg}"
return "result = #{arg}"
endand when #make_thingy is resent to nm1, the existing method is called and
outputs:
method #make_thingy has been called
#make_thingy was originally called on #<NeedsMethods:0x007fa1bc17f498>
thingy was passed from matching to code generation
the current call has arguments: 10
result = 10
Similarly, the line:
puts nm1.make_widget("hi")generates the code:
def make_widget(arg)
puts "method #make_widget has been called"
puts "#make_widget was originally called on #<NeedsMethods:0x007fa1bc17f498>"
puts "widget was passed from matching to code generation"
puts "the current call has arguments: #{arg}"
return "result = #{arg}"
endand outputs:
method #make_widget has been called
#make_widget was originally called on #<NeedsMethods:0x007fa1bc17f498>
widget was passed from matching to code generation
the current call has arguments: hi
result = hi
Note that the following lines do NOT trigger #method_missing because both methods
have already been added to NeedsMethods:
puts nm2.make_thingy(20)
puts nm2.make_widget("hello")This can be seen by examining the identity of the original receiver in the output:
method #make_thingy has been called
#make_thingy was originally called on #<NeedsMethods:0x007fa1bc17f498>
thingy was passed from matching to code generation
the current call has arguments: 20
result = 20
method #make_widget has been called
#make_widget was originally called on #<NeedsMethods:0x007fa1bc17f498>
widget was passed from matching to code generation
the current call has arguments: hello
result = hello
String substitutions which were part of the generated code body (emphasized) reflect the circumstances of the first set of method calls, as opposed to those which reflect the current call's argument.
Finally, the call:
nm2.dont_make_this
will cause NeedsMethods to examine all of its MethodMatchers and finally call
super's #method_missing. Because no superclass of NeedsMethods handles
#dont_make_this, the output is:
undefined method `dont_make_this' for #<NeedsMethods:0x007f8e2b991f90> (NoMethodError)
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Added some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request