Skip to content

Commit

Permalink
WIP: 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#add_schemata_to_document_store`.
  • Loading branch information
Laila Winner authored and seanpdoyle committed Apr 13, 2018
1 parent 8909636 commit f9bb0d1
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 16 deletions.
3 changes: 2 additions & 1 deletion json_matchers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ 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_dependency("activesupport", '>= 3.0.0')

spec.add_development_dependency "bundler", "~> 1.7"
spec.add_development_dependency "pry"
Expand Down
42 changes: 27 additions & 15 deletions lib/json_matchers/matcher.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require "json-schema"
require "json_matchers/validator"
require "json_schema"

module JsonMatchers
class Matcher
Expand All @@ -9,16 +8,22 @@ def initialize(schema_path, options = {})
end

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

self.errors = validator.validate!

errors.empty?
rescue JSON::Schema::ValidationError => error
self.errors = [error.message]
false
rescue JSON::Schema::JsonParseError
raise InvalidSchemaError
begin
add_schemata_to_document_store
schema_data = JSON.parse(File.read(@schema_path.to_s))
response_body = JSON.parse(@response.body)
json_schema = JsonSchema.parse!(schema_data)

json_schema.expand_references!(store: document_store)
json_schema.validate!(response_body)
rescue RuntimeError => ex
@validation_failure_message = ex.message
return false
rescue JsonSchema::SchemaError, JSON::ParserError => ex
raise InvalidSchemaError
end

true
end

def validation_failure_message
Expand All @@ -28,10 +33,13 @@ def validation_failure_message
private

attr_reader :schema_path, :options
attr_accessor :errors

def default_options
JsonMatchers.configuration.options || {}
def add_schemata_to_document_store
Dir.glob("#{JsonMatchers.schema_root}/**/*.json").each do |path|
schema_data = JSON.parse(File.read(path))
extra_schema = JsonSchema.parse!(schema_data)
document_store.add_schema(extra_schema)
end
end

def build_validator(payload)
Expand All @@ -41,5 +49,9 @@ def build_validator(payload)
schema_path: schema_path,
)
end

def document_store
@document_store ||= JsonSchema::DocumentStore.new
end
end
end
61 changes: 61 additions & 0 deletions spec/json_matchers/match_json_schema_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,67 @@
expect(json_as_array).not_to match_json_schema(schema)
end

it "supports $ref" do
create_schema("user", {
"id": "file:/user.json#",
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"address": { "type": "string" },
},
})
create_schema("users/index", {
"id": "file:/users/index.json#",
"type": "object",
"definitions": {
"users": {
"description": "A collection of users",
"example": [{ "id": "1" }],
"type": "array",
"items": { "$ref": "file:/user.json#" },
},
},
"required": ["users"],
"properties": { "users": { "$ref": "#/definitions/users" } },
})

valid_response = response_for({
"users": [{ "id": 1, "name": "Me!", "address": "Here!" }],
})
invalid_response = response_for({
"users": [{ "id": "invalid", "name": "You!", "address": "There!" }],
})

expect(valid_response).to match_response_schema("users/index")
expect(invalid_response).not_to match_response_schema("users/index")
end

it "supports the 'id' keyword" do
create_schema("top-level-schema", {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"a": { "$ref": "file:/#{JsonMatchers.schema_root}/nested.json#" },
},
})
create_schema("nested-schema", {
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "file:/#{JsonMatchers.schema_root}/nested.json#",
"type": "object",
"required": ["b"],
"properties": { "b": { "type": "string" } },
})
response_json = { a: { b: "foo" } }
invalid_response_json = { a: { b: 4 } }

expect(response_for(response_json)).
to match_response_schema("top-level-schema")
expect(response_for(invalid_response_json)).
not_to match_response_schema("top-level-schema")
end

context "when options are passed directly to the matcher" do
it "forwards options to the validator" do
schema = create(:schema, :object)
Expand Down

0 comments on commit f9bb0d1

Please sign in to comment.