Reduced boilerplate needed for method_missing in ruby.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
lib
spec
.gitignore
.travis.yml
Gemfile
LICENSE.txt
README.md
Rakefile
missingly.gemspec

README.md

Missingly

A DSL for handling method_missing hooks.

Code Climate Build Status Coverage Status

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

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request
  6. Please no tabs or trailing whitespace
  7. Features and bug fixes should have specs