Skip to content

Commit

Permalink
Merge configuration options to pass to subschemas
Browse files Browse the repository at this point in the history
This effectively merges `Schema.new` options with the provided
`Configuration` object (or the default global one) so that subschemas
validate using the same configuration as the parent schema.

`base_uri` and `meta_schema` are still instance variables because
they're modified by keywords (that's why they're `attr_accessor`).

`resolve_ref` still passes `ref_resolver` and `regexp_resolver` to
subschemas to support caching (because the `CachedResolver` instances
are created in `Schema`). They could maybe be cached in `Configuration`
instead, but that felt like a bigger change.

Fixes: #198
  • Loading branch information
davishmcclurg committed Jan 21, 2025
1 parent 03193ce commit a1a2137
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 22 deletions.
1 change: 1 addition & 0 deletions lib/json_schemer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true
require 'bigdecimal'
require 'forwardable'
require 'ipaddr'
require 'json'
require 'net/http'
Expand Down
53 changes: 31 additions & 22 deletions lib/json_schemer/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def original_instance(instance_location)
end
end

extend Forwardable
include Output

SCHEMA_KEYWORD_CLASS = Draft202012::Vocab::Core::Schema
Expand Down Expand Up @@ -44,7 +45,8 @@ def original_instance(instance_location)

attr_accessor :base_uri, :meta_schema, :keywords, :keyword_order
attr_reader :value, :parent, :root, :configuration, :parsed
attr_reader :vocabulary, :format, :formats, :content_encodings, :content_media_types, :custom_keywords, :before_property_validation, :after_property_validation, :insert_property_defaults
def_delegators :@configuration, :vocabulary, :format, :formats, :content_encodings, :content_media_types, :before_property_validation, :after_property_validation, :insert_property_defaults
def_delegator :@configuration, :keywords, :custom_keywords

def initialize(
value,
Expand Down Expand Up @@ -75,32 +77,35 @@ def initialize(
@root = root
@keyword = keyword
@schema = self
@configuration = configuration
@base_uri = base_uri
@meta_schema = meta_schema
@vocabulary = vocabulary
@format = format
@formats = formats
@content_encodings = content_encodings
@content_media_types = content_media_types
@custom_keywords = keywords
@before_property_validation = Array(before_property_validation)
@after_property_validation = Array(after_property_validation)
@insert_property_defaults = insert_property_defaults
@property_default_resolver = property_default_resolver
@original_ref_resolver = ref_resolver
@original_regexp_resolver = regexp_resolver
@output_format = output_format
@resolve_enumerators = resolve_enumerators
@access_mode = access_mode
@configuration = Configuration.new(
:base_uri => base_uri,
:meta_schema => meta_schema,
:vocabulary => vocabulary,
:format => format,
:formats => formats,
:content_encodings => content_encodings,
:content_media_types => content_media_types,
:keywords => keywords,
:before_property_validation => Array(before_property_validation),
:after_property_validation => Array(after_property_validation),
:insert_property_defaults => insert_property_defaults,
:property_default_resolver => property_default_resolver,
:ref_resolver => ref_resolver,
:regexp_resolver => regexp_resolver,
:output_format => output_format,
:resolve_enumerators => resolve_enumerators,
:access_mode => access_mode
)
@parsed = parse
end

def valid?(instance, **options)
validate(instance, :output_format => 'flag', **options).fetch('valid')
end

def validate(instance, output_format: @output_format, resolve_enumerators: @resolve_enumerators, access_mode: @access_mode)
def validate(instance, output_format: @configuration.output_format, resolve_enumerators: @configuration.resolve_enumerators, access_mode: @configuration.access_mode)
instance_location = Location.root
context = Context.new(instance, [], nil, (!insert_property_defaults && output_format == 'flag'), access_mode)
result = validate_instance(deep_stringify_keys(instance), instance_location, root_keyword_location, context)
Expand Down Expand Up @@ -340,17 +345,17 @@ def error(formatted_instance_location:, **options)
end

def ref_resolver
@ref_resolver ||= @original_ref_resolver == 'net/http' ? CachedResolver.new(&NET_HTTP_REF_RESOLVER) : @original_ref_resolver
@ref_resolver ||= @configuration.ref_resolver == 'net/http' ? CachedResolver.new(&NET_HTTP_REF_RESOLVER) : @configuration.ref_resolver
end

def regexp_resolver
@regexp_resolver ||= case @original_regexp_resolver
@regexp_resolver ||= case @configuration.regexp_resolver
when 'ecma'
CachedResolver.new(&ECMA_REGEXP_RESOLVER)
when 'ruby'
CachedResolver.new(&RUBY_REGEXP_RESOLVER)
else
@original_regexp_resolver
@configuration.regexp_resolver
end
end

Expand Down Expand Up @@ -407,7 +412,11 @@ def root_keyword_location
end

def property_default_resolver
@property_default_resolver ||= insert_property_defaults == :symbol ? SYMBOL_PROPERTY_DEFAULT_RESOLVER : DEFAULT_PROPERTY_DEFAULT_RESOLVER
@property_default_resolver ||= if @configuration.property_default_resolver
@configuration.property_default_resolver
else
insert_property_defaults == :symbol ? SYMBOL_PROPERTY_DEFAULT_RESOLVER : DEFAULT_PROPERTY_DEFAULT_RESOLVER
end
end

def resolve_enumerators!(output)
Expand Down
71 changes: 71 additions & 0 deletions test/json_schemer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,77 @@ def test_schema_ref
assert_equal(subschemer, subsubschemer.ref('#'))
end

def test_schema_ref_configuration
schema1 = {
'type' => 'integer',
'$ref' => 'schema2',
'$defs' => {
'x' => {
'properties' => {
'y' => {
'type' => 'string',
'$ref' => 'schema2',
'default' => 'z',
'pattern' => '[1z]+',
'format' => 'email'
}
}
}
}
}
refs = {
URI('json-schemer://schema/schema2') => {
'$ref' => 'schema3'
},
URI('json-schemer://schema/schema3') => {
'maximum' => 1,
'maxLength' => 1,
'pattern' => '[1z]+',
'format' => 'email'
}
}

ref_counts = Hash.new(0)
net_http_get = proc do |uri|
ref_counts[uri] += 1
refs.fetch(uri).to_json
end
regexp_counts = Hash.new(0)
ruby_equivalent = proc do |pattern|
regexp_counts[pattern] += 1
pattern
end

Net::HTTP.stub(:get, net_http_get) do
JSONSchemer::EcmaRegexp.stub(:ruby_equivalent, ruby_equivalent) do
schemer = JSONSchemer.schema(
schema1,
:format => false,
:insert_property_defaults => true,
:ref_resolver => 'net/http',
:regexp_resolver => 'ecma'
)
assert(schemer.valid?(1))
refute(schemer.valid?(2))
assert_equal(1, ref_counts[URI('json-schemer://schema/schema2')])
assert_equal(1, ref_counts[URI('json-schemer://schema/schema3')])
assert_equal(1, regexp_counts['[1z]+'])

subschemer = schemer.ref('#/$defs/x')
assert(subschemer.valid?({ 'y' => '1' }))
refute(subschemer.valid?({ 'y' => '2' }))
refute(subschemer.valid?({ 'y' => '11' }))
assert_equal(1, ref_counts[URI('json-schemer://schema/schema2')])
assert_equal(1, ref_counts[URI('json-schemer://schema/schema3')])
assert_equal(1, regexp_counts['[1z]+'])

data = {}
assert(subschemer.valid?(data))
assert_equal('z', data.fetch('y'))
end
end
end

def test_published_meta_schemas
[
JSONSchemer::Draft202012::SCHEMA,
Expand Down

0 comments on commit a1a2137

Please sign in to comment.