rspec-resembles_json_matchers 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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