Skip to content

Latest commit

 

History

History
283 lines (212 loc) · 6.55 KB

README.md

File metadata and controls

283 lines (212 loc) · 6.55 KB

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