json_spec 0.1.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.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ .rvmrc
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright © 2011 Steve Richert
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,224 @@
1
+ JsonSpec
2
+ ========
3
+ Easily handle JSON in RSpec and Cucumber
4
+
5
+ Installation
6
+ ------------
7
+ gem install json_spec
8
+
9
+ or with Bundler:
10
+
11
+ gem "json_spec"
12
+
13
+ Documentation
14
+ -------------
15
+ Please help write documentation!
16
+
17
+ [http://rdoc.info/github/collectiveidea/json_spec](http://rdoc.info/github/collectiveidea/json_spec)
18
+
19
+ Continuous Integration
20
+ ----------------------
21
+ Coming soon…
22
+
23
+ RSpec
24
+ --------------
25
+ JsonSpec defines four new RSpec matchers:
26
+
27
+ * `be_json_eql`
28
+ * `have_json_path`
29
+ * `have_json_type`
30
+ * `have_json_size`
31
+
32
+ The new matchers could be used in RSpec as follows:
33
+
34
+ describe User do
35
+ let(:user){ User.create!(:first_name => "Steve", :last_name => "Richert") }
36
+
37
+ context "#to_json" do
38
+ let(:json){ user.to_json }
39
+
40
+ it "includes the name" do
41
+ json.should be_json_eql(%({"first_name":"Steve","last_name":"Richert"})).excluding("friends")
42
+ end
43
+
44
+ it "includes the ID" do
45
+ json.should have_json_path("id")
46
+ json.should have_json_type(Integer).at_path("id")
47
+ end
48
+
49
+ it "includes friends" do
50
+ json.should have_json_size(0).at_path("friends")
51
+ user.friends << User.create!(:first_name => "Catie", :last_name => "Richert")
52
+ json.should have_json_size(1).at_path("friends")
53
+ end
54
+ end
55
+ end
56
+
57
+ ### Exclusions
58
+
59
+ JsonSpec ignores certain hash keys by default when comparing JSON:
60
+
61
+ * `id`
62
+ * `created_at`
63
+ * `updated_at`
64
+
65
+ It's oftentimes helpful when evaluating JSON representations of newly-created ActiveRecord records
66
+ so that the new ID and timestamps don't have to be known. These exclusions are globally
67
+ customizeable:
68
+
69
+ JsonSpec.configure do
70
+ exclude_keys "created_at", "updated_at"
71
+ end
72
+
73
+ Now, the `id` key will be included in JsonSpec's comparisons. Keys can also be excluded/included
74
+ per matcher by chaining the `excluding` or `including` methods (as shown above) which will add or subtract from
75
+ the globally excluded keys, respectively.
76
+
77
+ ### Paths
78
+
79
+ Each of JsonSpec's matchers deal with JSON "paths." These are simple strings of "/" separated
80
+ hash keys and array indexes. For instance, with the following JSON:
81
+
82
+ {
83
+ "first_name": "Steve",
84
+ "last_name": "Richert",
85
+ "friends": [
86
+ {
87
+ "first_name": "Catie",
88
+ "last_name": "Richert"
89
+ }
90
+ ]
91
+ }
92
+
93
+ We could access the first friend's first name with the path `"friends/0/first_name"`.
94
+
95
+ Cucumber
96
+ --------
97
+ JsonSpec provides Cucumber steps that utilize its RSpec matchers and that's where JsonSpec really
98
+ shines. This is perfect for testing your app's JSON API.
99
+
100
+ In order to use the Cucumber steps, in your `env.rb` you must:
101
+
102
+ require "json_spec/cucumber"
103
+
104
+ You also need to define a `last_json` method. If you're using Capybara, it could be as simple as:
105
+
106
+ def last_json
107
+ page.source
108
+ end
109
+
110
+ Now, you can use the JsonSpec steps in your features:
111
+
112
+ Feature: User API
113
+ Background:
114
+ Given the following users exist:
115
+ | id | first_name | last_name |
116
+ | 1 | Steve | Richert |
117
+ | 2 | Catie | Richert |
118
+ And "Steve Richert" is friends with "Catie Richert"
119
+
120
+ Scenario: Index action
121
+ When I visit "/users.json"
122
+ Then the JSON response should have 2 users
123
+ And the JSON response at "0/id" should be 1
124
+ And the JSON response at "1/id" should be 2
125
+
126
+ Scenario: Show action
127
+ When I visit "/users/1.json"
128
+ Then the JSON response at "first_name" should be "Steve"
129
+ And the JSON response at "last_name" should be "Richert"
130
+ And the JSON response should have "created_at"
131
+ And the JSON response at "created_at" should be a string
132
+ And the JSON response at "friends" should be:
133
+ """
134
+ [
135
+ {
136
+ "id": 2,
137
+ "first_name": "Catie",
138
+ "last_name": "Richert"
139
+ }
140
+ ]
141
+ """
142
+
143
+ The background steps above aren't provided by JsonSpec and the "visit" steps are provided by
144
+ Capybara. The remaining steps all stem from the five steps that JsonSpec provides. They're
145
+ versatile and can be used in plenty of different formats:
146
+
147
+ Then the JSON should be:
148
+ """
149
+ {
150
+ "key": "value"
151
+ }
152
+ """
153
+ Then the JSON at "path" should be:
154
+ """
155
+ [
156
+ "entry",
157
+ "entry"
158
+ ]
159
+ """
160
+
161
+ Then the JSON should be {"key":"value"}
162
+ Then the JSON at "path" should be {"key":"value"}
163
+ Then the JSON should be ["entry","entry"]
164
+ Then the JSON at "path" should be ["entry","entry"]
165
+ Then the JSON at "path" should be "string"
166
+ Then the JSON at "path" should be 10
167
+ Then the JSON at "path" should be 10.0
168
+ Then the JSON at "path" should be 1e+1
169
+ Then the JSON at "path" should be true
170
+ Then the JSON at "path" should be false
171
+ Then the JSON at "path" should be null
172
+
173
+ Then the JSON should have "path"
174
+
175
+ Then the JSON should be a hash
176
+ Then the JSON at "path" should be an array
177
+ Then the JSON at "path" should be a float
178
+
179
+ Then the JSON should have 1 entry
180
+ Then the JSON at "path" should have 2 entries
181
+ Then the JSON should have 3 keys
182
+ Then the JSON should have 4 whatevers
183
+
184
+ _All instances of "JSON" above could also be downcased and/or followed by "response."_
185
+
186
+ Contributing
187
+ ------------
188
+ In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project.
189
+
190
+ Here are some ways *you* can contribute:
191
+
192
+ * using alpha, beta, and prerelease versions
193
+ * reporting bugs
194
+ * suggesting new features
195
+ * writing or editing documentation
196
+ * writing specifications
197
+ * writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace)
198
+ * refactoring code
199
+ * closing [issues](https://github.com/collectiveidea/json_spec/issues)
200
+ * reviewing patches
201
+
202
+ Submitting an Issue
203
+ -------------------
204
+ We use the [GitHub issue tracker](https://github.com/collectiveidea/json_spec/issues) to track bugs
205
+ and features. Before submitting a bug report or feature request, check to make sure it hasn't already
206
+ been submitted. You can indicate support for an existing issuse by voting it up. When submitting a
207
+ bug report, please include a [Gist](https://gist.github.com/) that includes a stack trace and any
208
+ details that may be necessary to reproduce the bug, including your gem version, Ruby version, and
209
+ operating system. Ideally, a bug report should include a pull request with failing specs.
210
+
211
+ Submitting a Pull Request
212
+ -------------------------
213
+ 1. Fork the project.
214
+ 2. Create a topic branch.
215
+ 3. Implement your feature or bug fix.
216
+ 4. Add specs for your feature or bug fix.
217
+ 5. Run <tt>bundle exec rake</tt>. If your changes are not 100% covered and passing, go back to step 4.
218
+ 6. Commit and push your changes.
219
+ 7. Submit a pull request. Please do not include changes to the gemspec, version, or history file. (If you want to create your own version for some reason, please do so in a separate commit.)
220
+
221
+ Copyright
222
+ ---------
223
+ Copyright © 2011 Steve Richert
224
+ See [LICENSE](https://github.com/collectiveidea/json_spec/blob/master/LICENSE.md) for details.
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "cucumber/rake/task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ Cucumber::Rake::Task.new(:cucumber)
7
+
8
+ task :test => [:spec, :cucumber]
9
+ task :default => :test
@@ -0,0 +1,233 @@
1
+ Feature: Equivalence
2
+ Background:
3
+ Given the JSON is:
4
+ """
5
+ {
6
+ "array": [
7
+ "json",
8
+ "spec"
9
+ ],
10
+ "created_at": "2011-07-08 02:27:34",
11
+ "empty_array": [
12
+
13
+ ],
14
+ "empty_hash": {
15
+ },
16
+ "false": false,
17
+ "float": 10.0,
18
+ "hash": {
19
+ "json": "spec"
20
+ },
21
+ "id": 1,
22
+ "integer": 10,
23
+ "negative": -10,
24
+ "null": null,
25
+ "string": "json_spec",
26
+ "true": true,
27
+ "updated_at": "2011-07-08 02:28:50"
28
+ }
29
+ """
30
+
31
+ Scenario: Identical JSON
32
+ When I get the JSON
33
+ Then the JSON should be:
34
+ """
35
+ {
36
+ "array": [
37
+ "json",
38
+ "spec"
39
+ ],
40
+ "created_at": "2011-07-08 02:27:34",
41
+ "empty_array": [
42
+
43
+ ],
44
+ "empty_hash": {
45
+ },
46
+ "false": false,
47
+ "float": 10.0,
48
+ "hash": {
49
+ "json": "spec"
50
+ },
51
+ "id": 1,
52
+ "integer": 10,
53
+ "negative": -10,
54
+ "null": null,
55
+ "string": "json_spec",
56
+ "true": true,
57
+ "updated_at": "2011-07-08 02:28:50"
58
+ }
59
+ """
60
+
61
+ Scenario: Reverse order
62
+ When I get the JSON
63
+ Then the JSON should be:
64
+ """
65
+ {
66
+ "updated_at": "2011-07-08 02:28:50",
67
+ "true": true,
68
+ "string": "json_spec",
69
+ "null": null,
70
+ "negative": -10,
71
+ "integer": 10,
72
+ "id": 1,
73
+ "hash": {
74
+ "json": "spec"
75
+ },
76
+ "float": 10.0,
77
+ "false": false,
78
+ "empty_hash": {
79
+ },
80
+ "empty_array": [
81
+
82
+ ],
83
+ "created_at": "2011-07-08 02:27:34",
84
+ "array": [
85
+ "json",
86
+ "spec"
87
+ ]
88
+ }
89
+ """
90
+
91
+ Scenario: Excluding keys
92
+ When I get the JSON
93
+ Then the JSON should be:
94
+ """
95
+ {
96
+ "array": [
97
+ "json",
98
+ "spec"
99
+ ],
100
+ "empty_array": [
101
+
102
+ ],
103
+ "empty_hash": {
104
+ },
105
+ "false": false,
106
+ "float": 10.0,
107
+ "hash": {
108
+ "json": "spec"
109
+ },
110
+ "integer": 10,
111
+ "negative": -10,
112
+ "null": null,
113
+ "string": "json_spec",
114
+ "true": true
115
+ }
116
+ """
117
+
118
+ Scenario: String
119
+ When I get the JSON
120
+ Then the JSON at "string" should be "json_spec"
121
+ And the JSON at "string" should be:
122
+ """
123
+ "json_spec"
124
+ """
125
+
126
+ Scenario: Integer
127
+ When I get the JSON
128
+ Then the JSON at "integer" should be 10
129
+ And the JSON at "integer" should be:
130
+ """
131
+ 10
132
+ """
133
+
134
+ Scenario: Negative integer
135
+ When I get the JSON
136
+ Then the JSON at "negative" should be -10
137
+ And the JSON at "negative" should be:
138
+ """
139
+ -10
140
+ """
141
+
142
+ Scenario: Float
143
+ When I get the JSON
144
+ Then the JSON at "float" should be 10.0
145
+ And the JSON at "float" should be 10.0e0
146
+ And the JSON at "float" should be 10.0e+0
147
+ And the JSON at "float" should be 10.0e-0
148
+ And the JSON at "float" should be 10e0
149
+ And the JSON at "float" should be 10e+0
150
+ And the JSON at "float" should be 10e-0
151
+ And the JSON at "float" should be 1.0e1
152
+ And the JSON at "float" should be 1.0e+1
153
+ And the JSON at "float" should be 1e1
154
+ And the JSON at "float" should be 1e+1
155
+ And the JSON at "float" should be 100.0e-1
156
+ And the JSON at "float" should be 100e-1
157
+ And the JSON at "float" should be:
158
+ """
159
+ 10.0
160
+ """
161
+
162
+ Scenario: Array
163
+ When I get the JSON
164
+ Then the JSON at "array" should be ["json","spec"]
165
+ And the JSON at "array/0" should be "json"
166
+ And the JSON at "array/1" should be "spec"
167
+ And the JSON at "array" should be:
168
+ """
169
+ [
170
+ "json",
171
+ "spec"
172
+ ]
173
+ """
174
+
175
+ Scenario: Empty array
176
+ When I get the JSON
177
+ Then the JSON at "empty_array" should be []
178
+ And the JSON at "empty_array" should be:
179
+ """
180
+ [
181
+
182
+ ]
183
+ """
184
+
185
+ Scenario: Hash
186
+ When I get the JSON
187
+ Then the JSON at "hash" should be {"json":"spec"}
188
+ And the JSON at "hash/json" should be "spec"
189
+ And the JSON at "hash" should be:
190
+ """
191
+ {
192
+ "json": "spec"
193
+ }
194
+ """
195
+
196
+ Scenario: Empty hash
197
+ When I get the JSON
198
+ Then the JSON at "empty_hash" should be {}
199
+ And the JSON at "empty_hash" should be:
200
+ """
201
+ {
202
+ }
203
+ """
204
+
205
+ Scenario: True
206
+ When I get the JSON
207
+ Then the JSON at "true" should be true
208
+ And the JSON at "true" should be:
209
+ """
210
+ true
211
+ """
212
+
213
+ Scenario: False
214
+ When I get the JSON
215
+ Then the JSON at "false" should be false
216
+ And the JSON at "false" should be:
217
+ """
218
+ false
219
+ """
220
+
221
+ Scenario: Null
222
+ When I get the JSON
223
+ Then the JSON at "null" should be null
224
+ And the JSON at "null" should be:
225
+ """
226
+ null
227
+ """
228
+
229
+ Scenario: Excluded value
230
+ When I get the JSON
231
+ Then the JSON at "created_at" should be "2011-07-08 02:27:34"
232
+ And the JSON at "id" should be 1
233
+ And the JSON at "updated_at" should be "2011-07-08 02:28:50"
@@ -0,0 +1,43 @@
1
+ Feature: Paths
2
+ Background:
3
+ Given the JSON is:
4
+ """
5
+ {
6
+ "array": [
7
+ {
8
+ "one": 1,
9
+ "two": 2
10
+ },
11
+ {
12
+ "four": 4,
13
+ "three": 3
14
+ }
15
+ ],
16
+ "hash": {
17
+ "even": [
18
+ 6,
19
+ 8
20
+ ],
21
+ "odd": [
22
+ 5,
23
+ 7
24
+ ]
25
+ },
26
+ "id": null
27
+ }
28
+ """
29
+
30
+ Scenario: Nested paths
31
+ When I get the JSON
32
+ Then the JSON should have "array/0/one"
33
+ And the JSON should have "array/0/two"
34
+ And the JSON should have "array/1/four"
35
+ And the JSON should have "array/1/three"
36
+ And the JSON should have "hash/even/0"
37
+ And the JSON should have "hash/even/1"
38
+ And the JSON should have "hash/odd/0"
39
+ And the JSON should have "hash/odd/1"
40
+
41
+ Scenario: Ignored paths
42
+ When I get the JSON
43
+ Then the JSON should have "id"
@@ -0,0 +1,38 @@
1
+ Feature: Sizes
2
+ Background:
3
+ Given the JSON is:
4
+ """
5
+ {
6
+ "id": null,
7
+ "one": [
8
+ 1
9
+ ],
10
+ "three": [
11
+ 1,
12
+ 2,
13
+ 3
14
+ ],
15
+ "two": [
16
+ 1,
17
+ 2
18
+ ],
19
+ "zero": [
20
+
21
+ ]
22
+ }
23
+ """
24
+
25
+ Scenario: Hash
26
+ When I get the JSON
27
+ Then the JSON should have 5 keys
28
+ And the JSON should have 5 values
29
+
30
+ Scenario: Empty array
31
+ When I get the JSON
32
+ Then the JSON at "zero" should have 0 entries
33
+
34
+ Scenario: Array
35
+ When I get the JSON
36
+ Then the JSON at "one" should have 1 entry
37
+ And the JSON at "two" should have 2 values
38
+ And the JSON at "three" should have 3 numbers
@@ -0,0 +1,7 @@
1
+ Given "the JSON is:" do |json|
2
+ @json = json
3
+ end
4
+
5
+ When "I get the JSON" do
6
+ @last_json = @json
7
+ end
@@ -0,0 +1,6 @@
1
+ $: << File.expand_path("../../../lib", __FILE__)
2
+ require "json_spec/cucumber"
3
+
4
+ def last_json
5
+ @last_json.to_s
6
+ end
@@ -0,0 +1,18 @@
1
+ Feature: Types
2
+ Scenario: All types
3
+ Given the JSON is:
4
+ """
5
+ {
6
+ "array": [],
7
+ "float": 10.0,
8
+ "hash": {},
9
+ "integer": 10,
10
+ "string": "json_spec"
11
+ }
12
+ """
13
+ When I get the JSON
14
+ Then the JSON should be a hash
15
+ And the JSON at "array" should be an array
16
+ And the JSON at "float" should be a float
17
+ And the JSON at "hash" should be a hash
18
+ And the JSON at "integer" should be an integer
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "json_spec/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "json_spec"
7
+ s.version = JsonSpec::VERSION
8
+ s.authors = ["Steve Richert"]
9
+ s.email = ["steve.richert@gmail.com"]
10
+ s.homepage = "https://github.com/collectiveidea/json_spec"
11
+ s.summary = "Easily handle JSON in RSpec and Cucumber"
12
+ s.description = "Easily handle JSON in RSpec and Cucumber"
13
+
14
+ s.rubyforge_project = "json_spec"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "json", "~> 1.0"
22
+ s.add_dependency "rspec", "~> 2.0"
23
+
24
+ s.add_development_dependency "cucumber", "~> 1.0"
25
+ end
@@ -0,0 +1,9 @@
1
+ require "json_spec/version"
2
+ require "json_spec/errors"
3
+ require "json_spec/configuration"
4
+ require "json_spec/matchers"
5
+ require "json_spec/helpers"
6
+
7
+ module JsonSpec
8
+ extend Configuration
9
+ end
@@ -0,0 +1,27 @@
1
+ require "set"
2
+
3
+ module JsonSpec
4
+ module Configuration
5
+ DEFAULT_EXCLUDED_KEYS = %w(id created_at updated_at)
6
+
7
+ def configure(&block)
8
+ instance_eval(&block)
9
+ end
10
+
11
+ def excluded_keys
12
+ @excluded_keys ||= DEFAULT_EXCLUDED_KEYS
13
+ end
14
+
15
+ def excluded_keys=(keys)
16
+ @excluded_keys = keys.map{|k| k.to_s }.uniq
17
+ end
18
+
19
+ def exclude_keys(*keys)
20
+ self.excluded_keys = keys
21
+ end
22
+
23
+ def reset
24
+ instance_variables.each{|iv| remove_instance_variable(iv) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ require File.expand_path("../../json_spec", __FILE__)
2
+
3
+ World(JsonSpec::Helpers)
4
+
5
+ Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should be:$/ do |path, json|
6
+ last_json.should be_json_eql(json).at_path(path)
7
+ end
8
+
9
+ Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should be (".*"|\-?\d+(?:\.\d+)?(?:[eE][\+\-]?\d+)?|\[.*\]|\{.*\}|true|false|null)$/ do |path, value|
10
+ last_json.should be_json_eql(value).at_path(path)
11
+ end
12
+
13
+ Then /^the (?:JSON|json)(?: response)? should have "(.*)"$/ do |path|
14
+ last_json.should have_json_path(path)
15
+ end
16
+
17
+ Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should be an? (.*)$/ do |path, type|
18
+ klass = Module.const_get(type.gsub(/^./){|x| x.upcase })
19
+ last_json.should have_json_type(klass).at_path(path)
20
+ end
21
+
22
+ Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should have (\d+)/ do |path, size|
23
+ last_json.should have_json_size(size.to_i).at_path(path)
24
+ end
@@ -0,0 +1,13 @@
1
+ module JsonSpec
2
+ class MissingPathError < StandardError
3
+ attr_reader :path
4
+
5
+ def initialize(path)
6
+ @path = path
7
+ end
8
+
9
+ def to_s
10
+ %(Missing JSON path "#{path}")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ module JsonSpec
2
+ module Helpers
3
+ extend self
4
+
5
+ def json_at_path(json, path)
6
+ pretty_json_value(ruby_at_json_path(json, path))
7
+ end
8
+
9
+ def pretty_json_value(ruby)
10
+ case ruby
11
+ when Hash, Array then JSON.pretty_generate(ruby)
12
+ when NilClass then "null"
13
+ else ruby.inspect
14
+ end
15
+ end
16
+
17
+ def ruby_at_json_path(json, path)
18
+ json_path_to_keys(path).inject(parse_json_value(json)) do |value, key|
19
+ case value
20
+ when Hash, Array then value.fetch(key){ missing_json_path!(path) }
21
+ else missing_json_path!(path)
22
+ end
23
+ end
24
+ end
25
+
26
+ def json_path_to_keys(path)
27
+ path.to_s.gsub(/(?:^\/|\/$)/, "").split("/").map{|k| k =~ /^\d+$/ ? k.to_i : k }
28
+ end
29
+
30
+ def parse_json_value(json)
31
+ JSON.parse(%({"root":#{json}}))["root"]
32
+ rescue JSON::ParserError
33
+ # Re-raise more appropriate parsing error
34
+ JSON.parse(json)
35
+ end
36
+
37
+ def missing_json_path!(path)
38
+ raise JsonSpec::MissingPathError.new(path)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,113 @@
1
+ require "json"
2
+ require "rspec"
3
+
4
+ RSpec::Matchers.define :be_json_eql do |expected_json|
5
+ include JsonSpec::Helpers
6
+
7
+ diffable
8
+
9
+ match do |actual_json|
10
+ @actual, @expected = scrub(actual_json, @path), [scrub(expected_json)]
11
+ @actual == @expected.first
12
+ end
13
+
14
+ chain :at_path do |path|
15
+ @path = path
16
+ end
17
+
18
+ chain :excluding do |*keys|
19
+ excluded_keys.add(*keys.map{|k| k.to_s })
20
+ end
21
+
22
+ chain :including do |*keys|
23
+ excluded_keys.subtract(keys.map{|k| k.to_s })
24
+ end
25
+
26
+ failure_message_for_should do
27
+ message = "Expected equivalent JSON"
28
+ message << %( at path "#{@path}") if @path
29
+ message
30
+ end
31
+
32
+ def scrub(json, path = nil)
33
+ ruby = path ? ruby_at_json_path(json, path) : parse_json_value(json)
34
+ pretty_json_value(exclude_keys(ruby)).chomp + "\n"
35
+ end
36
+
37
+ def exclude_keys(ruby)
38
+ case ruby
39
+ when Hash
40
+ ruby.sort.inject({}) do |hash, (key, value)|
41
+ hash[key] = exclude_keys(value) unless exclude_key?(key)
42
+ hash
43
+ end
44
+ when Array
45
+ ruby.map{|v| exclude_keys(v) }
46
+ else ruby
47
+ end
48
+ end
49
+
50
+ def exclude_key?(key)
51
+ excluded_keys.include?(key)
52
+ end
53
+
54
+ def excluded_keys
55
+ @excluded_keys ||= Set.new(JsonSpec.excluded_keys)
56
+ end
57
+ end
58
+
59
+ RSpec::Matchers.define :have_json_path do |path|
60
+ include JsonSpec::Helpers
61
+
62
+ match do |json|
63
+ begin
64
+ ruby_at_json_path(json, path)
65
+ true
66
+ rescue JsonSpec::MissingPathError
67
+ false
68
+ end
69
+ end
70
+
71
+ failure_message_for_should do
72
+ %(Expected JSON path "#{path}")
73
+ end
74
+ end
75
+
76
+ RSpec::Matchers.define :have_json_type do |klass|
77
+ include JsonSpec::Helpers
78
+
79
+ match do |json|
80
+ ruby = @path ? ruby_at_json_path(json, @path) : parse_json_value(json)
81
+ ruby.is_a?(klass)
82
+ end
83
+
84
+ chain :at_path do |path|
85
+ @path = path
86
+ end
87
+
88
+ failure_message_for_should do
89
+ message = "Expected JSON value type of #{klass}"
90
+ message << %( at path "#{@path}") if @path
91
+ message
92
+ end
93
+ end
94
+
95
+ RSpec::Matchers.define :have_json_size do |expected_size|
96
+ include JsonSpec::Helpers
97
+
98
+ match do |json|
99
+ ruby = @path ? ruby_at_json_path(json, @path) : parse_json_value(json)
100
+ actual_size = ruby.is_a?(Enumerable) ? ruby.size : 1
101
+ actual_size == expected_size
102
+ end
103
+
104
+ chain :at_path do |path|
105
+ @path = path
106
+ end
107
+
108
+ failure_message_for_should do
109
+ message = "Expected JSON value size of #{expected_size}"
110
+ message << %( at path "#{@path}") if @path
111
+ message
112
+ end
113
+ end
@@ -0,0 +1,3 @@
1
+ module JsonSpec
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ describe JsonSpec::Configuration do
4
+ it "excludes id and timestamps by default" do
5
+ JsonSpec.excluded_keys.should == ["id", "created_at", "updated_at"]
6
+ end
7
+
8
+ it "excludes custom keys" do
9
+ JsonSpec.exclude_keys("token")
10
+ JsonSpec.excluded_keys.should == ["token"]
11
+ end
12
+
13
+ it "excludes custom keys via setter" do
14
+ JsonSpec.excluded_keys = ["token"]
15
+ JsonSpec.excluded_keys.should == ["token"]
16
+ end
17
+
18
+ it "excludes custom keys via block" do
19
+ JsonSpec.configure{|c| c.exclude_keys("token") }
20
+ JsonSpec.excluded_keys.should == ["token"]
21
+ end
22
+
23
+ it "excludes custom keys via block setter" do
24
+ JsonSpec.configure{|c| c.excluded_keys = ["token"] }
25
+ JsonSpec.excluded_keys.should == ["token"]
26
+ end
27
+
28
+ it "excludes custom keys via instance-evaluated block" do
29
+ JsonSpec.configure{ exclude_keys("token") }
30
+ JsonSpec.excluded_keys.should == ["token"]
31
+ end
32
+
33
+ it "ensures its excluded keys are strings" do
34
+ JsonSpec.exclude_keys(:token)
35
+ JsonSpec.excluded_keys.should == ["token"]
36
+ end
37
+
38
+ it "ensures its excluded keys are unique" do
39
+ JsonSpec.exclude_keys("token", :token)
40
+ JsonSpec.excluded_keys.should == ["token"]
41
+ end
42
+
43
+ it "resets its excluded keys" do
44
+ original = JsonSpec.excluded_keys
45
+
46
+ JsonSpec.exclude_keys("token")
47
+ JsonSpec.excluded_keys.should_not == original
48
+
49
+ JsonSpec.reset
50
+ JsonSpec.excluded_keys.should == original
51
+ end
52
+ end
@@ -0,0 +1,108 @@
1
+ require "spec_helper"
2
+
3
+ describe "Matchers:" do
4
+ context "be_json_eql" do
5
+ it "matches identical JSON" do
6
+ %({"json":"spec"}).should be_json_eql(%({"json":"spec"}))
7
+ end
8
+
9
+ it "matches differently-formatted JSON" do
10
+ %({"json": "spec"}).should be_json_eql(%({"json":"spec"}))
11
+ end
12
+
13
+ it "matches out-of-order hashes" do
14
+ %({"laser":"lemon","json":"spec"}).should be_json_eql(%({"json":"spec","laser":"lemon"}))
15
+ end
16
+
17
+ it "doesn't match out-of-order arrays" do
18
+ %(["json","spec"]).should_not be_json_eql(%(["spec","json"]))
19
+ end
20
+
21
+ it "ignores excluded-by-default hash keys" do
22
+ JsonSpec.excluded_keys.should_not be_empty
23
+
24
+ actual = expected = {"json" => "spec"}
25
+ JsonSpec.excluded_keys.each{|k| actual[k] = k }
26
+ actual.to_json.should be_json_eql(expected.to_json)
27
+ end
28
+
29
+ it "ignores custom excluded hash keys" do
30
+ JsonSpec.exclude_keys("ignore")
31
+ %({"json":"spec","ignore":"please"}).should be_json_eql(%({"json":"spec"}))
32
+ end
33
+
34
+ it "ignores nested, excluded hash keys" do
35
+ JsonSpec.exclude_keys("ignore")
36
+ %({"json":"spec","please":{"ignore":"this"}}).should be_json_eql(%({"json":"spec","please":{}}))
37
+ end
38
+
39
+ it "ignores hash keys when included in the expected value" do
40
+ JsonSpec.exclude_keys("ignore")
41
+ %({"json":"spec","ignore":"please"}).should be_json_eql(%({"json":"spec","ignore":"this"}))
42
+ end
43
+
44
+ it "doesn't match Ruby-equivalent, JSON-inequivalent values" do
45
+ %({"one":1}).should_not be_json_eql(%({"one":1.0}))
46
+ end
47
+
48
+ it "excludes extra hash keys per matcher" do
49
+ JsonSpec.excluded_keys = %w(ignore)
50
+ %({"id":1,"json":"spec","ignore":"please"}).should be_json_eql(%({"id":"2","json":"spec","ignore":"this"})).excluding("id")
51
+ end
52
+
53
+ it "excludes extra hash keys given as symbols" do
54
+ JsonSpec.excluded_keys = []
55
+ %({"id":1,"json":"spec"}).should be_json_eql(%({"id":2,"json":"spec"})).excluding(:id)
56
+ end
57
+
58
+ it "includes globally-excluded hash keys per matcher" do
59
+ JsonSpec.excluded_keys = %w(id ignore)
60
+ %({"id":1,"json":"spec","ignore":"please"}).should_not be_json_eql(%({"id":"2","json":"spec","ignore":"this"})).including("id")
61
+ end
62
+
63
+ it "includes globally-included hash keys given as symbols" do
64
+ JsonSpec.excluded_keys = %w(id)
65
+ %({"id":1,"json":"spec"}).should_not be_json_eql(%({"id":2,"json":"spec"})).including(:id)
66
+ end
67
+ end
68
+
69
+ context "have_json_size" do
70
+ it "counts array entries" do
71
+ %([1,2,3]).should have_json_size(3)
72
+ end
73
+
74
+ it "counts null array entries" do
75
+ %([1,null,3]).should have_json_size(3)
76
+ end
77
+
78
+ it "counts hash key/value pairs" do
79
+ %({"one":1,"two":2,"three":3}).should have_json_size(3)
80
+ end
81
+
82
+ it "counts null hash values" do
83
+ %({"one":1,"two":null,"three":3}).should have_json_size(3)
84
+ end
85
+ end
86
+
87
+ context "have_json_path" do
88
+ it "matches hash keys" do
89
+ %({"one":{"two":{"three":4}}}).should have_json_path("one/two/three")
90
+ end
91
+
92
+ it "doesn't match values" do
93
+ %({"one":{"two":{"three":4}}}).should_not have_json_path("one/two/three/4")
94
+ end
95
+
96
+ it "matches array indexes" do
97
+ %([1,[1,2,[1,2,3,4]]]).should have_json_path("1/2/3")
98
+ end
99
+
100
+ it "respects null array values" do
101
+ %([null,[null,null,[null,null,null,null]]]).should have_json_path("1/2/3")
102
+ end
103
+
104
+ it "matches hash keys and array indexes" do
105
+ %({"one":[1,2,{"three":4}]}).should have_json_path("one/2/three")
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,5 @@
1
+ require "spec_helper"
2
+
3
+ describe JsonSpec do
4
+ it "is awesome"
5
+ end
@@ -0,0 +1,7 @@
1
+ require "json_spec"
2
+
3
+ RSpec.configure do |config|
4
+ config.before do
5
+ JsonSpec.reset
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_spec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steve Richert
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-07-08 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ requirement: &2153487840 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '1.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2153487840
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: &2153487340 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2153487340
37
+ - !ruby/object:Gem::Dependency
38
+ name: cucumber
39
+ requirement: &2153486880 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: '1.0'
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *2153486880
48
+ description: Easily handle JSON in RSpec and Cucumber
49
+ email:
50
+ - steve.richert@gmail.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - LICENSE.md
58
+ - README.md
59
+ - Rakefile
60
+ - features/equivalence.feature
61
+ - features/paths.feature
62
+ - features/sizes.feature
63
+ - features/step_definitions/steps.rb
64
+ - features/support/env.rb
65
+ - features/types.feature
66
+ - json_spec.gemspec
67
+ - lib/json_spec.rb
68
+ - lib/json_spec/configuration.rb
69
+ - lib/json_spec/cucumber.rb
70
+ - lib/json_spec/errors.rb
71
+ - lib/json_spec/helpers.rb
72
+ - lib/json_spec/matchers.rb
73
+ - lib/json_spec/version.rb
74
+ - spec/json_spec/configuration_spec.rb
75
+ - spec/json_spec/matchers_spec.rb
76
+ - spec/json_spec_spec.rb
77
+ - spec/spec_helper.rb
78
+ has_rdoc: true
79
+ homepage: https://github.com/collectiveidea/json_spec
80
+ licenses: []
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project: json_spec
99
+ rubygems_version: 1.6.2
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Easily handle JSON in RSpec and Cucumber
103
+ test_files:
104
+ - features/equivalence.feature
105
+ - features/paths.feature
106
+ - features/sizes.feature
107
+ - features/step_definitions/steps.rb
108
+ - features/support/env.rb
109
+ - features/types.feature
110
+ - spec/json_spec/configuration_spec.rb
111
+ - spec/json_spec/matchers_spec.rb
112
+ - spec/json_spec_spec.rb
113
+ - spec/spec_helper.rb