Missingly
A DSL for handling method_missing hooks.
Installation
Add this line to your application's Gemfile:
gem 'missingly'
And then execute:
$ bundle
Or install it yourself as:
$ gem install missingly
Usage
Use regular expression matching
class ArrayWithHashes
include Missingly::Matchers
handle_missingly /^find_by_(\w+)$/ do |matches, *args, &block|
fields = matches[1].split("_and_")
hashes.find do |hash|
fields.inject(true) do |fields_match, field|
index_of_field = fields.index(field)
arg_for_field = args[index_of_field]
fields_match = fields_match && hash[field.to_sym] == arg_for_field
break false unless fields_match
true
end
end
end
handle_missingly /^find_all_by_(\w+)$/ do |matches, *args, &block|
fields = matches[1].split("_and_")
hashes.find_all do |hash|
fields.inject(true) do |fields_match, field|
index_of_field = fields.index(field)
arg_for_field = args[index_of_field]
fields_match = fields_match && hash[field.to_sym] == arg_for_field
break false unless fields_match
true
end
end
end
attr_reader :hashes
def initialize(hashes)
@hashes = hashes
end
end
hashes = [
{ id: 1, name: 'Pat', gender: 'f' },
{ id: 2, name: 'Pat', gender: 'm' },
{ id: 3, name: 'Steve', gender: 'm' },
{ id: 4, name: 'Sue', gender: 'f' },
]
instance = ArrayWithHashes.new(hashes)
instance.find_by_name_and_gender('Pat', 'm') # { id: 2, name: 'Pat', gender: 'm' }
instance.find_all_by_name('Pat') # both male and female Pat's
instance.respond_to?(:find_by_name_and_gender) # true
instance.method(:find_by_name_and_gender) # method object
Use array matching
class NetJSON
include Missingly::Matchers
handle_missingly [:get, :put, :post, :delete] do |method_name, url, params|
uri = URI.parse(url)
requester = Net::HTTP.new(uri.host, uri.port)
request = "Net::HTTP::#{method_name.to_s.classify}".constantize.new(uri.path)
request.body = params.to_json
requester.request(request)
end
end
requester = NetJSON.new
requester.get 'http://www.example.com/some_path/', {first_name: 'John'}
requester.put 'http://www.example.com/some_resource/1/', {admin: true}
Use for delegation
class UserDecorator
include Missingly::Matchers
handle_missingly [:roles], to: :user
def can_edit?
roles.include?(:editor)
end
end
Use custom matchers
In the example with the regex block matchers, our code has to do a fair amount of work which is not looking up a value in a hash, for example:
fields = matches[1].split("_and_")
will run every time and can have a performance impact. Likewise we are always running:
field.to_sym
In the hash lookup. If the field was already a symbol, there would be less work. And the fields were already split up, there would be less work each time. Custom block matchers can be done as follows:
class OurMatcher < Missingly::BlockMatcher
attr_reader :some_matcher, :options_hash, :method_block
def initialize(some_matcher, options_hash, method_block)
@some_matcher, @method_block = some_matcher, method_block
end
def should_respond_to?(instance, name)
# our custom code
end
def setup_method_name_args(method_name)
# args we will pass to block
end
def matchable; some_matcher; end
end
Since we essentially want to re-use the regex block helper, we can inherit and override setup_method_name_args. These args will be passed to the block in the handle_missingly call:
class FindByFieldsWithAndsMatcher < Missingly::RegexBlockMatcher
attr_reader :options
def initialize(regex, options, block)
super regex, block
end
def setup_method_name_args(method_name)
matches = regex.match(method_name)
fields = matches[1].split("_and_")
fields.map(&:to_sym)
end
end
From here, we can use our custom matcher:
class ArrayWithHashes
include Missingly::Matchers
handle_missingly /^find_by_(\w+)$/, with: FindByFieldsWithAndsMatcher do |fields, *args, &block|
hashes.find do |hash|
fields.inject(true) do |fields_match, field|
index_of_field = fields.index(field)
arg_for_field = args[index_of_field]
fields_match = fields_match && hash[field] == arg_for_field
break false unless fields_match
true
end
end
end
attr_reader :hashes
def initialize(hashes)
@hashes = hashes
end
end
hashes = [
{ id: 1, name: 'Pat', gender: 'f' },
{ id: 2, name: 'Pat', gender: 'm' },
{ id: 3, name: 'Steve', gender: 'm' },
{ id: 4, name: 'Sue', gender: 'f' },
]
instance = ArrayWithHashes.new(hashes)
instance.find_by_name_and_gender('Pat', 'm') # { id: 2, name: 'Pat', gender: 'm' }
instance.respond_to?(:find_by_name_and_gender) # true
instance.method(:find_by_name_and_gender) # method object
For more fine grain controll, you can write should_respond_to? which should return true if method responds to, and handle, which should define method and return results of first run of method.
How inheritance works
The handle_missingly method is designed to be both inherited and overwritable by child classes. The following scenarios should work:
Straight up inheritance:
class Parent
handle_missingly /foo/ do
:foo
end
end
class Child < Parent
end
Child.new.foo # should return :foo
Overwriting:
class Parent
handle_missingly /foo/ do
:foo
end
end
class Child < Parent
handle_missingly /foo/ do
:bar
end
end
Child.new.foo # should return :bar
Missingly handlers are based off of "matchable" passed to matcher, so the following will also be overwritten:
class Parent
handle_missingly /foo/ do
:foo
end
end
class Child < Parent
handle_missingly /foo/, to: :something
end
Child.new.foo # should return whatever something returns
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
- Please no tabs or trailing whitespace
- Features and bug fixes should have specs