Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only parse $schema during schema validation #171

Merged
merged 3 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions lib/json_schemer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,32 +114,21 @@ class InvalidEcmaRegexp < StandardError; end

class << self
def schema(schema, meta_schema: draft202012, **options)
case schema
when String
schema = JSON.parse(schema)
when Pathname
base_uri = URI.parse(File.join('file:', URI::DEFAULT_PARSER.escape(schema.realpath.to_s)))
options[:base_uri] = base_uri
schema = if options.key?(:ref_resolver)
FILE_URI_REF_RESOLVER.call(base_uri)
else
ref_resolver = CachedResolver.new(&FILE_URI_REF_RESOLVER)
options[:ref_resolver] = ref_resolver
ref_resolver.call(base_uri)
end
end
schema = resolve(schema, options)
unless meta_schema.is_a?(Schema)
meta_schema = META_SCHEMAS_BY_BASE_URI_STR[meta_schema] || raise(UnsupportedMetaSchema, meta_schema)
end
Schema.new(schema, :meta_schema => meta_schema, **options)
end

def valid_schema?(schema, **options)
schema(schema, **options).valid_schema?
schema = resolve(schema, options)
meta_schema(schema, options).valid?(schema, **options.slice(:output_format, :resolve_enumerators, :access_mode))
end

def validate_schema(schema, **options)
schema(schema, **options).validate_schema
schema = resolve(schema, options)
meta_schema(schema, options).validate(schema, **options.slice(:output_format, :resolve_enumerators, :access_mode))
end

def draft202012
Expand Down Expand Up @@ -245,6 +234,36 @@ def openapi30_document
def openapi(document, **options)
OpenAPI.new(document, **options)
end

private

def resolve(schema, options)
case schema
when String
JSON.parse(schema)
when Pathname
base_uri = URI.parse(File.join('file:', URI::DEFAULT_PARSER.escape(schema.realpath.to_s)))
options[:base_uri] = base_uri
if options.key?(:ref_resolver)
FILE_URI_REF_RESOLVER.call(base_uri)
else
ref_resolver = CachedResolver.new(&FILE_URI_REF_RESOLVER)
options[:ref_resolver] = ref_resolver
ref_resolver.call(base_uri)
end
else
schema
end
end

def meta_schema(schema, options)
parseable_schema = {}
if schema.is_a?(Hash)
meta_schema = schema['$schema'] || schema[:'$schema']
parseable_schema['$schema'] = meta_schema if meta_schema.is_a?(String)
end
schema(parseable_schema, **options).meta_schema
end
end

META_SCHEMA_CALLABLES_BY_BASE_URI_STR = {
Expand Down
8 changes: 4 additions & 4 deletions lib/json_schemer/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,12 @@ def validate(instance, output_format: @output_format, resolve_enumerators: @reso
output
end

def valid_schema?
meta_schema.valid?(value)
def valid_schema?(**options)
meta_schema.valid?(value, **options)
end

def validate_schema
meta_schema.validate(value)
def validate_schema(**options)
meta_schema.validate(value, **options)
end

def ref(value)
Expand Down
145 changes: 145 additions & 0 deletions test/json_schemer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,151 @@ def test_it_allows_validating_schemas
assert_equal([required_error], JSONSchemer.validate_schema(invalid_detected_draft4_schema).to_a)
end

def test_schema_validation_invalid_meta_schema
refute(JSONSchemer.valid_schema?({ '$schema' => {} }))
end

def test_schema_validation_parse_error
refute(JSONSchemer.valid_schema?({ 'properties' => '' }))
assert(JSONSchemer.valid_schema?({ 'properties' => {} }))
assert(JSONSchemer.valid_schema?({ 'items' => {} }))
refute(JSONSchemer.valid_schema?({ 'items' => [{}] }))
assert(JSONSchemer.valid_schema?({ 'items' => [{}] }, :meta_schema => JSONSchemer.draft201909))
assert(JSONSchemer.valid_schema?({ '$schema' => 'https://json-schema.org/draft/2019-09/schema', 'items' => [{}] }))
assert(JSONSchemer.valid_schema?({ '$schema': 'https://json-schema.org/draft/2019-09/schema', 'items' => [{}] }))

refute_empty(JSONSchemer.validate_schema({ 'properties' => '' }).to_a)
assert_empty(JSONSchemer.validate_schema({ 'properties' => {} }).to_a)
assert_empty(JSONSchemer.validate_schema({ 'items' => {} }).to_a)
refute_empty(JSONSchemer.validate_schema({ 'items' => [{}] }).to_a)
assert_empty(JSONSchemer.validate_schema({ 'items' => [{}] }, :meta_schema => JSONSchemer.draft201909).to_a)
assert_empty(JSONSchemer.validate_schema({ '$schema' => 'https://json-schema.org/draft/2019-09/schema', 'items' => [{}] }).to_a)
assert_empty(JSONSchemer.validate_schema({ '$schema': 'https://json-schema.org/draft/2019-09/schema', 'items' => [{}] }).to_a)
end

def test_schema_validation_parse_error_with_custom_meta_schema
custom_meta_schema = {
'$vocabulary' => {},
'oneOf' => [
{ 'required' => ['$id'] },
{ 'required' => ['items'] }
],
'properties' => {
'items' => {
'type' => 'string'
}
}
}
custom_meta_schemer = JSONSchemer.schema(custom_meta_schema)
ref_resolver = {
URI('https://example.com/custom-meta-schema') => custom_meta_schema
}.to_proc

assert(JSONSchemer.valid_schema?({}))
refute(JSONSchemer.valid_schema?({}, meta_schema: custom_meta_schemer))
refute(JSONSchemer.valid_schema?({ '$schema' => 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver))
refute(JSONSchemer.valid_schema?({ '$schema': 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver))
refute(JSONSchemer.valid_schema?({ '$id' => {} }))
assert(JSONSchemer.valid_schema?({ '$id' => {} }, meta_schema: custom_meta_schemer))
assert(JSONSchemer.valid_schema?({ '$id' => {}, '$schema' => 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver))
assert(JSONSchemer.valid_schema?({ '$id' => {}, '$schema': 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver))
assert(JSONSchemer.valid_schema?({ 'items' => {} }))
refute(JSONSchemer.valid_schema?({ 'items' => 'yah' }))
refute(JSONSchemer.valid_schema?({ 'items' => {} }, meta_schema: custom_meta_schemer))
assert(JSONSchemer.valid_schema?({ 'items' => 'yah' }, meta_schema: custom_meta_schemer))
refute(JSONSchemer.valid_schema?({ 'items' => {}, '$schema' => 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver))
assert(JSONSchemer.valid_schema?({ 'items' => 'yah', '$schema' => 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver))
refute(JSONSchemer.valid_schema?({ 'items' => {}, '$schema': 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver))
assert(JSONSchemer.valid_schema?({ 'items' => 'yah', '$schema': 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver))

assert_empty(JSONSchemer.validate_schema({}).to_a)
refute_empty(JSONSchemer.validate_schema({}, meta_schema: custom_meta_schemer).to_a)
refute_empty(JSONSchemer.validate_schema({ '$schema' => 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver).to_a)
refute_empty(JSONSchemer.validate_schema({ '$schema': 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver).to_a)
refute_empty(JSONSchemer.validate_schema({ '$id' => {} }).to_a)
assert_empty(JSONSchemer.validate_schema({ '$id' => {} }, meta_schema: custom_meta_schemer).to_a)
assert_empty(JSONSchemer.validate_schema({ '$id' => {}, '$schema' => 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver).to_a)
assert_empty(JSONSchemer.validate_schema({ '$id' => {}, '$schema': 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver).to_a)
assert_empty(JSONSchemer.validate_schema({ 'items' => {} }).to_a)
refute_empty(JSONSchemer.validate_schema({ 'items' => 'yah' }).to_a)
refute_empty(JSONSchemer.validate_schema({ 'items' => {} }, meta_schema: custom_meta_schemer).to_a)
assert_empty(JSONSchemer.validate_schema({ 'items' => 'yah' }, meta_schema: custom_meta_schemer).to_a)
refute_empty(JSONSchemer.validate_schema({ 'items' => {}, '$schema' => 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver).to_a)
assert_empty(JSONSchemer.validate_schema({ 'items' => 'yah', '$schema' => 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver).to_a)
refute_empty(JSONSchemer.validate_schema({ 'items' => {}, '$schema': 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver).to_a)
assert_empty(JSONSchemer.validate_schema({ 'items' => 'yah', '$schema': 'https://example.com/custom-meta-schema' }, ref_resolver: ref_resolver).to_a)
end

def test_schema_validation_json
refute(JSONSchemer.valid_schema?('{"$schema":{}}'))
assert(JSONSchemer.valid_schema?('{"items":{}}'))
refute(JSONSchemer.valid_schema?('{"items":[{}]}'))
assert(JSONSchemer.valid_schema?('{"items":[{}]}', :meta_schema => JSONSchemer.draft201909))
assert(JSONSchemer.valid_schema?('{"items":[{}],"$schema":"https://json-schema.org/draft/2019-09/schema"}'))

refute_empty(JSONSchemer.validate_schema('{"$schema":{}}').to_a)
assert_empty(JSONSchemer.validate_schema('{"items":{}}').to_a)
refute_empty(JSONSchemer.validate_schema('{"items":[{}]}').to_a)
assert_empty(JSONSchemer.validate_schema('{"items":[{}]}', :meta_schema => JSONSchemer.draft201909).to_a)
assert_empty(JSONSchemer.validate_schema('{"items":[{}],"$schema":"https://json-schema.org/draft/2019-09/schema"}').to_a)
end

def test_schema_validation_pathname
schema_invalid = Pathname.new(__dir__).join('schemas', '$schema_invalid.json')
items_object = Pathname.new(__dir__).join('schemas', 'items_object.json')
items_array = Pathname.new(__dir__).join('schemas', 'items_array.json')
schema_items_array = Pathname.new(__dir__).join('schemas', '$schema_items_array.json')

refute(JSONSchemer.valid_schema?(schema_invalid))
assert(JSONSchemer.valid_schema?(items_object))
refute(JSONSchemer.valid_schema?(items_array))
assert(JSONSchemer.valid_schema?(items_array, :meta_schema => JSONSchemer.draft201909))
assert(JSONSchemer.valid_schema?(schema_items_array))

refute_empty(JSONSchemer.validate_schema(schema_invalid).to_a)
assert_empty(JSONSchemer.validate_schema(items_object).to_a)
refute_empty(JSONSchemer.validate_schema(items_array).to_a)
assert_empty(JSONSchemer.validate_schema(items_array, :meta_schema => JSONSchemer.draft201909).to_a)
assert_empty(JSONSchemer.validate_schema(schema_items_array).to_a)
end

def test_schema_validation_options
custom_meta_schemer = JSONSchemer.schema({
'$vocabulary' => {},
'properties' => {
'yah' => {
'readOnly' => true
}
}
})
read_only_schemer = JSONSchemer.schema({ 'yah' => 1 }, meta_schema: custom_meta_schemer)
invalid_ref_schemer = JSONSchemer.schema({ '$ref' => {} })

assert(JSONSchemer.valid_schema?({ 'yah' => 1 }, meta_schema: custom_meta_schemer))
assert(JSONSchemer.valid_schema?({ 'yah' => 1 }, meta_schema: custom_meta_schemer, access_mode: 'read'))
refute(JSONSchemer.valid_schema?({ 'yah' => 1 }, meta_schema: custom_meta_schemer, access_mode: 'write'))

assert(read_only_schemer.valid_schema?)
assert(read_only_schemer.valid_schema?(access_mode: 'read'))
refute(read_only_schemer.valid_schema?(access_mode: 'write'))

assert_equal(['string'], JSONSchemer.validate_schema({ '$schema' => {} }).map { |result| result.fetch('type') })
refute(JSONSchemer.validate_schema({ '$schema' => {} }, output_format: 'basic').fetch('valid'))
assert_kind_of(Enumerator, JSONSchemer.validate_schema({ '$schema' => {} }, output_format: 'basic').fetch('errors'))
assert_kind_of(Array, JSONSchemer.validate_schema({ '$schema' => {} }, output_format: 'basic', resolve_enumerators: true).fetch('errors'))
assert_empty(JSONSchemer.validate_schema({ 'yah' => 1 }, meta_schema: custom_meta_schemer).to_a)
assert_empty(JSONSchemer.validate_schema({ 'yah' => 1 }, meta_schema: custom_meta_schemer, access_mode: 'read').to_a)
refute_empty(JSONSchemer.validate_schema({ 'yah' => 1 }, meta_schema: custom_meta_schemer, access_mode: 'write').to_a)

assert_equal(['string'], invalid_ref_schemer.validate_schema.map { |result| result.fetch('type') })
refute(invalid_ref_schemer.validate_schema(output_format: 'basic').fetch('valid'))
assert_kind_of(Enumerator, invalid_ref_schemer.validate_schema(output_format: 'basic').fetch('errors'))
assert_kind_of(Array, invalid_ref_schemer.validate_schema(output_format: 'basic', resolve_enumerators: true).fetch('errors'))
assert_empty(read_only_schemer.validate_schema.to_a)
assert_empty(read_only_schemer.validate_schema(access_mode: 'read').to_a)
refute_empty(read_only_schemer.validate_schema(access_mode: 'write').to_a)
end

def test_non_string_keys
schemer = JSONSchemer.schema({
properties: {
Expand Down
3 changes: 3 additions & 0 deletions test/schemas/$schema_invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$schema": {}
}
4 changes: 4 additions & 0 deletions test/schemas/$schema_items_array.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"items": [{}]
}
3 changes: 3 additions & 0 deletions test/schemas/items_array.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"items": [{}]
}
3 changes: 3 additions & 0 deletions test/schemas/items_object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"items": {}
}