From 52fb83fc4c23b4035521c4edb274668c942ec782 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Thu, 23 Nov 2023 09:26:20 -0800 Subject: [PATCH] Fix keyword refs This fixes two problems with JSON pointer refs and known keywords: - If the ref resolved to a `Keyword` object with a schema value (eg, `#/items`), `unknown_schema!` was called, which was only defined for `UnknownKeyword`. I changed the name to `parsed_schema` and defined it for all keywords, though it still feels brittle since it requires keywords to pre-parse their values into schema objects. - If the ref resolved through a `Keyword` object with a schema value (eg, '#/items/not`), `fetch` was called on the keyword's parsed schema, which wasn't defined. I settled on a `fetch` method for `Keyword` and `Schema` to find child objects by key/index. That way a keyword with a schema value will delegate `fetch` to its parsed schema object. `UnknownKeyword` also becomes less of a special case. I also added a `Schema` type check after pointer resolution to help people identify misdirected pointers. Closes: https://github.com/davishmcclurg/json_schemer/issues/159 --- lib/json_schemer/draft202012/vocab/core.rb | 6 +- lib/json_schemer/keyword.rb | 8 +++ lib/json_schemer/schema.rb | 15 ++-- test/ref_test.rb | 80 ++++++++++++++++++++++ 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/lib/json_schemer/draft202012/vocab/core.rb b/lib/json_schemer/draft202012/vocab/core.rb index 66fea2c..c5d1182 100644 --- a/lib/json_schemer/draft202012/vocab/core.rb +++ b/lib/json_schemer/draft202012/vocab/core.rb @@ -136,7 +136,7 @@ def parse end end - def fetch_unknown!(token) + def fetch(token) if value.is_a?(Hash) parsed[token] ||= JSONSchemer::Schema::UNKNOWN_KEYWORD_CLASS.new(value.fetch(token), self, token, schema) elsif value.is_a?(Array) @@ -146,8 +146,8 @@ def fetch_unknown!(token) end end - def unknown_schema! - @unknown_schema ||= subschema(value) + def parsed_schema + @parsed_schema ||= subschema(value) end def validate(instance, instance_location, keyword_location, _context) diff --git a/lib/json_schemer/keyword.rb b/lib/json_schemer/keyword.rb index 091d7d8..f61ea99 100644 --- a/lib/json_schemer/keyword.rb +++ b/lib/json_schemer/keyword.rb @@ -30,6 +30,14 @@ def error_key keyword end + def fetch(key) + parsed.fetch(parsed.is_a?(Array) ? key.to_i : key) + end + + def parsed_schema + parsed.is_a?(Schema) ? parsed : nil + end + private def parse diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index d77bfa7..baf1ec2 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -203,18 +203,13 @@ def resolve_ref(uri) end schema = Hana::Pointer.parse(pointer).reduce(schema) do |obj, token| - if obj.is_a?(UNKNOWN_KEYWORD_CLASS) - obj.fetch_unknown!(token) - elsif obj.parsed.is_a?(Array) - obj.parsed.fetch(token.to_i) - else - obj.parsed.fetch(token) - end + obj.fetch(token) rescue IndexError raise InvalidRefPointer, pointer end - schema = schema.unknown_schema! unless schema.is_a?(Schema) + schema = schema.parsed_schema if schema.is_a?(Keyword) + raise InvalidRefPointer, pointer unless schema.is_a?(Schema) schema end @@ -299,6 +294,10 @@ def error_key '^' end + def fetch(key) + parsed.fetch(key) + end + def fetch_format(format, *args, &block) if meta_schema == self formats.fetch(format, *args, &block) diff --git a/test/ref_test.rb b/test/ref_test.rb index 1cf8ede..ac8905c 100644 --- a/test/ref_test.rb +++ b/test/ref_test.rb @@ -395,4 +395,84 @@ def test_exclusive_ref_supports_definitions_with_id_and_json_pointer assert(schema.valid?(1)) refute(schema.valid?('1')) end + + def test_keyword_ref + schemer = JSONSchemer.schema({ + '$ref' => '#/items', + 'items' => { + 'type' => 'integer' + } + }) + assert(schemer.valid?(1)) + refute(schemer.valid?('1')) + end + + def test_nested_keyword_ref + schemer = JSONSchemer.schema({ + '$ref' => '#/items/not', + 'items' => { + 'not' => { + 'type' => 'integer' + } + } + }) + assert(schemer.valid?(1)) + refute(schemer.valid?('1')) + + schemer = JSONSchemer.schema({ + '$ref' => '#/properties/a', + 'properties' => { + 'a' => { + 'type' => 'integer' + } + } + }) + assert(schemer.valid?(1)) + refute(schemer.valid?('1')) + end + + def test_complex_nested_keyword_ref + schemer = JSONSchemer.schema({ + '$ref' => '#/definitions/a/allOf/0/items/properties/b/unknown-array/0/unknown-hash/c', + 'definitions' => { + 'a' => { + 'allOf' => [ + { + 'items' => { + 'properties' => { + 'b' => { + 'unknown-array' => [ + { + 'unknown-hash' => { + 'c' => { + 'type' => 'integer' + } + } + } + ] + } + } + } + } + ] + } + } + }) + assert(schemer.valid?(1)) + refute(schemer.valid?('1')) + end + + def test_non_schema_ref_pointer + schemer = JSONSchemer.schema({ + '$ref' => '#/allOf', + 'allOf' => [true] + }) + assert_raises(JSONSchemer::InvalidRefPointer) { schemer.valid?(1) } + + schemer = JSONSchemer.schema({ + '$ref' => '#/type/0', + 'type' => ['integer', 'string'] + }) + assert_raises(JSONSchemer::InvalidRefPointer) { schemer.valid?(1) } + end end