Generates a diff by matching against expected values, classes, regexes and/or procs.
Ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
doc
lib
spec
.gitignore
.rspec
.travis.yml
CHANGELOG.md
Gemfile
License.txt
README.md
Rakefile
TODO.txt
diff_matcher.gemspec

README.md

DiffMatcher

build status still maintained

Generates a diff by matching against user-defined matchers written in ruby.

DiffMatcher matches input data (eg. from a JSON API) against values, ranges, classes, regexes, procs, custom matchers and/or easily composed, nested combinations thereof to produce an easy to read diff string.

Actual input values are matched against expected matchers in the following way:

actual.is_a? expected  # when expected is a class
expected.match actual  # when expected is a regexp
expected.call actual   # when expected is a proc
actual == expected     # when expected is anything else
expected.diff actual   # when expected is a built-in DiffMatcher

Using these building blocks, more complicated nested matchers can be composed. eg.

expected = { :a=>{ :a1=>11          }, :b=>[ 21, 22 ], :c=>/\d/, :d=>Fixnum, :e=>lambda { |x| (4..6).include? x } },
actual   = { :a=>{ :a1=>10, :a2=>12 }, :b=>[ 21     ], :c=>'3' , :d=>4     , :e=>5                                },
puts DiffMatcher::difference(expected, actual, :color_scheme=>:white_background)

example output

Installation

gem install diff_matcher

Usage

require 'diff_matcher'

DiffMatcher::difference(expected, actual, opts={})

Simple matchers

Using plain ruby objects produces the following diffs:

+-------------+--------+-------------+
| expected    | actual | diff        |
+-------------+--------+-------------+
| 1           | 2      | - 1+ 2      |
| 1           | 1      |             |
| String      | 1      | - String+ 1 |
| String      | "1"    |             |
| /[a-z]/     | 1      | -/[a-z]/+ 1 |
| /[a-z]/     | "a"    |             |
| 1..3        | 4      | - 1..3+ 4   |
| 1..3        | 3      |             |
| is_boolean  | true   |             |
+-------------+--------+-------------+

Where:
  is_boolean = lambda { |x| [FalseClass, TrueClass].include? x.class }

When actual is missing one of the expected values

expected = [1, 2]
puts DiffMatcher::difference(expected, [1])
# => [
# =>   1
# => - 2
# => ]
# => Where, - 1 missing

When actual has additional values to the expected

expected = [1]
puts DiffMatcher::difference(expected, [1, 2])
# => [
# =>   1
# => + 2
# => ]
# => Where, + 1 additional

More complicated matchers

Sometimes you'll need to wrap plain ruby objects with DiffMatcher's built-in matchers, to provide extra matching abilities.

When expected is a Hash, but has optional keys, wrap the Hash with a built-in Matcher

exp = {:name=>String, :age=>Fixnum}
expected = DiffMatcher::Matcher.new(exp, :optional_keys=>[:age])
puts DiffMatcher::difference(expected, {:name=>0})
# => {
# =>   :name=>- String+ 0
# => }
# => Where, - 1 missing, + 1 additional

When multiple expected values can be matched against, simply wrap them in Matchers and || them together

exp1 = Fixnum
exp2 = Float
expected = DiffMatcher::Matcher.new(exp1) || DiffMatcher::Matcher.new(exp2)
puts DiffMatcher::difference(expected, "3")
# => - Float+ "3"
# => Where, - 1 missing, + 1 additional

Or to do the same thing using a shorter syntax

exp1 = Fixnum
exp2 = Float
expected = DiffMatcher::Matcher[exp1, exp2]
puts DiffMatcher::difference(expected, "3")
# => - Float+ "3"
# => Where, - 1 missing, + 1 additional

When actual is an array of unknown size use an AllMatcher to match against all the elements in the array

exp = Fixnum
expected = DiffMatcher::AllMatcher.new(exp)
puts DiffMatcher::difference(expected, [1, 2, "3"])
# => [
# =>   : 1,
# =>   : 2,
# =>   - Fixnum+ "3"
# => ]
# => Where, - 1 missing, + 1 additional, : 2 match_class

When actual is an array with a limited size use an AllMatcher to match against all the elements in the array adhering to the limits of :min and or :max or :size (where :size is a Fixnum or range of Fixnum).

exp = Fixnum
expected = DiffMatcher::AllMatcher.new(exp, :min=>3)
puts DiffMatcher::difference(expected, [1, 2])
# => [
# =>   : 1,
# =>   : 2,
# =>   - Fixnum
# => ]
# => Where, - 1 missing, : 2 match_class
exp = Fixnum
expected = DiffMatcher::AllMatcher.new(exp, :size=>3..5)
puts DiffMatcher::difference(expected, [1, 2])
# => [
# =>   : 1,
# =>   : 2,
# =>   - Fixnum
# => ]
# => Where, - 1 missing, : 2 match_class

When actual is an array of unknown size and expected can take multiple forms use a Matcher to || them together, then wrap that with an AllMatcher to match against all the elements in the array in any of the forms.

exp1 = Fixnum
exp2 = Float
expected = DiffMatcher::AllMatcher.new( DiffMatcher::Matcher[Fixnum, Float] )
puts DiffMatcher::difference(expected, [1, 2.00, "3"])
# => [
# =>   | 1,
# =>   | 2.0,
# =>   - Float+ "3"
# => ]
# => Where, - 1 missing, + 1 additional, | 2 match_matcher

Matcher options

Matcher options can be passed to DiffMatcher::difference or DiffMatcher::Matcher#diff or instances of DiffMatcher::Matcher

First consider:

expected = DiffMatcher::Matcher.new([1])
puts expected.diff([1, 2])
# => [
# =>   1,
# => + 2
# => ]

Using :ignore_additional=>true will now match even though actual has additional items.

It can be used in the following ways:

expected = DiffMatcher::Matcher.new([1])
puts expected.diff([1, 2], :ignore_additional=>true)
# => nil

or

expected = DiffMatcher::Matcher.new([1])
puts DiffMatcher::difference(expected, [1, 2], :ignore_additional=>true)
# => nil

or

expected = DiffMatcher::Matcher.new([1], :ignore_additional=>true)
puts expected.diff([1, 2])
# => nil

Now consider:

puts DiffMatcher::Matcher.new([Fixnum, 2]).diff([1])
# => [
# =>   : 1,
# => - 2
# => ]

Using :quiet=>true will only show missing and additional items in the output

puts DiffMatcher::Matcher.new([Fixnum, 2]).diff([1], :quiet=>true)
# => [
# => - 2
# => ]

:html_output=>true will convert ansii escape colour codes to html spans

puts DiffMatcher::difference(1, 2, :html_output=>true)
# => <pre>
# => <span style="color:red">- <b>1</b></span><span style="color:yellow">+ <b>2</b></span>
# => Where, <span style="color:red">- <b>1 missing</b></span>, <span style="color:yellow">+ <b>1 additional</b></span>
# => </pre>

Prefixes

A difference string is similar in appereance to the .inspect of plain ruby objects, however the matched elements it contains are prefixed in the following way:

missing       => "- "
additional    => "+ "
match value   =>
match regexp  => "~ "
match class   => ": "
match matcher => "| "
match range   => ". "
match proc    => "{ "

Colours

Colours (defined in colour schemes) can also appear in the difference.

Using the :default colour scheme items shown in a difference are coloured as follows:

missing       => red
additional    => yellow
match value   =>
match regexp  => green
match class   => blue
match matcher => blue
match range   => cyan
match proc    => cyan

Other colour schemes, eg. :color_scheme=>:white_background will use different colour mappings.

Similar gems

String differs

Object differs

JSON matchers

Why another differ?

This gem came about because rspec doesn't have a decent differ for matching hashes and/or JSON. It started out as a pull request, to be implemented as a be_hash_matching rspec matcher, but seemed useful enough to be its own stand alone gem.

Out of the similar gems above, easy_diff looks like a good alternative to this gem. It has extra functionality in also being able to recursively merge hashes and arrays. sub_diff can use regular expressions in its match and subsequent diff

DiffMatcher can match using not only regexes but classes and procs. And the difference string that it outputs can be formatted in several ways as needed.

As for matching JSON, the matchers above work well, but don't allow for matching patterns.

Update 2012/07/14:

json_expressions (as mentioned in Ruby5 - Episode #288) does do pattern matching and also looks like a good alternative to diff_matcher, it has the following advantages:

  • define capture symbols that can be used to extract values from the matched object
  • (if a symbol is used multiple times, it will make sure all the extracted values match)
  • can optionally match unordered arrays (diff_matcher only matches ordered arrays)
  • because it doesn't bother generating a pretty difference string it might be faster

Use with rspec

To use with rspec create the following custom matcher:

require 'diff_matcher'

module RSpec
  module Matchers
    class BeMatching
      include BaseMatcher

      def initialize(expected, opts)
        @expected = expected
        @opts = opts.update(:color_enabled=>RSpec::configuration.color_enabled?)
      end

      def matches?(actual)
        @difference = DiffMatcher::Difference.new(expected, actual, @opts)
        @difference.matching?
      end

      def failure_message_for_should
        @difference.to_s
      end
    end

    def be_matching(expected, opts={})
      Matchers::BeMatching.new(expected, opts)
    end
  end
end

And use it with:

describe "hash matcher" do
  subject { { :a=>1, :b=>2, :c=>'3', :d=>4, :e=>"additional stuff" } }
  let(:expected) { { :a=>1, :b=>Fixnum, :c=>/[0-9]/, :d=>lambda { |x| (3..5).include?(x) } } }

  it { should be_matching(expected, :ignore_additional=>true) }
  it { should be_matching(expected) }
end

Will result in:

  Failures:

    1) hash matcher
       Failure/Error: it { should be_matching(expected) }
         {
           :a=>1,
           :b=>: 2,
           :c=>~ (3),
           :d=>{ 4,
         + :e=>"additional stuff"
         }
         Where, + 1 additional, ~ 1 match_regexp, : 1 match_class, { 1 match_proc
      # ./hash_matcher_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.00601 seconds
2 examples, 1 failure

Contributing

Think up something DiffMatcher can't do! :) Fork, write some tests and send a pull request (bonus points for topic branches), or just submit an issue.

Status

Our company is using this gem to test our JSON API which has got above and beyond a stable v1.0.0 release.

There's a pull request to use this gem in a be_hash_matching rspec matcher.