RSpecのカスタムマッチャーに組込みマッチャーのfailure_messageを再利用する+マッチャ処理の大まかな流れについて

RSpecのカスタムマッチャーを利用して、複数の要素のアサーションを一つにまとめることがあります。
何に対してマッチさせたいのかをより説明的に書くことが出来、複雑なアサーションを使い回すことができて便利です。

```ruby
RSpec::Matchers.define :create_user_list do |*usernames|
  match do |actual|
    actual.all? { |u| u.is_a?(User) } &&
      actual.all?(&:persisted)
      actual.map(&:name).sort == usernames.sort
  end
end
```

まあ、これぐらいの内容だったらそんなに問題になりません。
しかし、もうちょっとやる事が複雑になると、このマッチャーのどこでどう失敗したのかを細かく知りたくなります。
とは言え、自分で`failure_message`を定義して色々とdiffを出したりするのは面倒です。

一方で、最近のRSpecの組み込みマッチャーは組み合わせが可能になっていて、失敗時にはそれなりに分かり易い情報を出力してくれます。
(本当はpower assertが欲しいですが)

組込みのマッチャーを組み合わせて利用し、その`failure_message`を再利用できれば、手軽により分かりやすいエラーメッセージが得られそうです。

というわけで実現する方法が無いかRSpecの中を調べてみました。
RSpecのマッチャ処理はrspec-expectationsに書かれています。

```ruby
# lib/rspec/expectations/expectation_target.rb

# ...

      # Runs the given expectation, passing if `matcher` returns true.
      # @example
      #   expect(value).to eq(5)
      #   expect { perform }.to raise_error
      # @param [Matcher]
      #   matcher
      # @param [String or Proc] message optional message to display when the expectation fails
      # @return [Boolean] true if the expectation succeeds (else raises)
      # @see RSpec::Matchers
      def to(matcher=nil, message=nil, &block)
        prevent_operator_matchers(:to) unless matcher
        RSpec::Expectations::PositiveExpectationHandler.handle_matcher(@target, matcher, message, &block)
      end

# ...

```

```ruby
# lib/rspec/expectations/handler.rb

# ...

      def self.handle_failure(matcher, message, failure_message_method)
        message = message.call if message.respond_to?(:call)
        message ||= matcher.__send__(failure_message_method)

        if matcher.respond_to?(:diffable?) && matcher.diffable?
          ::RSpec::Expectations.fail_with message, matcher.expected, matcher.actual
        else
          ::RSpec::Expectations.fail_with message
        end
      end
    end

    # @private
    class PositiveExpectationHandler
      def self.handle_matcher(actual, initial_matcher, message=nil, &block)
        matcher = ExpectationHelper.setup(self, initial_matcher, message)

        return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) unless initial_matcher
        matcher.matches?(actual, &block) || ExpectationHelper.handle_failure(matcher, message, :failure_message)
      end

# ...
```

```ruby
# lib/rspec/expectations/fail_with.rb

# ...

      # Raises an RSpec::Expectations::ExpectationNotMetError with message.
      # @param [String] message
      # @param [Object] expected
      # @param [Object] actual
      #
      # Adds a diff to the failure message when `expected` and `actual` are
      # both present.
      def fail_with(message, expected=nil, actual=nil)
        unless message
          raise ArgumentError, "Failure message is nil. Does your matcher define the " \
                               "appropriate failure_message[_when_negated] method to return a string?"
        end

        diff = differ.diff(actual, expected)
        message = "#{message}\nDiff:#{diff}" unless diff.empty?

        raise RSpec::Expectations::ExpectationNotMetError, message
      end

# ...
```

大体の流れを追っかけるとこんな感じです。
つまり、マッチャークラスで定義された`#matches?`がfalseを返すと、differという奴がいい感じにdiffを取ってメッセージに組み込んだ上で、`RSpec::Expectations::ExpectationNotMetError`という例外を起こすようです。

ここから先はrspec-coreのformatter辺りの仕事になります。

なので、カスタムマッチャーの中で普通にRSpecのアサーション処理を行い、`RSpec::Expectations::ExpectationNotMetError`例外を引っ掛けて例外オブジェクトからメッセージを引っ込ぬいて`failure_message`に利用してしまえば狙い通りのことができそうです。

具体的には以下のように書きます。

```ruby
RSpec::Matchers.define :create_user_list do |*usernames|
  match do |actual|
    begin
      expect(actual).to all(be_a(User)).and all(be_persisted)
      expect(actual.map(&:name)).to contain_exactly(usernames)
    rescue RSpec::Expectations::ExpectationNotMetError => e
      failure_position = e.backtrace.find {|str| str =~ /#{File.expand_path(__FILE__)}/}
      @failure_message = "#{e.message}\nat #{failure_position}"
      false
    end
  end

  failure_message do
    @failure_message
  end
end
```

これで、説明的な名前のカスタムマッチャーを利用しつつ、RSpec3のコンポーザブルマッチャやエラーメッセージを利用することができます。

ついでに`begin rescue`が邪魔なので、書き方を提携化したDSLも用意してみた。モンキーパッチだけど。

```ruby
module RSpec::Matchers::DSL::Macros
  def match_with_builtin(&match_block)
    define_user_override(:matches?, match_block) do |actual|
      begin
        @actual = actual
        super(*actual_arg_for(match_block))
      rescue RSpec::Expectations::ExpectationNotMetError => e
        failure_position = e.backtrace.find {|str| str =~ /#{File.expand_path(__FILE__)}/}
        @failure_message = "#{e.message}\nat #{failure_position}"
        false
      end
    end

    failure_message { @failure_message }
  end
end
```

これをspec/supportsの下に置いておけば、`match`の代わりに`match_with_builtin`を使うだけで書ける。