rspec-resembles_json_matchers 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/Appraisals +14 -0
- data/Gemfile +4 -0
- data/Guardfile +47 -0
- data/README.md +225 -0
- data/Rakefile +6 -0
- data/examples/example_spec.rb +123 -0
- data/gemfiles/rails_42.gemfile +8 -0
- data/gemfiles/rails_42.gemfile.lock +173 -0
- data/gemfiles/rails_42_rspec_33.gemfile +8 -0
- data/gemfiles/rails_42_rspec_33.gemfile.lock +173 -0
- data/gemfiles/rails_5.gemfile +8 -0
- data/gemfiles/rails_5.gemfile.lock +184 -0
- data/lib/rspec/resembles_json_matchers.rb +57 -0
- data/lib/rspec/resembles_json_matchers/attribute_matcher.rb +85 -0
- data/lib/rspec/resembles_json_matchers/helpers.rb +29 -0
- data/lib/rspec/resembles_json_matchers/json_matcher.rb +85 -0
- data/lib/rspec/resembles_json_matchers/matcherizer.rb +25 -0
- data/lib/rspec/resembles_json_matchers/resembles_any_of_matcher.rb +61 -0
- data/lib/rspec/resembles_json_matchers/resembles_array_matcher.rb +35 -0
- data/lib/rspec/resembles_json_matchers/resembles_class_matcher.rb +29 -0
- data/lib/rspec/resembles_json_matchers/resembles_date_matcher.rb +44 -0
- data/lib/rspec/resembles_json_matchers/resembles_hash_matcher.rb +110 -0
- data/lib/rspec/resembles_json_matchers/resembles_matcher.rb +59 -0
- data/lib/rspec/resembles_json_matchers/resembles_numeric_matcher.rb +32 -0
- data/lib/rspec/resembles_json_matchers/resembles_route_matcher.rb +25 -0
- data/lib/rspec/resembles_json_matchers/resembles_string_matcher.rb +25 -0
- data/lib/rspec/resembles_json_matchers/version.rb +5 -0
- data/rspec-resembles_json_matchers.gemspec +39 -0
- metadata +226 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
require "rspec/resembles_json_matchers/version"
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module ResemblesJsonMatchers
|
5
|
+
autoload :AttributeMatcher, "rspec/resembles_json_matchers/attribute_matcher"
|
6
|
+
# autoload :ResemblesMatcher, "rspec/resembles_json_matchers/resembles_matcher"
|
7
|
+
autoload :JsonMatcher, "rspec/resembles_json_matchers/json_matcher"
|
8
|
+
autoload :Helpers, "rspec/resembles_json_matchers/helpers"
|
9
|
+
# autoload :Matcherizer, "rspec/resembles_json_matchers/matcherizer"
|
10
|
+
|
11
|
+
autoload :ResemblesHashMatcher, "rspec/resembles_json_matchers/resembles_hash_matcher"
|
12
|
+
autoload :ResemblesArrayMatcher, "rspec/resembles_json_matchers/resembles_array_matcher"
|
13
|
+
autoload :ResemblesAnyOfMatcher, "rspec/resembles_json_matchers/resembles_any_of_matcher"
|
14
|
+
autoload :ResemblesRouteMatcher, "rspec/resembles_json_matchers/resembles_route_matcher"
|
15
|
+
autoload :ResemblesDateMatcher, "rspec/resembles_json_matchers/resembles_date_matcher"
|
16
|
+
autoload :ResemblesNumericMatcher, "rspec/resembles_json_matchers/resembles_numeric_matcher"
|
17
|
+
autoload :ResemblesStringMatcher, "rspec/resembles_json_matchers/resembles_string_matcher"
|
18
|
+
autoload :ResemblesClassMatcher, "rspec/resembles_json_matchers/resembles_class_matcher"
|
19
|
+
|
20
|
+
def iso8601_timestamp
|
21
|
+
match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/)
|
22
|
+
end
|
23
|
+
|
24
|
+
def match_json(*a)
|
25
|
+
JsonMatcher.new(*a)
|
26
|
+
end
|
27
|
+
|
28
|
+
def have_attribute(*a)
|
29
|
+
AttributeMatcher.new(*a)
|
30
|
+
end
|
31
|
+
|
32
|
+
def resembles(*a)
|
33
|
+
RSpec::ResemblesJsonMatchers.resembles_matcher_for(*a).new(*a)
|
34
|
+
end
|
35
|
+
alias resemble resembles
|
36
|
+
alias resemble_json resembles
|
37
|
+
|
38
|
+
def self.resembles_matcher_candidates
|
39
|
+
# Order matters
|
40
|
+
@candidates ||= [
|
41
|
+
ResemblesHashMatcher,
|
42
|
+
#ResemblesArrayMatcher,
|
43
|
+
ResemblesAnyOfMatcher,
|
44
|
+
ResemblesRouteMatcher,
|
45
|
+
ResemblesDateMatcher,
|
46
|
+
ResemblesNumericMatcher,
|
47
|
+
ResemblesStringMatcher,
|
48
|
+
ResemblesClassMatcher
|
49
|
+
].freeze
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.resembles_matcher_for(expected, **a)
|
53
|
+
resembles_matcher_candidates.detect { |candidate| candidate.can_match?(expected) }
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require "active_support/core_ext/hash/indifferent_access"
|
2
|
+
|
3
|
+
module RSpec::ResemblesJsonMatchers
|
4
|
+
|
5
|
+
class AttributeMatcher
|
6
|
+
include RSpec::ResemblesJsonMatchers::Helpers
|
7
|
+
|
8
|
+
attr_reader :attribute_name, :expected, :document
|
9
|
+
|
10
|
+
def initialize(attribute_name, expected = NullMatcher)
|
11
|
+
@attribute_name, @expected = attribute_name, expected
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
sentencize "have attribute #{attribute_name.inspect} #{expected.description}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(document)
|
19
|
+
@document = document.with_indifferent_access
|
20
|
+
|
21
|
+
@document.key?(attribute_name) &&
|
22
|
+
expected.matches?(@document.fetch(attribute_name, nil))
|
23
|
+
end
|
24
|
+
|
25
|
+
def failure_message
|
26
|
+
if expected === NullMatcher
|
27
|
+
msgs = ["Expected attribute",
|
28
|
+
attribute_name.inspect,
|
29
|
+
"to be present"]
|
30
|
+
else
|
31
|
+
msgs = ["Expected value of attribute",
|
32
|
+
attribute_name.inspect,
|
33
|
+
"to",
|
34
|
+
expected.description,
|
35
|
+
"but it was",
|
36
|
+
document[attribute_name].inspect]
|
37
|
+
end
|
38
|
+
sentencize(*msgs)
|
39
|
+
end
|
40
|
+
|
41
|
+
def failure_message_when_negated
|
42
|
+
sentencize "Expected attribute #{attribute_name.inspect}",
|
43
|
+
expected.description,
|
44
|
+
"to be absent"
|
45
|
+
end
|
46
|
+
|
47
|
+
NullMatcher = Class.new do
|
48
|
+
def matches?(*_args)
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def failure_message
|
53
|
+
""
|
54
|
+
end
|
55
|
+
alias_method :failure_message_for_should, :failure_message
|
56
|
+
|
57
|
+
def description
|
58
|
+
"be present"
|
59
|
+
end
|
60
|
+
|
61
|
+
def ===(_other)
|
62
|
+
true
|
63
|
+
end
|
64
|
+
end.new
|
65
|
+
|
66
|
+
def matcherize(expected)
|
67
|
+
if matcher? expected
|
68
|
+
expected
|
69
|
+
|
70
|
+
elsif expected.respond_to? :===
|
71
|
+
RSpec::Matchers::Builtin::Match.new(expected)
|
72
|
+
|
73
|
+
else
|
74
|
+
RSpec::Matchers::Builtin::Eq.new(expected)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def matcher?(obj)
|
79
|
+
obj.respond_to(:matches?) && (obj.respond_to?(:failure_message) ||
|
80
|
+
obj.respond_to?(:failure_message_for_should))
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module RSpec::ResemblesJsonMatchers
|
4
|
+
module Helpers
|
5
|
+
# Returns string composed of the specified clauses with proper
|
6
|
+
# spacing between them. Empty and nil clauses are ignored.
|
7
|
+
def sentencize(*clauses)
|
8
|
+
clauses
|
9
|
+
.flatten
|
10
|
+
.compact
|
11
|
+
.reject(&:empty?)
|
12
|
+
.map(&:strip)
|
13
|
+
.join(" ")
|
14
|
+
end
|
15
|
+
|
16
|
+
def matcherize(expected)
|
17
|
+
if is_matcher? expected
|
18
|
+
expected
|
19
|
+
else
|
20
|
+
RSpec::ResemblesJsonMatchers.resembles_matcher_for(expected).new(expected)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def is_matcher?(obj)
|
25
|
+
obj.respond_to?(:matches?) && obj.respond_to?(:description)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require "active_support/core_ext/string/indent" # indent
|
2
|
+
require "active_support/core_ext/hash/keys" # stringify_keys
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module RSpec::ResemblesJsonMatchers
|
7
|
+
class JsonMatcher
|
8
|
+
include RSpec::Matchers::Composable
|
9
|
+
include RSpec::ResemblesJsonMatchers::Helpers
|
10
|
+
|
11
|
+
def initialize(expected_json)
|
12
|
+
@expected_json = expected_json.stringify_keys
|
13
|
+
@failed_matchers = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?(actual_json)
|
17
|
+
@actual_json = actual_json
|
18
|
+
expected_matchers.each do |expected_key, value_matcher|
|
19
|
+
attr_matcher = AttributeMatcher.new(expected_key, value_matcher)
|
20
|
+
match = attr_matcher.matches?(@actual_json)
|
21
|
+
@failed_matchers[expected_key] = attr_matcher unless match
|
22
|
+
end
|
23
|
+
@failed_matchers.size == 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def description
|
27
|
+
# TODO Figure out how to discover the right indent level
|
28
|
+
"have json that looks like\n#{expected_formatted.indent(6)}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def failure_message
|
32
|
+
msgs = [ "Expected:",
|
33
|
+
pretty_actual.indent(2),
|
34
|
+
"To match:",
|
35
|
+
expected_formatted.indent(2),
|
36
|
+
"Failures:",
|
37
|
+
pretty_errors.indent(2) ]
|
38
|
+
msgs.join("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
def pretty_json(obj)
|
42
|
+
JSON.pretty_generate(obj)
|
43
|
+
end
|
44
|
+
|
45
|
+
def expected_formatted
|
46
|
+
pretty_json(@expected_json)
|
47
|
+
end
|
48
|
+
|
49
|
+
def pretty_actual
|
50
|
+
pretty_json(@actual_json)
|
51
|
+
end
|
52
|
+
|
53
|
+
def expected_matchers
|
54
|
+
@expected_matchers ||= {}.tap do |hsh|
|
55
|
+
@expected_json.each do |k,v|
|
56
|
+
hsh[k] = v.respond_to?(:description) ? v : RSpec::Matchers::BuiltIn::Eq.new(v)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def expected_formatted
|
62
|
+
out = "{\n"
|
63
|
+
out << expected_matchers.map do |k,v|
|
64
|
+
case v
|
65
|
+
when RSpec::Matchers::BuiltIn::Eq
|
66
|
+
%{"#{k}": #{v.expected_formatted}}.indent(2)
|
67
|
+
else
|
68
|
+
%{"#{k}": #{v.description}}.indent(2)
|
69
|
+
end
|
70
|
+
end.join(",\n")
|
71
|
+
out << "\n}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def pretty_errors
|
75
|
+
out = "{\n"
|
76
|
+
out << @failed_matchers.map do |k,v|
|
77
|
+
%{"#{k}": #{v.failure_message}}.indent(2)
|
78
|
+
end.join(",\n")
|
79
|
+
out << "\n}"
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module RSpec::ResemblesJsonMatchers
|
2
|
+
|
3
|
+
module Matcherizer
|
4
|
+
def matcherize(expected)
|
5
|
+
if matcher? expected
|
6
|
+
expected
|
7
|
+
|
8
|
+
elsif expected.is_a?(Hash)
|
9
|
+
RSpec::ResemblesJsonMatchers::JsonMatcher.new(expected)
|
10
|
+
|
11
|
+
elsif expected.respond_to? :===
|
12
|
+
RSpec::Matchers::BuiltIn::Match.new(expected)
|
13
|
+
|
14
|
+
else
|
15
|
+
RSpec::Matchers::BuiltIn::Eq.new(expected)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def matcher?(obj)
|
20
|
+
obj.respond_to?(:matches?) && obj.respond_to?(:description)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "active_support/core_ext/array/wrap"
|
2
|
+
|
3
|
+
module RSpec::ResemblesJsonMatchers
|
4
|
+
class ResemblesAnyOfMatcher
|
5
|
+
include Helpers
|
6
|
+
|
7
|
+
def self.can_match?(array)
|
8
|
+
array.is_a? Array
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(expected)
|
12
|
+
@expected = expected
|
13
|
+
end
|
14
|
+
|
15
|
+
def matches?(actual)
|
16
|
+
Array.wrap(actual).flatten.all? do |a|
|
17
|
+
expected_matchers.any? { |m| m.matches? a }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def description
|
22
|
+
if @expected.size == 1
|
23
|
+
"have every item #{expected_matchers.first.description}"
|
24
|
+
else
|
25
|
+
"have every item match one of:\n#{expected_formatted}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def failure_message
|
30
|
+
sentencize ["Expected every item to match one of:\n",
|
31
|
+
expected_formatted,
|
32
|
+
"The item at",
|
33
|
+
# failed_item_indexes,
|
34
|
+
"did not because:\n",
|
35
|
+
failure_messages]
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def expected_matchers
|
40
|
+
@expected.map { |e| matcherize(e) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def expected_formatted
|
44
|
+
"".tap do |out|
|
45
|
+
out << expected_matchers.map do |v|
|
46
|
+
case v
|
47
|
+
when RSpec::Matchers::BuiltIn::Eq
|
48
|
+
"should #{v.description}".indent(2)
|
49
|
+
else
|
50
|
+
"should #{v.description}".indent(2)
|
51
|
+
end
|
52
|
+
end.join("\n")
|
53
|
+
end << "\n"
|
54
|
+
end
|
55
|
+
|
56
|
+
def failure_messages
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
module RSpec::ResemblesJsonMatchers
|
3
|
+
class ResemblesArrayMatcher
|
4
|
+
include Helpers
|
5
|
+
|
6
|
+
def self.can_match?(array)
|
7
|
+
array.is_a? Array
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(expected)
|
11
|
+
@expected = expected
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"resemble #{@expected.inspect}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(actual)
|
19
|
+
actual.is_a?(Array) && actual.all? do |a|
|
20
|
+
expected_matchers.any? do |e|
|
21
|
+
e.matches? a
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def expected_matchers
|
27
|
+
@expected.map { |e| matcherize(e) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def failure_message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
module RSpec::ResemblesJsonMatchers
|
3
|
+
class ResemblesClassMatcher
|
4
|
+
def self.can_match?(klass)
|
5
|
+
klass.is_a? Class
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(expected)
|
9
|
+
@expected = expected
|
10
|
+
end
|
11
|
+
|
12
|
+
def description
|
13
|
+
"resemble #{@expected}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?(actual)
|
17
|
+
@actual = actual
|
18
|
+
actual.kind_of? @expected
|
19
|
+
end
|
20
|
+
|
21
|
+
def expected_formatted
|
22
|
+
@expected
|
23
|
+
end
|
24
|
+
|
25
|
+
def failure_message
|
26
|
+
"#{@actual.inspect} does not resemble a #{@expected}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "time"
|
2
|
+
|
3
|
+
module RSpec::ResemblesJsonMatchers
|
4
|
+
class ResemblesDateMatcher
|
5
|
+
DATE_REGEX = /\A\d{4}-\d{2}-\d{2}\Z/.freeze
|
6
|
+
ISO8601_REGEX = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z/.freeze
|
7
|
+
def self.can_match?(date_or_str)
|
8
|
+
case date_or_str
|
9
|
+
when Date, Time, DateTime
|
10
|
+
true
|
11
|
+
when DATE_REGEX, ISO8601_REGEX
|
12
|
+
true
|
13
|
+
when String
|
14
|
+
begin
|
15
|
+
Time.iso8601(date_or_str)
|
16
|
+
true
|
17
|
+
rescue ArgumentError
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(expected)
|
24
|
+
@expected = expected
|
25
|
+
end
|
26
|
+
|
27
|
+
def description
|
28
|
+
"resemble date #{@expected.inspect}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def matches?(actual)
|
32
|
+
@actual = actual
|
33
|
+
self.class.can_match?(actual)
|
34
|
+
end
|
35
|
+
|
36
|
+
def expected_formatted
|
37
|
+
@expected.inspect.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def failure_message
|
41
|
+
"#{@actual.inspect} does not resemble a Date or Timestamp"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|