Skip to content

benchristel/hashema

Repository files navigation

hashema

Hashema lets you validate JSONable objects (hashes and arrays) against a schema, and assert their validity in your RSpec examples.

Installation

gem install hashema

Or, if you're using bundler, put this in your Gemfile:

gem 'hashema'

Hashema hooks into your RSpec config to provide the conform_to_schema matcher. If rspec is listed in your Gemfile, you should be able to use conform_to_schema in your tests with no further setup.

RSpec usage

With hashema and RSpec, it's easy to ensure your JSON APIs return the data your clients expect.

describe BlogSerializer do
  before { @serializer = BlogSerializer.new(Blog.create) }

  describe '#as_json' do
    subject { @serializer.as_json }

    SCHEMA = {
      url: /^https?:\/\/.+/,
      posts: [
        { title: String,
          published: [true, false]
        }
      ]
    }

    it { is_expected_to conform_to_schema SCHEMA }
  end
end

The Schema DSL by example

Allowing any value

expect(
  Rotation.new('squirrel')
).to conform_to_schema Object

Checking for an exact match

expect(
  {error: 'not found'}
).to conform_to_schema({error: 'not found'})

Checking for membership in a class

expect(
  {berzerker: 'pasta'}
).to conform_to_schema Hash

Checking that a string value matches a regular expression

expect(
  'Hello! My name is Fridge.'
).to conform_to_schema /^Hello! My name is \w+\.$/

Checking for inclusion in a set of alternatives

expect(
  {is_awesome: true}
).to conform_to_schema({is_awesome: [true, false]})

Checking for inclusion in a range of legal values

expect(
  {kyu_rank: 17}
).to conform_to_schema({kyu_rank: 1..30})

Checking that all elements of an array share a schema

expect(
  [{name: 'Melody'}, {name: 'Elias'}, {name: 'Yoda'}]
).to conform_to_schema [{name: String}]

Matching an array of items that may have different schemas

expect(
  [{cash: '12.33'}, {credit: '28.95'}, {cash: '40.70'}]
).to conform_to_schema(
  [
    [{cash: /^\d+\.\d\d$/}, {credit: /^\d+\.\d\d$/}]
  ]
)

Making a hash key optional

expect({entree: "eggs"})
  .to conform_to_schema({entree: String, side: Hashema::Optional(String)})

RSpec matcher options

with_indifferent_access

Rails fans will be familiar with ActiveSupport's HashWithIndifferentAccess, which treats symbol and string keys as interchangeable. Calling with_indifferent_access on the conform_to_schema matcher will make the matcher similarly tolerant, allowing you to match a hash with string keys against a schema with symbol keys. This is especially useful when writing schemas for JSON, since parsed objects will always have string keys.

get :show

schema = {
  url: /^https?:\/\/.+/,
  posts: [
    { title: String,
      published: [true, false]
    }
  ]
}

expect(JSON(response.body)).to conform_to_schema(schema).with_indifferent_access

Hashema without RSpec

There are times when you want to validate the structure of a data object in your production code. For example, if your program parses data from a user-created file, you might want to check that the data you read in match the schema you expect. For such situations, you can use Hashema::Validator.

The API of Hashema::Validator consists of an initializer and two instance methods: valid? and failure_message. The initializer takes an object to validate and a schema, in that order. valid? will return true iff the object conforms to the schema.

validator = Hashema::Validator.new(
  # the object to validate
  { blog:
    { url: 'http://www.blagoblag.com',
      posts: [
        { title: 'hello',
          published: true
        },
        { title: 'test',
          published: false
        }
      ]
    }
  },

  # the schema
  { blog:
    { url: /^https?:\/\//,
      posts: [
        { title: String,
          published: [true, false]
        }
      ]
    }
  }
)

validator.valid? # true

If valid? is false, failure_message will return a human-readable description of the failure, which includes, at a minimum:

  • the path through the data structure to the point where the first mismatch occurred
  • the expected value at that point
  • the actual value
validator = Hashema::Validator.new(
  # the object to validate
  { blog:
    { url: 'http://www.blagoblag.com',
      posts: [
        { title: 'hello',
          published: true
        },
        { title: 123,
          published: false
        }
      ]
    }
  },

  # the schema
  { blog:
    { url: /^https?:\/\//,
      posts: [
        { title: String,
          published: [true, false]
        }
      ]
    }
  }
)

validator.valid? # false
puts validator.failure_message
# prints:
#   expected /blog/posts/1/title to match
#   String
#   but got
#   123

About

Validates JSONables against intuitive schemas

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages