Skip to content

Commit

Permalink
Merge pull request #175 from pd/schema-loader
Browse files Browse the repository at this point in the history
JSON::Schema::Reader
  • Loading branch information
iainbeeston committed Dec 2, 2014
2 parents 5657095 + 229e849 commit 7c9d44a
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 30 deletions.
26 changes: 25 additions & 1 deletion README.textile
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ JSON::Validator.validate(schema,data) # => false

h3. Custom format validation

The JSON schema standard allows custom formats in schema definitions which should be ignored by validators that do not support them. JSON::Schema allows registering procs as custom format validators which receive the value to be checked as parameter and must raise a <code>JSON::Schema::CustomFormatError</code> to indicate a format violation. The error message will be prepended by the property namne, e.g. "The property '#a'":
The JSON schema standard allows custom formats in schema definitions which should be ignored by validators that do not support them. JSON::Schema allows registering procs as custom format validators which receive the value to be checked as parameter and must raise a <code>JSON::Schema::CustomFormatError</code> to indicate a format violation. The error message will be prepended by the property name, e.g. "The property '#a'":

<pre>
require 'rubygems'
Expand Down Expand Up @@ -397,6 +397,30 @@ errors = JSON::Validator.fully_validate(schema, {"a" => "23"})

</pre>

h2. Controlling Remote Schema Reading

In some cases, you may wish to prevent the JSON Schema library from making HTTP calls or reading local files in order to resolve <code>$ref</code> schemas. If you fully control all schemas which should be used by validation, this could be accomplished by registering all referenced schemas with the validator in advance:

<pre>
schema = JSON::Schema.new(some_schema_definition, Addressable::URI.parse('http://example.com/my-schema'))
JSON::Validator.add_schema(schema)
</pre>

If more extensive control is necessary, the <code>JSON::Schema::Reader</code> instance used can be configured in a few ways:

<pre>
# Change the default schema reader used
JSON::Validator.schema_reader = JSON::Schema::Reader.new(:accept_uri => true, :accept_file => false)

# For this validation call, use a reader which only accepts URIs from my-website.com
schema_reader = JSON::Schema::Reader.new(
:accept_uri => proc { |uri| uri.host == 'my-website.com' }
)
JSON::Validator.validate(some_schema, some_object, :schema_reader => schema_reader)
</pre>

The <code>JSON::Schema::Reader</code> interface requires only an object which responds to <code>read(string)</code> and returns a <code>JSON::Schema</code> instance. See the "API documentation":http://www.rubydoc.info/github/ruby-json-schema/json-schema/master/JSON/Schema/Reader for more information.

h2. JSON Backends

The JSON Schema library currently supports the <code>json</code> and <code>yajl-ruby</code> backend JSON parsers. If either of these libraries are installed, they will be automatically loaded and used to parse any JSON strings supplied by the user.
Expand Down
2 changes: 2 additions & 0 deletions lib/json-schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

require 'json-schema/util/array_set'
require 'json-schema/schema'
require 'json-schema/schema/reader'
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 }
Dir[File.join(File.dirname(__FILE__), "json-schema/validators/*.rb")].sort!.each {|file| require file }
7 changes: 7 additions & 0 deletions lib/json-schema/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ def self.stringify(schema)
end
end

# @return [JSON::Schema] a new schema matching an array whose items all match this schema.
def to_array_schema
array_schema = { 'type' => 'array', 'items' => schema }
array_schema['$schema'] = schema['$schema'] unless schema['$schema'].nil?
JSON::Schema.new(array_schema, uri, validator)
end

def to_s
@schema.to_json
end
Expand Down
113 changes: 113 additions & 0 deletions lib/json-schema/schema/reader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
require 'open-uri'
require 'addressable/uri'
require 'pathname'

module JSON
class Schema
# Raised by {JSON::Schema::Reader} when one of its settings indicate
# a schema should not be readed.
class ReadRefused < 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("Read of #{type == :uri ? 'URI' : type} at #{location} refused!")
end
end

# When an unregistered schema is encountered, the {JSON::Schema::Reader} is
# used to fetch its contents and register it with the {JSON::Validator}.
#
# This default reader will read schemas from the filesystem or from a URI.
class Reader
# The behavior of the schema reader can be controlled by providing
# callbacks to determine whether to permit reading 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 read 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_reader = JSON::Schema::Reader.new(
# :accept_uri => false,
# :accept_file => false
# )
#
# @example Only permit URIs from certain hosts
# JSON::Validator.schema_reader = JSON::Schema::Reader.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 read the schema
# @return [JSON::Schema]
# @raise [JSON::Schema::ReadRefused] if +accept_uri+ or +accept_file+
# indicated the schema should not be readed
# @raise [JSON::ParserError] if the schema was not a valid JSON object
def read(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::ReadRefused.new(uri.to_s, :uri)
end
end

def read_file(pathname)
if accept_file?(pathname)
File.read(Addressable::URI.unescape(pathname.to_s))
else
raise JSON::Schema::ReadRefused.new(pathname.to_s, :file)
end
end
end
end
end
58 changes: 29 additions & 29 deletions lib/json-schema/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require 'thread'
require 'yaml'

require 'json-schema/schema/reader'
require 'json-schema/errors/schema_error'
require 'json-schema/errors/json_parse_error'

Expand Down Expand Up @@ -40,6 +41,7 @@ def initialize(schema_data, data, opts={})

validator = JSON::Validator.validator_for_name(@options[:version])
@options[:version] = validator
@options[:schema_reader] ||= JSON::Validator.schema_reader

@validation_options = @options[:record_errors] ? {:record_errors => true} : {}
@validation_options[:insert_defaults] = true if @options[:insert_defaults]
Expand Down Expand Up @@ -100,10 +102,12 @@ def schema_from_fragment(base_schema, fragment)
raise JSON::Schema::SchemaError.new("Invalid schema encountered when resolving :fragment option")
end
end
if @options[:list] #check if the schema is validating a list
base_schema.schema = schema_to_list(base_schema.schema)

if @options[:list]
base_schema.to_array_schema
else
base_schema
end
base_schema
end

# Run a simple true/false validation of data against a schema
Expand All @@ -128,8 +132,7 @@ def load_ref_schema(parent_schema, ref)

return true if self.class.schema_loaded?(schema_uri)

schema_data = self.class.parse(custom_open(schema_uri))
schema = JSON::Schema.new(schema_data, schema_uri, @options[:version])
schema = @options[:schema_reader].read(schema_uri)
self.class.add_schema(schema)
build_schemas(schema)
end
Expand Down Expand Up @@ -219,7 +222,7 @@ def build_schemas(parent_schema)
def handle_schema(parent_schema, obj)
if obj.is_a?(Hash)
schema_uri = parent_schema.uri.clone
schema = JSON::Schema.new(obj,schema_uri,parent_schema.validator)
schema = JSON::Schema.new(obj, schema_uri, parent_schema.validator)
if obj['id']
Validator.add_schema(schema)
end
Expand Down Expand Up @@ -290,6 +293,14 @@ def fully_validate_uri(schema, data, opts={})
fully_validate(schema, data, opts.merge(:uri => true))
end

def schema_reader
@@schema_reader ||= JSON::Schema::Reader.new
end

def schema_reader=(reader)
@@schema_reader = reader
end

def clear_cache
@@schemas = {} if @@cache_schemas == false
end
Expand Down Expand Up @@ -492,55 +503,45 @@ def fake_uuid schema
@@fake_uuid_generator.call(schema)
end

def schema_to_list(schema)
new_schema = {"type" => "array", "items" => schema}
if !schema["$schema"].nil?
new_schema["$schema"] = schema["$schema"]
end

new_schema
end

def initialize_schema(schema)
if schema.is_a?(String)
begin
# Build a fake URI for this
schema_uri = Addressable::URI.parse(fake_uuid(schema))
schema = JSON::Validator.parse(schema)
schema = JSON::Schema.new(JSON::Validator.parse(schema), schema_uri, @options[:version])
if @options[:list] && @options[:fragment].nil?
schema = schema_to_list(schema)
schema = schema.to_array_schema
end
schema = JSON::Schema.new(schema,schema_uri,@options[:version])
Validator.add_schema(schema)
rescue
# Build a uri for it
schema_uri = normalized_uri(schema)
if !self.class.schema_loaded?(schema_uri)
schema = JSON::Validator.parse(custom_open(schema_uri))
schema = @options[:schema_reader].read(schema_uri)
schema = JSON::Schema.stringify(schema)

if @options[:list] && @options[:fragment].nil?
schema = schema_to_list(schema)
schema = schema.to_array_schema
end
schema = JSON::Schema.new(schema,schema_uri,@options[:version])

Validator.add_schema(schema)
else
schema = self.class.schema_for_uri(schema_uri)
if @options[:list] && @options[:fragment].nil?
schema = schema_to_list(schema.schema)
schema_uri = Addressable::URI.parse(fake_uuid(serialize(schema)))
schema = JSON::Schema.new(schema, schema_uri, @options[:version])
schema = schema.to_array_schema
schema.uri = Addressable::URI.parse(fake_uuid(serialize(schema.schema)))
Validator.add_schema(schema)
end
schema
end
end
elsif schema.is_a?(Hash)
if @options[:list] && @options[:fragment].nil?
schema = schema_to_list(schema)
end
schema_uri = Addressable::URI.parse(fake_uuid(serialize(schema)))
schema = JSON::Schema.stringify(schema)
schema = JSON::Schema.new(schema,schema_uri,@options[:version])
schema = JSON::Schema.new(schema, schema_uri, @options[:version])
if @options[:list] && @options[:fragment].nil?
schema = schema.to_array_schema
end
Validator.add_schema(schema)
else
raise "Invalid schema - must be either a string or a hash"
Expand All @@ -549,7 +550,6 @@ def initialize_schema(schema)
schema
end


def initialize_data(data)
if @options[:parse_data]
if @options[:json]
Expand Down
18 changes: 18 additions & 0 deletions test/schemas/address_microformat.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading

0 comments on commit 7c9d44a

Please sign in to comment.