Skip to content

Commit

Permalink
Use json_schema instead of json-schema
Browse files Browse the repository at this point in the history
The problem:

* `json_matchers` cannot easily be used concurrently with Heroku's
  JSON API tools, i.e. `prmd` and `committee`, because `json_matchers`
  makes different assumptions about the structure of the user's
  schemata. An example of an incompatibility can be found in
  #25: `json_matchers`
  breaks when the `id` property is present within a schema, but the Heroku
  tools require the presence of the `id` property
  ([reference](https://github.com/interagent/prmd/blob/master/docs/schemata.md#meta-data)).

  This is happening because the libraries used to dereference JSON
  pointers behave differently. `json-schema`, the library we're
  currently using, appears to conform less strictly to the JSON Schema
  specification than the library the Heroku tools use, `json_schema`.

The solution:

* One solution to this problem is to update `json_matchers` to use the
  same approach to validating schemata as the Heroku tools. This will
  require the following changes:

  1. Use `json_schema` instead of `json-schema` to validate schemata
  2. Update documentation to instruct readers to follow Heroku's
  guidelines for structuring schemata:
https://github.com/interagent/prmd/blob/master/docs/schemata.md

* In this commit I've replaced `json-schema` with `json_schema` and
  updated the schemata fixtures in the specs. Per [this json_schema
  issue](brandur/json_schema#22), in order to
  dereference JSON pointers referencing schemata in other files we need
  to access the gem's DocumentStore API directly. This is done in
  `Matcher#build_and_populate_document_store`.
  • Loading branch information
Laila Winner authored and seanpdoyle committed Apr 13, 2018
1 parent 48e7620 commit dfba814
Show file tree
Hide file tree
Showing 14 changed files with 218 additions and 72 deletions.
6 changes: 5 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
master
======

* *Breaking Change* - remove support for configuring validation options.
* *BREAKING CHANGE* Implement the validation with the `json_schema` gem instead
of `json-schema`. [#31]
* *BREAKING CHANGE* - remove support for configuring validation options.

[#31]: https://github.com/thoughtbot/json_matchers/pull/31

0.9.0
=====
Expand Down
106 changes: 68 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Or install it yourself as:

Inspired by [Validating JSON Schemas with an RSpec Matcher][original-blog-post].

[original-blog-post]: (http://robots.thoughtbot.com/validating-json-schemas-with-an-rspec-matcher)
[original-blog-post]: (https://robots.thoughtbot.com/validating-json-schemas-with-an-rspec-matcher)

First, configure it in your test suite's helper file:

Expand Down Expand Up @@ -55,26 +55,27 @@ Minitest::Test.send(:include, JsonMatchers::Minitest::Assertions)

### Declare

Declare your [JSON Schema](http://json-schema.org/example1.html) in the schema
Declare your [JSON Schema](https://json-schema.org/example1.html) in the schema
directory.

`spec/support/api/schemas/posts.json` or `test/support/api/schemas/posts.json`:
`spec/support/api/schemas/location.json` or
`test/support/api/schemas/location.json`:

Define your [JSON Schema](https://json-schema.org/example1.html) in the schema
directory.

```json
{
"id": "https://json-schema.org/geo",
"$schema": "https://json-schema.org/draft-06/schema#",
"description": "A geographical coordinate",
"type": "object",
"required": ["posts"],
"properties": {
"posts": {
"type": "array",
"items":{
"required": ["id", "title", "body"],
"properties": {
"id": { "type": "integer" },
"title": { "type": "string" },
"body": { "type": "string" }
}
}
"latitude": {
"type": "number"
},
"longitude": {
"type": "number"
}
}
}
Expand All @@ -84,18 +85,20 @@ directory.

#### RSpec

```ruby

Validate a JSON response, a Hash, or a String against a JSON Schema with
`match_json_schema`:

`spec/requests/posts_spec.rb`
`spec/requests/locations_spec.rb`

```ruby
describe "GET /posts" do
it "returns Posts" do
get posts_path, format: :json
describe "GET /locations" do
it "returns Locations" do
get locations_path, format: :json
expect(response.status).to eq 200
expect(response).to match_json_schema("posts")
expect(response).to match_json_schema("locations")
end
end
```
Expand All @@ -105,60 +108,87 @@ end
Validate a JSON response, a Hash, or a String against a JSON Schema with
`assert_matches_json_schema`:

`test/integration/posts_test.rb`
`test/integration/locations_test.rb`

```ruby
def test_GET_posts_returns_Posts
get posts_path, format: :json
def test_GET_posts_returns_Locations
get locations_path, format: :json
assert_equal response.status, 200
assert_matches_json_schema response, "posts"
assert_matches_json_schema response, "locations"
end
```

### Embedding other Schemas

To DRY up your schema definitions, use JSON schema's `$ref`.
To re-use other schema definitions, include `$ref` keys that refer to their
definitions.

First, declare the singular version of your schema.

`spec/support/api/schemas/post.json`:
`spec/support/api/schemas/user.json`:

```json
{
"id": "file:/user.json#",
"type": "object",
"required": ["id", "title", "body"],
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"title": { "type": "string" },
"body": { "type": "string" }
}
"name": { "type": "string" },
"address": { "type": "string" },
},
}
```

Then, when you declare your collection schema, reference your singular schemas.

`spec/support/api/schemas/posts.json`:
`spec/support/api/schemas/users/index.json`:

```json
{
"id": "file:/users/index.json#",
"type": "object",
"required": ["posts"],
"properties": {
"posts": {
"definitions": {
"users": {
"description": "A collection of users",
"example": [{ "id": "1" }],
"type": "array",
"items": { "$ref": "post.json" }
"items": {
"$ref": "file:/user.json#"
},
},
},
"required": ["users"],
"properties": {
"users": {
"$ref": "#/definitions/users"
}
}
},
}
```

NOTE: `$ref` resolves paths relative to the schema in question.

In this case `"post.json"` will be resolved relative to
`"spec/support/api/schemas"`.
In this case `"user.json"` and `"users/index.json"` are resolved relative to
`"spec/support/api/schemas"` or `"test/support/api/schemas"`.

To learn more about `$ref`, check out [Understanding JSON Schema Structuring](http://spacetelescope.github.io/understanding-json-schema/structuring.html)
To learn more about `$ref`, check out
[Understanding JSON Schema Structuring][$ref].

[$ref]: https://spacetelescope.github.io/understanding-json-schema/structuring.html

## Configuration

By default, the schema directory is `spec/support/api/schemas`.

This can be configured via `JsonMatchers.schema_root`.

```ruby
# spec/support/json_matchers.rb
JsonMatchers.schema_root = "docs/api/schemas"
```

## Upgrading from `0.9.x`

Expand Down
2 changes: 1 addition & 1 deletion json_matchers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]

spec.add_dependency("json-schema", "~> 2.7")
spec.add_dependency("json_schema")

spec.add_development_dependency "bundler", "~> 1.7"
spec.add_development_dependency "pry"
Expand Down
3 changes: 2 additions & 1 deletion lib/json_matchers.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "pathname"
require "json_matchers/version"
require "json_matchers/matcher"
require "json_matchers/errors"
Expand All @@ -8,6 +9,6 @@ class << self
end

def self.path_to_schema(schema_name)
Pathname(schema_root).join("#{schema_name}.json")
Pathname.new(schema_root).join("#{schema_name}.json")
end
end
4 changes: 2 additions & 2 deletions lib/json_matchers/assertion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def initialize(schema_name)
def valid?(json)
@payload = Payload.new(json)

matcher.matches?(payload.to_s)
matcher.matches?(payload)
end

def valid_failure_message
Expand Down Expand Up @@ -58,7 +58,7 @@ def last_error_message
end

def schema_body
File.read(schema_path)
schema_path.read
end

def format_json(json)
Expand Down
33 changes: 18 additions & 15 deletions lib/json_matchers/matcher.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
require "json-schema"
require "json_schema"
require "json_matchers/parser"
require "json_matchers/validator"

module JsonMatchers
class Matcher
def initialize(schema_path)
@schema_path = schema_path
@document_store = build_and_populate_document_store
end

def matches?(payload)
validator = build_validator(payload)

self.errors = validator.validate!
self.errors = validator.validate(payload)

errors.empty?
rescue JSON::Schema::ValidationError => error
self.errors = [error.message]
false
rescue JSON::Schema::JsonParseError
raise InvalidSchemaError
end

def validation_failure_message
Expand All @@ -26,14 +21,22 @@ def validation_failure_message

private

attr_reader :schema_path
attr_accessor :errors
attr_reader :document_store, :schema_path

def validator
Validator.new(schema_path: schema_path, document_store: document_store)
end

def build_and_populate_document_store
document_store = JsonSchema::DocumentStore.new

Dir.glob("#{JsonMatchers.schema_root}/**/*.json").
map { |path| Pathname.new(path) }.
map { |schema_path| Parser.new(schema_path).parse }.
each { |schema| document_store.add_schema(schema) }

def build_validator(payload)
Validator.new(
payload: payload,
schema_path: schema_path,
)
document_store
end
end
end
21 changes: 21 additions & 0 deletions lib/json_matchers/parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module JsonMatchers
class Parser
def initialize(schema_path)
@schema_path = schema_path
end

def parse
JsonSchema.parse!(schema_data)
rescue JSON::ParserError, JsonSchema::SchemaError => error
raise InvalidSchemaError.new(error)
end

private

attr_reader :schema_path

def schema_data
JSON.parse(schema_path.read)
end
end
end
4 changes: 4 additions & 0 deletions lib/json_matchers/payload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ def initialize(payload)
@payload = extract_json_string(payload)
end

def as_json
JSON.parse(payload)
end

def to_s
payload
end
Expand Down
30 changes: 23 additions & 7 deletions lib/json_matchers/validator.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
require "json-schema"
require "json_matchers/parser"

module JsonMatchers
class Validator
def initialize(payload:, schema_path:)
@payload = payload
@schema_path = schema_path.to_s
def initialize(document_store:, schema_path:)
@document_store = document_store
@schema_path = schema_path
end

def validate!
JSON::Validator.fully_validate(schema_path, payload, record_errors: true)
def validate(payload)
json_schema.validate!(payload.as_json)

[]
rescue JsonSchema::Error => error
[error.message]
end

private

attr_reader :payload, :schema_path
attr_reader :document_store, :schema_path

def json_schema
@json_schema ||= build_json_schema_with_expanded_references
end

def build_json_schema_with_expanded_references
json_schema = Parser.new(schema_path).parse

json_schema.expand_references!(store: document_store)

json_schema
end
end
end
Loading

0 comments on commit dfba814

Please sign in to comment.