Explicit duck-typing for ruby
Ruby
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
docs
lib
spec
.gitignore
.rspec
.travis.yml
Gemfile
Gemfile.lock
LICENSE
README.md
Rakefile
duckpond.gemspec

README.md

Gem Version Build Status Coverage Status

Duckpond

Explicit duck typing for ruby.

Introduction

Duck Typing can make code confusing and unreadable, particularly when multiple developers are working on the same project or when projects are inherited by new developers.

  • the_problem - This outlines the problems with duck typing.

  • the_solution - This outlines how duckpond gets around these problems.

Installation

Add this line to your application's Gemfile:

gem 'duckpond'

And then execute:

$ bundle

Or install it yourself as:

$ gem install duckpond

Usage

There is now a screencast demonstrating the duckpond gem!

Usage is demonstrated in 'the_solution', but in a nutshell you create "contract" classes by inheriting from DuckPond::Contract. These classes should be commented extensively.

The "has_method" method is used to specify which methods the contract expects to see. The following contract describes classes which respond to #length and #to_s

class MyContract < DuckPond::Contract
  has_method :length
  has_method :to_s
end

Once you've declared a contract, you can compare objects to it to see if the contract is fulfilled by the object:

MyContract.fulfilled_by? "Hello"
=> true
MyContract.fulfilled_by? 12
=> false

There is also a "bang" version of the #fulfilled_by method, that raises an error instead of returning false. The error message details why it got raised.

MyContract.fulfilled_by! :foo
=> DuckPond::Contract::ContractInfringementError:
   One or more clauses from MyContract were not fulfilled by :foo (Symbol)
   Expected subject to respond to method 'length'

Contracts can be combined into composite "super contracts" - contracts which are made up of various other contracts. This ties in with the reccomendation of preferring composition over inheritance:

class MyCompositeConrtact < DuckPond::Contract
  include_clauses_from MyContract
  include_clauses_from MyOtherContract
end

Another feature of duckpond is the ability to specify what your expected result from a contract should be. For example, the following contract expects a subject to have a method called "even" and for that method, when called, to return a true value:

class MyEvenContract < DuckPond::Contract
  has_method :even?, responds_with: true
end

If the method needs args, you can specify those too. This next contract will pass the string "Hell" to the #include? method, and expect the value to be true. This contract will therefore be satisfied by strings containing the word "Hell" that are five characters long (such as the word "Hello"):

class MyHellContract < DuckPond::Contract
  has_method :include?, responds_with: true, given_args: "Hell"
  has_method :length, responds_with: 5
end

Finally, if you really want to get down and dirty with interrogating method results, you can use a block thusly (the result of calling the method will be yielded to the block):

class MyBlockContract < DuckPond::Contract
  has_method :length do |method_result|
    method_result == 5
  end
end

class MyOtherBlockContract < DuckPond::Contract
  has_method :include?, given_args: "Hell" do |method_result|
    method_result == true
  end
end

Note: The method will only be called if you use the :reponds_with option. Otherwise, it just tests for the method's existence.

In the real world, a contract might look like this:

class IEmailable < DuckPond::Contract
  #send: should send the results of :message via email to :to
  has_method :send

  #to: Should be an email address to which this will be sent
  has_method :to

  #message: The message to send
  has_method :message
end

And then be implemented in a method like this:

class Emailer
  def send(email)
    IEmailable.fulfilled_by! email
    email.send
  end
end

Compatibility

CI tests exist for the following rubies, other versions are not supported but should still work;

  • 2.3.0
  • 2.2.4
  • 2.1.8
  • jruby-19mode

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