├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── json_expressions.gemspec ├── lib ├── json_expressions.rb └── json_expressions │ ├── core_extensions.rb │ ├── matcher.rb │ ├── minitest.rb │ ├── minitest │ └── assertions.rb │ ├── rspec.rb │ ├── rspec │ ├── matchers.rb │ └── matchers │ │ └── match_json_expression.rb │ ├── test │ └── unit │ │ └── helpers.rb │ └── testunit.rb ├── spec ├── json_expressions │ ├── rspec │ │ └── matchers │ │ │ └── match_json_expression_spec.rb │ └── rspec_spec.rb └── spec_helper.rb └── test ├── json_expressions ├── minitest │ └── test_assertions.rb ├── test_core_extensions.rb ├── test_matcher.rb └── test_minitest.rb ├── minitest_helper.rb └── test_json_expressions.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | doc/ 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.9.0 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.8.3...0.9.0) 2 | 3 | * Support for unified Integer class in Ruby 2.4+ (#39 from @iamliamnorton) 4 | 5 | ### v0.8.3 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.8.2...0.8.3) 6 | 7 | * Print more detailed error message when matching arrays (#26 from @Maxim-Filimonov) 8 | * Add support for rspec 3+ (#24 from @george) 9 | * Minitest 5 compat (#21 from @ffmike) 10 | * Support using match_json_expression as an argument matcher for rspec. (#17 by @cupakromer) 11 | 12 | ### v0.8.2 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.8.1...0.8.2) 13 | 14 | * Bugfix: require 'rspec/core' instead of 'rspec' (#12 by @pda) 15 | * Improved matcher output when using RSpec (#11 by @milkcocoa) 16 | * Bugfix: fixed a bug where reusing the same matcher sometimes causes false negatives (#10 by @kophyo) 17 | * Various documentation improvements 18 | * Various Rakefile improvements. The gem now builds correctly on Travis 19 | 20 | ### v0.8.1 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.8.0...0.8.1) 21 | 22 | * Fat finger: reverted a change in 0.8.0 which changed the default value of `assume_unordered_arrays` from true to false. Added tests to make sure this never happens again. 23 | 24 | ### v0.8.0 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.7.2...0.8.0) 25 | 26 | * Added Test::Unit support. 27 | * Added MiniTest::Spec support. 28 | * BREAKING: Changed internal structure of MiniTest support code. This shouldn't affect you unless you have been manually requiring and including the MiniTest helpers yourself. 29 | * Use of `WILDCARD_MATCHER` (the constant) inside a `MiniTest::Unit::TestCase` is now discouraged. Instead, you are encouraged to use `wildcard_matcher` (the method) instead. README has been updated. 30 | * Removed WILDCARD_MATCHER#match and the corresponding test. Since support for Object#match has been removed in v0.7.0, this should no longer be necessary. 31 | 32 | ### v0.7.2 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.7.1...0.7.2) 33 | 34 | * Bugfix: Corrected a misbehaving require statement in minitest helpers (Fixes #2) 35 | 36 | ### v0.7.1 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.7.0...0.7.1) 37 | 38 | * Bugfix: Correctly matching `false` inside a symbol-keyed hash (Fixes #1) 39 | 40 | ### v0.7.0 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.6.0...0.7.0) 41 | 42 | * BREAKING: Removed support for Object#match in favor of Object#=== 43 | * BREAKING: Removed configuration option JsonExpressions::Matcher.skip_match_on 44 | 45 | ### v0.6.0 [view commit logs](https://github.com/chancancode/json_expressions/compare/0.5.0...0.6.0) 46 | 47 | * Added non-bang version of `strict`, `forgiving`, `ordered` and `unordered` 48 | * Added RSpec support (thanks @bobthecow for the [initial implementation](https://gist.github.com/3086558)) 49 | * Added support for `Symobl` hash keys 50 | * Switched examples in README to use `Symbol` hash keys 51 | * Improved error messages 52 | 53 | ### v0.5.0 54 | 55 | * Initial release 56 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :test do 4 | gem 'rake' 5 | gem 'turn' 6 | gem 'guard' 7 | gem 'guard-minitest' 8 | gem 'guard-rspec' 9 | gem 'growl' 10 | gem 'minitest' 11 | gem 'rspec' 12 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ansi (1.4.3) 5 | diff-lcs (1.1.3) 6 | ffi (1.0.11) 7 | growl (1.0.3) 8 | guard (1.2.3) 9 | listen (>= 0.4.2) 10 | thor (>= 0.14.6) 11 | guard-minitest (0.5.0) 12 | guard (>= 0.4) 13 | guard-rspec (1.2.0) 14 | guard (>= 1.1) 15 | listen (0.4.7) 16 | rb-fchange (~> 0.0.5) 17 | rb-fsevent (~> 0.9.1) 18 | rb-inotify (~> 0.8.8) 19 | minitest (3.2.0) 20 | rake (10.0.3) 21 | rb-fchange (0.0.5) 22 | ffi 23 | rb-fsevent (0.9.1) 24 | rb-inotify (0.8.8) 25 | ffi (>= 0.5.0) 26 | rspec (2.9.0) 27 | rspec-core (~> 2.9.0) 28 | rspec-expectations (~> 2.9.0) 29 | rspec-mocks (~> 2.9.0) 30 | rspec-core (2.9.0) 31 | rspec-expectations (2.9.0) 32 | diff-lcs (~> 1.1.3) 33 | rspec-mocks (2.9.0) 34 | thor (0.15.4) 35 | turn (0.9.6) 36 | ansi 37 | 38 | PLATFORMS 39 | ruby 40 | 41 | DEPENDENCIES 42 | growl 43 | guard 44 | guard-minitest 45 | guard-rspec 46 | minitest 47 | rake 48 | rspec 49 | turn 50 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'minitest', :rubygems => true do 2 | watch(%r|^test/(.*/)?test_(.*)\.rb|) 3 | watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" } 4 | watch(%r|^test/minitest_helper\.rb|) { "test" } 5 | end 6 | 7 | guard 'rspec' do 8 | watch(%r|^spec/(.*/)?(.*)_spec\.rb|) 9 | watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 10 | watch(%r|^spec/spec_helper\.rb|) { "spec" } 11 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSON Expressions 2 | ================ 3 | 4 | [![Build Status](https://travis-ci.org/chancancode/json_expressions.png?branch=master)](https://travis-ci.org/chancancode/json_expressions) 5 | 6 | ## Introduction 7 | 8 | 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: 9 | 10 | ```ruby 11 | # MiniTest::Unit example 12 | class UsersControllerTest < MiniTest::Unit::TestCase 13 | def test_get_a_user 14 | server_response = get '/users/chancancode.json' 15 | 16 | json = JSON.parse server_response.body 17 | 18 | assert user = json['user'] 19 | 20 | assert user_id = user['id'] 21 | assert_equal 'chancancode', user['username'] 22 | assert_equal 'Godfrey Chan', user['full_name'] 23 | assert_equal 'godfrey@example.com', user['email'] 24 | assert_equal 'Administrator', user['type'] 25 | assert_kind_of Integer, user['points'] 26 | assert_match /\Ahttps?\:\/\/.*\z/i, user['homepage'] 27 | 28 | assert posts = user['posts'] 29 | 30 | assert_kind_of Integer, posts[0]['id'] 31 | assert_equal 'Hello world!', posts[0]['subject'] 32 | assert_equal user_id, posts[0]['user_id'] 33 | assert_include posts[0]['tags'], 'announcement' 34 | assert_include posts[0]['tags'], 'welcome' 35 | assert_include posts[0]['tags'], 'introduction' 36 | 37 | assert_kind_of Integer, posts[1]['id'] 38 | assert_equal 'An awesome blog post', posts[1]['subject'] 39 | assert_equal user_id, posts[1]['user_id'] 40 | assert_include posts[0]['tags'], 'blog' 41 | assert_include posts[0]['tags'], 'life' 42 | end 43 | end 44 | ``` 45 | 46 | There are many problems with this approach of JSON matching: 47 | 48 | * It could get out of hand really quickly 49 | * It is not very readable 50 | * It flattens the structure of the JSON and it's difficult to visualize what the JSON actually looks like 51 | * It does not guard against extra parameters that you might have accidentally included (password hashes, credit card numbers etc) 52 | * Matching nested objects and arrays is tricky, especially when you don't want to enforce a particular ordering of the returned objects 53 | 54 | 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. 55 | 56 | ## Dependencies 57 | 58 | * Ruby 1.9+ 59 | 60 | ## Usage 61 | 62 | Add it to your Gemfile: 63 | 64 | ```ruby 65 | gem 'json_expressions' 66 | ``` 67 | 68 | Add this to your test/spec helper file: 69 | ```ruby 70 | # For MiniTest::Unit 71 | require 'json_expressions/minitest' 72 | 73 | # For RSpec 74 | require 'json_expressions/rspec' 75 | ``` 76 | 77 | Which allows you to do... 78 | ```ruby 79 | # MiniTest::Unit example 80 | class UsersControllerTest < MiniTest::Unit::TestCase 81 | def test_get_a_user 82 | server_response = get '/users/chancancode.json' 83 | 84 | # This is what we expect the returned JSON to look like 85 | pattern = { 86 | user: { 87 | id: :user_id, # "Capture" this value for later 88 | username: 'chancancode', # Match this exact string 89 | full_name: 'Godfrey Chan', 90 | email: 'godfrey@example.com', 91 | type: 'Administrator', 92 | points: Integer, # Any integer value 93 | homepage: /\Ahttps?\:\/\/.*\z/i, # Let's get serious 94 | created_at: wildcard_matcher, # Don't care as long as it exists 95 | updated_at: wildcard_matcher, 96 | posts: [ 97 | { 98 | id: Integer, 99 | subject: 'Hello world!', 100 | user_id: :user_id, # Match against the captured value 101 | tags: [ 102 | 'announcement', 103 | 'welcome', 104 | 'introduction' 105 | ] # Ordering of elements does not matter by default 106 | }.ignore_extra_keys!, # Skip the uninteresting stuff 107 | { 108 | id: Integer, 109 | subject: 'An awesome blog post', 110 | user_id: :user_id, 111 | tags: ['blog' , 'life'] 112 | }.ignore_extra_keys! 113 | ].ordered! # Ensure the posts are in this exact order 114 | } 115 | } 116 | 117 | matcher = assert_json_match pattern, server_response.body # Returns the Matcher object 118 | 119 | # You can use the captured values for other purposes 120 | assert matcher.captures[:user_id] > 0 121 | end 122 | end 123 | 124 | # MiniTest::Spec example 125 | describe UsersController, "#show" do 126 | it "returns a user" do 127 | pattern = # See above... 128 | 129 | server_response = get '/users/chancancode.json' 130 | 131 | server_response.body.must_match_json_expression(pattern) 132 | end 133 | end 134 | 135 | # RSpec example 136 | describe UsersController, "#show" do 137 | it "returns a user" do 138 | pattern = # See above... 139 | 140 | server_response = get '/users/chancancode.json' 141 | 142 | server_response.body.should match_json_expression(pattern) 143 | end 144 | end 145 | ``` 146 | 147 | ### Basic Matching 148 | 149 | This pattern 150 | ```ruby 151 | { 152 | integer: 1, 153 | float: 1.1, 154 | string: 'Hello world!', 155 | boolean: true, 156 | array: [1,2,3], 157 | object: {key1: 'value1',key2: 'value2'}, 158 | null: nil, 159 | } 160 | ``` 161 | matches the JSON object 162 | ```json 163 | { 164 | "integer": 1, 165 | "float": 1.1, 166 | "string": "Hello world!", 167 | "boolean": true, 168 | "array": [1,2,3], 169 | "object": {"key1": "value1", "key2": "value2"}, 170 | "null": null 171 | } 172 | ``` 173 | 174 | ### Wildcard Matching 175 | 176 | You can use `wildcard_matcher` to ignore keys that you don't care about (other than the fact that they exist). 177 | 178 | This pattern 179 | ```ruby 180 | [ wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher ] 181 | ``` 182 | matches the JSON array 183 | ```json 184 | [ 1, 1.1, "Hello world!", true, [1,2,3], {"key1": "value1","key2": "value2"}, null] 185 | ``` 186 | 187 | Furthermore, because the pattern is just plain old Ruby code, you can also write: 188 | ```ruby 189 | [ wildcard_matcher ] * 7 190 | ``` 191 | 192 | Note: Previously, the examples here uses `WILDCARD_MATCHER` which is a constant defined on `MiniTest::Unit::TestCase`. Since 0.8.0, the use of this constant is discouraged because it doesn't work for `MiniTest::Spec` and `RSpec` due to how Ruby scoping works for blocks. Instead, `wildcard_matcher` (a method) has been added. This is now the preferred way to retrieve the wildcard matcher in order to maintain consistency among the different test frameworks. 193 | 194 | ### Object Equality 195 | 196 | By default, json_expressions uses `Object#===` to match against the corresponding value in the target JSON. In most cases, this method behaves exactly the same as `Object#==`. However, certain classes override this method to provide specialized behavior (notably `Regexp`, `Module` and `Range`, see below). If you find this undesirable for certain classes, you can explicitly opt them out and json_expressions will call `Object#==` instead: 197 | 198 | ```ruby 199 | # This is the default setting 200 | JsonExpressions::Matcher.skip_triple_equal_on = [ ] 201 | 202 | # To add more modules/classes 203 | # JsonExpressions::Matcher.skip_triple_equal_on << MyClass 204 | 205 | # To turn this off completely 206 | # JsonExpressions::Matcher.skip_triple_equal_on = [ BasicObject ] 207 | ``` 208 | 209 | ### Regular Expressions 210 | 211 | Since `Regexp` overrides `Object#===` to mean "matches", you can use them in your patterns and json_expressions will do the right thing: 212 | ```ruby 213 | { hex: /\A0x[0-9a-f]+\z/i } 214 | ``` 215 | matches 216 | ```json 217 | { "hex": "0xC0FFEE" } 218 | ``` 219 | but not 220 | ```json 221 | { "hex": "Hello world!" } 222 | ``` 223 | 224 | ### Type Matching 225 | 226 | `Module` (and by inheritance, `Class`) overrides `===` to mean `instance of`. You can exploit this behavior to do type matching: 227 | ```ruby 228 | { 229 | integer: Integer, 230 | float: Float, 231 | string: String, 232 | boolean: Boolean, # See http://stackoverflow.com/questions/3028243/check-if-ruby-object-is-a-boolean#answer-3028378 233 | array: Array, 234 | object: Hash, 235 | null: NilClass, 236 | } 237 | ``` 238 | matches the JSON object 239 | ```json 240 | { 241 | "integer": 1, 242 | "float": 1.1, 243 | "string": "Hello world!", 244 | "boolean": true, 245 | "array": [1,2,3], 246 | "object": {"key1": "value1", "key2": "value2"}, 247 | "null": null 248 | } 249 | ``` 250 | 251 | ### Ranges 252 | 253 | `Range` overrides `===` to mean `include?`. Therefore, 254 | ```ruby 255 | { day: (1..31), month: (1..12) } 256 | ``` 257 | matches the JSON object 258 | ```json 259 | { "day": 3, "month": 11 } 260 | ``` 261 | but not 262 | ```json 263 | { "day": -1, "month": 13 } 264 | ``` 265 | 266 | This is also helpful for comparing Floats to a certain precision. 267 | ```ruby 268 | { pi: 3.141593 } 269 | ``` 270 | won't match 271 | ```json 272 | { "pi": 3.1415926536 } 273 | ``` 274 | But this will: 275 | ```ruby 276 | { pi: (3.141592..3.141593) } 277 | ``` 278 | 279 | ### Capturing 280 | 281 | Similar to how "captures" work in Regexp, you can capture the value of certain keys for later use: 282 | ```ruby 283 | matcher = JsonExpressions::Matcher.new({ 284 | key1: :key1, 285 | key2: :key2, 286 | key3: :key3 287 | }) 288 | 289 | matcher =~ JSON.parse('{"key1":"value1", "key2":"value2", "key3":"value3"}') # => true 290 | 291 | matcher.captures[:key1] # => "value1" 292 | matcher.captures[:key2] # => "value2" 293 | matcher.captures[:key3] # => "value3" 294 | ``` 295 | 296 | If the same symbol is used multiple times, json_expression will make sure they agree. This pattern 297 | ```ruby 298 | { 299 | key1: :capture_me, 300 | key2: :capture_me, 301 | key3: :capture_me 302 | } 303 | ``` 304 | matches 305 | ```json 306 | { 307 | "key1": "Hello world!", 308 | "key2": "Hello world!", 309 | "key3": "Hello world!" 310 | } 311 | ``` 312 | but not 313 | ```json 314 | { 315 | "key1": "value1", 316 | "key2": "value2", 317 | "key3": "value3" 318 | } 319 | ``` 320 | 321 | ### Ordering 322 | 323 | By default, all arrays and JSON objects (i.e. Ruby hashes) are assumed to be unordered. This means 324 | ```ruby 325 | [ 1, 2, 3, 4, 5 ] 326 | ``` 327 | will match 328 | ```json 329 | [ 5, 3, 2, 1, 4 ] 330 | ``` 331 | and 332 | ```ruby 333 | { key1: 'value1', key2: 'value2' } 334 | ``` 335 | will match 336 | ```json 337 | { "key2": "value2", "key1": "value1" } 338 | ``` 339 | 340 | You can change this behavior in a case-by-case manner: 341 | ```ruby 342 | { 343 | unordered_array: [1,2,3,4,5].unordered!, # calling unordered! is optional as it's the default 344 | ordered_array: [1,2,3,4,5].ordered!, 345 | unordered_hash: {a: 1, b: 2}.unordered!, 346 | ordered_hash: {a: 1, b: 2}.ordered! 347 | } 348 | ``` 349 | 350 | Or you can change the defaults: 351 | ```ruby 352 | # Default for these are true 353 | JsonExpressions::Matcher.assume_unordered_arrays = false 354 | JsonExpressions::Matcher.assume_unordered_hashes = false 355 | ``` 356 | 357 | ### "Strictness" 358 | 359 | 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: 360 | ```ruby 361 | [ 1, 2, 3, 4, 5 ] 362 | ``` 363 | will not match 364 | ```json 365 | [ 1, 2, 3, 4, 5, 6 ] 366 | ``` 367 | and 368 | ```ruby 369 | { key1: 'value1', key2: 'value2' } 370 | ``` 371 | will not match 372 | ```json 373 | { "key1": "value1", "key2": "value2", "key3": "value3" } 374 | ``` 375 | 376 | You can change this behavior in a case-by-case manner: 377 | ```ruby 378 | { 379 | strict_array: [1,2,3,4,5].strict!, # calling strict! is optional as it's the default 380 | forgiving_array: [1,2,3,4,5].forgiving!, 381 | strict_hash: {a: 1, b: 2}.strict!, 382 | forgiving_hash: {a: 1, b: 2}.forgiving! 383 | } 384 | ``` 385 | 386 | They also come with some more sensible aliases: 387 | ```ruby 388 | { 389 | strict_array: [1,2,3,4,5].reject_extra_values!, 390 | forgiving_array: [1,2,3,4,5].ignore_extra_values!, 391 | strict_hash: {a: 1, b: 2}.reject_extra_keys!, 392 | forgiving_hash: {a: 1, b: 2}.ignore_extra_keys! 393 | } 394 | ``` 395 | 396 | Or you can change the defaults: 397 | ```ruby 398 | # Default for these are true 399 | JsonExpressions::Matcher.assume_strict_arrays = false 400 | JsonExpressions::Matcher.assume_strict_hashes = false 401 | ``` 402 | 403 | ## Support for other test frameworks 404 | 405 | The `Matcher` class itself is written in a framework-agnostic manner. This allows you to easily write custom helpers/matchers for your favorite testing framework. If you wrote an adapter for another test frameworks and you'd like to share yhat with the world, please open a Pull Request. 406 | 407 | ## Contributing 408 | 409 | 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! 410 | 411 | ## License 412 | 413 | This library is distributed under the MIT license. Please see the LICENSE file. 414 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rspec/core/rake_task' 4 | 5 | task :default => [:test, :spec] 6 | 7 | desc 'Run all test' 8 | Rake::TestTask.new do |t| 9 | t.libs << 'test' 10 | t.test_files = FileList['test/**/test_*.rb'] 11 | t.verbose = true 12 | end 13 | 14 | desc 'Run all spec' 15 | RSpec::Core::RakeTask.new(:spec) do |t| 16 | t.pattern = 'spec/**/*_spec.rb' 17 | t.rspec_opts = ['--color', '--backtrace'] 18 | end -------------------------------------------------------------------------------- /json_expressions.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib/', __FILE__) 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "json_expressions" 7 | s.version = "0.9.0" 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Godfrey Chan"] 10 | s.email = ["godfreykfc@gmail.com"] 11 | s.homepage = "https://github.com/chancancode/json_expressions" 12 | s.summary = "JSON Expressions" 13 | s.description = "JSON matchmaking for all your API testing needs." 14 | 15 | s.required_rubygems_version = ">= 1.3.6" 16 | 17 | s.files = Dir.glob("{lib,vendor}/**/*") + %w(README.md CHANGELOG.md LICENSE) 18 | s.require_path = 'lib' 19 | end 20 | -------------------------------------------------------------------------------- /lib/json_expressions.rb: -------------------------------------------------------------------------------- 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.to_s 19 | 'WILDCARD_MATCHER' 20 | end 21 | end -------------------------------------------------------------------------------- /lib/json_expressions/core_extensions.rb: -------------------------------------------------------------------------------- 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 | self.clone.ordered! 18 | end 19 | 20 | def unordered 21 | self.clone.unordered! 22 | end 23 | 24 | def ordered! 25 | if self.unordered? 26 | raise "cannot mark an unordered #{self.class} as ordered!" 27 | else 28 | self.extend Ordered 29 | end 30 | end 31 | 32 | def unordered! 33 | if self.ordered? 34 | raise "cannot mark an ordered #{self.class} as unordered!" 35 | else 36 | self.extend Unordered 37 | end 38 | end 39 | 40 | def strict? 41 | self.is_a? Strict 42 | end 43 | 44 | def forgiving? 45 | self.is_a? Forgiving 46 | end 47 | 48 | def strict 49 | self.clone.strict! 50 | end 51 | 52 | def forgiving 53 | self.clone.forgiving! 54 | end 55 | 56 | def strict! 57 | if self.forgiving? 58 | raise "cannot mark a forgiving #{self.class} as strict!" 59 | else 60 | self.extend Strict 61 | end 62 | end 63 | 64 | def forgiving! 65 | if self.strict? 66 | raise "cannot mark a strict #{self.class} as forgiving!" 67 | else 68 | self.extend Forgiving 69 | end 70 | end 71 | end 72 | end 73 | 74 | class Hash 75 | include JsonExpressions::CoreExtensions 76 | alias_method :reject_extra_keys, :strict 77 | alias_method :reject_extra_keys!, :strict! 78 | alias_method :ignore_extra_keys, :forgiving 79 | alias_method :ignore_extra_keys!, :forgiving! 80 | end 81 | 82 | class Array 83 | include JsonExpressions::CoreExtensions 84 | alias_method :reject_extra_values, :strict 85 | alias_method :reject_extra_values!, :strict! 86 | alias_method :ignore_extra_values, :forgiving 87 | alias_method :ignore_extra_values!, :forgiving! 88 | end -------------------------------------------------------------------------------- /lib/json_expressions/matcher.rb: -------------------------------------------------------------------------------- 1 | require 'json_expressions' 2 | require 'json_expressions/core_extensions' 3 | 4 | module JsonExpressions 5 | class Matcher 6 | class << self 7 | # JsonExpressions::Matcher.skip_triple_equal_on (Array) 8 | # An array of classes and modules with undesirable `===` behavior 9 | # Default: [] 10 | attr_accessor :skip_triple_equal_on 11 | JsonExpressions::Matcher.skip_triple_equal_on = [] 12 | 13 | # JsonExpressions::Matcher.assume_unordered_arrays (Boolean) 14 | # By default, assume arrays are unordered when not specified 15 | # Default: true 16 | attr_accessor :assume_unordered_arrays 17 | JsonExpressions::Matcher.assume_unordered_arrays = true 18 | 19 | # JsonExpressions::Matcher.assume_strict_arrays (Boolean) 20 | # By default, reject arrays with extra elements when not specified 21 | # Default: true 22 | attr_accessor :assume_strict_arrays 23 | JsonExpressions::Matcher.assume_strict_arrays = true 24 | 25 | # JsonExpressions::Matcher.assume_unordered_hashes (Boolean) 26 | # By default, assume hashes are unordered when not specified 27 | # Default: true 28 | attr_accessor :assume_unordered_hashes 29 | JsonExpressions::Matcher.assume_unordered_hashes = true 30 | 31 | # JsonExpressions::Matcher.assume_strict_hashes (Boolean) 32 | # By default, reject hashes with extra keys when not specified 33 | # Default: true 34 | attr_accessor :assume_strict_hashes 35 | JsonExpressions::Matcher.assume_strict_hashes = true 36 | end 37 | 38 | attr_reader :last_error 39 | attr_reader :captures 40 | 41 | def initialize(json, options = {}) 42 | defaults = {} 43 | @json = json 44 | @options = defaults.merge(options) 45 | reset! 46 | end 47 | 48 | def =~(other) 49 | reset! 50 | match_json('(JSON ROOT)', @json, other) 51 | end 52 | 53 | alias_method :match, :=~ 54 | 55 | def to_s 56 | @json.to_s 57 | end 58 | 59 | private 60 | 61 | def reset! 62 | @last_error = nil 63 | @captures = {} 64 | end 65 | 66 | def match_json(path, matcher, other) 67 | if matcher.is_a? Symbol 68 | capture path, matcher, other 69 | elsif matcher.is_a? Array 70 | match_array path, matcher, other 71 | elsif matcher.is_a? Hash 72 | match_hash path, matcher, other 73 | elsif triple_equable?(matcher) 74 | match_obj path, matcher, other, :=== 75 | else 76 | match_obj path, matcher, other, :== 77 | end 78 | end 79 | 80 | def capture(path, name, value) 81 | if @captures.key? name 82 | if match_json nil, @captures[name], value 83 | true 84 | else 85 | set_last_error path, "At %path%: expected capture with key #{name.inspect} and value #{@captures[name]} to match #{value.inspect}" 86 | false 87 | end 88 | else 89 | @captures[name] = value 90 | true 91 | end 92 | end 93 | 94 | def match_obj(path, matcher, other, meth) 95 | if matcher.__send__ meth, other 96 | true 97 | else 98 | set_last_error path, "At %path%: expected #{matcher.inspect} to match #{other.inspect}" 99 | return false 100 | end 101 | end 102 | 103 | def match_array(path, matcher, other) 104 | unless other.is_a? Array 105 | set_last_error path, "%path% is not an array" 106 | return false 107 | end 108 | 109 | apply_array_defaults matcher 110 | 111 | if matcher.size > other.size 112 | set_last_error path, "%path% contains too few elements (#{matcher.size} expected but was #{other.size})" 113 | return false 114 | end 115 | 116 | if matcher.strict? && matcher.size < other.size 117 | set_last_error path, "%path% contains too many elements (#{matcher.size} expected but was #{other.size})" 118 | return false 119 | end 120 | 121 | if matcher.ordered? 122 | matcher.zip(other).each_with_index { |(v1,v2),i| return false unless match_json(make_path(path,i), v1, v2) } 123 | else 124 | other = other.clone 125 | 126 | matcher.all? do |v1| 127 | if i = other.find_index { |v2| match_json(make_path(path, '*'), v1, v2) } 128 | other.delete_at i 129 | true 130 | else 131 | set_last_error path, "%path% does not contain a matching element for #{v1.inspect} - #{last_error}" 132 | false 133 | end 134 | end 135 | end 136 | end 137 | 138 | def match_hash(path, matcher, other) 139 | unless other.is_a? Hash 140 | set_last_error path, "%path% is not a hash" 141 | return false 142 | end 143 | 144 | apply_hash_defaults matcher 145 | 146 | missing_keys = matcher.keys.map(&:to_s) - other.keys.map(&:to_s) 147 | extra_keys = other.keys.map(&:to_s) - matcher.keys.map(&:to_s) 148 | 149 | unless missing_keys.empty? 150 | set_last_error path, "%path% does not contain the key #{missing_keys.first.to_s}" 151 | return false 152 | end 153 | 154 | if matcher.strict? && ! extra_keys.empty? 155 | set_last_error path, "%path% contains an extra key #{extra_keys.first.to_s}" 156 | return false 157 | end 158 | 159 | if matcher.ordered? && matcher.keys != other.keys 160 | set_last_error path, "Incorrect key-ordering at %path% (#{matcher.keys.map(&:to_s).inspect} expected but was #{other.keys.map(&:to_s).inspect})" 161 | return false 162 | end 163 | 164 | matcher.keys.all? do |k| 165 | match_json make_path(path,k), matcher[k], other.key?(k.to_s) ? other[k.to_s] : other[k.to_sym] 166 | end 167 | end 168 | 169 | def set_last_error(path, message) 170 | @last_error = message.gsub('%path%',path) if path 171 | end 172 | 173 | def make_path(path, segment) 174 | if path 175 | segment.is_a?(0.class) ? path + "[#{segment}]" : path + ".#{segment.to_s}" 176 | end 177 | end 178 | 179 | def apply_array_defaults(array) 180 | if ! array.ordered? && ! array.unordered? 181 | self.class.assume_unordered_arrays ? array.unordered! : array.ordered! 182 | end 183 | 184 | if ! array.strict? && ! array.forgiving? 185 | self.class.assume_strict_arrays ? array.strict! : array.forgiving! 186 | end 187 | end 188 | 189 | def apply_hash_defaults(hash) 190 | if ! hash.ordered? && ! hash.unordered? 191 | self.class.assume_unordered_hashes ? hash.unordered! : hash.ordered! 192 | end 193 | 194 | if ! hash.strict? && ! hash.forgiving? 195 | self.class.assume_strict_hashes ? hash.strict! : hash.forgiving! 196 | end 197 | end 198 | 199 | def triple_equable?(obj) 200 | if self.class.skip_triple_equal_on.include? obj.class 201 | false 202 | else 203 | self.class.skip_triple_equal_on.none? { |klass| obj.is_a? klass } 204 | end 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /lib/json_expressions/minitest.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/unit' 2 | require 'minitest/spec' 3 | require 'json_expressions' 4 | require 'json_expressions/minitest/assertions' 5 | 6 | if defined?(MiniTest::VERSION) && (MiniTest::VERSION.to_i > 4) 7 | class MiniTest::Test 8 | WILDCARD_MATCHER = JsonExpressions::WILDCARD_MATCHER 9 | 10 | def wildcard_matcher 11 | ::JsonExpressions::WILDCARD_MATCHER 12 | end 13 | end 14 | else 15 | class MiniTest::Unit::TestCase 16 | WILDCARD_MATCHER = JsonExpressions::WILDCARD_MATCHER 17 | 18 | def wildcard_matcher 19 | ::JsonExpressions::WILDCARD_MATCHER 20 | end 21 | end 22 | end 23 | 24 | Object.infect_an_assertion :assert_json_match, :must_match_json_expression 25 | Object.infect_an_assertion :refute_json_match, :wont_match_json_expression 26 | -------------------------------------------------------------------------------- /lib/json_expressions/minitest/assertions.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module MiniTest 4 | module Assertions 5 | def assert_json_match(exp, act, msg = nil) 6 | unless JsonExpressions::Matcher === exp 7 | exp = JsonExpressions::Matcher.new(exp) 8 | end 9 | 10 | if String === act 11 | assert act = JSON.parse(act), "Expected #{mu_pp(act)} to be valid JSON" 12 | end 13 | 14 | assert exp =~ act, ->{ "Expected #{mu_pp(exp)} to match #{mu_pp(act)}\n" + exp.last_error} 15 | 16 | # Return the matcher 17 | return exp 18 | end 19 | 20 | def refute_json_match(exp, act, msg = nil) 21 | unless JsonExpressions::Matcher === exp 22 | exp = JsonExpressions::Matcher.new(exp) 23 | end 24 | 25 | if String === act 26 | assert act = JSON.parse(act), "Expected #{mu_pp(act)} to be valid JSON" 27 | end 28 | 29 | refute exp =~ act, "Expected #{mu_pp(exp)} to not match #{mu_pp(act)}" 30 | 31 | # Return the matcher 32 | return exp 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/json_expressions/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core' 2 | require 'json_expressions' 3 | require 'json_expressions/rspec/matchers' 4 | 5 | RSpec::configure do |config| 6 | config.include(JsonExpressions::RSpec::Matchers) 7 | end 8 | 9 | module RSpec 10 | module Core 11 | class ExampleGroup 12 | def wildcard_matcher 13 | ::JsonExpressions::WILDCARD_MATCHER 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/json_expressions/rspec/matchers.rb: -------------------------------------------------------------------------------- 1 | require 'json_expressions/rspec/matchers/match_json_expression' -------------------------------------------------------------------------------- /lib/json_expressions/rspec/matchers/match_json_expression.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'rspec/core' 3 | 4 | module JsonExpressions 5 | module RSpec 6 | module Matchers 7 | class MatchJsonExpression 8 | def initialize(expected) 9 | if JsonExpressions::Matcher === expected 10 | @expected = expected 11 | else 12 | @expected = JsonExpressions::Matcher.new(expected) 13 | end 14 | end 15 | 16 | def matches?(target) 17 | @target = (String === target) ? JSON.parse(target) : target 18 | @expected =~ @target 19 | end 20 | alias_method :===, :matches? 21 | 22 | def failure_message_for_should 23 | "expected #{@target.inspect} to match JSON expression #{@expected.inspect}\n" + @expected.last_error 24 | end 25 | alias_method :failure_message, :failure_message_for_should 26 | 27 | def failure_message_for_should_not 28 | "expected #{@target.inspect} not to match JSON expression #{@expected.inspect}" 29 | end 30 | alias_method :failure_message_when_negated, :failure_message_for_should_not 31 | 32 | def description 33 | "should equal JSON expression #{@expected.inspect}" 34 | end 35 | end 36 | 37 | def match_json_expression(expected) 38 | MatchJsonExpression.new(expected) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/json_expressions/test/unit/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'pp' 3 | 4 | module JsonExpressions 5 | module Test 6 | module Unit 7 | module Helpers 8 | def assert_json_match(exp, act, msg = nil) 9 | unless JsonExpressions::Matcher === exp 10 | exp = JsonExpressions::Matcher.new(exp) 11 | end 12 | 13 | if String === act 14 | begin 15 | act = JSON.parse(act) 16 | rescue 17 | assert false, "Expected #{pp(act)} to be valid JSON" 18 | end 19 | end 20 | 21 | unless exp =~ act 22 | assert false, "Expected #{pp(exp)} to match #{pp(act)}\n #{exp.last_error}" 23 | end 24 | 25 | # Return the matcher 26 | return exp 27 | end 28 | end 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/json_expressions/testunit.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'json_expressions' 3 | require 'json_expressions/test/unit/helpers' 4 | 5 | class Test::Unit::TestCase 6 | include JsonExpressions::Test::Unit::Helpers 7 | WILDCARD_MATCHER = JsonExpressions::WILDCARD_MATCHER 8 | 9 | def wildcard_matcher 10 | ::JsonExpressions::WILDCARD_MATCHER 11 | end 12 | end -------------------------------------------------------------------------------- /spec/json_expressions/rspec/matchers/match_json_expression_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'json_expressions/rspec' 3 | 4 | module JsonExpressions 5 | module RSpec 6 | describe Matchers, "#match_json_expression" do 7 | before(:each) do 8 | @expression = { 9 | l1_string: 'Hello world!', 10 | l1_regexp: /\A0x[0-9a-f]+\z/i, 11 | l1_boolean: false, 12 | l1_module: Numeric, 13 | l1_wildcard: wildcard_matcher, 14 | l1_array: ['l1: Hello world',1,true,nil,wildcard_matcher], 15 | l1_object: { 16 | l2_string: 'Hi there!', 17 | l2_regexp: /\A[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}\z/i, 18 | l2_boolean: true, 19 | l2_module: Enumerable, 20 | l2_wildcard: wildcard_matcher, 21 | l2_array: ['l2: Hello world',2,true,nil,wildcard_matcher], 22 | l2_object: { 23 | l3_string: 'Good day...', 24 | l3_regexp: /\A.*\z/, 25 | l3_boolean: false, 26 | l3_module: String, 27 | l3_wildcard: wildcard_matcher, 28 | l3_array: ['l3: Hello world',3,true,nil,wildcard_matcher], 29 | } 30 | } 31 | } 32 | end 33 | 34 | it "works with json hashes" do 35 | positive_json_hash = { 36 | l1_string: 'Hello world!', 37 | l1_regexp: '0xC0FFEE', 38 | l1_boolean: false, 39 | l1_module: 1.1, 40 | l1_wildcard: true, 41 | l1_array: ['l1: Hello world',1,true,nil,false], 42 | l1_object: { 43 | l2_string: 'Hi there!', 44 | l2_regexp: '1234-5678-1234-5678', 45 | l2_boolean: true, 46 | l2_module: [1,2,3,4], 47 | l2_wildcard: 'Whatever', 48 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 49 | l2_object: { 50 | l3_string: 'Good day...', 51 | l3_regexp: '', 52 | l3_boolean: false, 53 | l3_module: 'This is like... inception!', 54 | l3_wildcard: nil, 55 | l3_array: ['l3: Hello world',3,true,nil,[]] 56 | } 57 | } 58 | } 59 | 60 | negative_json_hash = { 61 | l1_string: 'Hello world!', 62 | l1_regexp: '0xC0FFEE', 63 | l1_boolean: false, 64 | l1_module: 1.1, 65 | l1_wildcard: true, 66 | l1_array: ['l1: Hello world',1,true,nil,false], 67 | l1_object: { 68 | l2_string: 'Hi there!', 69 | l2_regexp: '1234-5678-1234-5678', 70 | l2_boolean: true, 71 | l2_module: [1,2,3,4], 72 | l2_wildcard: 'Whatever', 73 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 74 | l2_object: { 75 | l3_string: 'Good day...', 76 | l3_regexp: '', 77 | l3_regexp: false, 78 | l3_module: 'This is like... inception!', 79 | l3_wildcard: nil, 80 | l3_array: ['***THIS SHOULD BREAK THINGS***',3,true,nil,[]] 81 | } 82 | } 83 | } 84 | 85 | positive_json_hash.should match_json_expression(@expression) 86 | ->{ negative_json_hash.should match_json_expression(@expression) }.should raise_error(::RSpec::Expectations::ExpectationNotMetError) 87 | negative_json_hash.should_not match_json_expression(@expression) 88 | ->{ positive_json_hash.should_not match_json_expression(@expression) }.should raise_error(::RSpec::Expectations::ExpectationNotMetError) 89 | end 90 | 91 | it "works with JSON strings" do 92 | positive_json_string = '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["l3: Hello world",3,true,null,[]]}}}' 93 | negative_json_string = '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["***THIS SHOULD BREAK THINGS***",3,true,null,[]]}}}' 94 | 95 | positive_json_string.should match_json_expression(@expression) 96 | ->{ negative_json_string.should match_json_expression(@expression) }.should raise_error(::RSpec::Expectations::ExpectationNotMetError) 97 | negative_json_string.should_not match_json_expression(@expression) 98 | ->{ positive_json_string.should_not match_json_expression(@expression) }.should raise_error(::RSpec::Expectations::ExpectationNotMetError) 99 | end 100 | end 101 | 102 | module Matchers 103 | describe MatchJsonExpression, "#match?" do 104 | before(:each) do 105 | @matcher = MatchJsonExpression.new({ 106 | l1_string: 'Hello world!', 107 | l1_regexp: /\A0x[0-9a-f]+\z/i, 108 | l1_boolean: false, 109 | l1_module: Numeric, 110 | l1_wildcard: wildcard_matcher, 111 | l1_array: ['l1: Hello world',1,true,nil,wildcard_matcher], 112 | l1_object: { 113 | l2_string: 'Hi there!', 114 | l2_regexp: /\A[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}\z/i, 115 | l2_boolean: true, 116 | l2_module: Enumerable, 117 | l2_wildcard: wildcard_matcher, 118 | l2_array: ['l2: Hello world',2,true,nil,wildcard_matcher], 119 | l2_object: { 120 | l3_string: 'Good day...', 121 | l3_regexp: /\A.*\z/, 122 | l3_boolean: false, 123 | l3_module: String, 124 | l3_wildcard: wildcard_matcher, 125 | l3_array: ['l3: Hello world',3,true,nil,wildcard_matcher], 126 | } 127 | } 128 | }) 129 | end 130 | 131 | it "returns true when passed a matching JSON hash" do 132 | json = { 133 | l1_string: 'Hello world!', 134 | l1_regexp: '0xC0FFEE', 135 | l1_boolean: false, 136 | l1_module: 1.1, 137 | l1_wildcard: true, 138 | l1_array: ['l1: Hello world',1,true,nil,false], 139 | l1_object: { 140 | l2_string: 'Hi there!', 141 | l2_regexp: '1234-5678-1234-5678', 142 | l2_boolean: true, 143 | l2_module: [1,2,3,4], 144 | l2_wildcard: 'Whatever', 145 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 146 | l2_object: { 147 | l3_string: 'Good day...', 148 | l3_regexp: '', 149 | l3_boolean: false, 150 | l3_module: 'This is like... inception!', 151 | l3_wildcard: nil, 152 | l3_array: ['l3: Hello world',3,true,nil,[]] 153 | } 154 | } 155 | } 156 | 157 | @matcher.matches?(json).should be_true 158 | end 159 | 160 | it "returns false when passed a non-matching JSON hash" do 161 | json = { 162 | l1_string: 'Hello world!', 163 | l1_regexp: '0xC0FFEE', 164 | l1_boolean: false, 165 | l1_module: 1.1, 166 | l1_wildcard: true, 167 | l1_array: ['l1: Hello world',1,true,nil,false], 168 | l1_object: { 169 | l2_string: 'Hi there!', 170 | l2_regexp: '1234-5678-1234-5678', 171 | l2_boolean: true, 172 | l2_module: [1,2,3,4], 173 | l2_wildcard: 'Whatever', 174 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 175 | l2_object: { 176 | l3_string: 'Good day...', 177 | l3_regexp: '', 178 | l3_boolean: false, 179 | l3_module: 'This is like... inception!', 180 | l3_wildcard: nil, 181 | l3_array: ['***THIS SHOULD BREAK THINGS***',3,true,nil,[]] 182 | } 183 | } 184 | } 185 | 186 | @matcher.matches?(json).should be_false 187 | end 188 | 189 | it "returns true when passed a matching JSON string" do 190 | json_str = '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["l3: Hello world",3,true,null,[]]}}}' 191 | @matcher.matches?(json_str).should be_true 192 | end 193 | 194 | it "returns false when passed a non-matching JSON string" do 195 | json_str = '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["***THIS SHOULD BREAK THINGS***",3,true,null,[]]}}}' 196 | @matcher.matches?(json_str).should be_false 197 | end 198 | end 199 | end 200 | end 201 | end -------------------------------------------------------------------------------- /spec/json_expressions/rspec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'json_expressions' 3 | require 'json_expressions/rspec' 4 | 5 | describe RSpec do 6 | it "includes JsonExpressions::RSpec::Matchers" do 7 | modules = ::RSpec.configuration.include_or_extend_modules 8 | modules.select! { |(mode,mod,_)| mode == :include } 9 | modules.map! { |(mode,mod,_)| mod } 10 | modules.should include(::JsonExpressions::RSpec::Matchers) 11 | end 12 | 13 | it "defines wildcard_matcher" do 14 | wildcard_matcher.object_id.should equal ::JsonExpressions::WILDCARD_MATCHER.object_id 15 | end 16 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' -------------------------------------------------------------------------------- /test/json_expressions/minitest/test_assertions.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | require 'json_expressions' 3 | require 'json_expressions/minitest/assertions' 4 | 5 | module MiniTest 6 | class TestAssertions < ::MiniTest::Unit::TestCase 7 | def setup 8 | @pattern = { 9 | l1_string: 'Hello world!', 10 | l1_regexp: /\A0x[0-9a-f]+\z/i, 11 | l1_boolean: false, 12 | l1_module: Numeric, 13 | l1_wildcard: wildcard_matcher, 14 | l1_array: ['l1: Hello world',1,true,nil,wildcard_matcher], 15 | l1_object: { 16 | l2_string: 'Hi there!', 17 | l2_regexp: /\A[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}\z/i, 18 | l2_boolean: true, 19 | l2_module: Enumerable, 20 | l2_wildcard: wildcard_matcher, 21 | l2_array: ['l2: Hello world',2,true,nil,wildcard_matcher], 22 | l2_object: { 23 | l3_string: 'Good day...', 24 | l3_regexp: /\A.*\z/, 25 | l3_boolean: false, 26 | l3_module: String, 27 | l3_wildcard: wildcard_matcher, 28 | l3_array: ['l3: Hello world',3,true,nil,wildcard_matcher], 29 | } 30 | } 31 | } 32 | end 33 | 34 | def test_assert_json_match 35 | assert_json_match(@pattern,{ 36 | l1_string: 'Hello world!', 37 | l1_regexp: '0xC0FFEE', 38 | l1_boolean: false, 39 | l1_module: 1.1, 40 | l1_wildcard: true, 41 | l1_array: ['l1: Hello world',1,true,nil,false], 42 | l1_object: { 43 | l2_string: 'Hi there!', 44 | l2_regexp: '1234-5678-1234-5678', 45 | l2_boolean: true, 46 | l2_module: [1,2,3,4], 47 | l2_wildcard: 'Whatever', 48 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 49 | l2_object: { 50 | l3_string: 'Good day...', 51 | l3_regexp: '', 52 | l3_boolean: false, 53 | l3_module: 'This is like... inception!', 54 | l3_wildcard: nil, 55 | l3_array: ['l3: Hello world',3,true,nil,[]] 56 | } 57 | } 58 | }) 59 | 60 | assert_raises(::MiniTest::Assertion) do 61 | assert_json_match(@pattern,{ 62 | l1_string: 'Hello world!', 63 | l1_regexp: '0xC0FFEE', 64 | l1_boolean: false, 65 | l1_module: 1.1, 66 | l1_wildcard: true, 67 | l1_array: ['l1: Hello world',1,true,nil,false], 68 | l1_object: { 69 | l2_string: 'Hi there!', 70 | l2_regexp: '1234-5678-1234-5678', 71 | l2_boolean: true, 72 | l2_module: [1,2,3,4], 73 | l2_wildcard: 'Whatever', 74 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 75 | l2_object: { 76 | l3_string: 'Good day...', 77 | l3_regexp: '', 78 | l3_boolean: false, 79 | l3_module: 'This is like... inception!', 80 | l3_wildcard: nil, 81 | l3_array: ['***THIS SHOULD BREAK THINGS***',3,true,nil,[]] 82 | } 83 | } 84 | }) 85 | end 86 | end 87 | 88 | def test_refute_json_match 89 | assert_raises(::MiniTest::Assertion) do 90 | refute_json_match(@pattern,{ 91 | l1_string: 'Hello world!', 92 | l1_regexp: '0xC0FFEE', 93 | l1_boolean: false, 94 | l1_module: 1.1, 95 | l1_wildcard: true, 96 | l1_array: ['l1: Hello world',1,true,nil,false], 97 | l1_object: { 98 | l2_string: 'Hi there!', 99 | l2_regexp: '1234-5678-1234-5678', 100 | l2_boolean: true, 101 | l2_module: [1,2,3,4], 102 | l2_wildcard: 'Whatever', 103 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 104 | l2_object: { 105 | l3_string: 'Good day...', 106 | l3_regexp: '', 107 | l3_boolean: false, 108 | l3_module: 'This is like... inception!', 109 | l3_wildcard: nil, 110 | l3_array: ['l3: Hello world',3,true,nil,[]] 111 | } 112 | } 113 | }) 114 | end 115 | 116 | refute_json_match(@pattern,{ 117 | l1_string: 'Hello world!', 118 | l1_regexp: '0xC0FFEE', 119 | l1_boolean: false, 120 | l1_module: 1.1, 121 | l1_wildcard: true, 122 | l1_array: ['l1: Hello world',1,true,nil,false], 123 | l1_object: { 124 | l2_string: 'Hi there!', 125 | l2_regexp: '1234-5678-1234-5678', 126 | l2_boolean: true, 127 | l2_module: [1,2,3,4], 128 | l2_wildcard: 'Whatever', 129 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 130 | l2_object: { 131 | l3_string: 'Good day...', 132 | l3_regexp: '', 133 | l3_boolean: false, 134 | l3_module: 'This is like... inception!', 135 | l3_wildcard: nil, 136 | l3_array: ['***THIS SHOULD BREAK THINGS***',3,true,nil,[]] 137 | } 138 | } 139 | }) 140 | end 141 | 142 | def test_json_match_with_json_string 143 | assert_json_match @pattern, '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["l3: Hello world",3,true,null,[]]}}}' 144 | assert_raises(::MiniTest::Assertion) { assert_json_match @pattern, '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["***THIS SHOULD BREAK THINGS***",3,true,null,[]]}}}' } 145 | refute_json_match @pattern, '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["***THIS SHOULD BREAK THINGS***",3,true,null,[]]}}}' 146 | assert_raises(::MiniTest::Assertion) { refute_json_match @pattern, '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["l3: Hello world",3,true,null,[]]}}}' } 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /test/json_expressions/test_core_extensions.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | require 'json_expressions/core_extensions' 3 | 4 | module JsonExpressions 5 | class TestCoreExtensions < ::MiniTest::Unit::TestCase 6 | METHODS_MODULE_MAPPING = { 7 | :ordered => Ordered, 8 | :unordered => Unordered, 9 | :strict => Strict, 10 | :forgiving => Forgiving 11 | }.freeze 12 | 13 | def setup 14 | @hash = {'a'=>1, 'b'=>2, 'c'=>3} 15 | @array = [1, 2, 3] 16 | end 17 | 18 | METHODS_MODULE_MAPPING.each do |meth, mod| 19 | ['array', 'hash'].each do |klass| 20 | eval <<-EOM, nil, __FILE__, __LINE__ + 1 21 | def test_#{klass}_#{meth} 22 | refute @#{klass}.is_a? #{mod} 23 | #{klass} = @#{klass}.#{meth} 24 | refute_equal #{klass}.object_id, @#{klass}.object_id 25 | assert #{klass}.is_a? #{mod} 26 | end 27 | 28 | def test_#{klass}_#{meth}! 29 | refute @#{klass}.is_a? #{mod} 30 | @#{klass}.#{meth}! 31 | assert @#{klass}.is_a? #{mod} 32 | end 33 | 34 | def test_#{klass}_#{meth}? 35 | refute @#{klass}.#{meth}? 36 | @#{klass}.extend #{mod} 37 | assert @#{klass}.#{meth}? 38 | end 39 | EOM 40 | end 41 | end 42 | 43 | def test_hash_reject_extra_keys 44 | refute @hash.strict? 45 | assert @hash.reject_extra_keys.strict? 46 | refute @hash.strict? 47 | end 48 | 49 | def test_hash_reject_extra_keys! 50 | refute @hash.strict? 51 | @hash.reject_extra_keys! 52 | assert @hash.strict? 53 | end 54 | 55 | def test_hash_ignore_extra_keys 56 | refute @hash.forgiving? 57 | assert @hash.ignore_extra_keys.forgiving? 58 | refute @hash.forgiving? 59 | end 60 | 61 | def test_hash_ignore_extra_keys! 62 | refute @hash.forgiving? 63 | @hash.ignore_extra_keys! 64 | assert @hash.forgiving? 65 | end 66 | 67 | def test_array_reject_extra_values 68 | refute @array.strict? 69 | assert @array.reject_extra_values.strict? 70 | refute @array.strict? 71 | end 72 | 73 | def test_array_reject_extra_values! 74 | refute @array.strict? 75 | @array.reject_extra_values! 76 | assert @array.strict? 77 | end 78 | 79 | def test_array_ignore_extra_values 80 | refute @array.forgiving? 81 | assert @array.ignore_extra_values.forgiving? 82 | refute @array.forgiving? 83 | end 84 | 85 | def test_array_ignore_extra_values! 86 | refute @array.forgiving? 87 | @array.ignore_extra_values! 88 | assert @array.forgiving? 89 | end 90 | 91 | def test_cannot_mark_an_unordered_object_as_ordered 92 | @hash.unordered! 93 | assert_raises RuntimeError do 94 | @hash.ordered! 95 | end 96 | end 97 | 98 | def test_cannot_mark_an_ordered_object_as_unordered 99 | @hash.ordered! 100 | assert_raises RuntimeError do 101 | @hash.unordered! 102 | end 103 | end 104 | 105 | def test_cannot_mark_a_forgiving_object_as_strict 106 | @hash.forgiving! 107 | assert_raises RuntimeError do 108 | @hash.strict! 109 | end 110 | end 111 | 112 | def test_cannot_mark_a_strict_object_as_forgiving 113 | @hash.strict! 114 | assert_raises RuntimeError do 115 | @hash.forgiving! 116 | end 117 | end 118 | end 119 | end -------------------------------------------------------------------------------- /test/json_expressions/test_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | require 'json_expressions/matcher' 3 | 4 | module JsonExpressions 5 | class TestMatcher < ::MiniTest::Unit::TestCase 6 | def setup 7 | @simple_object = { 8 | integer: 1, 9 | float: 1.1, 10 | string: 'Hello world!', 11 | boolean: false, 12 | array: [1,2,3], 13 | object: {'key1' => 'value1','key2' => 'value2'}, 14 | null: nil, 15 | } 16 | 17 | @simple_array = [ 18 | 1, 19 | 1.1, 20 | 'Hello world!', 21 | false, 22 | [1,2,3], 23 | {key1: 'value1', key2: 'value2'}, 24 | nil 25 | ] 26 | 27 | @complex_pattern = { 28 | l1_string: 'Hello world!', 29 | l1_regexp: /\A0x[0-9a-f]+\z/i, 30 | l1_boolean: false, 31 | l1_module: Numeric, 32 | l1_wildcard: WILDCARD_MATCHER, 33 | l1_array: ['l1: Hello world',1,true,nil,WILDCARD_MATCHER], 34 | l1_object: { 35 | l2_string: 'Hi there!', 36 | l2_regexp: /\A[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}\z/i, 37 | l2_boolean: true, 38 | l2_module: Enumerable, 39 | l2_wildcard: WILDCARD_MATCHER, 40 | l2_array: ['l2: Hello world',2,true,nil,WILDCARD_MATCHER], 41 | l2_object: { 42 | l3_string: 'Good day...', 43 | l3_regexp: /\A.*\z/, 44 | l3_boolean: false, 45 | l3_module: String, 46 | l3_wildcard: WILDCARD_MATCHER, 47 | l3_array: ['l3: Hello world',3,true,nil,WILDCARD_MATCHER], 48 | } 49 | } 50 | } 51 | end 52 | 53 | def test_defaults 54 | assert_equal [], Matcher.skip_triple_equal_on 55 | assert_equal true, Matcher.assume_unordered_arrays 56 | assert_equal true, Matcher.assume_strict_arrays 57 | assert_equal true, Matcher.assume_unordered_hashes 58 | assert_equal true, Matcher.assume_strict_hashes 59 | end 60 | 61 | def test_match_numbers 62 | assert_match Matcher.new(1), 1 63 | assert_match Matcher.new(1.1), 1.1 64 | assert_match Matcher.new(1.0), 1 65 | assert_match Matcher.new(1), 1.0 66 | refute_match Matcher.new(1.1), 1 67 | refute_match Matcher.new(1), 1.1 68 | end 69 | 70 | def test_match_strings 71 | assert_match Matcher.new('Hello world!'), 'Hello world!' 72 | refute_match Matcher.new('Hello world!'), '' 73 | refute_match Matcher.new(''), 'Hello world!' 74 | refute_match Matcher.new('Hello world!'), 'HELLO WORLD!' 75 | end 76 | 77 | def test_match_booleans 78 | assert_match Matcher.new(true), true 79 | assert_match Matcher.new(false), false 80 | refute_match Matcher.new(true), false 81 | refute_match Matcher.new(false), true 82 | end 83 | 84 | def test_match_arrays 85 | assert_match Matcher.new([]), [] 86 | assert_match Matcher.new(@simple_array), @simple_array 87 | refute_match Matcher.new(@simple_array), [] 88 | refute_match Matcher.new([]), @simple_array 89 | end 90 | 91 | def test_match_arrays_ordered 92 | assert_match Matcher.new(@simple_array.ordered!), @simple_array 93 | refute_match Matcher.new(@simple_array.ordered!), @simple_array.reverse 94 | refute_match Matcher.new(@simple_array.ordered!), [] 95 | end 96 | 97 | def test_match_arrays_unordered 98 | assert_match Matcher.new(@simple_array.unordered!), @simple_array 99 | assert_match Matcher.new(@simple_array.unordered!), @simple_array.reverse 100 | refute_match Matcher.new(@simple_array.unordered!), [] 101 | end 102 | 103 | def test_match_arrays_strict 104 | assert_match Matcher.new(@simple_array.strict!), @simple_array 105 | refute_match Matcher.new(@simple_array.strict!), @simple_array + ['extra'] 106 | refute_match Matcher.new(@simple_array.strict!), @simple_array[1..-1] 107 | end 108 | 109 | def test_match_arrays_forgiving 110 | assert_match Matcher.new(@simple_array.forgiving!), @simple_array 111 | assert_match Matcher.new(@simple_array.forgiving!), @simple_array + ['extra'] 112 | refute_match Matcher.new(@simple_array.forgiving!), @simple_array[1..-1] 113 | end 114 | 115 | def test_match_objects 116 | assert_match Matcher.new({}), {} 117 | assert_match Matcher.new(@simple_object), @simple_object 118 | refute_match Matcher.new(@simple_object), {} 119 | refute_match Matcher.new({}), @simple_object 120 | end 121 | 122 | def test_match_objects_ordered 123 | reversed = @simple_object.reverse_each.inject({}){ |hash,(k,v)| hash[k] = v; hash } 124 | assert_match Matcher.new(@simple_object.ordered!), @simple_object 125 | refute_match Matcher.new(@simple_object.ordered!), reversed 126 | refute_match Matcher.new(@simple_object.ordered!), {} 127 | end 128 | 129 | def test_match_objects_unordered 130 | reversed = @simple_object.reverse_each.inject({}){ |hash,(k,v)| hash[k] = v; hash } 131 | assert_match Matcher.new(@simple_object.unordered!), @simple_object 132 | assert_match Matcher.new(@simple_object.unordered!), reversed 133 | refute_match Matcher.new(@simple_object.unordered!), {} 134 | end 135 | 136 | def test_match_objects_strict 137 | assert_match Matcher.new(@simple_object.strict!), @simple_object 138 | refute_match Matcher.new(@simple_object.strict!), @simple_object.merge({extra: 'stuff'}) 139 | refute_match Matcher.new(@simple_object.strict!), @simple_object.clone.delete_if {|key| key == :integer} 140 | end 141 | 142 | def test_match_objects_forgiving 143 | assert_match Matcher.new(@simple_object.forgiving!), @simple_object 144 | assert_match Matcher.new(@simple_object.forgiving!), @simple_object.merge({extra: 'stuff'}) 145 | refute_match Matcher.new(@simple_object.forgiving!), @simple_object.clone.delete_if {|key| key == :integer} 146 | end 147 | 148 | def test_match_nil 149 | assert_match Matcher.new(nil), nil 150 | end 151 | 152 | def test_match_regexp 153 | assert_match Matcher.new(/\A0x[0-9a-f]+\z/i), '0xC0FFEE' 154 | refute_match Matcher.new(/\A0x[0-9a-f]+\z/i), 'Hello world!' 155 | end 156 | 157 | def test_match_modules 158 | assert_match Matcher.new(String), 'Hello world!' 159 | assert_match Matcher.new(Numeric), 1 160 | assert_match Matcher.new(Numeric), 1.1 161 | assert_match Matcher.new(Enumerable), [1,2,3] 162 | assert_match Matcher.new(Enumerable), (1..10) 163 | refute_match Matcher.new(String), nil 164 | refute_match Matcher.new(Numeric), {a:1} 165 | refute_match Matcher.new(Enumerable), Time.now 166 | end 167 | 168 | def test_match_wildcard 169 | assert_match Matcher.new(WILDCARD_MATCHER), 1 170 | assert_match Matcher.new(WILDCARD_MATCHER), 1.1 171 | assert_match Matcher.new(WILDCARD_MATCHER), 'Hello world!' 172 | assert_match Matcher.new(WILDCARD_MATCHER), true 173 | assert_match Matcher.new(WILDCARD_MATCHER), false 174 | assert_match Matcher.new(WILDCARD_MATCHER), [1,2,3] 175 | assert_match Matcher.new(WILDCARD_MATCHER), {key1: 'value1',key2: 'value2'} 176 | assert_match Matcher.new(WILDCARD_MATCHER), nil 177 | end 178 | 179 | def test_match_capture 180 | assert_match Matcher.new({key1: :capture1, key2: :capture2}), {key1: 'value1', key2: 'value2'} 181 | end 182 | 183 | def test_match_capture_common 184 | assert_match Matcher.new({key1: :common, key2: :common}), {key1: 'value1', key2: 'value1'} 185 | refute_match Matcher.new({key1: :common, key2: :common}), {key1: 'value1', key2: 'value2'} 186 | end 187 | 188 | def test_capture_result 189 | m = Matcher.new({key1: :capture1, key2: :capture2}) 190 | m =~ {key1: 'value1', key2: 'value2'} 191 | assert_equal 'value1', m.captures[:capture1] 192 | assert_equal 'value2', m.captures[:capture2] 193 | end 194 | 195 | def test_reuse_matcher 196 | m = Matcher.new({key1: :capture1}) 197 | 198 | assert_nil m.last_error 199 | assert_empty m.captures 200 | 201 | assert_match m, {key1: 'value1'} 202 | assert_nil m.last_error 203 | refute_empty m.captures 204 | 205 | refute_match m, {} 206 | refute_nil m.last_error 207 | assert_empty m.captures 208 | 209 | assert_match m, {key1: 'value1'} 210 | assert_nil m.last_error 211 | refute_empty m.captures 212 | end 213 | 214 | def test_match_recursive 215 | positive_target = { 216 | l1_string: 'Hello world!', 217 | l1_regexp: '0xC0FFEE', 218 | l1_boolean: false, 219 | l1_module: 1.1, 220 | l1_wildcard: true, 221 | l1_array: ['l1: Hello world',1,true,nil,false], 222 | l1_object: { 223 | l2_string: 'Hi there!', 224 | l2_regexp: '1234-5678-1234-5678', 225 | l2_boolean: true, 226 | l2_module: [1,2,3,4], 227 | l2_wildcard: 'Whatever', 228 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 229 | l2_object: { 230 | l3_string: 'Good day...', 231 | l3_regexp: '', 232 | l3_boolean: false, 233 | l3_module: 'This is like... inception!', 234 | l3_wildcard: nil, 235 | l3_array: ['l3: Hello world',3,true,nil,[]] 236 | } 237 | } 238 | } 239 | 240 | negative_target = { 241 | l1_string: 'Hello world!', 242 | l1_regexp: '0xC0FFEE', 243 | l1_boolean: false, 244 | l1_module: 1.1, 245 | l1_wildcard: true, 246 | l1_array: ['l1: Hello world',1,true,nil,false], 247 | l1_object: { 248 | l2_string: 'Hi there!', 249 | l2_regexp: '1234-5678-1234-5678', 250 | l2_boolean: true, 251 | l2_module: [1,2,3,4], 252 | l2_wildcard: 'Whatever', 253 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 254 | l2_object: { 255 | l3_string: 'Good day...', 256 | l3_regexp: '', 257 | l3_boolean: false, 258 | l3_module: 'This is like... inception!', 259 | l3_wildcard: nil, 260 | l3_array: ['***THIS SHOULD BREAK THINGS***',3,true,nil,[]] 261 | } 262 | } 263 | } 264 | 265 | assert_match Matcher.new(@complex_pattern), positive_target 266 | refute_match Matcher.new(@complex_pattern), negative_target 267 | end 268 | 269 | def test_error_not_match 270 | m = Matcher.new('Hello world!') 271 | m =~ nil 272 | assert_equal 'At (JSON ROOT): expected "Hello world!" to match nil', m.last_error 273 | end 274 | 275 | def test_error_not_match_capture 276 | m = Matcher.new({key1: :capture_me, key2: :capture_me}) 277 | m =~ {key1: 'value1', key2: nil} 278 | assert_equal 'At (JSON ROOT).key2: expected capture with key :capture_me and value value1 to match nil', m.last_error 279 | end 280 | 281 | def test_error_not_an_array 282 | m = Matcher.new([1,2,3,4,5]) 283 | m =~ nil 284 | assert_equal '(JSON ROOT) is not an array', m.last_error 285 | end 286 | 287 | def test_error_undersized_array 288 | m = Matcher.new([1,2,3,4,5]) 289 | m =~ [1,2,3,4] 290 | assert_equal '(JSON ROOT) contains too few elements (5 expected but was 4)', m.last_error 291 | end 292 | 293 | def test_error_oversized_array 294 | m = Matcher.new([1,2,3,4,5].strict!) 295 | m =~ [1,2,3,4,5,6] 296 | assert_equal '(JSON ROOT) contains too many elements (5 expected but was 6)', m.last_error 297 | end 298 | 299 | def test_error_array_ordered_no_match 300 | m = Matcher.new([1,2,3,4,5].ordered!) 301 | m =~ [1,2,3,4,6] 302 | assert_equal 'At (JSON ROOT)[4]: expected 5 to match 6', m.last_error 303 | end 304 | 305 | def test_error_array_unordered_no_match 306 | m = Matcher.new([1,2,3,4,5].unordered!) 307 | m =~ [1,2,3,4,6] 308 | assert_equal '(JSON ROOT) does not contain a matching element for 5 - At (JSON ROOT).*: expected 5 to match 6', m.last_error 309 | end 310 | 311 | def test_array_with_hash_inside 312 | m = Matcher.new([{foo: 'bar'}]) 313 | m =~ [{}] 314 | 315 | assert_equal "(JSON ROOT) does not contain a matching element for #{{foo: 'bar'}} - (JSON ROOT).* does not contain the key foo", m.last_error 316 | end 317 | 318 | def test_error_not_a_hash 319 | m = Matcher.new({key1: 'value1', key2: 'value2'}) 320 | m =~ nil 321 | assert_equal '(JSON ROOT) is not a hash', m.last_error 322 | end 323 | 324 | def test_error_hash_missing_key 325 | m = Matcher.new({key1: 'value1', key2: 'value2'}) 326 | m =~ {key1: 'value1'} 327 | assert_equal '(JSON ROOT) does not contain the key key2', m.last_error 328 | end 329 | 330 | def test_error_hash_extra_key 331 | m = Matcher.new({key1: 'value1', key2: 'value2'}.strict!) 332 | m =~ {key1: 'value1', key2: 'value2', key3: 'value3'} 333 | assert_equal '(JSON ROOT) contains an extra key key3', m.last_error 334 | end 335 | 336 | def test_error_hash_ordering 337 | m = Matcher.new({key1: 'value1', key2: 'value2'}.ordered!) 338 | m =~ {key2: 'value2', key1: 'value1'} 339 | assert_equal 'Incorrect key-ordering at (JSON ROOT) (["key1", "key2"] expected but was ["key2", "key1"])', m.last_error 340 | end 341 | 342 | def test_error_hash_no_match 343 | m = Matcher.new({key1: 'value1', key2: 'value2'}) 344 | m =~ {key1: 'value1', key2: nil} 345 | assert_equal 'At (JSON ROOT).key2: expected "value2" to match nil', m.last_error 346 | end 347 | 348 | def test_error_path 349 | m = Matcher.new({l1:{l2:[nil,nil,{l3:[nil,nil,nil,'THIS'].ordered!}].ordered!}}) 350 | m =~ {l1:{l2:[nil,nil,{l3:[nil,nil,nil,'THAT']}]}} 351 | assert_equal 'At (JSON ROOT).l1.l2[2].l3[3]: expected "THIS" to match "THAT"', m.last_error 352 | end 353 | 354 | def test_inspection 355 | test_cases = [ {}, @simple_object, [], @simple_array, @complex_pattern ] 356 | test_cases = [ ] # FIXME for Ruby 2+ 357 | 358 | test_cases.each do |e| 359 | assert_equal e.to_s, Matcher.new(e).to_s 360 | assert_equal e.inspect, Matcher.new(e).inspect 361 | end 362 | end 363 | 364 | def test_skip_triple_equal 365 | old_skip_triple_equal_on = Matcher.skip_triple_equal_on 366 | 367 | Matcher.skip_triple_equal_on = [String, Numeric, Enumerable] 368 | 369 | begin 370 | publicize_method(Matcher, :triple_equable?) do 371 | matcher = Matcher.new(nil) 372 | refute matcher.triple_equable? 'Hello world!' 373 | refute matcher.triple_equable? 1 374 | refute matcher.triple_equable? 1.1 375 | refute matcher.triple_equable? [1,2,3] 376 | refute matcher.triple_equable? (1..10) 377 | assert matcher.triple_equable? true 378 | assert matcher.triple_equable? nil 379 | assert matcher.triple_equable? Time.now 380 | end 381 | ensure 382 | Matcher.skip_triple_equal_on = old_skip_triple_equal_on 383 | end 384 | end 385 | 386 | def test_assume_unordered_arrays 387 | old_assume_unordered_arrays = Matcher.assume_unordered_arrays 388 | 389 | begin 390 | Matcher.assume_unordered_arrays = true 391 | assert_match Matcher.new(@simple_array.clone), @simple_array.reverse 392 | Matcher.assume_unordered_arrays = false 393 | refute_match Matcher.new(@simple_array.clone), @simple_array.reverse 394 | ensure 395 | Matcher.assume_unordered_arrays = old_assume_unordered_arrays 396 | end 397 | end 398 | 399 | def test_assume_unordered_arrays 400 | old_assume_unordered_arrays = Matcher.assume_unordered_arrays 401 | 402 | begin 403 | Matcher.assume_unordered_arrays = true 404 | assert_match Matcher.new(@simple_array.clone), @simple_array.reverse 405 | Matcher.assume_unordered_arrays = false 406 | refute_match Matcher.new(@simple_array.clone), @simple_array.reverse 407 | ensure 408 | Matcher.assume_unordered_arrays = old_assume_unordered_arrays 409 | end 410 | end 411 | 412 | def test_assume_strict_arrays 413 | old_assume_strict_arrays = Matcher.assume_strict_arrays 414 | 415 | begin 416 | Matcher.assume_strict_arrays = true 417 | refute_match Matcher.new(@simple_array.clone), @simple_array + ['extra'] 418 | Matcher.assume_strict_arrays = false 419 | assert_match Matcher.new(@simple_array.clone), @simple_array + ['extra'] 420 | ensure 421 | Matcher.assume_strict_arrays = old_assume_strict_arrays 422 | end 423 | end 424 | 425 | def test_assume_unordered_hashes 426 | old_assume_unordered_hashes = Matcher.assume_unordered_hashes 427 | 428 | begin 429 | reversed = @simple_object.reverse_each.inject({}){ |hash,(k,v)| hash[k] = v; hash } 430 | 431 | Matcher.assume_unordered_hashes = true 432 | assert_match Matcher.new(@simple_object.clone), reversed 433 | Matcher.assume_unordered_hashes = false 434 | refute_match Matcher.new(@simple_object.clone), reversed 435 | ensure 436 | Matcher.assume_unordered_hashes = old_assume_unordered_hashes 437 | end 438 | end 439 | 440 | def test_assume_strict_hashes 441 | old_assume_strict_hashes = Matcher.assume_strict_hashes 442 | 443 | begin 444 | Matcher.assume_strict_hashes = true 445 | refute_match Matcher.new(@simple_object.clone), @simple_object.merge({extra: 'stuff'}) 446 | Matcher.assume_strict_hashes = false 447 | assert_match Matcher.new(@simple_object.clone), @simple_object.merge({extra: 'stuff'}) 448 | ensure 449 | Matcher.assume_strict_hashes = old_assume_strict_hashes 450 | end 451 | end 452 | 453 | def test_hash_with_indifferent_access 454 | assert_match Matcher.new({a:1,b:false,c:nil}), {a:1,b:false,c:nil} 455 | assert_match Matcher.new({'a'=>1,'b'=>false,'c'=>nil}), {a:1,b:false,c:nil} 456 | assert_match Matcher.new({a:1,b:false,c:nil}), {'a'=>1,'b'=>false,'c'=>nil} 457 | assert_match Matcher.new({'a'=>1,'b'=>false,'c'=>nil}), {'a'=>1,'b'=>false,'c'=>nil} 458 | end 459 | 460 | private 461 | 462 | def jsonize() 463 | end 464 | 465 | def publicize_method(source, meth, &block) 466 | publicize_methods(source, [meth], &block) 467 | end 468 | 469 | def publicize_methods(source, pairs, &block) 470 | changed = [] 471 | 472 | begin 473 | pairs.each { |meth| change_visibility(source, meth, :public) } 474 | yield 475 | ensure 476 | changed.each { |meth,viz| change_visibility(source, meth, viz) } 477 | end 478 | end 479 | 480 | def change_visibility(source, meth, viz) 481 | old_viz = if source.public_method_defined? meth 482 | :public 483 | elsif source.private_method_defined? meth 484 | :private 485 | elsif source.protected_method_defined? meth 486 | :protected 487 | else 488 | # call the method to trigger a NoMethodError 489 | source.__send__ meth 490 | end 491 | source.__send__ viz, meth 492 | [meth, old_viz] 493 | end 494 | end 495 | end 496 | -------------------------------------------------------------------------------- /test/json_expressions/test_minitest.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | require 'json_expressions/minitest' 3 | 4 | class TestMiniTestUnit < ::MiniTest::Unit::TestCase 5 | def test_assertions_defined 6 | assert_includes ::MiniTest::Assertions.instance_methods, :assert_json_match 7 | assert_includes ::MiniTest::Assertions.instance_methods, :refute_json_match 8 | end 9 | 10 | def test_constant_defined 11 | assert_equal JsonExpressions::WILDCARD_MATCHER.object_id, ::MiniTest::Unit::TestCase::WILDCARD_MATCHER.object_id 12 | end 13 | 14 | def test_wildcard_matcher_defined 15 | assert_equal JsonExpressions::WILDCARD_MATCHER.object_id, wildcard_matcher.object_id 16 | end 17 | end 18 | 19 | describe MiniTest::Spec do 20 | before do 21 | @pattern = { 22 | l1_string: 'Hello world!', 23 | l1_regexp: /\A0x[0-9a-f]+\z/i, 24 | l1_boolean: false, 25 | l1_module: Numeric, 26 | l1_wildcard: wildcard_matcher, 27 | l1_array: ['l1: Hello world',1,true,nil,wildcard_matcher], 28 | l1_object: { 29 | l2_string: 'Hi there!', 30 | l2_regexp: /\A[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}\z/i, 31 | l2_boolean: true, 32 | l2_module: Enumerable, 33 | l2_wildcard: wildcard_matcher, 34 | l2_array: ['l2: Hello world',2,true,nil,wildcard_matcher], 35 | l2_object: { 36 | l3_string: 'Good day...', 37 | l3_regexp: /\A.*\z/, 38 | l3_boolean: false, 39 | l3_module: String, 40 | l3_wildcard: wildcard_matcher, 41 | l3_array: ['l3: Hello world',3,true,nil,wildcard_matcher], 42 | } 43 | } 44 | } 45 | 46 | @matching_json = { 47 | l1_string: 'Hello world!', 48 | l1_regexp: '0xC0FFEE', 49 | l1_boolean: false, 50 | l1_module: 1.1, 51 | l1_wildcard: true, 52 | l1_array: ['l1: Hello world',1,true,nil,false], 53 | l1_object: { 54 | l2_string: 'Hi there!', 55 | l2_regexp: '1234-5678-1234-5678', 56 | l2_boolean: true, 57 | l2_module: [1,2,3,4], 58 | l2_wildcard: 'Whatever', 59 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 60 | l2_object: { 61 | l3_string: 'Good day...', 62 | l3_regexp: '', 63 | l3_boolean: false, 64 | l3_module: 'This is like... inception!', 65 | l3_wildcard: nil, 66 | l3_array: ['l3: Hello world',3,true,nil,[]] 67 | } 68 | } 69 | } 70 | 71 | @non_matching_json = { 72 | l1_string: 'Hello world!', 73 | l1_regexp: '0xC0FFEE', 74 | l1_boolean: false, 75 | l1_module: 1.1, 76 | l1_wildcard: true, 77 | l1_array: ['l1: Hello world',1,true,nil,false], 78 | l1_object: { 79 | l2_string: 'Hi there!', 80 | l2_regexp: '1234-5678-1234-5678', 81 | l2_boolean: true, 82 | l2_module: [1,2,3,4], 83 | l2_wildcard: 'Whatever', 84 | l2_array: ['l2: Hello world',2,true,nil,'Whatever'], 85 | l2_object: { 86 | l3_string: 'Good day...', 87 | l3_regexp: '', 88 | l3_boolean: false, 89 | l3_module: 'This is like... inception!', 90 | l3_wildcard: nil, 91 | l3_array: ['***THIS SHOULD BREAK THINGS***',3,true,nil,[]] 92 | } 93 | } 94 | } 95 | end 96 | 97 | describe '#must_match_json_expression' do 98 | it 'should pass when the json matches the pattern' do 99 | @matching_json.must_match_json_expression @pattern 100 | end 101 | 102 | it 'should raise an exception when the json does not match the pattern' do 103 | assert_raises(::MiniTest::Assertion) do 104 | @non_matching_json.must_match_json_expression @pattern 105 | end 106 | end 107 | end 108 | 109 | describe '#wont_match_json_expression' do 110 | it 'should pass when the json does not match the pattern' do 111 | @non_matching_json.wont_match_json_expression @pattern 112 | end 113 | 114 | it 'should raise an exception when the json matches the pattern' do 115 | assert_raises(::MiniTest::Assertion) do 116 | @matching_json.wont_match_json_expression @pattern 117 | end 118 | end 119 | end 120 | 121 | it 'should also work with strings' do 122 | @matching_json_string = '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["l3: Hello world",3,true,null,[]]}}}' 123 | @non_matching_json_string = '{"l1_string":"Hello world!","l1_regexp":"0xC0FFEE","l1_boolean":false,"l1_module":1.1,"l1_wildcard":true,"l1_array":["l1: Hello world",1,true,null,false],"l1_object":{"l2_string":"Hi there!","l2_regexp":"1234-5678-1234-5678","l2_boolean":true,"l2_module":[1,2,3,4],"l2_wildcard":"Whatever","l2_array":["l2: Hello world",2,true,null,"Whatever"],"l2_object":{"l3_string":"Good day...","l3_regexp":"","l3_boolean":false,"l3_module":"This is like... inception!","l3_wildcard":null,"l3_array":["***THIS SHOULD BREAK THINGS***",3,true,null,[]]}}}' 124 | 125 | @matching_json_string.must_match_json_expression @pattern 126 | assert_raises(::MiniTest::Assertion) { @non_matching_json.must_match_json_expression @pattern } 127 | 128 | @non_matching_json_string.wont_match_json_expression @pattern 129 | assert_raises(::MiniTest::Assertion) { @matching_json.wont_match_json_expression @pattern } 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /test/minitest_helper.rb: -------------------------------------------------------------------------------- 1 | require 'turn' 2 | require 'minitest/autorun' 3 | 4 | Turn.config.format = :dot 5 | 6 | $:.unshift File.expand_path("../../lib", __FILE__) -------------------------------------------------------------------------------- /test/test_json_expressions.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | require 'json_expressions' 3 | 4 | module JsonExpressions 5 | class TestJsonExpressions < ::MiniTest::Unit::TestCase 6 | def test_wildcard_matcher_is_a? 7 | refute WILDCARD_MATCHER.is_a? Object 8 | refute WILDCARD_MATCHER.is_a? Array 9 | refute WILDCARD_MATCHER.is_a? Hash 10 | refute WILDCARD_MATCHER.is_a? Regexp 11 | refute WILDCARD_MATCHER.is_a? String 12 | end 13 | 14 | def test_wildcard_matcher_eqaulity 15 | assert WILDCARD_MATCHER === 1 16 | assert WILDCARD_MATCHER === 1.1 17 | assert WILDCARD_MATCHER === 'Hello world!' 18 | assert WILDCARD_MATCHER === true 19 | assert WILDCARD_MATCHER === false 20 | assert WILDCARD_MATCHER === nil 21 | assert WILDCARD_MATCHER === [1,2,3,4,5] 22 | assert WILDCARD_MATCHER === {k1: 'v1', k2: 'v2'} 23 | 24 | assert WILDCARD_MATCHER == 1 25 | assert WILDCARD_MATCHER == 1.1 26 | assert WILDCARD_MATCHER == 'Hello world!' 27 | assert WILDCARD_MATCHER == true 28 | assert WILDCARD_MATCHER == false 29 | assert WILDCARD_MATCHER == nil 30 | assert WILDCARD_MATCHER == [1,2,3,4,5] 31 | assert WILDCARD_MATCHER == {k1: 'v1', k2: 'v2'} 32 | end 33 | 34 | def test_wildcard_matcher_pattern_matching 35 | assert WILDCARD_MATCHER =~ 1 36 | assert WILDCARD_MATCHER =~ 1.1 37 | assert WILDCARD_MATCHER =~ 'Hello world!' 38 | assert WILDCARD_MATCHER =~ true 39 | assert WILDCARD_MATCHER =~ false 40 | assert WILDCARD_MATCHER =~ nil 41 | assert WILDCARD_MATCHER =~ [1,2,3,4,5] 42 | assert WILDCARD_MATCHER =~ {k1: 'v1', k2: 'v2'} 43 | 44 | assert_match WILDCARD_MATCHER, 1 45 | assert_match WILDCARD_MATCHER, 1.1 46 | assert_match WILDCARD_MATCHER, 'Hello world!' 47 | assert_match WILDCARD_MATCHER, true 48 | assert_match WILDCARD_MATCHER, false 49 | assert_match WILDCARD_MATCHER, nil 50 | assert_match WILDCARD_MATCHER, [1,2,3,4,5] 51 | assert_match WILDCARD_MATCHER, {k1: 'v1', k2: 'v2'} 52 | end 53 | 54 | def test_wildcard_matcher_inspection 55 | return # FIXME for Ruby 2+ 56 | assert_equal 'WILDCARD_MATCHER', WILDCARD_MATCHER.to_s 57 | assert_equal 'WILDCARD_MATCHER', WILDCARD_MATCHER.inspect 58 | end 59 | end 60 | end 61 | --------------------------------------------------------------------------------