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 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. 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:
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 'Hello world!', posts[1]['subject']
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
- Then add this to your test/spec helper file:
62
-
66
+ Add this to your test/spec helper file:
63
67
  ```ruby
64
- # MiniTest::Unit example
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
- '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' => [
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
- 'id' => Fixnum,
91
- 'subject' => 'Hello world!',
92
- 'user_id' => :user_id # Match against the captured value
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
- 'id' => Fixnum,
96
- 'subject' => 'An awesome blog post',
97
- 'user_id' => :user_id
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 they are returned in this exact order
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
- '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,
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 the key is included in the JSON).
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 expressed in plain old Ruby code, you can also write:
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, this includes `Regexp` objects, which means you can use regular expressions in your pattern:
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
- { 'hex' => /\A0x[0-9a-f]+\z/i }
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 wolrd!'.match '' => #<MatchData "">`!).
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 them instead (see the section below for `===` vs `==`).
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 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:
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
- 'integer' => Fixnum,
193
- 'float' => Float,
194
- 'string' => String,
195
- 'boolean' => Boolean,
196
- 'array' => Array,
197
- 'object' => Hash,
198
- 'null' => NilClass,
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 "capturs" work in Regex, you can capture the value of certain keys for later use:
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
- 'key1' => :key1,
233
- 'key2' => :key2,
234
- 'key3' => :key3
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
- 'key1' => :capture_me,
248
- 'key2' => :capture_me,
249
- 'key3' => :capture_me
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
- { 'key1' => 'value1', 'key2' => 'value2' }
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
- { 'key1' => 'value1', 'key2' => 'value2' }
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
- "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!
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
- "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!
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
- ## Testing Frameworks Support
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. 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).
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
- if segment.is_a? Fixnum
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
 
@@ -1,3 +1,4 @@
1
+ require 'minitest'
1
2
  require 'json_expressions'
2
3
  require 'json_expressions/minitest/unit/helpers'
3
4
 
@@ -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,7 @@
1
+ require 'rspec'
2
+ require 'json_expressions'
3
+ require 'json_expressions/rspec/matchers'
4
+
5
+ RSpec::configure do |config|
6
+ config.include(JsonExpressions::RSpec::Matchers)
7
+ end
@@ -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.5.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-10 00:00:00.000000000 Z
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