From a1a21373454429ec5c2f51814e28fd8948c67867 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Mon, 20 Jan 2025 17:35:57 -0800 Subject: [PATCH] Merge configuration options to pass to subschemas 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: https://github.com/davishmcclurg/json_schemer/issues/198 --- lib/json_schemer.rb | 1 + lib/json_schemer/schema.rb | 53 ++++++++++++++++------------ test/json_schemer_test.rb | 71 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 22 deletions(-) diff --git a/lib/json_schemer.rb b/lib/json_schemer.rb index 5d8f147..df81b54 100644 --- a/lib/json_schemer.rb +++ b/lib/json_schemer.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true require 'bigdecimal' +require 'forwardable' require 'ipaddr' require 'json' require 'net/http' diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index 51e38d5..de78060 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -15,6 +15,7 @@ def original_instance(instance_location) end end + extend Forwardable include Output SCHEMA_KEYWORD_CLASS = Draft202012::Vocab::Core::Schema @@ -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, @@ -75,24 +77,27 @@ 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 @@ -100,7 +105,7 @@ 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) @@ -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 @@ -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) diff --git a/test/json_schemer_test.rb b/test/json_schemer_test.rb index 0c8dc82..48574a2 100644 --- a/test/json_schemer_test.rb +++ b/test/json_schemer_test.rb @@ -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,