json-schema 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +6 -14
  2. data/README.textile +58 -7
  3. data/lib/json-schema.rb +3 -1
  4. data/lib/json-schema/attributes/additionalitems.rb +14 -11
  5. data/lib/json-schema/attributes/additionalproperties.rb +33 -43
  6. data/lib/json-schema/attributes/anyof.rb +4 -0
  7. data/lib/json-schema/attributes/dependencies.rb +31 -19
  8. data/lib/json-schema/attributes/disallow.rb +2 -3
  9. data/lib/json-schema/attributes/divisibleby.rb +11 -7
  10. data/lib/json-schema/attributes/enum.rb +14 -16
  11. data/lib/json-schema/attributes/format.rb +4 -7
  12. data/lib/json-schema/attributes/formats/date_time_v4.rb +5 -8
  13. data/lib/json-schema/attributes/formats/ip.rb +41 -0
  14. data/lib/json-schema/attributes/formats/uri.rb +10 -8
  15. data/lib/json-schema/attributes/items.rb +15 -16
  16. data/lib/json-schema/attributes/limit.rb +179 -0
  17. data/lib/json-schema/attributes/maxdecimal.rb +7 -6
  18. data/lib/json-schema/attributes/multipleof.rb +4 -11
  19. data/lib/json-schema/attributes/not.rb +1 -1
  20. data/lib/json-schema/attributes/oneof.rb +15 -6
  21. data/lib/json-schema/attributes/pattern.rb +7 -6
  22. data/lib/json-schema/attributes/patternproperties.rb +9 -12
  23. data/lib/json-schema/attributes/properties.rb +55 -39
  24. data/lib/json-schema/attributes/properties_optional.rb +13 -12
  25. data/lib/json-schema/attributes/ref.rb +4 -4
  26. data/lib/json-schema/attributes/required.rb +16 -13
  27. data/lib/json-schema/attributes/type.rb +13 -18
  28. data/lib/json-schema/attributes/type_v4.rb +11 -18
  29. data/lib/json-schema/attributes/uniqueitems.rb +5 -7
  30. data/lib/json-schema/schema.rb +8 -8
  31. data/lib/json-schema/schema/#validator.rb# +37 -0
  32. data/lib/json-schema/schema/reader.rb +113 -0
  33. data/lib/json-schema/util/uri.rb +16 -0
  34. data/lib/json-schema/validator.rb +123 -128
  35. data/lib/json-schema/validators/draft1.rb +1 -1
  36. data/lib/json-schema/validators/draft2.rb +1 -1
  37. data/lib/json-schema/validators/draft3.rb +1 -1
  38. data/lib/json-schema/validators/draft4.rb +1 -1
  39. data/lib/json-schema/validators/hyper-draft4.rb +1 -1
  40. data/test/schemas/address_microformat.json +18 -0
  41. data/test/schemas/definition_schema.json +15 -0
  42. data/test/schemas/ref john with spaces schema.json +11 -0
  43. data/test/schemas/relative_definition_schema.json +8 -0
  44. data/test/test_all_of_ref_schema.rb +12 -15
  45. data/test/test_any_of_ref_schema.rb +7 -9
  46. data/test/test_bad_schema_ref.rb +18 -12
  47. data/test/test_common_test_suite.rb +45 -29
  48. data/test/test_custom_format.rb +2 -3
  49. data/test/test_definition.rb +15 -0
  50. data/test/test_extended_schema.rb +25 -31
  51. data/test/test_extends_and_additionalProperties.rb +23 -21
  52. data/test/test_files_v3.rb +14 -23
  53. data/test/test_fragment_resolution.rb +6 -7
  54. data/test/test_fragment_validation_with_ref.rb +2 -8
  55. data/test/test_full_validation.rb +2 -3
  56. data/test/test_helper.rb +46 -1
  57. data/test/test_initialize_data.rb +118 -0
  58. data/test/test_jsonschema_draft1.rb +48 -600
  59. data/test/test_jsonschema_draft2.rb +48 -699
  60. data/test/test_jsonschema_draft3.rb +91 -861
  61. data/test/test_jsonschema_draft4.rb +173 -812
  62. data/test/test_list_option.rb +6 -7
  63. data/test/{test_merge_misisng_values.rb → test_merge_missing_values.rb} +2 -3
  64. data/test/test_minitems.rb +2 -4
  65. data/test/test_one_of.rb +9 -19
  66. data/test/test_ruby_schema.rb +5 -14
  67. data/test/test_schema_loader.rb +74 -0
  68. data/test/test_schema_type_attribute.rb +2 -3
  69. data/test/test_schema_validation.rb +4 -5
  70. data/test/test_stringify.rb +2 -3
  71. data/test/test_uri_related.rb +67 -0
  72. data/test/test_validator.rb +53 -0
  73. metadata +129 -51
  74. data/lib/json-schema/attributes/dependencies_v4.rb +0 -27
  75. data/lib/json-schema/attributes/formats/ip4.rb +0 -20
  76. data/lib/json-schema/attributes/formats/ip6.rb +0 -20
  77. data/lib/json-schema/attributes/maximum.rb +0 -17
  78. data/lib/json-schema/attributes/maximum_inclusive.rb +0 -17
  79. data/lib/json-schema/attributes/maxitems.rb +0 -14
  80. data/lib/json-schema/attributes/maxlength.rb +0 -16
  81. data/lib/json-schema/attributes/maxproperties.rb +0 -14
  82. data/lib/json-schema/attributes/minimum.rb +0 -17
  83. data/lib/json-schema/attributes/minimum_inclusive.rb +0 -17
  84. data/lib/json-schema/attributes/minitems.rb +0 -14
  85. data/lib/json-schema/attributes/minlength.rb +0 -16
  86. data/lib/json-schema/attributes/minproperties.rb +0 -14
  87. data/lib/json-schema/attributes/properties_v4.rb +0 -58
  88. data/lib/json-schema/uri/file.rb +0 -36
@@ -4,19 +4,20 @@ module JSON
4
4
  class Schema
5
5
  class PropertiesOptionalAttribute < Attribute
6
6
  def self.validate(current_schema, data, fragments, processor, validator, options = {})
7
- if data.is_a?(Hash)
8
- current_schema.schema['properties'].each do |property,property_schema|
9
- if (property_schema['optional'].nil? || property_schema['optional'] == false) && !data.has_key?(property.to_s)
10
- message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'"
11
- validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
12
- end
7
+ return unless data.is_a?(Hash)
13
8
 
14
- if data.has_key?(property.to_s)
15
- schema = JSON::Schema.new(property_schema,current_schema.uri,validator)
16
- fragments << property
17
- schema.validate(data[property],fragments,processor,options)
18
- fragments.pop
19
- end
9
+ schema = current_schema.schema
10
+ schema['properties'].each do |property, property_schema|
11
+ property = property.to_s
12
+
13
+ if !property_schema['optional'] && !data.key?(property)
14
+ message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'"
15
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
16
+ end
17
+
18
+ if data.has_key?(property)
19
+ expected_schema = JSON::Schema.new(property_schema, current_schema.uri, validator)
20
+ expected_schema.validate(data[property], fragments + [property], processor, options)
20
21
  end
21
22
  end
22
23
  end
@@ -21,7 +21,7 @@ module JSON
21
21
  def self.get_referenced_uri_and_schema(s, current_schema, validator)
22
22
  uri,schema = nil,nil
23
23
 
24
- temp_uri = URI.parse(s['$ref'])
24
+ temp_uri = Addressable::URI.parse(s['$ref'])
25
25
  if temp_uri.relative?
26
26
  temp_uri = current_schema.uri.clone
27
27
  # Check for absolute path
@@ -31,7 +31,7 @@ module JSON
31
31
  elsif path[0,1] == "/"
32
32
  temp_uri.path = Pathname.new(path).cleanpath.to_s
33
33
  else
34
- temp_uri = current_schema.uri.merge(path)
34
+ temp_uri = current_schema.uri.join(path)
35
35
  end
36
36
  temp_uri.fragment = s['$ref'].split("#")[1]
37
37
  end
@@ -40,7 +40,7 @@ module JSON
40
40
  # Grab the parent schema from the schema list
41
41
  schema_key = temp_uri.to_s.split("#")[0] + "#"
42
42
 
43
- ref_schema = JSON::Validator.schemas[schema_key]
43
+ ref_schema = JSON::Validator.schema_for_uri(schema_key)
44
44
 
45
45
  if ref_schema
46
46
  # Perform fragment resolution to retrieve the appropriate level for the schema
@@ -49,7 +49,7 @@ module JSON
49
49
  fragment_path = ''
50
50
  fragments.each do |fragment|
51
51
  if fragment && fragment != ''
52
- fragment = URI.unescape(fragment.gsub('~0', '~').gsub('~1', '/'))
52
+ fragment = Addressable::URI.unescape(fragment.gsub('~0', '~').gsub('~1', '/'))
53
53
  if target_schema.is_a?(Array)
54
54
  target_schema = target_schema[fragment.to_i]
55
55
  else
@@ -4,19 +4,22 @@ module JSON
4
4
  class Schema
5
5
  class RequiredAttribute < Attribute
6
6
  def self.validate(current_schema, data, fragments, processor, validator, options = {})
7
- if data.is_a?(Hash)
8
- current_schema.schema['required'].each do |property,property_schema|
9
- if !data.has_key?(property.to_s)
10
- prop_defaults = options[:insert_defaults] &&
11
- current_schema.schema['properties'] &&
12
- current_schema.schema['properties'][property] &&
13
- !current_schema.schema['properties'][property]["default"].nil? &&
14
- !current_schema.schema['properties'][property]["readonly"]
15
- if !prop_defaults
16
- message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'"
17
- validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
18
- end
19
- end
7
+ return unless data.is_a?(Hash)
8
+
9
+ schema = current_schema.schema
10
+ defined_properties = schema['properties']
11
+
12
+ schema['required'].each do |property, property_schema|
13
+ next if data.has_key?(property.to_s)
14
+ prop_defaults = options[:insert_defaults] &&
15
+ defined_properties &&
16
+ defined_properties[property] &&
17
+ !defined_properties[property]["default"].nil? &&
18
+ !defined_properties[property]["readonly"]
19
+
20
+ if !prop_defaults
21
+ message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'"
22
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
20
23
  end
21
24
  end
22
25
  end
@@ -32,7 +32,7 @@ module JSON
32
32
  pre_validation_error_count = validation_errors(processor).count
33
33
 
34
34
  begin
35
- schema.validate(data,fragments,processor,options)
35
+ schema.validate(data,fragments,processor,options.merge(:disallow => false))
36
36
  valid = true
37
37
  rescue ValidationError
38
38
  # We don't care that these schemas don't validate - we only care that one validated
@@ -49,36 +49,31 @@ module JSON
49
49
  break if valid
50
50
  end
51
51
 
52
- if (options[:disallow])
53
- if valid
54
- message = "The property '#{build_fragment(fragments)}' matched one or more of the following types:"
55
- types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
56
- message.chop!
57
- validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
58
- end
52
+ if options[:disallow]
53
+ return if !valid
54
+ message = "The property '#{build_fragment(fragments)}' matched one or more of the following types: #{list_types(types)}"
55
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
59
56
  elsif !valid
60
57
  if union
61
- message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match one or more of the following types:"
62
- types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
63
- message.chop!
58
+ message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match one or more of the following types: #{list_types(types)}"
64
59
  validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
65
60
  validation_errors(processor).last.sub_errors = union_errors
66
61
  else
67
- message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match the following type:"
68
- types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
69
- message.chop!
62
+ message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match the following type: #{list_types(types)}"
70
63
  validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
71
64
  end
72
65
  end
73
66
  end
74
67
 
68
+ def self.list_types(types)
69
+ types.map { |type| type.is_a?(String) ? type : '(schema)' }.join(', ')
70
+ end
71
+
75
72
  # Lookup Schema type of given class instance
76
73
  def self.type_of_data(data)
77
- type, klass = TYPE_CLASS_MAPPINGS.map { |k,v| [k,v] }.sort_by { |i|
78
- k,v = i
74
+ type, _ = TYPE_CLASS_MAPPINGS.map { |k,v| [k,v] }.sort_by { |(_, v)|
79
75
  -Array(v).map { |klass| klass.ancestors.size }.max
80
- }.find { |i|
81
- k,v = i
76
+ }.find { |(_, v)|
82
77
  Array(v).any? { |klass| data.kind_of?(klass) }
83
78
  }
84
79
  type
@@ -10,26 +10,19 @@ module JSON
10
10
  types = [types]
11
11
  union = false
12
12
  end
13
- valid = false
14
13
 
15
- types.each do |type|
16
- valid = data_valid_for_type?(data, type)
17
- break if valid
18
- end
14
+ return if types.any? { |type| data_valid_for_type?(data, type) }
19
15
 
20
- if !valid
21
- if union
22
- message = "The property '#{build_fragment(fragments)}' of type #{data.class} did not match one or more of the following types:"
23
- types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
24
- message.chop!
25
- validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
26
- else
27
- message = "The property '#{build_fragment(fragments)}' of type #{data.class} did not match the following type:"
28
- types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
29
- message.chop!
30
- validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
31
- end
32
- end
16
+ types = types.map { |type| type.is_a?(String) ? type : '(schema)' }.join(', ')
17
+ message = format(
18
+ "The property '%s' of type %s did not match %s: %s",
19
+ build_fragment(fragments),
20
+ data.class,
21
+ union ? 'one or more of the following types' : 'the following type',
22
+ types
23
+ )
24
+
25
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
33
26
  end
34
27
  end
35
28
  end
@@ -4,13 +4,11 @@ module JSON
4
4
  class Schema
5
5
  class UniqueItemsAttribute < Attribute
6
6
  def self.validate(current_schema, data, fragments, processor, validator, options = {})
7
- if data.is_a?(Array)
8
- d = data.clone
9
- dupes = d.uniq!
10
- if dupes
11
- message = "The property '#{build_fragment(fragments)}' contained duplicated array values"
12
- validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
13
- end
7
+ return unless data.is_a?(Array)
8
+
9
+ if data.clone.uniq!
10
+ message = "The property '#{build_fragment(fragments)}' contained duplicated array values"
11
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors])
14
12
  end
15
13
  end
16
14
  end
@@ -6,15 +6,14 @@ module JSON
6
6
  attr_accessor :schema, :uri, :validator
7
7
 
8
8
  def initialize(schema,uri,parent_validator=nil)
9
- @schema = self.class.stringify(schema)
9
+ @schema = schema
10
10
  @uri = uri
11
11
 
12
12
  # If there is an ID on this schema, use it to generate the URI
13
13
  if @schema['id'] && @schema['id'].kind_of?(String)
14
- temp_uri = URI.parse(@schema['id'])
14
+ temp_uri = Addressable::URI.parse(@schema['id'])
15
15
  if temp_uri.relative?
16
- uri = uri.merge(@schema['id'])
17
- temp_uri = uri
16
+ temp_uri = uri.join(temp_uri)
18
17
  end
19
18
  @uri = temp_uri
20
19
  end
@@ -49,10 +48,11 @@ module JSON
49
48
  end
50
49
  end
51
50
 
52
- def base_uri
53
- parts = @uri.to_s.split('/')
54
- parts.pop
55
- parts.join('/') + '/'
51
+ # @return [JSON::Schema] a new schema matching an array whose items all match this schema.
52
+ def to_array_schema
53
+ array_schema = { 'type' => 'array', 'items' => schema }
54
+ array_schema['$schema'] = schema['$schema'] unless schema['$schema'].nil?
55
+ JSON::Schema.new(array_schema, uri, validator)
56
56
  end
57
57
 
58
58
  def to_s
@@ -0,0 +1,37 @@
1
+
2
+ module JSON
3
+ class Schema
4
+ class Validator
5
+ attr_accessor :attributes, :formats, :uri, :names
6
+ attr_reader :default_formats
7
+
8
+ def initialize()
9
+ @attributes = {}
10
+ @formats = {}
11
+ @default_formats = {}
12
+ @uri = nil
13
+ @names = []
14
+ @metaschema_name = ''
15
+ end
16
+
17
+ def extend_schema_definition(schema_uri)
18
+ validator = JSON::Validator.validator_for(schema_uri)
19
+ @attributes.merge!(validator.attributes)
20
+ end
21
+
22
+ def validate(current_schema, data, fragments, processor, options = {})
23
+ current_schema.schema.each do |attr_name,attribute|
24
+ if @attributes.has_key?(attr_name.to_s)
25
+ @attributes[attr_name.to_s].validate(current_schema, data, fragments, processor, self, options)
26
+ end
27
+ end
28
+ data
29
+ end
30
+
31
+ def metaschema
32
+ resources = File.expand_path('../../../../resources', __FILE__)
33
+ File.join(resources, @metaschema_name)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,113 @@
1
+ require 'open-uri'
2
+ require 'addressable/uri'
3
+ require 'pathname'
4
+
5
+ module JSON
6
+ class Schema
7
+ # Raised by {JSON::Schema::Reader} when one of its settings indicate
8
+ # a schema should not be readed.
9
+ class ReadRefused < StandardError
10
+ # @return [String] the requested schema location which was refused
11
+ attr_reader :location
12
+
13
+ # @return [Symbol] either +:uri+ or +:file+
14
+ attr_reader :type
15
+
16
+ def initialize(location, type)
17
+ @location = location
18
+ @type = type
19
+ super("Read of #{type == :uri ? 'URI' : type} at #{location} refused!")
20
+ end
21
+ end
22
+
23
+ # When an unregistered schema is encountered, the {JSON::Schema::Reader} is
24
+ # used to fetch its contents and register it with the {JSON::Validator}.
25
+ #
26
+ # This default reader will read schemas from the filesystem or from a URI.
27
+ class Reader
28
+ # The behavior of the schema reader can be controlled by providing
29
+ # callbacks to determine whether to permit reading referenced schemas.
30
+ # The options +accept_uri+ and +accept_file+ should be procs which
31
+ # accept a +URI+ or +Pathname+ object, and return a boolean value
32
+ # indicating whether to read the referenced schema.
33
+ #
34
+ # URIs using the +file+ scheme will be normalized into +Pathname+ objects
35
+ # and passed to the +accept_file+ callback.
36
+ #
37
+ # @param options [Hash]
38
+ # @option options [Boolean, #call] accept_uri (true)
39
+ # @option options [Boolean, #call] accept_file (true)
40
+ #
41
+ # @example Reject all unregistered schemas
42
+ # JSON::Validator.schema_reader = JSON::Schema::Reader.new(
43
+ # :accept_uri => false,
44
+ # :accept_file => false
45
+ # )
46
+ #
47
+ # @example Only permit URIs from certain hosts
48
+ # JSON::Validator.schema_reader = JSON::Schema::Reader.new(
49
+ # :accept_file => false,
50
+ # :accept_uri => proc { |uri| ['mycompany.com', 'json-schema.org'].include?(uri.host) }
51
+ # )
52
+ def initialize(options = {})
53
+ @accept_uri = options.fetch(:accept_uri, true)
54
+ @accept_file = options.fetch(:accept_file, true)
55
+ end
56
+
57
+ # @param location [#to_s] The location from which to read the schema
58
+ # @return [JSON::Schema]
59
+ # @raise [JSON::Schema::ReadRefused] if +accept_uri+ or +accept_file+
60
+ # indicated the schema should not be readed
61
+ # @raise [JSON::ParserError] if the schema was not a valid JSON object
62
+ def read(location)
63
+ uri = Addressable::URI.parse(location.to_s)
64
+ body = if uri.scheme.nil? || uri.scheme == 'file'
65
+ uri = Addressable::URI.convert_path(uri.path)
66
+ read_file(Pathname.new(uri.path).expand_path)
67
+ else
68
+ read_uri(uri)
69
+ end
70
+
71
+ JSON::Schema.new(JSON::Validator.parse(body), uri)
72
+ end
73
+
74
+ # @param uri [Addressable::URI]
75
+ # @return [Boolean]
76
+ def accept_uri?(uri)
77
+ if @accept_uri.respond_to?(:call)
78
+ @accept_uri.call(uri)
79
+ else
80
+ @accept_uri
81
+ end
82
+ end
83
+
84
+ # @param pathname [Pathname]
85
+ # @return [Boolean]
86
+ def accept_file?(pathname)
87
+ if @accept_file.respond_to?(:call)
88
+ @accept_file.call(pathname)
89
+ else
90
+ @accept_file
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def read_uri(uri)
97
+ if accept_uri?(uri)
98
+ open(uri.to_s).read
99
+ else
100
+ raise JSON::Schema::ReadRefused.new(uri.to_s, :uri)
101
+ end
102
+ end
103
+
104
+ def read_file(pathname)
105
+ if accept_file?(pathname)
106
+ File.read(Addressable::URI.unescape(pathname.to_s))
107
+ else
108
+ raise JSON::Schema::ReadRefused.new(pathname.to_s, :file)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,16 @@
1
+ module JSON
2
+ module Util
3
+ module URI
4
+ def self.normalized_uri(uri)
5
+ uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
6
+ # Check for absolute path
7
+ if uri.relative?
8
+ data = uri.to_s
9
+ data = "#{Dir.pwd}/#{data}" if data[0,1] != '/'
10
+ uri = Addressable::URI.convert_path(data)
11
+ end
12
+ uri
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,4 +1,4 @@
1
- require 'uri'
1
+ require 'addressable/uri'
2
2
  require 'open-uri'
3
3
  require 'pathname'
4
4
  require 'bigdecimal'
@@ -7,6 +7,7 @@ require 'date'
7
7
  require 'thread'
8
8
  require 'yaml'
9
9
 
10
+ require 'json-schema/schema/reader'
10
11
  require 'json-schema/errors/schema_error'
11
12
  require 'json-schema/errors/json_parse_error'
12
13
 
@@ -24,7 +25,8 @@ module JSON
24
25
  :errors_as_objects => false,
25
26
  :insert_defaults => false,
26
27
  :clear_cache => true,
27
- :strict => false
28
+ :strict => false,
29
+ :parse_data => true
28
30
  }
29
31
  @@validators = {}
30
32
  @@default_validator = nil
@@ -39,6 +41,7 @@ module JSON
39
41
 
40
42
  validator = JSON::Validator.validator_for_name(@options[:version])
41
43
  @options[:version] = validator
44
+ @options[:schema_reader] ||= JSON::Validator.schema_reader
42
45
 
43
46
  @validation_options = @options[:record_errors] ? {:record_errors => true} : {}
44
47
  @validation_options[:insert_defaults] = true if @options[:insert_defaults]
@@ -99,10 +102,12 @@ module JSON
99
102
  raise JSON::Schema::SchemaError.new("Invalid schema encountered when resolving :fragment option")
100
103
  end
101
104
  end
102
- if @options[:list] #check if the schema is validating a list
103
- base_schema.schema = schema_to_list(base_schema.schema)
105
+
106
+ if @options[:list]
107
+ base_schema.to_array_schema
108
+ else
109
+ base_schema
104
110
  end
105
- base_schema
106
111
  end
107
112
 
108
113
  # Run a simple true/false validation of data against a schema
@@ -122,65 +127,50 @@ module JSON
122
127
  end
123
128
  end
124
129
 
130
+ def load_ref_schema(parent_schema, ref)
131
+ schema_uri = absolutize_ref_uri(ref, parent_schema.uri)
125
132
 
126
- def load_ref_schema(parent_schema,ref)
127
- uri = URI.parse(ref)
128
- if uri.relative?
129
- uri = parent_schema.uri.clone
133
+ return true if self.class.schema_loaded?(schema_uri)
130
134
 
131
- # Check for absolute path
132
- path = ref.split("#")[0]
135
+ schema = @options[:schema_reader].read(schema_uri)
136
+ self.class.add_schema(schema)
137
+ build_schemas(schema)
138
+ end
133
139
 
134
- # This is a self reference and thus the schema does not need to be re-loaded
135
- if path.nil? || path == ''
136
- return
137
- end
140
+ def absolutize_ref_uri(ref, parent_schema_uri)
141
+ ref_uri = Addressable::URI.parse(ref)
138
142
 
139
- if path && path[0,1] == '/'
140
- uri.path = Pathname.new(path).cleanpath.to_s
141
- else
142
- uri = parent_schema.uri.merge(path)
143
- end
144
- uri.fragment = ''
145
- end
143
+ return ref_uri if ref_uri.absolute?
144
+ # This is a self reference and thus the schema does not need to be re-loaded
145
+ return parent_schema_uri if ref_uri.path.empty?
146
146
 
147
- if Validator.schemas[uri.to_s].nil?
148
- schema = JSON::Schema.new(JSON::Validator.parse(open(uri.to_s).read), uri, @options[:version])
149
- Validator.add_schema(schema)
150
- build_schemas(schema)
151
- end
147
+ uri = parent_schema_uri.clone
148
+ uri.fragment = ''
149
+ Util::URI.normalized_uri(uri.join(ref_uri.path))
152
150
  end
153
151
 
154
-
155
152
  # Build all schemas with IDs, mapping out the namespace
156
153
  def build_schemas(parent_schema)
154
+ schema = parent_schema.schema
155
+
157
156
  # Build ref schemas if they exist
158
- if parent_schema.schema["$ref"]
159
- load_ref_schema(parent_schema, parent_schema.schema["$ref"])
160
- end
161
- if parent_schema.schema["extends"]
162
- if parent_schema.schema["extends"].is_a?(String)
163
- load_ref_schema(parent_schema, parent_schema.schema["extends"])
164
- elsif parent_schema.schema["extends"].is_a?(Array)
165
- parent_schema.schema["extends"].each do |type|
166
- handle_schema(parent_schema, type)
167
- end
168
- end
157
+ if schema["$ref"]
158
+ load_ref_schema(parent_schema, schema["$ref"])
169
159
  end
170
160
 
171
- # handle validations that always contain schemas
172
- ["allOf", "anyOf", "oneOf", "not"].each do |key|
173
- if parent_schema.schema.has_key?(key)
174
- validations = parent_schema.schema[key]
175
- validations = [validations] unless validations.is_a?(Array)
176
- validations.each {|v| handle_schema(parent_schema, v) }
161
+ case schema["extends"]
162
+ when String
163
+ load_ref_schema(parent_schema, schema["extends"])
164
+ when Array
165
+ schema['extends'].each do |type|
166
+ handle_schema(parent_schema, type)
177
167
  end
178
168
  end
179
169
 
180
170
  # Check for schemas in union types
181
171
  ["type", "disallow"].each do |key|
182
- if parent_schema.schema[key] && parent_schema.schema[key].is_a?(Array)
183
- parent_schema.schema[key].each_with_index do |type,i|
172
+ if schema[key].is_a?(Array)
173
+ schema[key].each do |type|
184
174
  if type.is_a?(Hash)
185
175
  handle_schema(parent_schema, type)
186
176
  end
@@ -188,48 +178,42 @@ module JSON
188
178
  end
189
179
  end
190
180
 
191
- # "definitions" are schemas in V4
192
- if parent_schema.schema["definitions"]
193
- parent_schema.schema["definitions"].each do |k,v|
194
- handle_schema(parent_schema, v)
181
+ # Schema properties whose values are objects, the values of which
182
+ # are themselves schemas.
183
+ %w[definitions properties patternProperties].each do |key|
184
+ next unless value = schema[key]
185
+ value.each do |k, inner_schema|
186
+ handle_schema(parent_schema, inner_schema)
195
187
  end
196
188
  end
197
189
 
198
- # All properties are schemas
199
- if parent_schema.schema["properties"]
200
- parent_schema.schema["properties"].each do |k,v|
201
- handle_schema(parent_schema, v)
202
- end
190
+ # Schema properties whose values are themselves schemas.
191
+ %w[additionalProperties additionalItems dependencies extends].each do |key|
192
+ next unless schema[key].is_a?(Hash)
193
+ handle_schema(parent_schema, schema[key])
203
194
  end
204
- if parent_schema.schema["patternProperties"]
205
- parent_schema.schema["patternProperties"].each do |k,v|
206
- handle_schema(parent_schema, v)
195
+
196
+ # Schema properties whose values may be an array of schemas.
197
+ %w[allOf anyOf oneOf not].each do |key|
198
+ next unless value = schema[key]
199
+ Array(value).each do |inner_schema|
200
+ handle_schema(parent_schema, inner_schema)
207
201
  end
208
202
  end
209
203
 
210
204
  # Items are always schemas
211
- if parent_schema.schema["items"]
212
- items = parent_schema.schema["items"].clone
213
- single = false
214
- if !items.is_a?(Array)
215
- items = [items]
216
- single = true
217
- end
218
- items.each_with_index do |item,i|
205
+ if schema["items"]
206
+ items = schema["items"].clone
207
+ items = [items] unless items.is_a?(Array)
208
+
209
+ items.each do |item|
219
210
  handle_schema(parent_schema, item)
220
211
  end
221
212
  end
222
213
 
223
214
  # Convert enum to a ArraySet
224
- if parent_schema.schema["enum"] && parent_schema.schema["enum"].is_a?(Array)
225
- parent_schema.schema["enum"] = ArraySet.new(parent_schema.schema["enum"])
226
- end
227
-
228
- # Each of these might be schemas
229
- ["additionalProperties", "additionalItems", "dependencies", "extends"].each do |key|
230
- if parent_schema.schema[key].is_a?(Hash)
231
- handle_schema(parent_schema, parent_schema.schema[key])
232
- end
215
+ if schema["enum"].is_a?(Array)
216
+ schema["enum"] = ArraySet.new(schema["enum"])
233
217
  end
234
218
 
235
219
  end
@@ -238,7 +222,7 @@ module JSON
238
222
  def handle_schema(parent_schema, obj)
239
223
  if obj.is_a?(Hash)
240
224
  schema_uri = parent_schema.uri.clone
241
- schema = JSON::Schema.new(obj,schema_uri,parent_schema.validator)
225
+ schema = JSON::Schema.new(obj, schema_uri, parent_schema.validator)
242
226
  if obj['id']
243
227
  Validator.add_schema(schema)
244
228
  end
@@ -309,6 +293,14 @@ module JSON
309
293
  fully_validate(schema, data, opts.merge(:uri => true))
310
294
  end
311
295
 
296
+ def schema_reader
297
+ @@schema_reader ||= JSON::Schema::Reader.new
298
+ end
299
+
300
+ def schema_reader=(reader)
301
+ @@schema_reader = reader
302
+ end
303
+
312
304
  def clear_cache
313
305
  @@schemas = {} if @@cache_schemas == false
314
306
  end
@@ -318,7 +310,22 @@ module JSON
318
310
  end
319
311
 
320
312
  def add_schema(schema)
321
- @@schemas[schema.uri.to_s] = schema if @@schemas[schema.uri.to_s].nil?
313
+ @@schemas[schema_key_for(schema.uri)] ||= schema
314
+ end
315
+
316
+ def schema_for_uri(uri)
317
+ # We only store normalized uris terminated with fragment #, so we can try whether
318
+ # normalization can be skipped
319
+ @@schemas[uri] || @@schemas[schema_key_for(uri)]
320
+ end
321
+
322
+ def schema_loaded?(schema_uri)
323
+ !schema_for_uri(schema_uri).nil?
324
+ end
325
+
326
+ def schema_key_for(uri)
327
+ key = Util::URI.normalized_uri(uri).to_s
328
+ key.end_with?('#') ? key : "#{key}#"
322
329
  end
323
330
 
324
331
  def cache_schemas=(val)
@@ -336,7 +343,7 @@ module JSON
336
343
 
337
344
  def validator_for_uri(schema_uri)
338
345
  return default_validator unless schema_uri
339
- u = URI.parse(schema_uri)
346
+ u = Addressable::URI.parse(schema_uri)
340
347
  validator = validators["#{u.scheme}://#{u.host}#{u.path}"]
341
348
  if validator.nil?
342
349
  raise JSON::Schema::SchemaError.new("Schema not found: #{schema_uri}")
@@ -412,7 +419,7 @@ module JSON
412
419
  else
413
420
  case @@json_backend.to_s
414
421
  when 'json'
415
- JSON.parse(s)
422
+ JSON.parse(s, :quirks_mode => true)
416
423
  when 'yajl'
417
424
  json = StringIO.new(s)
418
425
  parser = Yajl::Parser.new
@@ -503,53 +510,45 @@ module JSON
503
510
  @@fake_uuid_generator.call(schema)
504
511
  end
505
512
 
506
- def schema_to_list(schema)
507
- new_schema = {"type" => "array", "items" => schema}
508
- if !schema["$schema"].nil?
509
- new_schema["$schema"] = schema["$schema"]
510
- end
511
-
512
- new_schema
513
- end
514
-
515
513
  def initialize_schema(schema)
516
514
  if schema.is_a?(String)
517
515
  begin
518
516
  # Build a fake URI for this
519
- schema_uri = URI.parse(fake_uuid(schema))
520
- schema = JSON::Validator.parse(schema)
517
+ schema_uri = Addressable::URI.parse(fake_uuid(schema))
518
+ schema = JSON::Schema.new(JSON::Validator.parse(schema), schema_uri, @options[:version])
521
519
  if @options[:list] && @options[:fragment].nil?
522
- schema = schema_to_list(schema)
520
+ schema = schema.to_array_schema
523
521
  end
524
- schema = JSON::Schema.new(schema,schema_uri,@options[:version])
525
522
  Validator.add_schema(schema)
526
523
  rescue
527
524
  # Build a uri for it
528
- schema_uri = normalized_uri(schema)
529
- if Validator.schemas[schema_uri.to_s].nil?
530
- schema = JSON::Validator.parse(open(schema_uri.to_s).read)
525
+ schema_uri = Util::URI.normalized_uri(schema)
526
+ if !self.class.schema_loaded?(schema_uri)
527
+ schema = @options[:schema_reader].read(schema_uri)
528
+ schema = JSON::Schema.stringify(schema)
529
+
531
530
  if @options[:list] && @options[:fragment].nil?
532
- schema = schema_to_list(schema)
531
+ schema = schema.to_array_schema
533
532
  end
534
- schema = JSON::Schema.new(schema,schema_uri,@options[:version])
533
+
535
534
  Validator.add_schema(schema)
536
535
  else
537
- schema = Validator.schemas[schema_uri.to_s]
536
+ schema = self.class.schema_for_uri(schema_uri)
538
537
  if @options[:list] && @options[:fragment].nil?
539
- schema = schema_to_list(schema.schema)
540
- schema_uri = URI.parse(fake_uuid(serialize(schema)))
541
- schema = JSON::Schema.new(schema, schema_uri, @options[:version])
538
+ schema = schema.to_array_schema
539
+ schema.uri = Addressable::URI.parse(fake_uuid(serialize(schema.schema)))
542
540
  Validator.add_schema(schema)
543
541
  end
544
542
  schema
545
543
  end
546
544
  end
547
545
  elsif schema.is_a?(Hash)
546
+ schema_uri = Addressable::URI.parse(fake_uuid(serialize(schema)))
547
+ schema = JSON::Schema.stringify(schema)
548
+ schema = JSON::Schema.new(schema, schema_uri, @options[:version])
548
549
  if @options[:list] && @options[:fragment].nil?
549
- schema = schema_to_list(schema)
550
+ schema = schema.to_array_schema
550
551
  end
551
- schema_uri = URI.parse(fake_uuid(serialize(schema)))
552
- schema = JSON::Schema.new(schema,schema_uri,@options[:version])
553
552
  Validator.add_schema(schema)
554
553
  else
555
554
  raise "Invalid schema - must be either a string or a hash"
@@ -558,40 +557,36 @@ module JSON
558
557
  schema
559
558
  end
560
559
 
561
-
562
560
  def initialize_data(data)
563
- if @options[:json]
564
- data = JSON::Validator.parse(data)
565
- elsif @options[:uri]
566
- json_uri = normalized_uri(data)
567
- data = JSON::Validator.parse(open(json_uri.to_s).read)
568
- elsif data.is_a?(String)
569
- begin
561
+ if @options[:parse_data]
562
+ if @options[:json]
570
563
  data = JSON::Validator.parse(data)
571
- rescue
564
+ elsif @options[:uri]
565
+ json_uri = Util::URI.normalized_uri(data)
566
+ data = JSON::Validator.parse(custom_open(json_uri))
567
+ elsif data.is_a?(String)
572
568
  begin
573
- json_uri = normalized_uri(data)
574
- data = JSON::Validator.parse(open(json_uri.to_s).read)
569
+ data = JSON::Validator.parse(data)
575
570
  rescue
576
- # Silently discard the error - the data will not change
571
+ begin
572
+ json_uri = Util::URI.normalized_uri(data)
573
+ data = JSON::Validator.parse(custom_open(json_uri))
574
+ rescue
575
+ # Silently discard the error - the data will not change
576
+ end
577
577
  end
578
578
  end
579
579
  end
580
580
  JSON::Schema.stringify(data)
581
581
  end
582
582
 
583
- def normalized_uri(data)
584
- uri = URI.parse(data)
585
- if uri.relative?
586
- # Check for absolute path
587
- if data[0,1] == '/'
588
- uri = URI.parse("file://#{data}")
589
- else
590
- uri = URI.parse("file://#{Dir.pwd}/#{data}")
591
- end
583
+ def custom_open(uri)
584
+ uri = Util::URI.normalized_uri(uri) if uri.is_a?(String)
585
+ if uri.absolute? && uri.scheme != 'file'
586
+ open(uri.to_s).read
587
+ else
588
+ File.read(Addressable::URI.unescape(uri.path))
592
589
  end
593
- uri
594
590
  end
595
-
596
591
  end
597
592
  end