lib/vcr/request_matcher_registry.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'vcr/errors'

module VCR
  # Keeps track of the different request matchers.
  class RequestMatcherRegistry

    # The default request matchers used for any cassette that does not
    # specify request matchers.
    DEFAULT_MATCHERS = [:method, :uri]

    # @private
    class Matcher < Struct.new(:callable)
      def matches?(request_1, request_2)
        callable.call(request_1, request_2)
      end
    end

    # @private
    class URIWithoutParamsMatcher < Struct.new(:params_to_ignore)
      def partial_uri_from(request)
        request.parsed_uri.tap do |uri|
          return uri unless uri.query # ignore uris without params, e.g. "http://example.com/"

          uri.query = uri.query.split('&').tap { |params|
            params.map! do |p|
              key, value = p.split('=')
              key.gsub!(/\[\]\z/, '') # handle params like tag[]=
              [key, value]
            end

            params.reject! { |p| params_to_ignore.include?(p.first) }
            params.map!    { |p| p.join('=') }
          }.join('&')

          uri.query = nil if uri.query.empty?
        end
      end

      def call(request_1, request_2)
        partial_uri_from(request_1) == partial_uri_from(request_2)
      end

      def to_proc
        lambda { |r1, r2| call(r1, r2) }
      end
    end

    # @private
    def initialize
      @registry = {}
      register_built_ins
    end

    # @private
    def register(name, &block)
      if @registry.has_key?(name)
        warn "WARNING: There is already a VCR request matcher registered for #{name.inspect}. Overriding it."
      end

      @registry[name] = Matcher.new(block)
    end

    # @private
    def [](matcher)
      @registry.fetch(matcher) do
        matcher.respond_to?(:call) ?
          Matcher.new(matcher) :
          raise_unregistered_matcher_error(matcher)
      end
    end

    # Builds a dynamic request matcher that matches on a URI while ignoring the
    # named query parameters. This is useful for dealing with non-deterministic
    # URIs (i.e. that have a timestamp or request signature parameter).
    #
    # @example
    #   without_timestamp = VCR.request_matchers.uri_without_param(:timestamp)
    #
    #   # use it directly...
    #   VCR.use_cassette('example', :match_requests_on => [:method, without_timestamp]) { }
    #
    #   # ...or register it as a named matcher
    #   VCR.configure do |c|
    #     c.register_request_matcher(:uri_without_timestamp, &without_timestamp)
    #   end
    #
    #   VCR.use_cassette('example', :match_requests_on => [:method, :uri_without_timestamp]) { }
    #
    # @param ignores [Array<#to_s>] The names of the query parameters to ignore
    # @return [#call] the request matcher
    def uri_without_params(*ignores)
      uri_without_param_matchers[ignores]
    end
    alias uri_without_param uri_without_params

  private

    def uri_without_param_matchers
      @uri_without_param_matchers ||= Hash.new do |hash, params|
        params = params.map(&:to_s)
        hash[params] = URIWithoutParamsMatcher.new(params)
      end
    end

    def raise_unregistered_matcher_error(name)
      raise Errors::UnregisteredMatcherError.new \
        "There is no matcher registered for #{name.inspect}. " +
        "Did you mean one of #{@registry.keys.map(&:inspect).join(', ')}?"
    end

    def register_built_ins
      register(:method)  { |r1, r2| r1.method == r2.method }
      register(:uri)     { |r1, r2| r1.parsed_uri == r2.parsed_uri }
      register(:body)    { |r1, r2| r1.body == r2.body }
      register(:headers) { |r1, r2| r1.headers == r2.headers }

      register(:host) do |r1, r2|
        r1.parsed_uri.host.chomp('.') == r2.parsed_uri.host.chomp('.')
      end
      register(:path) do |r1, r2|
        r1.parsed_uri.path == r2.parsed_uri.path
      end

      register(:query) do |r1, r2|
        VCR.configuration.query_parser.call(r1.parsed_uri.query.to_s) ==
          VCR.configuration.query_parser.call(r2.parsed_uri.query.to_s)
      end

      try_to_register_body_as_json
    end

    def try_to_register_body_as_json
      begin
        require 'json'
      rescue LoadError
        return
      end

      register(:body_as_json) do |r1, r2|
        begin
          r1.body == r2.body || JSON.parse(r1.body) == JSON.parse(r2.body)
        rescue JSON::ParserError
          false
        end
      end
    end
  end
end