Project

hashema

0.0
No commit activity in last 3 years
No release in over 3 years
Assert that JSONable objects conform to a schema
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 2.0.0
 Project Readme

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