Giter Site home page Giter Site logo

json_expressions's Introduction

JSON Expressions

Build Status

Introduction

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:

# MiniTest::Unit example
class UsersControllerTest < MiniTest::Unit::TestCase
  def test_get_a_user
    server_response = get '/users/chancancode.json'

    json = JSON.parse server_response.body

    assert user = json['user']

    assert user_id = user['id']
    assert_equal 'chancancode', user['username']
    assert_equal 'Godfrey Chan', user['full_name']
    assert_equal '[email protected]', user['email']
    assert_equal 'Administrator', user['type']
    assert_kind_of Integer, user['points']
    assert_match /\Ahttps?\:\/\/.*\z/i, user['homepage']

    assert posts = user['posts']

    assert_kind_of Integer, posts[0]['id']
    assert_equal 'Hello world!', posts[0]['subject']
    assert_equal user_id, posts[0]['user_id']
    assert_include posts[0]['tags'], 'announcement'
    assert_include posts[0]['tags'], 'welcome'
    assert_include posts[0]['tags'], 'introduction'

    assert_kind_of Integer, posts[1]['id']
    assert_equal 'An awesome blog post', posts[1]['subject']
    assert_equal user_id, posts[1]['user_id']
    assert_include posts[0]['tags'], 'blog'
    assert_include posts[0]['tags'], 'life'
  end
end

There are many problems with this approach of JSON matching:

  • It could get out of hand really quickly
  • It is not very readable
  • It flattens the structure of the JSON and it's difficult to visualize what the JSON actually looks like
  • It does not guard against extra parameters that you might have accidentally included (password hashes, credit card numbers etc)
  • Matching nested objects and arrays is tricky, especially when you don't want to enforce a particular ordering of the returned objects

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.

Dependencies

  • Ruby 1.9+

Usage

Add it to your Gemfile:

gem 'json_expressions'

Add this to your test/spec helper file:

# For MiniTest::Unit
require 'json_expressions/minitest'

# For RSpec
require 'json_expressions/rspec'

Which allows you to do...

# MiniTest::Unit example
class UsersControllerTest < MiniTest::Unit::TestCase
  def test_get_a_user
    server_response = get '/users/chancancode.json'

    # This is what we expect the returned JSON to look like
    pattern = {
      user: {
        id:         :user_id,                    # "Capture" this value for later
        username:   'chancancode',               # Match this exact string
        full_name:  'Godfrey Chan',
        email:      '[email protected]',
        type:       'Administrator',
        points:     Integer,                     # Any integer value
        homepage:   /\Ahttps?\:\/\/.*\z/i,       # Let's get serious
        created_at: wildcard_matcher,            # Don't care as long as it exists
        updated_at: wildcard_matcher,
        posts: [
          {
            id:      Integer,
            subject: 'Hello world!',
            user_id: :user_id,                   # Match against the captured value
            tags: [
              'announcement',
              'welcome',
              'introduction'
            ]                                    # Ordering of elements does not matter by default
          }.ignore_extra_keys!,                  # Skip the uninteresting stuff
          {
            id:      Integer,
            subject: 'An awesome blog post',
            user_id: :user_id,
            tags:    ['blog' , 'life']
          }.ignore_extra_keys!
        ].ordered!                               # Ensure the posts are in this exact order
      }
    }

    matcher = assert_json_match pattern, server_response.body # Returns the Matcher object

    # You can use the captured values for other purposes
    assert matcher.captures[:user_id] > 0
  end
end

# MiniTest::Spec example
describe UsersController, "#show" do
  it "returns a user" do
    pattern = # See above...

    server_response = get '/users/chancancode.json'

    server_response.body.must_match_json_expression(pattern)
  end
end

# RSpec example
describe UsersController, "#show" do
  it "returns a user" do
    pattern = # See above...

    server_response = get '/users/chancancode.json'

    server_response.body.should match_json_expression(pattern)
  end
end

Basic Matching

This pattern

{
  integer: 1,
  float:   1.1,
  string:  'Hello world!',
  boolean: true,
  array:   [1,2,3],
  object:  {key1: 'value1',key2: 'value2'},
  null:    nil,
}

matches the JSON object

{
  "integer": 1,
  "float": 1.1,
  "string": "Hello world!",
  "boolean": true,
  "array": [1,2,3],
  "object": {"key1": "value1", "key2": "value2"},
  "null": null
}

Wildcard Matching

You can use wildcard_matcher to ignore keys that you don't care about (other than the fact that they exist).

This pattern

[ wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher, wildcard_matcher ]

matches the JSON array

[ 1, 1.1, "Hello world!", true, [1,2,3], {"key1": "value1","key2": "value2"}, null]

Furthermore, because the pattern is just plain old Ruby code, you can also write:

[ wildcard_matcher ] * 7

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.

Object Equality

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:

# This is the default setting
JsonExpressions::Matcher.skip_triple_equal_on = [ ]

# To add more modules/classes
# JsonExpressions::Matcher.skip_triple_equal_on << MyClass

# To turn this off completely
# JsonExpressions::Matcher.skip_triple_equal_on = [ BasicObject ]

Regular Expressions

Since Regexp overrides Object#=== to mean "matches", you can use them in your patterns and json_expressions will do the right thing:

{ hex: /\A0x[0-9a-f]+\z/i }

matches

{ "hex": "0xC0FFEE" }

but not

{ "hex": "Hello world!" }

Type Matching

Module (and by inheritance, Class) overrides === to mean instance of. You can exploit this behavior to do type matching:

{
  integer: Integer,
  float:   Float,
  string:  String,
  boolean: Boolean, # See http://stackoverflow.com/questions/3028243/check-if-ruby-object-is-a-boolean#answer-3028378
  array:   Array,
  object:  Hash,
  null:    NilClass,
}

matches the JSON object

{
  "integer": 1,
  "float": 1.1,
  "string": "Hello world!",
  "boolean": true,
  "array": [1,2,3],
  "object": {"key1": "value1", "key2": "value2"},
  "null": null
}

Ranges

Range overrides === to mean include?. Therefore,

{ day: (1..31), month: (1..12) }

matches the JSON object

{ "day": 3, "month": 11 }

but not

{ "day": -1, "month": 13 }

This is also helpful for comparing Floats to a certain precision.

{ pi: 3.141593 }

won't match

{ "pi": 3.1415926536 }

But this will:

{ pi: (3.141592..3.141593) }

Capturing

Similar to how "captures" work in Regexp, you can capture the value of certain keys for later use:

matcher = JsonExpressions::Matcher.new({
  key1: :key1,
  key2: :key2,
  key3: :key3
})

matcher =~ JSON.parse('{"key1":"value1", "key2":"value2", "key3":"value3"}') # => true

matcher.captures[:key1] # => "value1"
matcher.captures[:key2] # => "value2"
matcher.captures[:key3] # => "value3"

If the same symbol is used multiple times, json_expression will make sure they agree. This pattern

{
  key1: :capture_me,
  key2: :capture_me,
  key3: :capture_me
}

matches

{
  "key1": "Hello world!",
  "key2": "Hello world!",
  "key3": "Hello world!"
}

but not

{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3"
}

Ordering

By default, all arrays and JSON objects (i.e. Ruby hashes) are assumed to be unordered. This means

[ 1, 2, 3, 4, 5 ]

will match

[ 5, 3, 2, 1, 4 ]

and

{ key1: 'value1', key2: 'value2' }

will match

{ "key2": "value2", "key1": "value1" }

You can change this behavior in a case-by-case manner:

{
  unordered_array: [1,2,3,4,5].unordered!, # calling unordered! is optional as it's the default
  ordered_array:   [1,2,3,4,5].ordered!,
  unordered_hash:  {a: 1, b: 2}.unordered!,
  ordered_hash:    {a: 1, b: 2}.ordered!
}

Or you can change the defaults:

# Default for these are true
JsonExpressions::Matcher.assume_unordered_arrays = false
JsonExpressions::Matcher.assume_unordered_hashes = false

"Strictness"

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:

[ 1, 2, 3, 4, 5 ]

will not match

[ 1, 2, 3, 4, 5, 6 ]

and

{ key1: 'value1', key2: 'value2' }

will not match

{ "key1": "value1", "key2": "value2", "key3": "value3" }

You can change this behavior in a case-by-case manner:

{
  strict_array:    [1,2,3,4,5].strict!, # calling strict! is optional as it's the default
  forgiving_array: [1,2,3,4,5].forgiving!,
  strict_hash:     {a: 1, b: 2}.strict!,
  forgiving_hash:  {a: 1, b: 2}.forgiving!
}

They also come with some more sensible aliases:

{
  strict_array:    [1,2,3,4,5].reject_extra_values!,
  forgiving_array: [1,2,3,4,5].ignore_extra_values!,
  strict_hash:     {a: 1, b: 2}.reject_extra_keys!,
  forgiving_hash:  {a: 1, b: 2}.ignore_extra_keys!
}

Or you can change the defaults:

# Default for these are true
JsonExpressions::Matcher.assume_strict_arrays = false
JsonExpressions::Matcher.assume_strict_hashes = false

Support for other test frameworks

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.

Contributing

Please use the GitHub issue tracker for bugs and feature requests. If you could submit a pull request - that's even better!

License

This library is distributed under the MIT license. Please see the LICENSE file.

json_expressions's People

Contributors

chancancode avatar chrisberkhout avatar cupakromer avatar kophyo avatar maxim-filimonov avatar milkcocoa avatar miroslavcsonka avatar pda avatar unpublishedworks avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

json_expressions's Issues

Add support for unified Integer class

Ruby 2.4 first deprecates the Fixnum and Bignum classes in favor of the new Integer class, which unifies both into one. When using this gem in Ruby 2.4.0 I get the following warnings:

.rvm/gems/ruby-2.4.0/gems/json_expressions-0.8.3/lib/json_expressions/matcher.rb:175: warning: constant ::Fixnum is deprecated

Adding support for the Integer seems to be missing.

undefined method `get'

Getting error

 ฮป ruby login_test.rb 
Run options: 

# Running tests:

E

Finished tests in 0.002063s, 484.7310 tests/s, 0.0000 assertions/s.

  1) Error:
test_login(LoginTest):
NoMethodError: undefined method `get' for #<LoginTest:0x007ff23a113738>
    login_test.rb:8:in `test_login'

for this code snippet
https://gist.github.com/942e43ff3c050fe4e9ce

Custom Matcher's

Hey,

i was a bit confused on how to create custom matchers, so i wanted to share a solution. You can include the module in rspec/others and use it:

show action e.g.:

response.body.should match_json_expression(JSONPatterns.user)

index action e.g.:

response.body.should match_json_expression(JSONPatterns.users(User.count))
# spec/support/json_patterns.rb
module JSONPatterns

  class ValidDateMatcher
    def ==(other)
      return DateTime.parse other rescue nil
    end
  end

  class NilOrIntegerMatcher
    def ==(other)
      other.nil? || other.is_a?(Integer)
    end
  end

  class NilOrStringMatcher
    def ==(other)
      other.nil? || other.is_a?(String)
    end
  end

  class << self

    def valid_date
      @DATE_MATCHER ||= ValidDateMatcher.new
    end

    def nil_or_integer
      @NIL_OR_INTEGER_MATCHER ||= NilOrIntegerMatcher.new
    end

    def nil_or_string
      @NIL_OR_STRING_MATCHER ||= NilOrStringMatcher.new
    end

    def user
      {
        id: Integer,
        created_at: valid_date,
        updated_at: valid_date,
        name: String,
        avatar: nil_or_string
      }
    end

    def users n
      [user] * n
    end

  end

end

Symbolic Values Seem to be Skipped

Sorry if this is obvious. Can anyone explain to me why this is expected behavior?

require 'bundler/inline'

gemfile(true) do
  source 'https://rubygems.org'
  gem 'rspec'
  gem 'json_expressions'
end

require 'rspec'
require 'json_expressions/rspec'

class Test
  describe do
    it 'does not work' do
      expect({test: :test}).to match_json_expression({test: :test2})
    end
  end
end

results in

Fetching gem metadata from https://rubygems.org/...........
Fetching version metadata from https://rubygems.org/..
Resolving dependencies...
Using diff-lcs 1.2.5
Using json_expressions 0.8.3
Using rspec-support 3.4.1
Using bundler 1.11.2
Using rspec-core 3.4.4
Using rspec-expectations 3.4.0
Using rspec-mocks 3.4.1
Using rspec 3.4.0
.

Finished in 0.0005 seconds (files took 2 seconds to load)
1 example, 0 failures

how to set certain keys as optional?

Hello, I have constructed a pattern and using it to match the json lines. However I have some keys which are optional. i.e may or may not be present in json. How can we make it optional in pattern?

[feature]: Using awesome_print for prettier JSON output on failure

Hi all and thanks for this beautiful gem,

Do you think it would be useful to enable somekind of mechanism allowing to prettify JSON output in case if matching process fails? If yes, what would be the best way to implement this?
Thanks for your suggestion!

My ugly solution is to include this snippet in test_helper.rb:

module JsonExpressions
  class Matcher
   def json
     @json
   end
  end
end
module MiniTest
  module Assertions
    def mu_pp2(obj, msg = nil)
      ap msg, color: {string: :purpleish} if msg.present?
      ap obj
      ap msg.reverse, color: {string: :purpleish} if msg.present?
    end

    def assert_json_match(exp, act, msg = nil)
      unless JsonExpressions::Matcher === exp
        exp = JsonExpressions::Matcher.new(exp)
      end

      if String === act
        assert act = JSON.parse(act), "Expected #{mu_pp(act)} to be valid JSON"
      end
      assert exp =~ act, ->{ "Expected #{mu_pp2(exp.json, 'Expected---------->')} to match #{mu_pp2(act, 'Actual---------->')}\n" + exp.last_error}

      # Return the matcher
      return exp
    end

    def refute_json_match(exp, act, msg = nil)
      unless JsonExpressions::Matcher === exp
        exp = JsonExpressions::Matcher.new(exp)
      end

      if String === act
        assert act = JSON.parse(act), "Expected #{mu_pp(act)} to be valid JSON"
      end

      refute exp =~ act, ->{ "Expected #{mu_pp2(exp.json, 'Expected')} to match #{mu_pp2(act, 'Actual')}\n" + exp.last_error}
      # Return the matcher
      return exp
    end
  end
end

Circular load warning

I get a circular require warning when I include json_expressions in my tests. This seems legitimate:

  • requiring json_expressions/minitest requires json_expressions (line 3)
  • requiring json_expressions requires json_expressions/matcher (line 1)
  • requiring json_expressions/matcher requires json_expressions (line 1)

Output:

$ rake test
/home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54: 
warning: loading in progress, circular require considered harmful - 
/home/ian/.rbenv/versions/2.2.7/lib/ruby/gems/2.2.0/gems/json_expressions-0.9.0/lib/json_expressions.rb
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/gems/2.2.0/gems/rake-12.0.0/lib/rake/rake_test_loader.rb:4:in  `<main>'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/gems/2.2.0/gems/rake-12.0.0/lib/rake/rake_test_loader.rb:4:in  `select'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/gems/2.2.0/gems/rake-12.0.0/lib/rake/rake_test_loader.rb:15:in  `block in <main>'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/workspace/epimorphics/ds-api-ruby/spec/data_services_api/aspect_spec.rb:1:in  `<top (required)>'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/workspace/epimorphics/ds-api-ruby/spec/minitest_helper.rb:7:in  `<top (required)>'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:39:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:128:in  `rescue in require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:128:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/gems/2.2.0/gems/json_expressions-0.9.0/lib/json_expressions/minitest.rb:3:in  `<top (required)>'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/gems/2.2.0/gems/json_expressions-0.9.0/lib/json_expressions.rb:1:in  `<top (required)>'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/gems/2.2.0/gems/json_expressions-0.9.0/lib/json_expressions/matcher.rb:1:in  `<top (required)>'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
from /home/ian/.rbenv/versions/2.2.7/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in  `require'
Run options: --seed 61437

It's just a warning, so it doesn't stop the test running. But it looks untidy!

How to use captures with Rspec?

Hi guys,

json_expressions are really cool! Thanks.

But I am unable to find out how to use "captures" with Rspec, is it possible?

The example shows how to use it with MiniTest:

matcher = assert_json_match pattern, server_response.body # Returns the Matcher object
You can use the captured values for other purposes
assert matcher.captures[:user_id] > 0

But In Rspec, if I use it like this:

matcher = response.body.should match_json_expression(pattern)

but matcher is "true"...

I also tried to get match_json_expression(pattern), this is JsonExpressions::RSpec::Matchers::MatchJsonExpression, but does not contain "captures" either.

So I included the require 'json_expressions/minitest' to be able to use the minitest

matcher = assert_json_match pattern, response.body

But is that right approach?

Thank you very much,
Petr

Array of indeterminate "repeats"?

Hello -

I'm enjoying json_expressions very much so far, but have a simple question (I hope!).

I have the following json for the home page of a store which I need to validate. At present, I am not sure how to specify that I want an array to be allowed to "repeat" 1 or more times. There are presently 12 entries, but there could easily be more or less.

{
title: "Shop Outdoor Gear",
categories: [
{
id: "3707807",
href: "/categories/3707807",
title: "Men's Clothing"
},
{
id: "3677318",
href: "/categories/3677318",
title: "Women's Clothing"
},
{...

Also, my pattern, in a very basic (albeit non-working) form:

            pattern = {
                    title: String,
                    categories: [{
                                    id: Integer,
                                    href: String,
                                    title: String
                            }]
            }

Is there a way in which this can be accomplished? Thank you kindly in advance.

Assert key does not exist; NOT_PRESENT matcher?

Myself and others have needed to assert that a key is not present in some JSON:

{
  id: Integer,
  email: String,
  password: NOT_PRESENT,
}

A simple nil matcher only passes if the key is present:

expr = JsonExpressions::Matcher.new(id: Integer, secret: nil)
expr.match(id: 123, secret: nil) # => true, needs to be false
expr.match(id: 123) # => false, needs to be true

Would a NOT_PRESENT matcher be possible without a lot of internal changes?
I imagine it's quite different to matching the value of a key that does exist.

I'm happy to work on it and open a pull request if you think it's worth doing.

Tests fail for Ruby 2+

When tests are run against Ruby versions > 2, there are some failures.

See #39 for the skipped tests.

Conflicts with 'rails/application' and 'rspec/rails'

Hello there!

Very useful gem, however it took me an hour to make it work in my environment.

When I put require 'json_expressions/rspec' line below the require 'rails/application' and require 'rspec/rails' I'm getting

NameError: uninitialized constant JsonExpressions

or

NoMethodError: undefined method `match_json_expression' for #RSpec::Core::ExampleGroup::Nested_1::Nested_1:0xc8553c0

The current workaround for this is to put the require 'json_expressions/rspec' statement above the require 'rails/application' and require 'rspec/rails' lines.

Nested schema matching

working with APIs there are some endpoints that return compound data... for example (just for demonstration... not a real example)

// child schema
{ "name": "Luke Skywalker", "age": 30 }
// parent schema
{ "name": "Darth Vader", "age": 63, "children": [{ "name": "Luke Skywalker", "age": 30 }] }

is there a way to nest the deffinition of the schemas ?
so that it's possible to define the parent schema as follows ?

JsonSchema.define :child do
  {
    name: String,
    age: Numeric
  }
end

JsonSchema.define :parent do
  {
    name: String,
    age: Numeric,
    children: [ JsonSchema(:child) ]
  }
end

Define JSON pattern outside of tests?

Is it currently possible to define the JSON patterns outside of a test?

I've tried to define my patterns in a module in spec/support/json_patterns.rb, but have not been able to do so successfully. It would be nice to be able to have them all together so the patterns don't take up space in the tests.

Also, you'd be able to do something like.

module JsonPatterns
  class Person
    self.pattern = {
      id:         :person_id,
      first_name: String,
      last_name:  String,
      created_at: wildcard_matcher,
      updated_at: wildcard_matcher
    }
  end

  class Family
    self.pattern = {
      id:     :family_id,
      father: Person.pattern,
      mother: Person.pattern,
      ....
    }
  end
end

stack level too deep

This rather benign appearing spec:

        it 'loses its mind' do
          expect({
            m: [
              {
                s: {
                  a: :i,
                  b: "",
                  c: :r
                }
              }
            ],
            s: {
              a: :o,
              b: nil,
              c: :r
            }
          }).to match_json_expression({
            m: [
              {
                s: {
                  a: :i,
                  b: "",
                  c: :r
                }
              }
            ],
            s: {
              a: :o,
              b: nil,
              c: :r
            }
          })
        end

blows up:

  Failure/Error: expect({
  SystemStackError:
    stack level too deep

Rspec 3 Deprecation Warning

When using Rspec 3 I get the following deprecation warning:

JsonExpressions::RSpec::Matchers::MatchJsonExpression implements a legacy RSpec matcher protocol. For the current protocol you should expose the failure messages
via the failure_message and failure_message_when_negated methods.

Support for arbitrary custom matchers

Love the library so far. I have a feeling that what I want will be possible if I start perusing your implementation details, but just to put this out there, I think it'd be really nice to support custom matchers. Suppose I have a lot of ISO8601 dates that I publish from my JSON API as strings.

//...
"received_at": "2014-03-19T16:25:28.311016Z",
//...

I'd like a pattern to match that value with an API like this:

"received_at"=> iso_date_matcher(1.minute.ago)

And the matcher's implementation could/should look something like this:

class IsoDateMatcher
  def initialize(earliest_matching_time)
    @earliest_matching_time = earliest_matching_time
  end

  def match?(actual)
     Time.zone.parse(actual) > @earliest_matching_time && actual =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z/
  end
end
def iso_date_matcher(earliest_matching_time)
  IsoDateMatcher.new(earliest_matching_time)
end

Would be really nice to be able to do this, that way I could easily one-line assertions of very large documents without having to break up my assertions into multiple imperative statements.

Thoughts? Perhaps this is already possible depending on how what built-in matchers respond to.

Make schema matching easier

Proposed API:

jexp = match({
  id: match(Fixnum).capture_as(:user_id),
  username: match(String).and(->(username){ /a-z0-9/i =~ username }),
  first_name: String,
  last_name: String,
  full_name: ->(full_name, json){ full_name == "#{json['first_name']} #{json['last_name']}" },
  password: match(nothing),
  homepage: /\Ahttps?\:\/\/.*\z/i,
  created_at: match.not(nil),
  updated_at: match.not(nil),
  posts: match({
    id: Fixnum,
    author_id: :user_id,
    title: String,
    tags: match(String).repeated(0..Infinity).or(nil)
  }).fuzzy.repeated(0..Infinity)
})

some_json.should match_json_expression(jexp)

jexp[:user_id] # => 1
  1. match (perhaps need a better name, not sure if this conflicts with other gems) is a factory method that always returns a JsonExpressions::Matcher. Passing a JsonExpressions::Matcher to match is a no-op, otherwise it would wrap that with the appropriate subclass of JsonExpression::Matcher.
  2. JsonExpressions::Matcher defines #and, #or which does what you would expect
  3. JsonExpressions::Matcher also defines #repeated which takes an integer or a integer range which turns its receiver into a JsonExpression::RepetitionMatcher and does what you would expect
  4. anything returns an instance of JsonExpressions::WildcardMatcher which matches anything (including nil), which is basically
  5. nothing returns an instance of `JsonExpression::NothingMatcher, which asserts that key doesn't exists
  6. Calling match, JsonExpressions::Matcher#and and JsonExpressions::Matcher#or with no arguments returns a JsonExpressions::NotProxy which defines a #not method, and that wraps its argument in a JsonExpressions::NotMatcher which negates its results, and then passes this to the original receiver + method
  7. JsonExpression::ArrayMatcher, JsonExpression::ObjectMatcher defines #fuzzy, #exact, #ordered and #unordered, so no more monkey-patching Array and Hash
  8. This would probably be a backwards-incompatible rewrite, We are pre-1.0 after all, so I get to do whatever I want :)

Would like some feedback on this, especially on the naming (fuzzy/exact vs forgiving/strict, etc) and any use cases that I missed.

*Also, please let me know if you expect the match method to conflict with a library you are using today. I'm also open to other suggestions for the top level method that we expose/inject (pattern, expression, expr?)

No ETA. This is probably gonna take some time and tbh it won't be on the top of my priority list for the next while, but I'll do what I can. But now that the spec is here, if you really want it now you can always implement this spec yourself :) (If you do, please pick a different name to avoid confusion)

Thank you for sticking around after I ignored you pull request for so long! (To be clear, I did not abandon the gem โ€“ I've been actively using it on Rails 4 / Ruby 2.0 myself, and I'll fix any bugs that prevents it from working correctly, I just haven't been needing these schema oriented functionalities.)

/cc @pda @tdumitrescu @martinstreicher @iangreenleaf @matthew-bb @sricc

Cucumber support

Hi @chancancode,

Is there anyway to include json_expressions into cucumber? I'm currently using it to test a API and your json_expressions project would be really handy!

I've tried to include it into cucumber's world but with no joy. If possible can you let me know the way to get this added if this is supported?

Cheers,
Jon

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.