From 5b1e09c77096cd745fbdb54877fbcebdf84f89fd Mon Sep 17 00:00:00 2001 From: Jonas Peschla Date: Sat, 24 Jan 2015 02:21:54 +0100 Subject: [PATCH 1/2] Draft to resolve refs in schemas in one extrapolated hash --- lib/json-schema/validator.rb | 49 ++++++++ .../fully_resolve/leave_integer_schema.json | 8 ++ .../fully_resolve/leave_string_schema.json | 8 ++ .../fully_resolve/linked_list_schema.json | 17 +++ test/test_fully_resolve.rb | 111 ++++++++++++++++++ 5 files changed, 193 insertions(+) create mode 100644 test/schemas/fully_resolve/leave_integer_schema.json create mode 100644 test/schemas/fully_resolve/leave_string_schema.json create mode 100644 test/schemas/fully_resolve/linked_list_schema.json create mode 100644 test/test_fully_resolve.rb diff --git a/lib/json-schema/validator.rb b/lib/json-schema/validator.rb index 571a823d..51f173fb 100644 --- a/lib/json-schema/validator.rb +++ b/lib/json-schema/validator.rb @@ -137,6 +137,10 @@ def load_ref_schema(parent_schema, ref) build_schemas(schema) end + def base_schema + @base_schema + end + def absolutize_ref_uri(ref, parent_schema_uri) ref_uri = Addressable::URI.parse(ref) @@ -238,6 +242,46 @@ def validation_errors @errors end + def resolve_nested_references(schema, schema_portion, list) + case schema_portion + when Array + # for arrays resolve all items which have not been resolved yet + schema_portion.each_with_index do |item, i| + schema_portion[i] = resolve_nested_references(schema, item, list) unless list.include?(schema_portion[i]) + end + when Hash + if schema_portion["$ref"] + # for hashes with a reference, resolve their referenced schema and copy(!) the result into the currently handled portion + _, ref_schema = JSON::Schema::RefAttribute.get_referenced_uri_and_schema(schema_portion, schema, self) + resolved_portion = resolve(ref_schema, ref_schema.schema, list) + schema_portion.clear + resolved_portion.each { |key, value| schema_portion[key] = value } + else + # for hashes without a reference we also resolve recursively what has not been resolved yet + schema_portion.each do |key, value| + schema_portion[key] = resolve_nested_references(schema, value, list) unless list.include?(value) + end + end + end + schema_portion + end + + def resolve(schema, handle_schema_hash, list = []) + # if we already dealt with the schema hash we have nothing left to do + return handle_schema_hash if list.include?(handle_schema_hash) + # remember the hash we are dealing with for the future + list << handle_schema_hash + # $ref is specified as 'SHOULD replace the current schema with the schema referenced by the value's URI' so we do exactly that + if handle_schema_hash["$ref"] + _, ref_schema = JSON::Schema::RefAttribute.get_referenced_uri_and_schema(handle_schema_hash, schema, self) + fail "Could not find referenced schema #{handle_schema_hash["$ref"]}" unless ref_schema + # translates to 'replace the current schema with the referenced one and resolve this instead' + handle_schema_hash = resolve(ref_schema, ref_schema.schema, list) + end + # finally we must recursively walk through our schema and resolve nested schemas + resolve_nested_references schema, handle_schema_hash, list + handle_schema_hash + end class << self def validate(schema, data,opts={}) @@ -279,6 +323,11 @@ def fully_validate(schema, data, opts={}) validator.validate end + def fully_resolve(schema) + validator = JSON::Validator.new(schema, {}, {}) + validator.resolve(validator.base_schema, validator.base_schema.schema) + end + def fully_validate_schema(schema, opts={}) data = schema schema = JSON::Validator.validator_for_name(opts[:version]).metaschema diff --git a/test/schemas/fully_resolve/leave_integer_schema.json b/test/schemas/fully_resolve/leave_integer_schema.json new file mode 100644 index 00000000..f6c7b164 --- /dev/null +++ b/test/schemas/fully_resolve/leave_integer_schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": ["age"], + "properties" : { + "age" : { "type": "integer" } + } +} diff --git a/test/schemas/fully_resolve/leave_string_schema.json b/test/schemas/fully_resolve/leave_string_schema.json new file mode 100644 index 00000000..357df4fe --- /dev/null +++ b/test/schemas/fully_resolve/leave_string_schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": ["name"], + "properties" : { + "name" : { "type": "string" } + } +} diff --git a/test/schemas/fully_resolve/linked_list_schema.json b/test/schemas/fully_resolve/linked_list_schema.json new file mode 100644 index 00000000..8a5d4a49 --- /dev/null +++ b/test/schemas/fully_resolve/linked_list_schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://example.com/linked_list#", + "type": "object", + "required": ["head", "tail"], + "properties" : { + "head" : { + "type": "object" + }, + "tail": { + "oneOf": [ + { "$ref": "#" }, + { "type": "null" } + ] + } + } +} diff --git a/test/test_fully_resolve.rb b/test/test_fully_resolve.rb new file mode 100644 index 00000000..856e75c0 --- /dev/null +++ b/test/test_fully_resolve.rb @@ -0,0 +1,111 @@ +# encoding: utf-8 +require File.expand_path('../test_helper', __FILE__) + +class FullyResovleDraft4Test < Minitest::Test + def test_top_level_ref + schema = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "$ref" => "test/schemas/fully_resolve/leave_string_schema.json#" + } + + expected = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "type" => "object", + "required" => ["name"], + "properties" => { + "name" => { "type" => "string" } + } + } + + resolved = JSON::Validator.fully_resolve(schema) + assert_equal(resolved, expected, "should resolve a top level $ref reference") + + # Test that the result is usable for validation + data = {name: '2'} + assert_valid(schema, data) + assert_valid(resolved, data) + end + + def test_one_of_ref + schema = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "oneOf" => [ + { "$ref" => "test/schemas/fully_resolve/leave_string_schema.json#" }, + { "$ref" => "test/schemas/fully_resolve/leave_integer_schema.json#" } + ] + } + + expected = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "oneOf" => [ + { + "$schema" => "http://json-schema.org/draft-04/schema#", + "type" => "object", + "required" => ["name"], + "properties" => { + "name" => { "type" => "string" } + } + }, + { + "$schema" => "http://json-schema.org/draft-04/schema#", + "type" => "object", + "required" => ["age"], + "properties" => { + "age" => { "type" => "integer" } + } + } + ] + } + + resolved = JSON::Validator.fully_resolve(schema) + assert_equal(resolved, expected, "should resolve a top level $ref reference") + + # Test that the result is usable for validation + data = {name: '2'} + assert_valid(schema, data) + assert_valid(resolved, data) + + data = {age: 2} + assert_valid(schema, data) + assert_valid(resolved, data) + end + + def test_linked_list_ref + stub_request(:get, "example.com/linked_list").to_return(:body => File.new('test/schemas/fully_resolve/linked_list_schema.json'), :status => 200) + + schema = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "$ref" => "http://example.com/linked_list#" + # it does not work to reference the schema by path + # "$ref" => 'test/schemas/fully_resolve/linked_list_schema.json' + } + + expected = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "id" => "http://example.com/linked_list#", + "type" => "object", + "required" => ["head", "tail"], + "properties" => { + "head" => { + "type" => "object" + }, + "tail" => { + } + } + } + expected["properties"]["tail"]["oneOf"] = [ + expected, + { "type" => "null" } + ] + + resolved = JSON::Validator.fully_resolve(schema) + assert_equal(resolved, expected, "should resolve a top level $ref reference") + + data = {"head" => {}, "tail" => nil} + assert_valid(schema, data) + # you don't want to do that it causes infinite recursion aka SystemStackError: stack level too deep + #assert_valid(resolved, data) + end +end + + From 5719c0c78d4fbc989f16a90f1bede4b7d4ab87d3 Mon Sep 17 00:00:00 2001 From: Jonas Peschla Date: Sat, 24 Jan 2015 13:32:05 +0100 Subject: [PATCH 2/2] Add resolution of draft 4 schema spec as test case --- test/test_fully_resolve.rb | 171 +++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/test/test_fully_resolve.rb b/test/test_fully_resolve.rb index 856e75c0..2369fa63 100644 --- a/test/test_fully_resolve.rb +++ b/test/test_fully_resolve.rb @@ -106,6 +106,177 @@ def test_linked_list_ref # you don't want to do that it causes infinite recursion aka SystemStackError: stack level too deep #assert_valid(resolved, data) end + + def test_resolution_of_draft_4_spec + stub_request(:get, "http://json-schema.org/draft-04/schema#").to_return(:body => File.new('resources/draft-04.json'), :status => 200) + schema = { + "$schema" => "http://json-schema.org/draft-04/schema#", + "$ref" => "http://json-schema.org/draft-04/schema#" + } + + schema_array = { + "type" => "array", + "minItems" => 1, + "items" => { "$ref" => "#" } + } + positive_integer = { + "type" => "integer", + "minimum" => 0 + } + positive_integer_default_0 = { + "allOf" => [ positive_integer, { "default" => 0 } ] + } + string_array = { + "type" => "array", + "items" => { "type" => "string" }, + "minItems" => 1, + "uniqueItems" => true + } + simple_types = { + "enum" => [ "array", "boolean", "integer", "null", "number", "object", "string" ] + } + expected = {} + expected.merge!({ + "id" => "http://json-schema.org/draft-04/schema#", + "$schema" => "http://json-schema.org/draft-04/schema#", + "description" => "Core schema meta-schema", + "definitions" => { + "schemaArray" => schema_array, + "positiveInteger" => positive_integer, + "positiveIntegerDefault0" => positive_integer_default_0, + "simpleTypes" => simple_types, + "stringArray" => string_array + }, + "type" => "object", + "properties" => { + "id" => { + "type" => "string", + "format" => "uri" + }, + "$schema" => { + "type" => "string", + "format" => "uri" + }, + "title" => { + "type" => "string" + }, + "description" => { + "type" => "string" + }, + "default" => {}, + "multipleOf" => { + "type" => "number", + "minimum" => 0, + "exclusiveMinimum" => true + }, + "maximum" => { + "type" => "number" + }, + "exclusiveMaximum" => { + "type" => "boolean", + "default" => false + }, + "minimum" => { + "type" => "number" + }, + "exclusiveMinimum" => { + "type" => "boolean", + "default" => false + }, + "maxLength" => positive_integer, + "minLength" => positive_integer_default_0, + "pattern" => { + "type" => "string", + "format" => "regex" + }, + "additionalItems" => { + "anyOf" => [ + { "type" => "boolean" }, + expected + ], + "default" => {} + }, + "items" => { + "anyOf" => [ + expected, + schema_array + ], + "default" => {} + }, + "maxItems" => positive_integer, + "minItems" => positive_integer_default_0, + "uniqueItems" => { + "type" => "boolean", + "default" => false + }, + "maxProperties" => positive_integer, + "minProperties" => positive_integer_default_0, + "required" => string_array, + "additionalProperties" => { + "anyOf" => [ + { "type" => "boolean" }, + expected + ], + "default" => {} + }, + "definitions" => { + "type" => "object", + "additionalProperties" => expected, + "default" => {} + }, + "properties" => { + "type" => "object", + "additionalProperties" => expected, + "default" => {} + }, + "patternProperties" => { + "type" => "object", + "additionalProperties" => expected, + "default" => {} + }, + "dependencies" => { + "type" => "object", + "additionalProperties" => { + "anyOf" => [ + expected, + string_array + ] + } + }, + "enum" => { + "type" => "array", + "minItems" => 1, + "uniqueItems" => true + }, + "type" => { + "anyOf" => [ + simple_types, + { + "type" => "array", + "items" => simple_types, + "minItems" => 1, + "uniqueItems" => true + } + ] + }, + "allOf" => schema_array, + "anyOf" => schema_array, + "oneOf" => schema_array, + "not" => expected + }, + "dependencies" => { + "exclusiveMaximum" => [ "maximum" ], + "exclusiveMinimum" => [ "minimum" ] + }, + "default" => {} + }) + schema_array['items'] = expected + + resolved = JSON::Validator.fully_resolve(schema) + assert_equal(resolved, expected, "should resolve the draft 4 spec") + + assert_valid(schema, schema) + end end