rspec-resembles_json_matchers 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|