From c123bb2a4765ffb8e45e92d26b785d570235ed23 Mon Sep 17 00:00:00 2001 From: Kyle Hargraves Date: Mon, 3 Nov 2014 09:52:35 -0600 Subject: [PATCH] JSON::Schema::Loader --- Gemfile | 1 + json-schema.gemspec | 1 + lib/json-schema.rb | 1 + lib/json-schema/schema/loader.rb | 113 ++++++++++++++++++++++++++ test/schemas/address_microformat.json | 18 ++++ test/test_schema_loader.rb | 86 ++++++++++++++++++++ 6 files changed, 220 insertions(+) create mode 100644 lib/json-schema/schema/loader.rb create mode 100644 test/schemas/address_microformat.json create mode 100644 test/test_schema_loader.rb diff --git a/Gemfile b/Gemfile index 5e2797e3..97b7a11a 100644 --- a/Gemfile +++ b/Gemfile @@ -7,4 +7,5 @@ gem "json", :platforms => :mri_18 group :development do gem "rake" gem "minitest", '~> 5.0' + gem "pry" end diff --git a/json-schema.gemspec b/json-schema.gemspec index 1ee467ab..a1370f60 100644 --- a/json-schema.gemspec +++ b/json-schema.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |s| s.license = "MIT" s.required_rubygems_version = ">= 1.8" + s.add_runtime_dependency "addressable" s.add_development_dependency "webmock" s.add_runtime_dependency "addressable", '~> 2.3' end diff --git a/lib/json-schema.rb b/lib/json-schema.rb index 96e2c4d7..02d3d8c0 100644 --- a/lib/json-schema.rb +++ b/lib/json-schema.rb @@ -10,6 +10,7 @@ require 'json-schema/util/array_set' require 'json-schema/schema' +require 'json-schema/schema/loader' require 'json-schema/validator' Dir[File.join(File.dirname(__FILE__), "json-schema/attributes/*.rb")].each {|file| require file } Dir[File.join(File.dirname(__FILE__), "json-schema/attributes/formats/*.rb")].each {|file| require file } diff --git a/lib/json-schema/schema/loader.rb b/lib/json-schema/schema/loader.rb new file mode 100644 index 00000000..4c6c4275 --- /dev/null +++ b/lib/json-schema/schema/loader.rb @@ -0,0 +1,113 @@ +require 'open-uri' +require 'addressable/uri' +require 'pathname' + +module JSON + class Schema + # Raised by {JSON::Schema::Loader} when one of its settings indicate + # a schema should not be loaded. + class LoadRefused < StandardError + # @return [String] the requested schema location which was refused + attr_reader :location + + # @return [Symbol] either +:uri+ or +:file+ + attr_reader :type + + def initialize(location, type) + @location = location + @type = type + super("Load of #{type == :uri ? 'URI' : type} at #{location} refused!") + end + end + + # When an unregistered schema is encountered, the {JSON::Schema::Loader} is + # used to fetch its contents and register it with the {JSON::Validator}. + # + # This default loader will load schemas from the filesystem or from a URI. + class Loader + # The behavior of the schema loader can be controlled by providing + # callbacks to determine whether to permit loading referenced schemas. + # The options +accept_uri+ and +accept_file+ should be procs which + # accept a +URI+ or +Pathname+ object, and return a boolean value + # indicating whether to load the referenced schema. + # + # URIs using the +file+ scheme will be normalized into +Pathname+ objects + # and passed to the +accept_file+ callback. + # + # @param options [Hash] + # @option options [Boolean, #call] accept_uri (true) + # @option options [Boolean, #call] accept_file (true) + # + # @example Reject all unregistered schemas + # JSON::Validator.schema_loader = JSON::Schema::Loader.new( + # :accept_uri => false, + # :accept_file => false + # ) + # + # @example Only permit URIs from certain hosts + # JSON::Validator.schema_loader = JSON::Schema::Loader.new( + # :accept_file => false, + # :accept_uri => proc { |uri| ['mycompany.com', 'json-schema.org'].include?(uri.host) } + # ) + def initialize(options = {}) + @accept_uri = options.fetch(:accept_uri, true) + @accept_file = options.fetch(:accept_file, true) + end + + # @param location [#to_s] The location from which to load the schema + # @return [JSON::Schema] + # @raise [JSON::Schema::LoadRefused] if +accept_uri+ or +accept_file+ + # indicated the schema should not be loaded + # @raise [JSON::ParserError] if the schema was not a valid JSON object + def load(location) + uri = Addressable::URI.parse(location.to_s) + body = if uri.scheme.nil? || uri.scheme == 'file' + uri = Addressable::URI.convert_path(uri.path) + read_file(Pathname.new(uri.path).expand_path) + else + read_uri(uri) + end + + JSON::Schema.new(JSON::Validator.parse(body), uri) + end + + # @param uri [Addressable::URI] + # @return [Boolean] + def accept_uri?(uri) + if @accept_uri.respond_to?(:call) + @accept_uri.call(uri) + else + @accept_uri + end + end + + # @param pathname [Pathname] + # @return [Boolean] + def accept_file?(pathname) + if @accept_file.respond_to?(:call) + @accept_file.call(pathname) + else + @accept_file + end + end + + private + + def read_uri(uri) + if accept_uri?(uri) + open(uri.to_s).read + else + raise JSON::Schema::LoadRefused.new(uri.to_s, :uri) + end + end + + def read_file(pathname) + if accept_file?(pathname) + pathname.read + else + raise JSON::Schema::LoadRefused.new(pathname.to_s, :file) + end + end + end + end +end diff --git a/test/schemas/address_microformat.json b/test/schemas/address_microformat.json new file mode 100644 index 00000000..e1cdd202 --- /dev/null +++ b/test/schemas/address_microformat.json @@ -0,0 +1,18 @@ +{ + "description": "An Address following the convention of http://microformats.org/wiki/hcard", + "type": "object", + "properties": { + "post-office-box": { "type": "string" }, + "extended-address": { "type": "string" }, + "street-address": { "type": "string" }, + "locality":{ "type": "string" }, + "region": { "type": "string" }, + "postal-code": { "type": "string" }, + "country-name": { "type": "string"} + }, + "required": ["locality", "region", "country-name"], + "dependencies": { + "post-office-box": "street-address", + "extended-address": "street-address" + } +} diff --git a/test/test_schema_loader.rb b/test/test_schema_loader.rb new file mode 100644 index 00000000..4ac9ea08 --- /dev/null +++ b/test/test_schema_loader.rb @@ -0,0 +1,86 @@ +require File.expand_path('../test_helper', __FILE__) +require 'webmock' + +class TestSchemaLoader < Minitest::Test + ADDRESS_SCHEMA_URI = 'http://json-schema.org/address' + ADDRESS_SCHEMA_PATH = File.expand_path('../schemas/address_microformat.json', __FILE__) + + include WebMock::API + + def setup + WebMock.enable! + WebMock.disable_net_connect! + end + + def teardown + WebMock.reset! + WebMock.disable! + end + + def stub_address_request(body = File.read(ADDRESS_SCHEMA_PATH)) + stub_request(:get, ADDRESS_SCHEMA_URI). + to_return(:body => body, :status => 200) + end + + def test_accept_all_uris + stub_address_request + + loader = JSON::Schema::Loader.new + schema = loader.load(ADDRESS_SCHEMA_URI) + + assert_equal schema.uri, Addressable::URI.parse("#{ADDRESS_SCHEMA_URI}#") + end + + def test_accept_all_files + loader = JSON::Schema::Loader.new + schema = loader.load(ADDRESS_SCHEMA_PATH) + + assert_equal schema.uri, Addressable::URI.convert_path(ADDRESS_SCHEMA_PATH + '#') + end + + def test_refuse_all_uris + loader = JSON::Schema::Loader.new(:accept_uri => false) + refute loader.accept_uri?(Addressable::URI.parse('http://foo.com')) + end + + def test_refuse_all_files + loader = JSON::Schema::Loader.new(:accept_file => false) + refute loader.accept_file?(Pathname.new('/foo/bar/baz')) + end + + def test_accept_uri_proc + loader = JSON::Schema::Loader.new( + :accept_uri => proc { |uri| uri.host == 'json-schema.org' } + ) + + assert loader.accept_uri?(Addressable::URI.parse('http://json-schema.org/address')) + refute loader.accept_uri?(Addressable::URI.parse('http://sub.json-schema.org/address')) + end + + def test_accept_file_proc + test_root = Pathname.new(__FILE__).expand_path.dirname + + loader = JSON::Schema::Loader.new( + :accept_file => proc { |path| path.to_s.start_with?(test_root.to_s) } + ) + + assert loader.accept_file?(test_root.join('anything.json')) + refute loader.accept_file?(test_root.join('..', 'anything.json')) + end + + def test_file_scheme + loader = JSON::Schema::Loader.new(:accept_uri => true, :accept_file => false) + assert_raises(JSON::Schema::LoadRefused) do + loader.load('file://' + ADDRESS_SCHEMA_PATH) + end + end + + def test_parse_error + stub_address_request('this is totally not valid JSON!') + + loader = JSON::Schema::Loader.new + assert_raises(JSON::ParserError) do + loader.load(ADDRESS_SCHEMA_URI) + end + end +end