json_expressions 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +92 -65
- data/lib/json_expressions/core_extensions.rb +20 -0
- data/lib/json_expressions/matcher.rb +7 -11
- data/lib/json_expressions/minitest.rb +1 -0
- data/lib/json_expressions/minitest/unit/helpers.rb +16 -1
- data/lib/json_expressions/rspec.rb +7 -0
- data/lib/json_expressions/rspec/matchers.rb +1 -0
- data/lib/json_expressions/rspec/matchers/match_json_expression.rb +34 -0
- metadata +5 -2
data/README.md
CHANGED
@@ -3,7 +3,7 @@ JSON Expressions
|
|
3
3
|
|
4
4
|
## Introduction
|
5
5
|
|
6
|
-
Your API is a contract between your service and your developers.
|
6
|
+
Your API is a contract between your service and your developers. It is important for you to know exactly what your JSON API is returning to the developers in order to make sure you don't accidentally change things without updating the documentations and/or bumping the API version number. Perhaps some controller tests for your JSON endpoints would help:
|
7
7
|
|
8
8
|
```ruby
|
9
9
|
# MiniTest::Unit example
|
@@ -28,10 +28,15 @@ class UsersControllerTest < MiniTest::Unit::TestCase
|
|
28
28
|
assert_kind_of Fixnum, posts[0]['id']
|
29
29
|
assert_equal 'Hello world!', posts[0]['subject']
|
30
30
|
assert_equal user_id, posts[0]['user_id']
|
31
|
+
assert_include posts[0]['tags'], 'announcement'
|
32
|
+
assert_include posts[0]['tags'], 'welcome'
|
33
|
+
assert_include posts[0]['tags'], 'introduction'
|
31
34
|
|
32
35
|
assert_kind_of Fixnum, posts[1]['id']
|
33
|
-
assert_equal '
|
36
|
+
assert_equal 'An awesome blog post', posts[1]['subject']
|
34
37
|
assert_equal user_id, posts[1]['user_id']
|
38
|
+
assert_include posts[0]['tags'], 'blog'
|
39
|
+
assert_include posts[0]['tags'], 'life'
|
35
40
|
end
|
36
41
|
end
|
37
42
|
```
|
@@ -58,15 +63,16 @@ Add it to your Gemfile:
|
|
58
63
|
gem 'json_expressions'
|
59
64
|
```
|
60
65
|
|
61
|
-
|
62
|
-
|
66
|
+
Add this to your test/spec helper file:
|
63
67
|
```ruby
|
64
|
-
# MiniTest::Unit
|
68
|
+
# For MiniTest::Unit
|
65
69
|
require 'json_expressions/minitest'
|
70
|
+
|
71
|
+
# For RSpec
|
72
|
+
require 'json_expressions/rspec'
|
66
73
|
```
|
67
74
|
|
68
75
|
Which allows you to do...
|
69
|
-
|
70
76
|
```ruby
|
71
77
|
# MiniTest::Unit example
|
72
78
|
class UsersControllerTest < MiniTest::Unit::TestCase
|
@@ -75,28 +81,34 @@ class UsersControllerTest < MiniTest::Unit::TestCase
|
|
75
81
|
|
76
82
|
# This is what we expect the returned JSON to look like
|
77
83
|
pattern = {
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
84
|
+
user: {
|
85
|
+
id: :user_id, # "Capture" this value for later
|
86
|
+
username: 'chancancode', # Match this exact string
|
87
|
+
full_name: 'Godfrey Chan',
|
88
|
+
email: 'godfrey@example.com',
|
89
|
+
type: 'Administrator',
|
90
|
+
points: Fixnum, # Any integer value
|
91
|
+
homepage: /\Ahttps?\:\/\/.*\z/i, # Let's get serious
|
92
|
+
created_at: WILDCARD_MATCHER, # Don't care as long as it exists
|
93
|
+
updated_at: WILDCARD_MATCHER,
|
94
|
+
posts: [
|
89
95
|
{
|
90
|
-
|
91
|
-
|
92
|
-
|
96
|
+
id: Fixnum,
|
97
|
+
subject: 'Hello world!',
|
98
|
+
user_id: :user_id, # Match against the captured value
|
99
|
+
tags: [
|
100
|
+
'announcement',
|
101
|
+
'welcome',
|
102
|
+
'introduction'
|
103
|
+
] # Ordering of elements does not matter by default
|
93
104
|
}.ignore_extra_keys!, # Skip the uninteresting stuff
|
94
105
|
{
|
95
|
-
|
96
|
-
|
97
|
-
|
106
|
+
id: Fixnum,
|
107
|
+
subject: 'An awesome blog post',
|
108
|
+
user_id: :user_id,
|
109
|
+
tags: ['blog' , 'life']
|
98
110
|
}.ignore_extra_keys!
|
99
|
-
].ordered! # Ensure
|
111
|
+
].ordered! # Ensure the posts are in this exact order
|
100
112
|
}
|
101
113
|
}
|
102
114
|
|
@@ -106,20 +118,35 @@ class UsersControllerTest < MiniTest::Unit::TestCase
|
|
106
118
|
assert matcher.captures[:user_id] > 0
|
107
119
|
end
|
108
120
|
end
|
121
|
+
|
122
|
+
# RSpec example
|
123
|
+
describe UsersController, "#show" do
|
124
|
+
it "returns a user" do
|
125
|
+
pattern = # See above...
|
126
|
+
|
127
|
+
server_response = get '/users/chancancode.json'
|
128
|
+
|
129
|
+
server_response.body.should match_json_expression(pattern)
|
130
|
+
end
|
131
|
+
end
|
109
132
|
```
|
110
133
|
|
134
|
+
### `RSpec` Integration
|
135
|
+
|
136
|
+
|
137
|
+
|
111
138
|
### Basic Matching
|
112
139
|
|
113
140
|
This pattern
|
114
141
|
```ruby
|
115
142
|
{
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
143
|
+
integer: 1,
|
144
|
+
float: 1.1,
|
145
|
+
string: 'Hello world!',
|
146
|
+
boolean: true,
|
147
|
+
array: [1,2,3],
|
148
|
+
object: {key1: 'value1',key2: 'value2'},
|
149
|
+
null: nil,
|
123
150
|
}
|
124
151
|
```
|
125
152
|
matches the JSON object
|
@@ -137,7 +164,7 @@ matches the JSON object
|
|
137
164
|
|
138
165
|
### Wildcard Matching
|
139
166
|
|
140
|
-
You can use the WILDCARD_MATCHER to ignore keys that you don't care about (other than the fact that
|
167
|
+
You can use the WILDCARD_MATCHER to ignore keys that you don't care about (other than the fact that they exist).
|
141
168
|
|
142
169
|
This pattern
|
143
170
|
```ruby
|
@@ -148,17 +175,17 @@ matches the JSON array
|
|
148
175
|
[ 1, 1.1, "Hello world!", true, [1,2,3], {"key1": "value1","key2": "value2"}, null]
|
149
176
|
```
|
150
177
|
|
151
|
-
Furthermore, because the pattern is
|
178
|
+
Furthermore, because the pattern is just plain old Ruby code, you can also write:
|
152
179
|
```ruby
|
153
180
|
[ WILDCARD_MATCHER ] * 7
|
154
181
|
```
|
155
182
|
|
156
183
|
### Pattern Matching
|
157
184
|
|
158
|
-
When an object `.respond_to? :match`, `match` will be called to match against the corresponding value in the JSON. Notably,
|
185
|
+
When an object `.respond_to? :match`, `match` will be called to by json_expression to match against the corresponding value in the target JSON. Notably, `Regexp` objects responds to `match`, which means you can use regular expressions in your pattern:
|
159
186
|
|
160
187
|
```ruby
|
161
|
-
{
|
188
|
+
{ hex: /\A0x[0-9a-f]+\z/i }
|
162
189
|
```
|
163
190
|
matches
|
164
191
|
```json
|
@@ -169,9 +196,9 @@ but not
|
|
169
196
|
{ "hex": "Hello world!" }
|
170
197
|
```
|
171
198
|
|
172
|
-
Sometimes this behavior is undesirable. For instance, String#match(other) converts `other` into a `Regexp` and use that to match against itself, which is probably not what you want (`'Hello
|
199
|
+
Sometimes this behavior is undesirable. For instance, String#match(other) converts `other` into a `Regexp` and use that to match against itself, which is probably not what you want (`''.match 'Hello world!' # => nil` but `'Hello world!'.match '' # => #<MatchData "">`!).
|
173
200
|
|
174
|
-
You can specific a list of classes/modules with undesirable `match` behavior, and json_expression will fall back to calling `===` on
|
201
|
+
You can specific a list of classes/modules with undesirable `match` behavior, and json_expression will fall back to calling `===` on these objects instead (see the section below for `===` vs `==`).
|
175
202
|
|
176
203
|
```ruby
|
177
204
|
# This is the default setting
|
@@ -186,16 +213,16 @@ JsonExpressions::Matcher.skip_match_on = [ String ]
|
|
186
213
|
|
187
214
|
### Type Matching
|
188
215
|
|
189
|
-
For objects that
|
216
|
+
For objects that do not `respond_to? :match` or those you opt-ed out explicitly (such as `String`), `===` will be called instead. For most objects, it behaves identical to `==`. A notable exception would be `Module` (and by inheritance, `Class`) objects, which overrides `===` to mean `instance of`. You can exploit this behavior to do type matching:
|
190
217
|
```ruby
|
191
218
|
{
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
219
|
+
integer: Fixnum,
|
220
|
+
float: Float,
|
221
|
+
string: String,
|
222
|
+
boolean: Boolean,
|
223
|
+
array: Array,
|
224
|
+
object: Hash,
|
225
|
+
null: NilClass,
|
199
226
|
}
|
200
227
|
```
|
201
228
|
matches the JSON object
|
@@ -226,12 +253,12 @@ JsonExpressions::Matcher.skip_triple_equal_on = [ ]
|
|
226
253
|
|
227
254
|
### Capturing
|
228
255
|
|
229
|
-
Similar to how "
|
256
|
+
Similar to how "captures" work in Regexp, you can capture the value of certain keys for later use:
|
230
257
|
```ruby
|
231
258
|
matcher = JsonExpressions::Matcher.new({
|
232
|
-
|
233
|
-
|
234
|
-
|
259
|
+
key1: :key1,
|
260
|
+
key2: :key2,
|
261
|
+
key3: :key3
|
235
262
|
})
|
236
263
|
|
237
264
|
matcher =~ JSON.parse('{"key1":"value1", "key2":"value2", "key3":"value3"}') # => true
|
@@ -244,9 +271,9 @@ matcher.captures[:key3] # => "value3"
|
|
244
271
|
If the same symbol is used multiple times, json_expression will make sure they agree. This pattern
|
245
272
|
```ruby
|
246
273
|
{
|
247
|
-
|
248
|
-
|
249
|
-
|
274
|
+
key1: :capture_me,
|
275
|
+
key2: :capture_me,
|
276
|
+
key3: :capture_me
|
250
277
|
}
|
251
278
|
```
|
252
279
|
matches
|
@@ -278,7 +305,7 @@ will match
|
|
278
305
|
```
|
279
306
|
and
|
280
307
|
```ruby
|
281
|
-
{
|
308
|
+
{ key1: 'value1', key2: 'value2' }
|
282
309
|
```
|
283
310
|
will match
|
284
311
|
```json
|
@@ -302,7 +329,7 @@ JsonExpressions::Matcher.assume_unordered_arrays = false
|
|
302
329
|
JsonExpressions::Matcher.assume_unordered_hashes = false
|
303
330
|
```
|
304
331
|
|
305
|
-
### Strictness
|
332
|
+
### "Strictness"
|
306
333
|
|
307
334
|
By default, all arrays and JSON objects (i.e. Ruby hashes) are assumed to be "strict". This means any extra elements or keys in the JSON target will cause the match to fail:
|
308
335
|
```ruby
|
@@ -314,7 +341,7 @@ will not match
|
|
314
341
|
```
|
315
342
|
and
|
316
343
|
```ruby
|
317
|
-
{
|
344
|
+
{ key1: 'value1', key2: 'value2' }
|
318
345
|
```
|
319
346
|
will not match
|
320
347
|
```json
|
@@ -324,20 +351,20 @@ will not match
|
|
324
351
|
You can change this behavior in a case-by-case manner:
|
325
352
|
```ruby
|
326
353
|
{
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
354
|
+
strict_array: [1,2,3,4,5].strict!, # calling strict! is optional as it's the default
|
355
|
+
forgiving_array: [1,2,3,4,5].forgiving!,
|
356
|
+
strict_hash: {'a'=>1, 'b'=>2}.strict!,
|
357
|
+
forgiving_hash: {'a'=>1, 'b'=>2}.forgiving!
|
331
358
|
}
|
332
359
|
```
|
333
360
|
|
334
361
|
They also come with some more sensible aliases:
|
335
362
|
```ruby
|
336
363
|
{
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
364
|
+
strict_array: [1,2,3,4,5].reject_extra_values!,
|
365
|
+
forgiving_array: [1,2,3,4,5].ignore_extra_values!,
|
366
|
+
strict_hash: {'a'=>1, 'b'=>2}.reject_extra_keys!,
|
367
|
+
forgiving_hash: {'a'=>1, 'b'=>2}.ignore_extra_keys!
|
341
368
|
}
|
342
369
|
```
|
343
370
|
|
@@ -348,9 +375,9 @@ JsonExpressions::Matcher.assume_strict_arrays = false
|
|
348
375
|
JsonExpressions::Matcher.assume_strict_hashes = false
|
349
376
|
```
|
350
377
|
|
351
|
-
##
|
378
|
+
## Support for `MiniTest::Spec` (and other testing frameworks)
|
352
379
|
|
353
|
-
The `Matcher` class itself is written in a testing-framework-agnostic manner. This allows you to easily write custom helpers/matchers for your favorite testing framework.
|
380
|
+
The `Matcher` class itself is written in a testing-framework-agnostic manner. This allows you to easily write custom helpers/matchers for your favorite testing framework. `MiniTest::Spec` is on my TODO list, but it is not a high priority for me personally, as I currently don't use it. If you need this now, write it yourself and submit a pull request - it's really easy, I promise (see `lib/json_expressions/minitest/unit/helpers.rb` for inspiration).
|
354
381
|
|
355
382
|
## Contributing
|
356
383
|
|
@@ -13,6 +13,14 @@ module JsonExpressions
|
|
13
13
|
self.is_a? Unordered
|
14
14
|
end
|
15
15
|
|
16
|
+
def ordered
|
17
|
+
self.clone.ordered!
|
18
|
+
end
|
19
|
+
|
20
|
+
def unordered
|
21
|
+
self.clone.unordered!
|
22
|
+
end
|
23
|
+
|
16
24
|
def ordered!
|
17
25
|
if self.unordered?
|
18
26
|
raise "cannot mark an unordered #{self.class} as ordered!"
|
@@ -37,6 +45,14 @@ module JsonExpressions
|
|
37
45
|
self.is_a? Forgiving
|
38
46
|
end
|
39
47
|
|
48
|
+
def strict
|
49
|
+
self.clone.strict!
|
50
|
+
end
|
51
|
+
|
52
|
+
def forgiving
|
53
|
+
self.clone.forgiving!
|
54
|
+
end
|
55
|
+
|
40
56
|
def strict!
|
41
57
|
if self.forgiving?
|
42
58
|
raise "cannot mark a forgiving #{self.class} as strict!"
|
@@ -57,12 +73,16 @@ end
|
|
57
73
|
|
58
74
|
class Hash
|
59
75
|
include JsonExpressions::CoreExtensions
|
76
|
+
alias_method :reject_extra_keys, :strict
|
60
77
|
alias_method :reject_extra_keys!, :strict!
|
78
|
+
alias_method :ignore_extra_keys, :forgiving
|
61
79
|
alias_method :ignore_extra_keys!, :forgiving!
|
62
80
|
end
|
63
81
|
|
64
82
|
class Array
|
65
83
|
include JsonExpressions::CoreExtensions
|
84
|
+
alias_method :reject_extra_values, :strict
|
66
85
|
alias_method :reject_extra_values!, :strict!
|
86
|
+
alias_method :ignore_extra_values, :forgiving
|
67
87
|
alias_method :ignore_extra_values!, :forgiving!
|
68
88
|
end
|
@@ -153,25 +153,25 @@ module JsonExpressions
|
|
153
153
|
|
154
154
|
apply_hash_defaults matcher
|
155
155
|
|
156
|
-
missing_keys = matcher.keys - other.keys
|
157
|
-
extra_keys = other.keys - matcher.keys
|
156
|
+
missing_keys = matcher.keys.map(&:to_s) - other.keys.map(&:to_s)
|
157
|
+
extra_keys = other.keys.map(&:to_s) - matcher.keys.map(&:to_s)
|
158
158
|
|
159
159
|
unless missing_keys.empty?
|
160
|
-
set_last_error path, "%path% does not contain the key #{missing_keys.first}"
|
160
|
+
set_last_error path, "%path% does not contain the key #{missing_keys.first.to_s}"
|
161
161
|
return false
|
162
162
|
end
|
163
163
|
|
164
164
|
if matcher.strict? && ! extra_keys.empty?
|
165
|
-
set_last_error path, "%path% contains an extra key #{extra_keys.first}"
|
165
|
+
set_last_error path, "%path% contains an extra key #{extra_keys.first.to_s}"
|
166
166
|
return false
|
167
167
|
end
|
168
168
|
|
169
169
|
if matcher.ordered? && matcher.keys != other.keys
|
170
|
-
set_last_error path, "Incorrect key-ordering at %path% (#{matcher.keys.inspect} expected but was #{other.keys.inspect})"
|
170
|
+
set_last_error path, "Incorrect key-ordering at %path% (#{matcher.keys.map(&:to_s).inspect} expected but was #{other.keys.map(&:to_s).inspect})"
|
171
171
|
return false
|
172
172
|
end
|
173
173
|
|
174
|
-
matcher.keys.all? { |k| match_json make_path(path,k), matcher[k], other[k] }
|
174
|
+
matcher.keys.all? { |k| match_json make_path(path,k), matcher[k] , other[k.to_s] || other[k.to_sym] }
|
175
175
|
end
|
176
176
|
|
177
177
|
def set_last_error(path, message)
|
@@ -180,11 +180,7 @@ module JsonExpressions
|
|
180
180
|
|
181
181
|
def make_path(path, segment)
|
182
182
|
if path
|
183
|
-
|
184
|
-
path + "[#{segment}]"
|
185
|
-
else
|
186
|
-
path + ".#{segment}"
|
187
|
-
end
|
183
|
+
segment.is_a?(Fixnum) ? path + "[#{segment}]" : path + ".#{segment.to_s}"
|
188
184
|
end
|
189
185
|
end
|
190
186
|
|
@@ -13,7 +13,22 @@ module JsonExpressions
|
|
13
13
|
assert act = JSON.parse(act), "Expected #{mu_pp(act)} to be valid JSON"
|
14
14
|
end
|
15
15
|
|
16
|
-
assert exp =~ act, ->{exp.last_error}
|
16
|
+
assert exp =~ act, ->{ "Expected #{mu_pp(exp)} to match #{mu_pp(act)}\n" + exp.last_error}
|
17
|
+
|
18
|
+
# Return the matcher
|
19
|
+
return exp
|
20
|
+
end
|
21
|
+
|
22
|
+
def refute_json_match(exp, act, msg = nil)
|
23
|
+
unless JsonExpressions::Matcher === exp
|
24
|
+
exp = JsonExpressions::Matcher.new(exp)
|
25
|
+
end
|
26
|
+
|
27
|
+
if String === act
|
28
|
+
assert act = JSON.parse(act), "Expected #{mu_pp(act)} to be valid JSON"
|
29
|
+
end
|
30
|
+
|
31
|
+
refute exp =~ act, "Expected #{mu_pp(exp)} to not match #{mu_pp(act)}"
|
17
32
|
|
18
33
|
# Return the matcher
|
19
34
|
return exp
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'json_expressions/rspec/matchers/match_json_expression'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module JsonExpressions
|
4
|
+
module RSpec
|
5
|
+
module Matchers
|
6
|
+
class MatchJsonExpression
|
7
|
+
def initialize(expected)
|
8
|
+
if JsonExpressions::Matcher === expected
|
9
|
+
@expected = expected
|
10
|
+
else
|
11
|
+
@expected = JsonExpressions::Matcher.new(expected)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def matches?(target)
|
16
|
+
@target = (String === target) ? JSON.parse(target) : target
|
17
|
+
@expected =~ @target
|
18
|
+
end
|
19
|
+
|
20
|
+
def failure_message_for_should
|
21
|
+
"expected #{@target.inspect} to match JSON expression #{@expected.inspect}\n" + @expected.last_error
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_message_for_should_not
|
25
|
+
"expected #{@target.inspect} not to match JSON expression #{@expected.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def match_json_expression(expected)
|
30
|
+
MatchJsonExpression.new(expected)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_expressions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-07-
|
12
|
+
date: 2012-07-11 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: JSON matchmaking for all your API testing needs.
|
15
15
|
email:
|
@@ -22,6 +22,9 @@ files:
|
|
22
22
|
- lib/json_expressions/matcher.rb
|
23
23
|
- lib/json_expressions/minitest/unit/helpers.rb
|
24
24
|
- lib/json_expressions/minitest.rb
|
25
|
+
- lib/json_expressions/rspec/matchers/match_json_expression.rb
|
26
|
+
- lib/json_expressions/rspec/matchers.rb
|
27
|
+
- lib/json_expressions/rspec.rb
|
25
28
|
- lib/json_expressions.rb
|
26
29
|
- README.md
|
27
30
|
- LICENSE
|