WhitePayments/match_json

View on GitHub
lib/match_json/matchers/include_json.rb

Summary

Maintainability
A
1 hr
Test Coverage

module MatchJson
  module Matchers
    class IncludeJson
      PATTERNS = {
        'date_time_iso8601' => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
        'date' => /^\d{4}-\d{2}-\d{2}/,
        'uuid' => /\h{32}/,
        'email' => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,6}\z/i,
        'string' => /\A.+\z/i
      }

      def initialize(expected_json)
        @expected_json = JSON.parse(expected_json.gsub(/(?<!")(\{\w+\})(?!")/, '"\1:non-string"'))
      end

      def matches?(actual_json)
        @actual_json = actual_json.respond_to?(:body) ? JSON.parse(actual_json.body) : JSON.parse(actual_json)

        catch(:match) { json_included?(@actual_json, @expected_json) }
      end

      def failure_message
        @failure_message
      end

      def failure_message_when_negated
        "does not support negation"
      end

      private

      def json_included?(actual, expected)
        equal_value?(actual, expected)
      end

      def hash_included?(actual, expected, nested, raise_error)
        if compared_different_types?(actual, expected)
          @failure_message = %Q(Different types of compared elements:\n #{actual.class} for #{actual.to_json}\nand #{expected.class} for #{expected.to_json})
          throw(:match, false)
        end

        expected.each do |key, value|
          if (!equal_value?(actual[key], value, "#{nested} > #{key}", raise_error))
            @failure_message = %Q("#{key}":#{value.to_json} was not found in\n #{actual.to_json})

            if raise_error
              throw(:match, false)
            else
              return false
            end
          end
        end
      end

      def array_included?(actual, expected, nested, raise_error)
        expected.each do |value|
          if actual.nil? || (!actual.any? { |actual_value| equal_value?(actual_value, value, nested, false) })
            @failure_message = %Q("#{value.to_json}" was not found in\n )
            @failure_message << %Q("#{nested}":) if !nested.empty?
            @failure_message << "#{actual.to_json}"

            if raise_error
              throw(:match, false)
            else
              return false
            end
          end
        end
      end

      def equal_value?(actual, expected, nested = '', raise_error = true)
        case expected
        when Array then array_included?(actual, expected, nested, raise_error)
        when Hash then hash_included?(actual, expected, nested, raise_error)
        when String then compare_with_pattern(expected, actual)
        else
          actual == expected
        end
      end

      def compare_with_pattern(value, actual)
        case
        when regexp?(value)
          reg_exp = value.match(/{re:(.*)}/)[1]
          actual =~ Regexp.new(reg_exp)
        when defined_regexp?(value)
          defined_regexps = PATTERNS.keys.find_all { |pattern| pattern.is_a? Regexp }
          matches = value.match(/{(\w+):(\w+)}/)
          reg_exp = defined_regexps.find {|re| re.match("#{matches[1]}:#{matches[2]}") }
          PATTERNS[reg_exp].call(actual, matches[2])
        when pattern?(value)
          actual =~ PATTERNS["#{value.tr('{}', '')}"]
        when non_string_pattern?(value) && !actual.is_a?(String)
          actual.to_s =~ PATTERNS["#{value.tr('{}', '').sub(':non-string', '')}"]
        else
          value == actual
        end
      end

      def pattern?(str)
        !!(str =~ /\A\{\w+\}\z/)
      end

      def non_string_pattern?(str)
        !!(str =~ /\A\{\w+\}:non-string\z/)
      end

      def regexp?(str)
        !!(str =~ /\A\{re:.+\}\z/)
      end

      def defined_regexp?(str)
        !!(str =~ /\A\{\w+:\w+\}\z/)
      end

      def compared_different_types?(actual, expected)
        actual.class != expected.class
      end
    end
  end
end