rspec-resembles_json_matchers 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 67063b7a2b5685ac47e58408e3eb9ab2556ebb9c
4
- data.tar.gz: 2bda36618f32e3b751c36274eb8f69cb790d4e2a
2
+ SHA256:
3
+ metadata.gz: 41ce9ddec87f87a2b3bbdca185b9987b6b61c89813c1e1a4b5a23f5af677d636
4
+ data.tar.gz: 1f04f3a51ffd0c508a551661946466f1910eb22e427205b49fb87e689e7d24a9
5
5
  SHA512:
6
- metadata.gz: d83fb0f51620c2066dd33e9783e42e8730a69648c738099dce74d037ce3208a4b9a9d90de984f9ca3621a8dcd1b79d1cafc5c8acfb9ddf58c1b5ba7a67cae7ef
7
- data.tar.gz: 88245931405c031a0c71a54bfa5a12d07930afbc2651ed1020033b8009d1ebf8d80f74b118f1d0548d3041a3b36890362e1021f2a25eb7e46fb9a3b10416343d
6
+ metadata.gz: 4bff40b1f820583202eecfc003676f46e63533ab9c8dc635a87616b95ce5169a478ed0cad7c2aa692717859320ad0d66a1d8cb898eec091aa955537155091d44
7
+ data.tar.gz: 725fc52f95deab1a7fb909191b9b1ca14c5f19f1f45316bcd59b5aa014e419cfaee0c7cb865620a79faea9e28bd9b48910b7d4c1aef435fb1806dc689bbf7ba8
data/Changelog.md CHANGED
@@ -1,3 +1,6 @@
1
+ # 0.7.0
2
+
3
+ - Change the output to be a diff-like view rather than verbose prose
1
4
 
2
5
  # 0.5.2
3
6
 
data/Guardfile CHANGED
@@ -24,7 +24,14 @@
24
24
  # * zeus: 'zeus rspec' (requires the server to be started separately)
25
25
  # * 'just' rspec: 'rspec'
26
26
 
27
- guard :rspec, cmd: "bundle exec rspec" do
27
+ guard :rspec,
28
+ # cmd: "bin/rspec --no-profile --fail-fast",
29
+ cmd: "bin/rspec --no-profile",
30
+ failed_mode: :keep,
31
+ run_all: { cmd: "bin/rspec --no-profile --tag ~type:feature" }, # skip feature specs on "all"
32
+ notification: true,
33
+ all_on_start: false,
34
+ all_on_pass: false do
28
35
  require "guard/rspec/dsl"
29
36
  dsl = Guard::RSpec::Dsl.new(self)
30
37
 
data/README.md CHANGED
@@ -2,79 +2,97 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rspec-json_api_matchers.svg)](https://badge.fury.io/rb/rspec-json_api_matchers)[![Build Status](https://travis-ci.org/paul/rspec-json_api_matchers.svg?branch=master)](https://travis-ci.org/paul/rspec-json_api_matchers)
4
4
 
5
- This gem provides a set of matchers that make testing JSON documents (actually
6
- the hashes parsed from them) simpler and more elegant.
5
+ This gem provides a set of matchers that make testing JSON documents (actually the hashes parsed from them) simpler and more elegant.
7
6
 
8
- It provides two primary matchers, `have_attribute` and `resembles`/`resembles_json`.
7
+ ## `resemble` Matcher
9
8
 
10
- ## `#have_attribute`
9
+ Oftentimes when testing your JSON API responses, you don't care about the actual values matching exactly, just that they "resemble" your expected values. This gem provides a variety of matchers to just get close:
11
10
 
12
- Use this matcher when you want to examine a single attribute, and optionally
13
- match against its value.
11
+ ### Numbers
14
12
 
15
- ### Example Usage
13
+ Anything that's a Ruby `Numeric` will match:
16
14
 
17
15
  ```ruby
18
- RSpec.describe "my hash" do
19
- include RSpec::ResemblesJsonMatchers
16
+ # These pass
17
+ expect(10).to resemble 42
18
+ expect(3.14).to resemble 42
19
+ expect(10).to resemble 42.4
20
+
21
+ # These fail
22
+ expect("string").to resemble 42
23
+ expect(Time.now).to resemble 42
24
+ ```
20
25
 
21
- subject(:response_document) do
22
- {
23
- author: "Paul",
24
- gems_published: 42,
25
- created_at: "2016-01-01T00:00:00Z"
26
- }
27
- end
26
+ I haven't needed it yet, but I'm open to discussing if more accurate matches would be needed. For example:
28
27
 
29
- # Test that the key is present, regardless of value (even nil)
30
- it { should have_attribute :author }
28
+ * Does 1_000_000_000 "resemble" 1?
29
+ * Does a float "resemble" an integer?
31
30
 
32
- # Test the value by using another matcher
33
- it { should have_attribute :author, eq("Paul") }
34
- it { should have_attribute :author, match(/Paul/) }
35
- it { should have_attribute :gems_published, be > 40 }
36
- it { should have_attribute :created_at, iso8601_timestamp }
37
- end
38
- ```
39
31
 
40
- It will also provide nice descriptions in the rspec doc format, and useful
41
- failure messages:
32
+ ### Dates/Times
42
33
 
34
+ Anything that is a Ruby `Date`/`Time`/`DateTime`, or a string that can be parsed by `Time.iso8601` will match:
35
+
36
+ ```ruby
37
+ # These pass
38
+ expect(Time.now).to resemble "2018-01-01T00:00:00Z"
39
+ expect(Time.now.iso8601).to resemble "2018-01-01T00:00:00Z"
40
+
41
+ # These fail
42
+ expect("Some string").to resemble "2018-01-01T00:00:00Z"
43
43
  ```
44
- my hash
45
- should have attribute :author be present
46
- should have attribute :author eq "Paul"
47
- should have attribute :author match /Paul/
48
- should have attribute :gems_published be > 40
49
- should have attribute :created_at match /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
50
- ```
51
44
 
45
+ Open questions:
46
+
47
+ * Does `"2018-01-01T00:00:00-0700"` "resemble" `"2018-01-01T00:00:00Z"`? That is, should it ensure the timezone matches?
48
+ * Do non-ISO8601 datetimes "resemble" ISO8601 ones?
49
+ * Is there a permissible time range? Does the year 1600 "resemble" 2017? Does `"0000-00-00T00:00:00Z"`? Does `Time.at(0)`?
50
+
51
+ ### Rails routes
52
+
53
+ If you're using Rails (specifically `ActionDispatch`), we can check that routes resemble each other:
54
+
55
+ ```ruby
56
+ # These pass
57
+ expect("/posts/1").to resemble posts_path(post)
58
+ expect("/posts/1000000").to resemble posts_path(post)
59
+ expect("https://example.com/posts/1").to resemble posts_path(post)
60
+
61
+ # These fail
62
+ expect("/users/1").to resemble posts_path(post)
63
+ expect("Some string").to resemble posts_path(post)
52
64
  ```
53
- Failures:
54
65
 
55
- 1) my hash should have attribute :full_name be present
56
- Failure/Error: it { should have_attribute :full_name }
57
- Expected attribute :full_name to be present
58
- # ./spec/examples_spec.rb:24:in `block (2 levels) in <top (required)>'
66
+ ### Strings
67
+
68
+ Any string that didn't match one of the other resembles matchers will match:
69
+
70
+ ```ruby
71
+ # These pass
72
+ expect("Some string").to resemble "some other string"
73
+ expect("").to resemble "some other string"
74
+ expect("a" * 100_000).to resemble "some other string"
75
+
76
+ # These fail
77
+ expect(42).to resemble "42"
78
+ expect(Time.now).to resemble "Time.now
59
79
 
60
- 2) my hash should have attribute :author match /paul/
61
- Failure/Error: it { should have_attribute :author, match(/paul/) }
62
- Expected value of attribute :author to match /paul/ but it was "Paul"
63
- # ./spec/examples_spec.rb:25:in `block (2 levels) in <top (required)>'
64
80
  ```
65
81
 
66
- ## `#resembles_json`
82
+ Open questions:
83
+
84
+ * Should there be some heuristic to decide if a string resembles another? Does length matter?
67
85
 
68
- This matcher builds upon `#have_attribute` to let you test an entire JSON document in a single example, but still provide detailed errors about each attribute.
86
+ ## `#resemble_json/match_json`
69
87
 
70
- Additionally, it does "fuzzy" matching on the fields (unless a matcher is explicitly given), because its primary purpose it do have a clear and concise example of the API output for documentation.
88
+ The resembles matchers are nice on their own, but their power shines when used with the `resembles_json` matcher. This allows you to write an example JSON document in your spec, and match it against the output from a request. Any values in the "expected" document that aren't already matchers will be converted to the best `resembles` matcher. You can write plain json documents as the expected, or be explicit by specifying matchers.
71
89
 
72
- See the examples folder for more.
73
90
 
74
91
  ### Example Usage
75
92
 
76
93
  ```ruby
77
94
  RSpec.describe "a basic json document" do
95
+ # This would probably actually come from something like `JSON.parse(response.body)`
78
96
  let(:document) do
79
97
  {
80
98
  "@id": "/posts/2016/test1",
@@ -89,19 +107,19 @@ RSpec.describe "a basic json document" do
89
107
  specify do
90
108
  expect(document).to resemble_json(
91
109
  {
92
- "@id": "/posts/:year/:title",
93
- "@type": eq("Post"),
94
- "title": "Hello World!",
95
- "body": "lorem ipsum",
96
- "created_at": "2016-03-01T00:03:42",
97
- "published_at": "2016-03-10T15:35:00"
110
+ "@id": post_path(post), # resembles route
111
+ "@type": eq("Post"), # using an explicit matcher to match exactly
112
+ "title": match(/^Hello/), # another explicit matcher
113
+ "body": "lorem ipsum", # resembles string
114
+ "created_at": "2016-03-01T00:03:42", # resembles time
115
+ "published_at": post.published_at # Also resembles time
98
116
  }
99
117
  )
100
118
  end
101
119
  end
102
120
  ```
103
121
 
104
- Again, it provides good descriptions:
122
+ It provides good descriptions if you run `rspec` with `--format=documentation`:
105
123
 
106
124
  ```
107
125
  a basic json document
@@ -116,21 +134,18 @@ Again, it provides good descriptions:
116
134
  }
117
135
  ```
118
136
 
119
- And useful failure messages:
137
+ It also provides a failure message as a diff of the JSON object:
120
138
 
121
139
  ```
122
- Failures:
123
-
124
- 1) The resembles json matcher a basic json document with several attributes that failed to match should resemble json
140
+ 1) The resembles json matcher a basic json document with several attributes that failed to match should have json that looks like
125
141
  {
126
- "@id": /posts/:year/:title,
142
+ "@id": "/posts/:year/:title",
127
143
  "@type": "PostCollection",
128
144
  "title": 42.0,
129
- "body": lorem ipsum,
145
+ "body": "lorem ipsum",
130
146
  "created_at": "2016-03-01T00:03:42",
131
147
  "published_at": "2016-03-10T15:35:00"
132
148
  }
133
-
134
149
  Failure/Error:
135
150
  expect(document).to resemble_json(
136
151
  {
@@ -143,65 +158,20 @@ Failures:
143
158
  }
144
159
  )
145
160
 
146
- failed because
147
- attribute "@type":
148
- expected: "PostCollection"
149
- got: "Post"
150
- attribute "title":
151
- "Hello World!" does not resemble a number
161
+ {
162
+ "@id": "/posts/:year/:title",
163
+ - "@type": eq "PostCollection",
164
+ + "@type": "Post",
165
+ - "title": 42.0,
166
+ + "title": "Hello World!",
167
+ "body": "lorem ipsum",
168
+ "created_at": "2016-03-01T00:03:42",
169
+ "published_at": "2016-03-10T15:35:00"
170
+ }
152
171
  # ./examples/example_spec.rb:40:in `block (4 levels) in <top (required)>'
153
-
154
- 2) The resembles json matcher a basic json document when the matcher is missing a field that is present in the document should resemble json
155
- {
156
- "@id": /posts/:year/:title,
157
- "@type": "Post",
158
- "body": lorem ipsum,
159
- "created_at": "2016-03-01T00:03:42",
160
- "published_at": "2016-03-10T15:35:00"
161
- }
162
-
163
- Failure/Error:
164
- expect(document).to resemble_json(
165
- {
166
- "@id": "/posts/:year/:title",
167
- "@type": eq("Post"),
168
- "body": "lorem ipsum",
169
- "created_at": "2016-03-01T00:03:42",
170
- "published_at": "2016-03-10T15:35:00"
171
- }
172
- )
173
-
174
- failed because
175
- attribute "title":
176
- is present, but no matcher provided to match it
177
- # ./examples/example_spec.rb:55:in `block (4 levels) in <top (required)>'
178
-
179
- 3) The resembles json matcher a basic json document when the document is missing a field that is present in the matcher should resemble json
180
- {
181
- "@id": /posts/:year/:title,
182
- "@type": "Post",
183
- "body": lorem ipsum,
184
- "created_at": "2016-03-01T00:03:42",
185
- "published_at": "2016-03-10T15:35:00"
186
- }
187
-
188
- Failure/Error:
189
- expect(document).to resemble_json(
190
- {
191
- "@id": "/posts/:year/:title",
192
- "@type": eq("Post"),
193
- "body": "lorem ipsum",
194
- "created_at": "2016-03-01T00:03:42",
195
- "published_at": "2016-03-10T15:35:00"
196
- }
197
- )
198
-
199
- failed because
200
- attribute "title":
201
- is present, but no matcher provided to match it
202
- # ./examples/example_spec.rb:69:in `block (4 levels) in <top (required)>'
203
172
  ```
204
173
 
174
+ It can also handle nested JSON documents and Arrays, showing the proper diffs. See the `./examples` directory, but it works pretty much how you'd expect.
205
175
 
206
176
  # Installation
207
177
 
data/bin/rspec ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
12
+ load(bundle_binstub) if File.file?(bundle_binstub)
13
+
14
+ require "pathname"
15
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
16
+ Pathname.new(__FILE__).realpath)
17
+
18
+ require "rubygems"
19
+ require "bundler/setup"
20
+
21
+ load Gem.bin_path("rspec-core", "rspec")
@@ -4,7 +4,7 @@ RSpec.describe "The resembles json matcher" do
4
4
  include RSpec::ResemblesJsonMatchers
5
5
 
6
6
  before :all do
7
- puts <<-WARNING.strip_heredoc
7
+ puts <<~WARNING
8
8
  NOTE: some of these are expected to fail, they are meant to demonstrate
9
9
  the output generated by failing specs
10
10
  WARNING
@@ -117,7 +117,65 @@ RSpec.describe "The resembles json matcher" do
117
117
  }
118
118
  )
119
119
  end
120
+ end
120
121
 
122
+ describe "empty nested documents" do
123
+ let(:document) do
124
+ {
125
+ "@id": "/posts/2016/test1",
126
+ "@type": "Post",
127
+ "title": "Hello World!",
128
+ "body": "lorem ipsum",
129
+ "created_at": "2016-03-01T00:03:42",
130
+ "published_at": "2016-03-10T15:35:00"
131
+ }
132
+ end
133
+
134
+ specify do
135
+ expect(document).to resemble_json(
136
+ {
137
+ "@id": "/posts/:year/:title",
138
+ "@type": eq("Post"),
139
+ "title": "Hello World!",
140
+ "body": "lorem ipsum",
141
+ "author": {
142
+ "@id": "/authors/:id",
143
+ "name": eq("Paul")
144
+ },
145
+ "created_at": "2016-03-01T00:03:42",
146
+ "published_at": "2016-03-10T15:35:00"
147
+ }
148
+ )
149
+ end
150
+ end
121
151
 
152
+ describe "empty nested array documents" do
153
+ let(:document) do
154
+ {
155
+ "@id": "/posts",
156
+ "@type": "PostCollection",
157
+ "nextPage": "/posts?page=2"
158
+ }
159
+ end
160
+
161
+ specify do
162
+ expect(document).to resemble_json(
163
+ {
164
+ "@id": "/posts",
165
+ "@type": eq("PostCollection"),
166
+ "nextPage": "/posts?page=2",
167
+ "members": [
168
+ {
169
+ "@id": "/posts/:year/:title",
170
+ "@type": eq("Post"),
171
+ "title": "Hello World!",
172
+ "body": "lorem ipsum",
173
+ "created_at": "2016-03-01T00:03:42",
174
+ "published_at": "2016-03-10T15:35:00"
175
+ }
176
+ ]
177
+ }
178
+ )
179
+ end
122
180
  end
123
181
  end
@@ -0,0 +1,150 @@
1
+ require "active_support/inflector"
2
+
3
+ module RSpec::ResemblesJsonMatchers
4
+ class AttributeDiffer
5
+ def initialize(matcher)
6
+ @matcher = matcher
7
+ end
8
+
9
+ def to_s
10
+ @buffer = StringIO.new
11
+ @buffer.puts NEUTRAL_COLOR + "Diff:"
12
+ render(@matcher)
13
+ @buffer.string
14
+ end
15
+
16
+ private
17
+
18
+ def render(matcher, **opts)
19
+ class_name = ActiveSupport::Inflector.demodulize(matcher.class.to_s)
20
+ method_name = :"render_#{class_name}"
21
+ send method_name, matcher, **opts
22
+ end
23
+
24
+ def render_JsonMatcher(matcher, prefix: "", starts_on_newline: false, **opts)
25
+ @buffer.print prefix if starts_on_newline
26
+ @buffer.print NORMAL_COLOR unless prefix.include?("-")
27
+ @buffer.puts "{"
28
+ matcher.expected_matchers.each do |key, attr_matcher|
29
+ last = (matcher.expected_matchers.keys.last == key)
30
+ render(attr_matcher, prefix: prefix, last: last, **opts)
31
+ end
32
+ if matcher.actual.nil?
33
+ @buffer.print REMOVED_COLOR
34
+ if prefix.include? "-"
35
+ @buffer.print prefix + "}"
36
+ else
37
+ @buffer.print prefix + "- }"
38
+ end
39
+ else
40
+ @buffer.print NORMAL_COLOR unless prefix.include?("-")
41
+ @buffer.print prefix + "}"
42
+ end
43
+ end
44
+
45
+ def render_AttributeMatcher(matcher, prefix: "", last: false)
46
+ if matcher.matched?
47
+ @buffer.print NORMAL_COLOR
48
+ @buffer.print prefix + " " + "#{matcher.attribute_name.to_json}: "
49
+ render(matcher.value_matcher, prefix: prefix + " ")
50
+ @buffer.print(",") unless last
51
+ @buffer.puts
52
+ else
53
+ if nested_matcher?(matcher.value_matcher)
54
+ if matcher.missing_attribute?
55
+ prefix = prefix + "- "
56
+ @buffer.print REMOVED_COLOR
57
+ @buffer.print prefix + "#{matcher.attribute_name.to_json}: "
58
+ render(matcher.value_matcher, prefix: prefix)
59
+ @buffer.print(",") unless last
60
+ @buffer.puts
61
+ else
62
+ @buffer.print NORMAL_COLOR
63
+ @buffer.print prefix + " " + "#{matcher.attribute_name.to_json}: "
64
+ render(matcher.value_matcher, prefix: prefix + " ")
65
+ @buffer.print(",") unless last
66
+ @buffer.puts
67
+ end
68
+ else
69
+ @buffer.print REMOVED_COLOR
70
+ @buffer.print prefix
71
+ if prefix.include? "-"
72
+ @buffer.print " "
73
+ else
74
+ @buffer.print "- "
75
+ end
76
+ @buffer.print "#{matcher.attribute_name.to_json}: "
77
+ render(matcher.value_matcher, prefix: prefix + " ")
78
+ @buffer.print NORMAL_COLOR
79
+ @buffer.print(",") unless last
80
+ @buffer.puts
81
+ unless matcher.missing_attribute?
82
+ @buffer.print ADDED_COLOR
83
+ @buffer.print prefix + "+ #{matcher.attribute_name.to_json}: "
84
+ render(matcher.actual_value, prefix: prefix + " ")
85
+ @buffer.print NORMAL_COLOR
86
+ @buffer.print(",") unless last
87
+ @buffer.puts
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def render_ResemblesAnyOfMatcher(matcher, prefix: "", **opts)
94
+ @buffer.puts "["
95
+ if matcher.actual.nil? || matcher.actual.empty?
96
+ example_matcher = matcher.expected.first
97
+ render example_matcher, prefix: prefix + " ", starts_on_newline: true
98
+ @buffer.puts
99
+ else
100
+ matcher.attempted_matchers.each do |attempted_matcher|
101
+ last = (matcher.attempted_matchers.last == attempted_matcher)
102
+ render attempted_matcher, prefix: prefix + " ", starts_on_newline: true
103
+ @buffer.print(",") unless last
104
+ @buffer.puts
105
+ end
106
+ end
107
+ @buffer.print prefix + "]"
108
+ end
109
+
110
+ def render_ResemblesStringMatcher(matcher, **opts)
111
+ @buffer.print matcher.expected.to_json
112
+ end
113
+
114
+ def render_ResemblesDateMatcher(matcher, **opts)
115
+ @buffer.print matcher.expected.to_json
116
+ end
117
+
118
+ def render_ResemblesNumericMatcher(matcher, **opts)
119
+ @buffer.print matcher.expected.to_json
120
+ end
121
+
122
+ def render_ResemblesClassMatcher(matcher, **opts)
123
+ @buffer.print matcher.expected.inspect
124
+ end
125
+
126
+ def method_missing(method_name, *args, &block)
127
+ if method_name.to_s.start_with?("render_")
128
+ raise NoMethodError, method_name if method_name.to_s.end_with?("Matcher")
129
+ @buffer.print RSpec::Support::ObjectFormatter.format(args.first)
130
+ else
131
+ super
132
+ end
133
+ end
134
+
135
+ def respond_to_missing?(method_name, include_private = false)
136
+ method_name.to_s.start_with?("render_")
137
+ end
138
+
139
+ def nested_matcher?(matcher)
140
+ matcher.is_a?(JsonMatcher) || matcher.is_a?(ResemblesAnyOfMatcher)
141
+ end
142
+
143
+ NORMAL_COLOR = "\e[0m".freeze
144
+ REMOVED_COLOR = "\e[31m".freeze # Red
145
+ ADDED_COLOR = "\e[32m".freeze # Green
146
+ NEUTRAL_COLOR = "\e[34m".freeze # Blue
147
+
148
+ end
149
+ end
150
+
@@ -18,30 +18,39 @@ module RSpec::ResemblesJsonMatchers
18
18
  def matches?(document)
19
19
  @document = document.with_indifferent_access
20
20
 
21
- @document.key?(attribute_name) &&
22
- expected.matches?(@document.fetch(attribute_name, nil))
21
+ @matched = !missing_attribute? && expected.matches?(actual_value)
23
22
  end
24
23
 
25
24
  def failure_message
26
- if expected === NullMatcher
27
- msgs = ["Expected attribute",
28
- attribute_name.inspect,
29
- "to be present"]
25
+ if missing_attribute?
26
+ %{Expected document to have attribute #{attribute_name.inspect}}
30
27
  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]
28
+ %{Expected attribute #{attribute_name.inspect} to #{value_matcher.description}, but it was #{actual_value.inspect}}
37
29
  end
38
- sentencize(*msgs)
39
30
  end
40
31
 
41
- def failure_message_when_negated
42
- sentencize "Expected attribute #{attribute_name.inspect}",
43
- expected.description,
44
- "to be absent"
32
+ def matched?
33
+ @matched
34
+ end
35
+
36
+ def value_matcher
37
+ @expected
38
+ end
39
+
40
+ def expected_value
41
+ value_matcher.expected
42
+ end
43
+
44
+ def actual_value
45
+ document.fetch(attribute_name, nil)
46
+ end
47
+
48
+ def missing_attribute?
49
+ !has_attribute?
50
+ end
51
+
52
+ def has_attribute?
53
+ document && document.key?(attribute_name)
45
54
  end
46
55
 
47
56
  NullMatcher = Class.new do
@@ -63,23 +72,5 @@ module RSpec::ResemblesJsonMatchers
63
72
  end
64
73
  end.new
65
74
 
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
75
  end
84
-
85
76
  end
@@ -1,58 +1,50 @@
1
- require "active_support/core_ext/hash/keys" # stringify_keys
2
-
1
+ require "active_support/core_ext/hash/keys" # stringify_keys
3
2
  require "json"
4
3
 
4
+ require "rspec/resembles_json_matchers/attribute_differ"
5
+
5
6
  module RSpec::ResemblesJsonMatchers
6
7
  class JsonMatcher
7
8
  include RSpec::Matchers::Composable
8
- include RSpec::ResemblesJsonMatchers::Helpers
9
+ include Helpers
10
+
11
+ def self.can_match?(hash)
12
+ hash.is_a? Hash
13
+ end
14
+
15
+ attr_reader :expected, :actual
9
16
 
10
17
  def initialize(expected_json)
11
- @expected_json = expected_json.stringify_keys
12
- @failed_matchers = {}
18
+ @expected = expected_json.deep_stringify_keys
13
19
  end
14
20
 
15
21
  def matches?(actual_json)
16
- @actual_json = actual_json
17
- expected_matchers.each do |expected_key, value_matcher|
18
- attr_matcher = AttributeMatcher.new(expected_key, value_matcher)
19
- match = attr_matcher.matches?(@actual_json)
20
- @failed_matchers[expected_key] = attr_matcher unless match
22
+ @actual = actual_json.deep_stringify_keys
23
+ all_passed = true
24
+ expected_matchers.each do |key, attr_matcher|
25
+ result = attr_matcher.matches?(actual)
26
+ all_passed &&= result
21
27
  end
22
- @failed_matchers.size == 0
28
+ all_passed
23
29
  end
24
30
 
25
31
  def description
26
32
  # TODO Figure out how to discover the right indent level
27
- "have json that looks like\n#{expected_formatted.indent(6)}"
33
+ "have json that looks like\n#{expected_formatted.indent(2)}"
28
34
  end
29
35
 
30
36
  def failure_message
31
- msgs = [ "Expected:",
32
- pretty_actual.indent(2),
33
- "To match:",
34
- expected_formatted.indent(2),
35
- "Failures:",
36
- pretty_errors.indent(2) ]
37
- msgs.join("\n")
37
+ AttributeDiffer.new(self).to_s
38
38
  end
39
39
 
40
- def pretty_json(obj)
41
- JSON.pretty_generate(obj)
42
- end
43
-
44
- def expected_formatted
45
- pretty_json(@expected_json)
46
- end
47
-
48
- def pretty_actual
49
- pretty_json(@actual_json)
40
+ def to_json
41
+ failure_message
50
42
  end
51
43
 
52
44
  def expected_matchers
53
45
  @expected_matchers ||= {}.tap do |hsh|
54
- @expected_json.each do |k,v|
55
- hsh[k] = v.respond_to?(:description) ? v : RSpec::Matchers::BuiltIn::Eq.new(v)
46
+ expected.each do |k,v|
47
+ hsh[k.to_s] = AttributeMatcher.new(k, matcherize(v))
56
48
  end
57
49
  end
58
50
  end
@@ -60,23 +52,34 @@ module RSpec::ResemblesJsonMatchers
60
52
  def expected_formatted
61
53
  out = "{\n"
62
54
  out << expected_matchers.map do |k,v|
63
- case v
64
- when RSpec::Matchers::BuiltIn::Eq
65
- %{"#{k}": #{v.expected_formatted}}.indent(2)
66
- else
67
- %{"#{k}": #{v.description}}.indent(2)
68
- end
55
+ %{"#{k}": #{RSpec::Support::ObjectFormatter.format(v.expected_value)}}.indent(2)
69
56
  end.join(",\n")
70
57
  out << "\n}"
71
58
  end
72
59
 
73
- def pretty_errors
74
- out = "{\n"
75
- out << @failed_matchers.map do |k,v|
76
- %{"#{k}": #{v.failure_message}}.indent(2)
77
- end.join(",\n")
78
- out << "\n}"
60
+ def color_lines(text)
61
+ text.split("\n").map do |line|
62
+ case line.chr[0]
63
+ when "-" then red line
64
+ when "+" then green line
65
+ end
66
+ end.compact
67
+ end
68
+
69
+ def color(text, color_code)
70
+ "\e[#{color_code}m#{text}\e[0m"
79
71
  end
80
72
 
73
+ def red(text)
74
+ color(text, 31)
75
+ end
76
+
77
+ def green(text)
78
+ color(text, 32)
79
+ end
80
+
81
+ def normal(text)
82
+ color(text, 0)
83
+ end
81
84
  end
82
85
  end
@@ -8,13 +8,16 @@ module RSpec::ResemblesJsonMatchers
8
8
  array.is_a? Array
9
9
  end
10
10
 
11
+ attr_reader :expected, :actual
12
+
11
13
  def initialize(expected)
12
- @expected = expected
14
+ @expected = expected.map { |e| matcherize(e) }
13
15
  end
14
16
 
15
17
  def matches?(actual)
16
- Array.wrap(actual).flatten.all? do |a|
17
- expected_matchers.any? { |m| m.matches? a }
18
+ @actual = Array.wrap(actual)
19
+ @actual.all? do |a|
20
+ expected_matchers.any? { |m| attempted_matchers << m; m.matches? a }
18
21
  end
19
22
  end
20
23
 
@@ -27,17 +30,15 @@ module RSpec::ResemblesJsonMatchers
27
30
  end
28
31
 
29
32
  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
33
 
37
34
  end
38
35
 
39
36
  def expected_matchers
40
- @expected.map { |e| matcherize(e) }
37
+ @expected
38
+ end
39
+
40
+ def attempted_matchers
41
+ @attempted_matchers ||= []
41
42
  end
42
43
 
43
44
  def expected_formatted
@@ -52,10 +53,5 @@ module RSpec::ResemblesJsonMatchers
52
53
  end.join("\n")
53
54
  end << "\n"
54
55
  end
55
-
56
- def failure_messages
57
- end
58
-
59
56
  end
60
-
61
57
  end
@@ -5,6 +5,8 @@ module RSpec::ResemblesJsonMatchers
5
5
  klass.is_a? Class
6
6
  end
7
7
 
8
+ attr_reader :expected
9
+
8
10
  def initialize(expected)
9
11
  @expected = expected
10
12
  end
@@ -20,8 +20,11 @@ module RSpec::ResemblesJsonMatchers
20
20
  end
21
21
  end
22
22
 
23
+ attr_reader :expected
24
+
23
25
  def initialize(expected)
24
26
  @expected = expected
27
+ # @expected = expected.is_a?(String) ? Time.parse(expected) : expected
25
28
  end
26
29
 
27
30
  def description
@@ -33,10 +36,6 @@ module RSpec::ResemblesJsonMatchers
33
36
  self.class.can_match?(actual)
34
37
  end
35
38
 
36
- def expected_formatted
37
- @expected.inspect.to_s
38
- end
39
-
40
39
  def failure_message
41
40
  "#{@actual.inspect} does not resemble a Date or Timestamp"
42
41
  end
@@ -20,7 +20,7 @@ module RSpec::ResemblesJsonMatchers
20
20
  expected_matchers.each do |expected_key, value_matcher|
21
21
  @matched_keys << expected_key
22
22
 
23
- attr_matcher = RSpec::ResemblesJsonMatchers::AttributeMatcher.new(expected_key, value_matcher)
23
+ attr_matcher = AttributeMatcher.new(expected_key, value_matcher)
24
24
  match = attr_matcher.matches?(actual)
25
25
 
26
26
  if match
@@ -42,11 +42,11 @@ module RSpec::ResemblesJsonMatchers
42
42
  end
43
43
 
44
44
  def failure_message
45
- "failed because\n" +
46
- pretty_failed_matches.indent(2) +
47
- pretty_unmatched_keys.indent(2) +
48
- pretty_unmatched_matchers.indent(2) +
49
- "\n"
45
+ "failed because\n" + [
46
+ pretty_failed_matches.indent(2),
47
+ pretty_unmatched_keys.indent(2),
48
+ pretty_unmatched_matchers.indent(2)
49
+ ].join("\n") + "\n"
50
50
  end
51
51
 
52
52
  protected
@@ -1,12 +1,12 @@
1
1
 
2
2
  module RSpec::ResemblesJsonMatchers
3
3
  class ResemblesNumericMatcher
4
- NUMBER_REGEX = /\A\d+\Z/.freeze
5
4
  def self.can_match?(number)
6
- number.is_a?(Numeric) ||
7
- number =~ NUMBER_REGEX
5
+ number.is_a?(Numeric)
8
6
  end
9
7
 
8
+ attr_reader :expected
9
+
10
10
  def initialize(expected)
11
11
  @expected = expected
12
12
  end
@@ -25,6 +25,10 @@ module RSpec::ResemblesJsonMatchers
25
25
  @expected
26
26
  end
27
27
 
28
+ def to_json
29
+ @expected
30
+ end
31
+
28
32
  def failure_message
29
33
  "#{@actual.inspect} does not resemble a number"
30
34
  end
@@ -5,6 +5,8 @@ module RSpec::ResemblesJsonMatchers
5
5
  string.is_a? String
6
6
  end
7
7
 
8
+ attr_reader :expected
9
+
8
10
  def initialize(expected)
9
11
  @expected = expected
10
12
  end
@@ -17,9 +19,5 @@ module RSpec::ResemblesJsonMatchers
17
19
  def matches?(actual)
18
20
  self.class.can_match?(actual)
19
21
  end
20
-
21
- def expected_formatted
22
- @expected
23
- end
24
22
  end
25
23
  end
@@ -1,5 +1,5 @@
1
1
  module Rspec
2
2
  module ResemblesJsonMatchers
3
- VERSION = "0.6.0"
3
+ VERSION = "0.7.0"
4
4
  end
5
5
  end
@@ -4,13 +4,9 @@ require "rspec/resembles_json_matchers/string_indent"
4
4
  module RSpec
5
5
  module ResemblesJsonMatchers
6
6
  autoload :AttributeMatcher, "rspec/resembles_json_matchers/attribute_matcher"
7
- # autoload :ResemblesMatcher, "rspec/resembles_json_matchers/resembles_matcher"
8
7
  autoload :JsonMatcher, "rspec/resembles_json_matchers/json_matcher"
9
8
  autoload :Helpers, "rspec/resembles_json_matchers/helpers"
10
- # autoload :Matcherizer, "rspec/resembles_json_matchers/matcherizer"
11
9
 
12
- autoload :ResemblesHashMatcher, "rspec/resembles_json_matchers/resembles_hash_matcher"
13
- autoload :ResemblesArrayMatcher, "rspec/resembles_json_matchers/resembles_array_matcher"
14
10
  autoload :ResemblesAnyOfMatcher, "rspec/resembles_json_matchers/resembles_any_of_matcher"
15
11
  autoload :ResemblesRouteMatcher, "rspec/resembles_json_matchers/resembles_route_matcher"
16
12
  autoload :ResemblesDateMatcher, "rspec/resembles_json_matchers/resembles_date_matcher"
@@ -27,22 +23,17 @@ module RSpec
27
23
  def match_json(*a)
28
24
  JsonMatcher.new(*a)
29
25
  end
30
-
31
- def have_attribute(*a)
32
- AttributeMatcher.new(*a)
33
- end
26
+ alias resemble_json match_json
34
27
 
35
28
  def resembles(*a)
36
29
  RSpec::ResemblesJsonMatchers.resembles_matcher_for(*a).new(*a)
37
30
  end
38
31
  alias resemble resembles
39
- alias resemble_json resembles
40
32
 
41
33
  def self.resembles_matcher_candidates
42
34
  # Order matters
43
35
  @candidates ||= [
44
- ResemblesHashMatcher,
45
- #ResemblesArrayMatcher,
36
+ JsonMatcher,
46
37
  ResemblesAnyOfMatcher,
47
38
  ResemblesRouteMatcher,
48
39
  ResemblesDateMatcher,
@@ -55,7 +46,7 @@ module RSpec
55
46
  end
56
47
 
57
48
  def self.resembles_matcher_for(expected, **a)
58
- resembles_matcher_candidates.detect { |candidate| candidate.can_match?(expected) } || raise("No matcher available for #{expected.inspect}")
49
+ resembles_matcher_candidates.detect { |candidate| candidate.can_match?(expected) } || RSpec::Matchers::BuiltIn::Eq
59
50
  end
60
51
 
61
52
  end
@@ -31,9 +31,12 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "appraisal"
32
32
  spec.add_development_dependency "guard"
33
33
  spec.add_development_dependency "guard-rspec"
34
+ spec.add_development_dependency "pry-byebug"
35
+ spec.add_development_dependency "pry-state"
34
36
  spec.add_development_dependency "awesome_print"
35
37
 
36
38
  spec.add_runtime_dependency "rspec", ">= 2.0", "< 4.0.0.a"
37
39
  spec.add_runtime_dependency "rspec-expectations", ">= 2.0", "< 4.0.0.a"
40
+ spec.add_runtime_dependency "rspec-support", ">= 2.0", "< 4.0.0.a"
38
41
  spec.add_runtime_dependency "activesupport", ">= 3.0" # For core extensions
39
42
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-resembles_json_matchers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Sadauskas
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-20 00:00:00.000000000 Z
11
+ date: 2017-12-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,6 +94,34 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry-state
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: awesome_print
99
127
  requirement: !ruby/object:Gem::Requirement
@@ -148,6 +176,26 @@ dependencies:
148
176
  - - "<"
149
177
  - !ruby/object:Gem::Version
150
178
  version: 4.0.0.a
179
+ - !ruby/object:Gem::Dependency
180
+ name: rspec-support
181
+ requirement: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '2.0'
186
+ - - "<"
187
+ - !ruby/object:Gem::Version
188
+ version: 4.0.0.a
189
+ type: :runtime
190
+ prerelease: false
191
+ version_requirements: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '2.0'
196
+ - - "<"
197
+ - !ruby/object:Gem::Version
198
+ version: 4.0.0.a
151
199
  - !ruby/object:Gem::Dependency
152
200
  name: activesupport
153
201
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +226,7 @@ files:
178
226
  - Guardfile
179
227
  - README.md
180
228
  - Rakefile
229
+ - bin/rspec
181
230
  - examples/example_spec.rb
182
231
  - gemfiles/rails_32.gemfile
183
232
  - gemfiles/rails_32.gemfile.lock
@@ -188,6 +237,7 @@ files:
188
237
  - gemfiles/rails_5.gemfile
189
238
  - gemfiles/rails_5.gemfile.lock
190
239
  - lib/rspec/resembles_json_matchers.rb
240
+ - lib/rspec/resembles_json_matchers/attribute_differ.rb
191
241
  - lib/rspec/resembles_json_matchers/attribute_matcher.rb
192
242
  - lib/rspec/resembles_json_matchers/helpers.rb
193
243
  - lib/rspec/resembles_json_matchers/json_matcher.rb
@@ -225,7 +275,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
225
275
  version: '0'
226
276
  requirements: []
227
277
  rubyforge_project:
228
- rubygems_version: 2.6.11
278
+ rubygems_version: 2.7.3
229
279
  signing_key:
230
280
  specification_version: 4
231
281
  summary: Helpful matchers for comparing JSON documents.