rspec-resembles_json_matchers 0.6.0 → 0.7.0

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