module Spec
  module Matchers
    class SimpleMatcher
      attr_writer :failure_message, :negative_failure_message, :description
      
      def initialize(description, &match_block)
        @description = description
        @match_block = match_block
        @failure_message = @negative_failure_message = nil
      end

      def matches?(given)
        @given = given
        case @match_block.arity
        when 2
          @match_block.call(@given, self)
        else
          @match_block.call(@given)
        end
      end
      
      def description
        @description || explanation
      end

      def failure_message_for_should
        @failure_message || (@description.nil? ? explanation : %[expected #{@description.inspect} but got #{@given.inspect}])
      end

      def failure_message_for_should_not
        @negative_failure_message || (@description.nil? ? explanation : %[expected not to get #{@description.inspect}, but got #{@given.inspect}])
      end

      def explanation
        "No description provided. See RDoc for simple_matcher()"
      end
    end
  
    # simple_matcher makes it easy for you to create your own custom matchers
    # in just a few lines of code when you don't need all the power of a
    # completely custom matcher object.
    #
    # The <tt>description</tt> argument will appear as part of any failure
    # message, and is also the source for auto-generated descriptions.
    #
    # The <tt>match_block</tt> can have an arity of 1 or 2. The first block
    # argument will be the given value. The second, if the block accepts it
    # will be the matcher itself, giving you access to set custom failure
    # messages in favor of the defaults.
    #
    # The <tt>match_block</tt> should return a boolean: <tt>true</tt>
    # indicates a match, which will pass if you use <tt>should</tt> and fail
    # if you use <tt>should_not</tt>. false (or nil) indicates no match,
    # which will do the reverse: fail if you use <tt>should</tt> and pass if
    # you use <tt>should_not</tt>.
    #
    # An error in the <tt>match_block</tt> will bubble up, resulting in a
    # failure.
    #
    # == Example with default messages
    #
    #   def be_even
    #     simple_matcher("an even number") { |given| given % 2 == 0 }
    #   end
    #                    
    #   describe 2 do
    #     it "should be even" do
    #       2.should be_even
    #     end
    #   end
    #
    # Given an odd number, this example would produce an error message stating:
    # expected "an even number", got 3.
    #
    # Unfortunately, if you're a fan of auto-generated descriptions, this will
    # produce "should an even number." Not the most desirable result. You can
    # control that using custom messages:
    #
    # == Example with custom messages
    #
    #   def rhyme_with(expected)
    #     simple_matcher("rhyme with #{expected.inspect}") do |given, matcher|
    #       matcher.failure_message = "expected #{given.inspect} to rhyme with #{expected.inspect}"
    #       matcher.negative_failure_message = "expected #{given.inspect} not to rhyme with #{expected.inspect}"
    #       given.rhymes_with? expected
    #     end
    #   end
    #
    #   # OR
    #
    #   def rhyme_with(expected)
    #     simple_matcher do |given, matcher|
    #       matcher.description = "rhyme with #{expected.inspect}"
    #       matcher.failure_message = "expected #{given.inspect} to rhyme with #{expected.inspect}"
    #       matcher.negative_failure_message = "expected #{given.inspect} not to rhyme with #{expected.inspect}"
    #       given.rhymes_with? expected
    #     end
    #   end
    #
    #   describe "pecan" do
    #     it "should rhyme with 'be gone'" do
    #       nut = "pecan"
    #       nut.extend Rhymer
    #       nut.should rhyme_with("be gone")
    #     end
    #   end
    #
    # The resulting messages would be:
    #   description:              rhyme with "be gone"
    #   failure_message:          expected "pecan" to rhyme with "be gone"
    #   negative failure_message: expected "pecan" not to rhyme with "be gone"
    #
    # == Wrapped Expectations
    #
    # Because errors will bubble up, it is possible to wrap other expectations
    # in a SimpleMatcher.
    #
    #   def be_even
    #     simple_matcher("an even number") { |given| (given % 2).should == 0 }
    #   end
    #
    # BE VERY CAREFUL when you do this. Only use wrapped expectations for
    # matchers that will always be used in only the positive
    # (<tt>should</tt>) or negative (<tt>should_not</tt>), but not both.
    # The reason is that is you wrap a <tt>should</tt> and call the wrapper
    # with <tt>should_not</tt>, the correct result (the <tt>should</tt>
    # failing), will fail when you want it to pass.
    #
    def simple_matcher(description=nil, &match_block)
      SimpleMatcher.new(description, &match_block)
    end
  end
end