json_expressions 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.md +361 -0
- data/lib/json_expressions/core_extensions.rb +68 -0
- data/lib/json_expressions/matcher.rb +227 -0
- data/lib/json_expressions/minitest/unit/helpers.rb +24 -0
- data/lib/json_expressions/minitest.rb +7 -0
- data/lib/json_expressions.rb +25 -0
- metadata +52 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Godfrey Chan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,361 @@
|
|
1
|
+
JSON Expressions
|
2
|
+
================
|
3
|
+
|
4
|
+
## Introduction
|
5
|
+
|
6
|
+
Your API is a contract between your service and your developers. Therefore it is important to know what exactly your JSON API is returning to the developers and make sure you don't accidentally change that contract without updating the documentations. Perhaps you should write some controller tests for your JSON endpoints:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
# MiniTest::Unit example
|
10
|
+
class UsersControllerTest < MiniTest::Unit::TestCase
|
11
|
+
def test_get_a_user
|
12
|
+
server_response = get '/users/chancancode.json'
|
13
|
+
|
14
|
+
json = JSON.parse server_response.body
|
15
|
+
|
16
|
+
assert user = json['user']
|
17
|
+
|
18
|
+
assert user_id = user['id']
|
19
|
+
assert_equal 'chancancode', user['username']
|
20
|
+
assert_equal 'Godfrey Chan', user['full_name']
|
21
|
+
assert_equal 'godfrey@example.com', user['email']
|
22
|
+
assert_equal 'Administrator', user['type']
|
23
|
+
assert_kind_of Fixnum, user['points']
|
24
|
+
assert_match /\Ahttps?\:\/\/.*\z/i, user['homepage']
|
25
|
+
|
26
|
+
assert posts = user['posts']
|
27
|
+
|
28
|
+
assert_kind_of Fixnum, posts[0]['id']
|
29
|
+
assert_equal 'Hello world!', posts[0]['subject']
|
30
|
+
assert_equal user_id, posts[0]['user_id']
|
31
|
+
|
32
|
+
assert_kind_of Fixnum, posts[1]['id']
|
33
|
+
assert_equal 'Hello world!', posts[1]['subject']
|
34
|
+
assert_equal user_id, posts[1]['user_id']
|
35
|
+
end
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
There are many problems with this approach of JSON matching:
|
40
|
+
|
41
|
+
* It could get out of hand really quickly
|
42
|
+
* It is not very readable
|
43
|
+
* It flattens the structure of the JSON and it's difficult to visualize what the JSON actually looks like
|
44
|
+
* It does not guard against extra parameters that you might have accidentally included (password hashes, credit card numbers etc)
|
45
|
+
* Matching nested objects and arrays is tricky, especially when you don't want to enforce a particular ordering of the returned objects
|
46
|
+
|
47
|
+
json_expression allows you to express the structure and content of the JSON you're expecting with very readable Ruby code while preserving the flexibility of the "manual" approach.
|
48
|
+
|
49
|
+
## Dependencies
|
50
|
+
|
51
|
+
* Ruby 1.9+
|
52
|
+
|
53
|
+
## Usage
|
54
|
+
|
55
|
+
Add it to your Gemfile:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
gem 'json_expressions'
|
59
|
+
```
|
60
|
+
|
61
|
+
Then add this to your test/spec helper file:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
# MiniTest::Unit example
|
65
|
+
require 'json_expressions/minitest'
|
66
|
+
```
|
67
|
+
|
68
|
+
Which allows you to do...
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# MiniTest::Unit example
|
72
|
+
class UsersControllerTest < MiniTest::Unit::TestCase
|
73
|
+
def test_get_a_user
|
74
|
+
server_response = get '/users/chancancode.json'
|
75
|
+
|
76
|
+
# This is what we expect the returned JSON to look like
|
77
|
+
pattern = {
|
78
|
+
'user' => {
|
79
|
+
'id' => :user_id, # "Capture" this value for later
|
80
|
+
'username' => 'chancancode', # Match this exact string
|
81
|
+
'full_name' => 'Godfrey Chan',
|
82
|
+
'email' => 'godfrey@example.com',
|
83
|
+
'type' => 'Administrator',
|
84
|
+
'points' => Fixnum, # Any integer value
|
85
|
+
'homepage' => /\Ahttps?\:\/\/.*\z/i, # Let's get serious
|
86
|
+
'created_at' => WILDCARD_MATCHER, # Don't care as long as it exists
|
87
|
+
'updated_at' => WILDCARD_MATCHER,
|
88
|
+
'posts' => [
|
89
|
+
{
|
90
|
+
'id' => Fixnum,
|
91
|
+
'subject' => 'Hello world!',
|
92
|
+
'user_id' => :user_id # Match against the captured value
|
93
|
+
}.ignore_extra_keys!, # Skip the uninteresting stuff
|
94
|
+
{
|
95
|
+
'id' => Fixnum,
|
96
|
+
'subject' => 'An awesome blog post',
|
97
|
+
'user_id' => :user_id
|
98
|
+
}.ignore_extra_keys!
|
99
|
+
].ordered! # Ensure they are returned in this exact order
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
matcher = assert_json_match pattern, server_response.body # Returns the Matcher object
|
104
|
+
|
105
|
+
# You can use the captured values for other purposes
|
106
|
+
assert matcher.captures[:user_id] > 0
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
### Basic Matching
|
112
|
+
|
113
|
+
This pattern
|
114
|
+
```ruby
|
115
|
+
{
|
116
|
+
'integer' => 1,
|
117
|
+
'float' => 1.1,
|
118
|
+
'string' => 'Hello world!',
|
119
|
+
'boolean' => true,
|
120
|
+
'array' => [1,2,3],
|
121
|
+
'object' => {'key1' => 'value1','key2' => 'value2'},
|
122
|
+
'null' => nil,
|
123
|
+
}
|
124
|
+
```
|
125
|
+
matches the JSON object
|
126
|
+
```json
|
127
|
+
{
|
128
|
+
"integer": 1,
|
129
|
+
"float": 1.1,
|
130
|
+
"string": "Hello world!",
|
131
|
+
"boolean": true,
|
132
|
+
"array": [1,2,3],
|
133
|
+
"object": {"key1": "value1", "key2": "value2"},
|
134
|
+
"null": null
|
135
|
+
}
|
136
|
+
```
|
137
|
+
|
138
|
+
### Wildcard Matching
|
139
|
+
|
140
|
+
You can use the WILDCARD_MATCHER to ignore keys that you don't care about (other than the fact that the key is included in the JSON).
|
141
|
+
|
142
|
+
This pattern
|
143
|
+
```ruby
|
144
|
+
[ WILDCARD_MATCHER, WILDCARD_MATCHER, WILDCARD_MATCHER, WILDCARD_MATCHER, WILDCARD_MATCHER, WILDCARD_MATCHER, WILDCARD_MATCHER ]
|
145
|
+
```
|
146
|
+
matches the JSON array
|
147
|
+
```json
|
148
|
+
[ 1, 1.1, "Hello world!", true, [1,2,3], {"key1": "value1","key2": "value2"}, null]
|
149
|
+
```
|
150
|
+
|
151
|
+
Furthermore, because the pattern is expressed in plain old Ruby code, you can also write:
|
152
|
+
```ruby
|
153
|
+
[ WILDCARD_MATCHER ] * 7
|
154
|
+
```
|
155
|
+
|
156
|
+
### Pattern Matching
|
157
|
+
|
158
|
+
When an object `.respond_to? :match`, `match` will be called to match against the corresponding value in the JSON. Notably, this includes `Regexp` objects, which means you can use regular expressions in your pattern:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
{ 'hex' => /\A0x[0-9a-f]+\z/i }
|
162
|
+
```
|
163
|
+
matches
|
164
|
+
```json
|
165
|
+
{ "hex": "0xC0FFEE" }
|
166
|
+
```
|
167
|
+
but not
|
168
|
+
```json
|
169
|
+
{ "hex": "Hello world!" }
|
170
|
+
```
|
171
|
+
|
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 wolrd!'.match '' => #<MatchData "">`!).
|
173
|
+
|
174
|
+
You can specific a list of classes/modules with undesirable `match` behavior, and json_expression will fall back to calling `===` on them instead (see the section below for `===` vs `==`).
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
# This is the default setting
|
178
|
+
JsonExpressions::Matcher.skip_match_on = [ String ]
|
179
|
+
|
180
|
+
# To add more modules/classes
|
181
|
+
# JsonExpressions::Matcher.skip_match_on << MyClass
|
182
|
+
|
183
|
+
# To turn this off completely
|
184
|
+
# JsonExpressions::Matcher.skip_match_on = [ BasicObject ]
|
185
|
+
```
|
186
|
+
|
187
|
+
### Type Matching
|
188
|
+
|
189
|
+
For objects that does not `respond_to? :match` or those you opt-ed out explicitly (such as `String`), `===` will be called to match against the corresponding value in the JSON. For most classes, this behaves identical to `==`. A notably 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
|
+
```ruby
|
191
|
+
{
|
192
|
+
'integer' => Fixnum,
|
193
|
+
'float' => Float,
|
194
|
+
'string' => String,
|
195
|
+
'boolean' => Boolean,
|
196
|
+
'array' => Array,
|
197
|
+
'object' => Hash,
|
198
|
+
'null' => NilClass,
|
199
|
+
}
|
200
|
+
```
|
201
|
+
matches the JSON object
|
202
|
+
```json
|
203
|
+
{
|
204
|
+
"integer": 1,
|
205
|
+
"float": 1.1,
|
206
|
+
"string": "Hello world!",
|
207
|
+
"boolean": true,
|
208
|
+
"array": [1,2,3],
|
209
|
+
"object": {"key1": "value1", "key2": "value2"},
|
210
|
+
"null": null
|
211
|
+
}
|
212
|
+
```
|
213
|
+
|
214
|
+
You can specific a list of classes/modules with undesirable `===` behavior, and json_expression will fall back to calling `==` on them instead.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
# This is the default setting
|
218
|
+
JsonExpressions::Matcher.skip_triple_equal_on = [ ]
|
219
|
+
|
220
|
+
# To add more modules/classes
|
221
|
+
# JsonExpressions::Matcher.skip_triple_equal_on << MyClass
|
222
|
+
|
223
|
+
# To turn this off completely
|
224
|
+
# JsonExpressions::Matcher.skip_triple_equal_on = [ BasicObject ]
|
225
|
+
```
|
226
|
+
|
227
|
+
### Capturing
|
228
|
+
|
229
|
+
Similar to how "capturs" work in Regex, you can capture the value of certain keys for later use:
|
230
|
+
```ruby
|
231
|
+
matcher = JsonExpressions::Matcher.new({
|
232
|
+
'key1' => :key1,
|
233
|
+
'key2' => :key2,
|
234
|
+
'key3' => :key3
|
235
|
+
})
|
236
|
+
|
237
|
+
matcher =~ JSON.parse('{"key1":"value1", "key2":"value2", "key3":"value3"}') # => true
|
238
|
+
|
239
|
+
matcher.captures[:key1] # => "value1"
|
240
|
+
matcher.captures[:key2] # => "value2"
|
241
|
+
matcher.captures[:key3] # => "value3"
|
242
|
+
```
|
243
|
+
|
244
|
+
If the same symbol is used multiple times, json_expression will make sure they agree. This pattern
|
245
|
+
```ruby
|
246
|
+
{
|
247
|
+
'key1' => :capture_me,
|
248
|
+
'key2' => :capture_me,
|
249
|
+
'key3' => :capture_me
|
250
|
+
}
|
251
|
+
```
|
252
|
+
matches
|
253
|
+
```json
|
254
|
+
{
|
255
|
+
"key1": "Hello world!",
|
256
|
+
"key2": "Hello world!",
|
257
|
+
"key3": "Hello world!"
|
258
|
+
}
|
259
|
+
```
|
260
|
+
but not
|
261
|
+
```json
|
262
|
+
{
|
263
|
+
"key1": "value1",
|
264
|
+
"key2": "value2",
|
265
|
+
"key3": "value3"
|
266
|
+
}
|
267
|
+
```
|
268
|
+
|
269
|
+
### Ordering
|
270
|
+
|
271
|
+
By default, all arrays and JSON objects (i.e. Ruby hashes) are assumed to be unordered. This means
|
272
|
+
```ruby
|
273
|
+
[ 1, 2, 3, 4, 5 ]
|
274
|
+
```
|
275
|
+
will match
|
276
|
+
```json
|
277
|
+
[ 5, 3, 2, 1, 4 ]
|
278
|
+
```
|
279
|
+
and
|
280
|
+
```ruby
|
281
|
+
{ 'key1' => 'value1', 'key2' => 'value2' }
|
282
|
+
```
|
283
|
+
will match
|
284
|
+
```json
|
285
|
+
{ "key2": "value2", "key1": "value1" }
|
286
|
+
```
|
287
|
+
|
288
|
+
You can change this behavior in a case-by-case manner:
|
289
|
+
```ruby
|
290
|
+
{
|
291
|
+
"unordered_array" => [1,2,3,4,5].unordered!, # calling unordered! is optional as it's the default
|
292
|
+
"ordered_array" => [1,2,3,4,5].ordered!,
|
293
|
+
"unordered_hash" => {'a'=>1, 'b'=>2}.unordered!,
|
294
|
+
"ordered_hash" => {'a'=>1, 'b'=>2}.ordered!
|
295
|
+
}
|
296
|
+
```
|
297
|
+
|
298
|
+
Or you can change the defaults:
|
299
|
+
```ruby
|
300
|
+
# Default for these are true
|
301
|
+
JsonExpressions::Matcher.assume_unordered_arrays = false
|
302
|
+
JsonExpressions::Matcher.assume_unordered_hashes = false
|
303
|
+
```
|
304
|
+
|
305
|
+
### Strictness
|
306
|
+
|
307
|
+
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
|
+
```ruby
|
309
|
+
[ 1, 2, 3, 4, 5 ]
|
310
|
+
```
|
311
|
+
will not match
|
312
|
+
```json
|
313
|
+
[ 1, 2, 3, 4, 5, 6 ]
|
314
|
+
```
|
315
|
+
and
|
316
|
+
```ruby
|
317
|
+
{ 'key1' => 'value1', 'key2' => 'value2' }
|
318
|
+
```
|
319
|
+
will not match
|
320
|
+
```json
|
321
|
+
{ "key1": "value1", "key2": "value2", "key3": "value3" }
|
322
|
+
```
|
323
|
+
|
324
|
+
You can change this behavior in a case-by-case manner:
|
325
|
+
```ruby
|
326
|
+
{
|
327
|
+
"strict_array" => [1,2,3,4,5].strict!, # calling strict! is optional as it's the default
|
328
|
+
"forgiving_array" => [1,2,3,4,5].forgiving!,
|
329
|
+
"strict_hash" => {'a'=>1, 'b'=>2}.strict!,
|
330
|
+
"forgiving_hash" => {'a'=>1, 'b'=>2}.forgiving!
|
331
|
+
}
|
332
|
+
```
|
333
|
+
|
334
|
+
They also come with some more sensible aliases:
|
335
|
+
```ruby
|
336
|
+
{
|
337
|
+
"strict_array" => [1,2,3,4,5].reject_extra_values!,
|
338
|
+
"forgiving_array" => [1,2,3,4,5].ignore_extra_values!,
|
339
|
+
"strict_hash" => {'a'=>1, 'b'=>2}.reject_extra_keys!,
|
340
|
+
"forgiving_hash" => {'a'=>1, 'b'=>2}.ignore_extra_keys!
|
341
|
+
}
|
342
|
+
```
|
343
|
+
|
344
|
+
Or you can change the defaults:
|
345
|
+
```ruby
|
346
|
+
# Default for these are true
|
347
|
+
JsonExpressions::Matcher.assume_strict_arrays = false
|
348
|
+
JsonExpressions::Matcher.assume_strict_hashes = false
|
349
|
+
```
|
350
|
+
|
351
|
+
## Testing Frameworks Support
|
352
|
+
|
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. Currently, the gem only ships with helpers for `MiniTest::Unit`. `MiniTest::Spec` and `RSpec` are on my TODO list, but it is not a high priority for me personally, as I currently don't use these frameworks actively in my projects. 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
|
+
|
355
|
+
## Contributing
|
356
|
+
|
357
|
+
Please use the [GitHub issue tracker](https://github.com/chancancode/json_expressions/issues) for bugs and feature requests. If you could submit a pull request - that's even better!
|
358
|
+
|
359
|
+
## License
|
360
|
+
|
361
|
+
This library is distributed under the MIT license. Please see the LICENSE file.
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module JsonExpressions
|
2
|
+
module Strict; end
|
3
|
+
module Forgiving; end
|
4
|
+
module Ordered; end
|
5
|
+
module Unordered; end
|
6
|
+
|
7
|
+
module CoreExtensions
|
8
|
+
def ordered?
|
9
|
+
self.is_a? Ordered
|
10
|
+
end
|
11
|
+
|
12
|
+
def unordered?
|
13
|
+
self.is_a? Unordered
|
14
|
+
end
|
15
|
+
|
16
|
+
def ordered!
|
17
|
+
if self.unordered?
|
18
|
+
raise "cannot mark an unordered #{self.class} as ordered!"
|
19
|
+
else
|
20
|
+
self.extend Ordered
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def unordered!
|
25
|
+
if self.ordered?
|
26
|
+
raise "cannot mark an ordered #{self.class} as unordered!"
|
27
|
+
else
|
28
|
+
self.extend Unordered
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def strict?
|
33
|
+
self.is_a? Strict
|
34
|
+
end
|
35
|
+
|
36
|
+
def forgiving?
|
37
|
+
self.is_a? Forgiving
|
38
|
+
end
|
39
|
+
|
40
|
+
def strict!
|
41
|
+
if self.forgiving?
|
42
|
+
raise "cannot mark a forgiving #{self.class} as strict!"
|
43
|
+
else
|
44
|
+
self.extend Strict
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def forgiving!
|
49
|
+
if self.strict?
|
50
|
+
raise "cannot mark a strict #{self.class} as forgiving!"
|
51
|
+
else
|
52
|
+
self.extend Forgiving
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Hash
|
59
|
+
include JsonExpressions::CoreExtensions
|
60
|
+
alias_method :reject_extra_keys!, :strict!
|
61
|
+
alias_method :ignore_extra_keys!, :forgiving!
|
62
|
+
end
|
63
|
+
|
64
|
+
class Array
|
65
|
+
include JsonExpressions::CoreExtensions
|
66
|
+
alias_method :reject_extra_values!, :strict!
|
67
|
+
alias_method :ignore_extra_values!, :forgiving!
|
68
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'json_expressions'
|
2
|
+
require 'json_expressions/core_extensions'
|
3
|
+
|
4
|
+
module JsonExpressions
|
5
|
+
class Matcher
|
6
|
+
class << self
|
7
|
+
# JsonExpressions::Matcher.skip_match_on (Array)
|
8
|
+
# An array of classes and modules with undesirable `match` behavior
|
9
|
+
# Default: [ String ]
|
10
|
+
attr_accessor :skip_match_on
|
11
|
+
JsonExpressions::Matcher.skip_match_on = [ String ]
|
12
|
+
|
13
|
+
# JsonExpressions::Matcher.skip_triple_equal_on (Array)
|
14
|
+
# An array of classes and modules with undesirable `===` behavior
|
15
|
+
# Default: []
|
16
|
+
attr_accessor :skip_triple_equal_on
|
17
|
+
JsonExpressions::Matcher.skip_triple_equal_on = []
|
18
|
+
|
19
|
+
# JsonExpressions::Matcher.assume_unordered_arrays (Boolean)
|
20
|
+
# By default, assume arrays are unordered when not specified
|
21
|
+
# Default: true
|
22
|
+
attr_accessor :assume_unordered_arrays
|
23
|
+
JsonExpressions::Matcher.assume_unordered_arrays = true
|
24
|
+
|
25
|
+
# JsonExpressions::Matcher.assume_strict_arrays (Boolean)
|
26
|
+
# By default, reject arrays with extra elements when not specified
|
27
|
+
# Default: true
|
28
|
+
attr_accessor :assume_strict_arrays
|
29
|
+
JsonExpressions::Matcher.assume_strict_arrays = true
|
30
|
+
|
31
|
+
# JsonExpressions::Matcher.assume_unordered_hashes (Boolean)
|
32
|
+
# By default, assume hashes are unordered when not specified
|
33
|
+
# Default: true
|
34
|
+
attr_accessor :assume_unordered_hashes
|
35
|
+
JsonExpressions::Matcher.assume_unordered_hashes = true
|
36
|
+
|
37
|
+
# JsonExpressions::Matcher.assume_strict_hashes (Boolean)
|
38
|
+
# By default, reject hashes with extra keys when not specified
|
39
|
+
# Default: true
|
40
|
+
attr_accessor :assume_strict_hashes
|
41
|
+
JsonExpressions::Matcher.assume_strict_hashes = true
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :last_error
|
45
|
+
attr_reader :captures
|
46
|
+
|
47
|
+
def initialize(json, options = {})
|
48
|
+
defaults = {}
|
49
|
+
@json = json
|
50
|
+
@options = defaults.merge(options)
|
51
|
+
reset!
|
52
|
+
end
|
53
|
+
|
54
|
+
def =~(other)
|
55
|
+
reset!
|
56
|
+
match_json('(JSON ROOT)', @json, other)
|
57
|
+
end
|
58
|
+
|
59
|
+
def match(other)
|
60
|
+
self =~ other
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
@json.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def reset!
|
70
|
+
@last_errot = nil
|
71
|
+
@captures = {}
|
72
|
+
end
|
73
|
+
|
74
|
+
def match_json(path, matcher, other)
|
75
|
+
if matcher.is_a? Symbol
|
76
|
+
capture path, matcher, other
|
77
|
+
elsif matcher.is_a? Array
|
78
|
+
match_array path, matcher, other
|
79
|
+
elsif matcher.is_a? Hash
|
80
|
+
match_hash path, matcher, other
|
81
|
+
elsif matcher.respond_to?(:match) && matchable?(matcher)
|
82
|
+
match_obj path, matcher, other, :match
|
83
|
+
elsif triple_equable?(matcher)
|
84
|
+
match_obj path, matcher, other, :===
|
85
|
+
else
|
86
|
+
match_obj path, matcher, other, :==
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def capture(path, name, value)
|
91
|
+
if @captures.key? name
|
92
|
+
if match_json nil, @captures[name], value
|
93
|
+
true
|
94
|
+
else
|
95
|
+
set_last_error path, "At %path%: expected capture with key #{name.inspect} and value #{@captures[name]} to match #{value.inspect}"
|
96
|
+
false
|
97
|
+
end
|
98
|
+
else
|
99
|
+
@captures[name] = value
|
100
|
+
true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def match_obj(path, matcher, other, meth)
|
105
|
+
if matcher.__send__ meth, other
|
106
|
+
true
|
107
|
+
else
|
108
|
+
set_last_error path, "At %path%: expected #{matcher.inspect} to match #{other.inspect}"
|
109
|
+
return false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def match_array(path, matcher, other)
|
114
|
+
unless other.is_a? Array
|
115
|
+
set_last_error path, "%path% is not an array"
|
116
|
+
return false
|
117
|
+
end
|
118
|
+
|
119
|
+
apply_array_defaults matcher
|
120
|
+
|
121
|
+
if matcher.size > other.size
|
122
|
+
set_last_error path, "%path% contains too few elements (#{matcher.size} expected but was #{other.size})"
|
123
|
+
return false
|
124
|
+
end
|
125
|
+
|
126
|
+
if matcher.strict? && matcher.size < other.size
|
127
|
+
set_last_error path, "%path% contains too many elements (#{matcher.size} expected but was #{other.size})"
|
128
|
+
return false
|
129
|
+
end
|
130
|
+
|
131
|
+
if matcher.ordered?
|
132
|
+
matcher.zip(other).each_with_index { |(v1,v2),i| return false unless match_json(make_path(path,i), v1, v2) }
|
133
|
+
else
|
134
|
+
other = other.clone
|
135
|
+
|
136
|
+
matcher.all? do |v1|
|
137
|
+
if i = other.find_index { |v2| match_json(nil, v1, v2) }
|
138
|
+
other.delete_at i
|
139
|
+
true
|
140
|
+
else
|
141
|
+
set_last_error path, "%path% does not contain an element matching #{v1.inspect}"
|
142
|
+
false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def match_hash(path, matcher, other)
|
149
|
+
unless other.is_a? Hash
|
150
|
+
set_last_error path, "%path% is not a hash"
|
151
|
+
return false
|
152
|
+
end
|
153
|
+
|
154
|
+
apply_hash_defaults matcher
|
155
|
+
|
156
|
+
missing_keys = matcher.keys - other.keys
|
157
|
+
extra_keys = other.keys - matcher.keys
|
158
|
+
|
159
|
+
unless missing_keys.empty?
|
160
|
+
set_last_error path, "%path% does not contain the key #{missing_keys.first}"
|
161
|
+
return false
|
162
|
+
end
|
163
|
+
|
164
|
+
if matcher.strict? && ! extra_keys.empty?
|
165
|
+
set_last_error path, "%path% contains an extra key #{extra_keys.first}"
|
166
|
+
return false
|
167
|
+
end
|
168
|
+
|
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})"
|
171
|
+
return false
|
172
|
+
end
|
173
|
+
|
174
|
+
matcher.keys.all? { |k| match_json make_path(path,k), matcher[k], other[k] }
|
175
|
+
end
|
176
|
+
|
177
|
+
def set_last_error(path, message)
|
178
|
+
@last_error = message.gsub('%path%',path) if path
|
179
|
+
end
|
180
|
+
|
181
|
+
def make_path(path, segment)
|
182
|
+
if path
|
183
|
+
if segment.is_a? Fixnum
|
184
|
+
path + "[#{segment}]"
|
185
|
+
else
|
186
|
+
path + ".#{segment}"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def apply_array_defaults(array)
|
192
|
+
if ! array.ordered? && ! array.unordered?
|
193
|
+
self.class.assume_unordered_arrays ? array.unordered! : array.ordered!
|
194
|
+
end
|
195
|
+
|
196
|
+
if ! array.strict? && ! array.forgiving?
|
197
|
+
self.class.assume_strict_arrays ? array.strict! : array.forgiving!
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def apply_hash_defaults(hash)
|
202
|
+
if ! hash.ordered? && ! hash.unordered?
|
203
|
+
self.class.assume_unordered_hashes ? hash.unordered! : hash.ordered!
|
204
|
+
end
|
205
|
+
|
206
|
+
if ! hash.strict? && ! hash.forgiving?
|
207
|
+
self.class.assume_strict_hashes ? hash.strict! : hash.forgiving!
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def matchable?(obj)
|
212
|
+
if self.class.skip_match_on.include? obj.class
|
213
|
+
false
|
214
|
+
else
|
215
|
+
self.class.skip_match_on.none? { |klass| obj.is_a? klass }
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def triple_equable?(obj)
|
220
|
+
if self.class.skip_triple_equal_on.include? obj.class
|
221
|
+
false
|
222
|
+
else
|
223
|
+
self.class.skip_triple_equal_on.none? { |klass| obj.is_a? klass }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module JsonExpressions
|
4
|
+
module MiniTest
|
5
|
+
module Unit
|
6
|
+
module Helpers
|
7
|
+
def assert_json_match(exp, act, msg = nil)
|
8
|
+
unless JsonExpressions::Matcher === exp
|
9
|
+
exp = JsonExpressions::Matcher.new(exp)
|
10
|
+
end
|
11
|
+
|
12
|
+
if String === act
|
13
|
+
assert act = JSON.parse(act), "Expected #{mu_pp(act)} to be valid JSON"
|
14
|
+
end
|
15
|
+
|
16
|
+
assert exp =~ act, ->{exp.last_error}
|
17
|
+
|
18
|
+
# Return the matcher
|
19
|
+
return exp
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'json_expressions/matcher'
|
2
|
+
|
3
|
+
module JsonExpressions
|
4
|
+
WILDCARD_MATCHER = Object.new
|
5
|
+
|
6
|
+
def WILDCARD_MATCHER.is_a?(klass)
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
def WILDCARD_MATCHER.==(other)
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def WILDCARD_MATCHER.=~(other)
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def WILDCARD_MATCHER.match(other)
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def WILDCARD_MATCHER.to_s
|
23
|
+
'WILDCARD_MATCHER'
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: json_expressions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Godfrey Chan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-10 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: JSON matchmaking for all your API testing needs.
|
15
|
+
email:
|
16
|
+
- godfreykfc@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/json_expressions/core_extensions.rb
|
22
|
+
- lib/json_expressions/matcher.rb
|
23
|
+
- lib/json_expressions/minitest/unit/helpers.rb
|
24
|
+
- lib/json_expressions/minitest.rb
|
25
|
+
- lib/json_expressions.rb
|
26
|
+
- README.md
|
27
|
+
- LICENSE
|
28
|
+
homepage: https://github.com/chancancode/json_expressions
|
29
|
+
licenses: []
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.3.6
|
46
|
+
requirements: []
|
47
|
+
rubyforge_project:
|
48
|
+
rubygems_version: 1.8.24
|
49
|
+
signing_key:
|
50
|
+
specification_version: 3
|
51
|
+
summary: JSON Expressions
|
52
|
+
test_files: []
|