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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +16 -0
  5. data/Appraisals +14 -0
  6. data/Gemfile +4 -0
  7. data/Guardfile +47 -0
  8. data/README.md +225 -0
  9. data/Rakefile +6 -0
  10. data/examples/example_spec.rb +123 -0
  11. data/gemfiles/rails_42.gemfile +8 -0
  12. data/gemfiles/rails_42.gemfile.lock +173 -0
  13. data/gemfiles/rails_42_rspec_33.gemfile +8 -0
  14. data/gemfiles/rails_42_rspec_33.gemfile.lock +173 -0
  15. data/gemfiles/rails_5.gemfile +8 -0
  16. data/gemfiles/rails_5.gemfile.lock +184 -0
  17. data/lib/rspec/resembles_json_matchers.rb +57 -0
  18. data/lib/rspec/resembles_json_matchers/attribute_matcher.rb +85 -0
  19. data/lib/rspec/resembles_json_matchers/helpers.rb +29 -0
  20. data/lib/rspec/resembles_json_matchers/json_matcher.rb +85 -0
  21. data/lib/rspec/resembles_json_matchers/matcherizer.rb +25 -0
  22. data/lib/rspec/resembles_json_matchers/resembles_any_of_matcher.rb +61 -0
  23. data/lib/rspec/resembles_json_matchers/resembles_array_matcher.rb +35 -0
  24. data/lib/rspec/resembles_json_matchers/resembles_class_matcher.rb +29 -0
  25. data/lib/rspec/resembles_json_matchers/resembles_date_matcher.rb +44 -0
  26. data/lib/rspec/resembles_json_matchers/resembles_hash_matcher.rb +110 -0
  27. data/lib/rspec/resembles_json_matchers/resembles_matcher.rb +59 -0
  28. data/lib/rspec/resembles_json_matchers/resembles_numeric_matcher.rb +32 -0
  29. data/lib/rspec/resembles_json_matchers/resembles_route_matcher.rb +25 -0
  30. data/lib/rspec/resembles_json_matchers/resembles_string_matcher.rb +25 -0
  31. data/lib/rspec/resembles_json_matchers/version.rb +5 -0
  32. data/rspec-resembles_json_matchers.gemspec +39 -0
  33. 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